Skip to content

fix(teleport): wrap Windows rename mock without mutating readonly Fil…

MacroscopeApp / Macroscope - UI Consistency failed Aug 27, 2026 in 6m 33s

UI Consistency: 1 issue found

1 finding

  • apps/web/src/components/chat/ChatComposer.tsx:2803 — insertComposerTextAtEnd now bails on any isSendDisabled reason, not just the teleport lock. Since this function backs ChatComposerHandle.insertTextAtEnd, file-tree "Add to chat" mentions, terminal-link insertion, and type-to-focus keystrokes are refused (with an error toast in FileBrowserPanel) while an attachment is uploading or the thread is still loading. Gate on the teleport-specific canMutateTeleportDraft() predicate used elsewhere in this change; it is also ref-based and already in the imperative-handle dep list, so the lock state cannot go stale.

An inline review comment with a suggested fix has been posted.

Details

Note

Your check run agent prompt is: .macroscope/check-run-agents/ui-consistency.md
More information about how Check Run Agents work can be found in our Help Center.

Scope reviewed (apps/web/src/**/*.ts|tsx|css): commandPaletteBus.ts, ChatView.logic.ts/tsx, CommandPalette.logic.ts/tsx, chat/ChatComposer.tsx, chat/ChatHeader.tsx, chat/DraftHeroHeadline.tsx, teleport/TeleportOutButton.tsx, teleport/TeleportNativeConflictBanner.tsx, teleport/useTeleportNativeConflict.ts, lib/teleport.ts, lib/teleportNativeConflict.ts, state/teleport.ts. No CSS files changed.

Checked and clean

  • Shared primitives: TeleportOutButton uses Button (variant="outline", size="icon-xs", same 7/6 height as the neighboring header controls) with data-toolbar-control plus a Tooltip trigger, matching ProjectScriptsControl; the aria-disabled + cursor-not-allowed opacity-64 busy idiom matches GitActionsControl's tooltip-preserving disabled button. teleportNativeConflictBannerItem composes the existing ComposerBannerStackItem contract and Button size="xs" with disabled/aria-busy, consistent with the other composer banners.
  • Raw elements: the new "Import a native CLI session" button is a text-link affordance matching the existing raw link buttons in the same file and in ChatHeader, not a reconstruction of Button.
  • Command palette: new views/items reuse ADDON_ICON_CLASS/ITEM_ICON_CLASS, enumerateCommandPaletteItems, and the existing disabled action-item path rendered by DisabledCommandPaletteResultRow.
  • Environment routing: teleport capability checks, RPC dispatch, and labels all resolve from the explicitly scoped environment (project.environmentId, activeThread.environmentId, route environmentId); no active-environment fallback was introduced for an environment-scoped action.
  • Prompt editor lock: disabled propagates to editor.setEditable(false), and the teleport placeholder takes precedence without altering other placeholder branches.
  • No Tailwind/global-CSS ownership, theme-variant, scroll/mask, or emitted-selector changes in this PR.

🔍 Agent activity — 37 tool calls

💭 I'm scanning through the changed web UI files, looking at command palette, chat view, composer, and header components to understand the scope of the modifications. I check the ChatComposer guard I'd flagged before around line 2668, and confirm it now correctly uses canMutateTeleportDraft() to prevent mutating the draft when the composer isn't ready, showing an error toast if the user tries to add to chat while it's busy. Good, that issue is resolved so I don't need to raise it again. Now I'm looking for consistency issues in the newer teleport-related UI code, starting with whether TeleportOutButton's use of the raw Button primitive with Tooltip and its data-toolbar-control attribute matches the patterns used elsewhere, like in ChatHeader, and then I move on to reviewing DraftHeroHeadline. I notice DraftHeroHeadline uses a raw button element styled to look like a link, and I'm weighing whether this should have used an existing Button variant like "link" or "ghost" instead of recreating that styling manually, though I want to check if similar raw-button patterns already exist elsewhere in the codebase before flagging it. I also check that TeleportNativeConflictBanner's Button size="xs" is a valid supported size, and I look at TeleportOutButton's use of size="icon-xs" plus an opacity-64 class, questioning whether Tailwind v4's opacity scale actually supports that specific value. Tailwind v4 does support arbitrary bare numeric values for opacity, so opacity-64 is valid, though I want to check if it's used elsewhere in the repo for consistency. I'm also considering TeleportOutButton's use of aria-disabled with a manual click guard instead of the disabled prop, which seems like an intentional choice to preserve tooltip functionality, and I want to see if this pattern matches similar icon-button-with-tooltip implementations elsewhere, like in ChatHeader, before deciding whether to flag it. Now I'm checking that environment routing is consistent throughout the shared renderers—TeleportOutButton, useTeleportNativeConflict, CommandPalette, and DraftHeroHeadline all correctly resolve and pass environmentId, with DraftHeroHeadline's fallback logic for teleportEnvironmentId gated properly behind hasResolvedProject.

1. browse_code — apps/web/src/components/teleport/TeleportOutButton.tsx

2. browse_code — apps/web/src/components/chat/DraftHeroHeadline.tsx

💭 The raw button in DraftHeroHeadline matches an existing local pattern, so that's not an issue. I'm now checking whether hasResolvedProject could be true while activeProjectRef is null, which would affect the environmentId fallback logic. Following the environment routing rule, capability gating should target the same environment as the action. If activeProjectRef is null, the affordance-visibility check falls back to primaryEnvironmentId while openImportSessions opens a generic palette that filters teleport-capable projects per their own environment — that mismatch is only a show/hide heuristic, so it's low severity rather than a real bug. I want to trace the ChatView call site to confirm whether that null state can actually occur.

3. git_tools — DraftHeroHeadline

💭 Since activeProjectTitle being set implies activeProjectRef is set, that fallback rarely matters, so it's not a real finding. Now I want to check ChatHeader for how the toolbar button styling pattern works, and whether data-toolbar-control shows up elsewhere in the css or other components.

4. git_tools — data-toolbar-control

5. browse_code — apps/web/src/components/chat/ChatHeader.tsx

💭 Let me check ProjectScriptsControl's toolbar-control button patterns to match the convention for size, variant, and disabled handling.

6. browse_code — apps/web/src/components/ProjectScriptsControl.tsx

7. browse_code — apps/web/src/index.css

💭 TeleportOutButton looks consistent with the header pattern. Now I'm wondering how disabled states are handled elsewhere—checking GitActionsControl to see if it uses the Button's disabled prop or an aria-disabled plus opacity approach, and searching the repo for existing "cursor-not-allowed" or opacity conventions to match.

8. git_tools — opacity-64|cursor-not-allowed|aria-disabled

💭 The disabled styling pattern matches GitActionsControl's existing approach, keeping tooltips functional. Now I want to verify the ComposerBannerStackItem type supports the warning variant with urgent/actions props, and confirm Button accepts an xs size before finalizing the TeleportNativeConflictBanner usage.

9. browse_code — apps/web/src/components/chat/ComposerBannerStack.tsx

💭 I'm checking how TeleportNativeConflictBanner uses the shared item contract with its Button size and variant, and want to grep other banner items in ChatView to confirm consistent styling conventions for action buttons.

10. git_tools — actions: (

💭 The disabled state and aria-busy handling on the new banner button look consistent with existing patterns, and the redundant aria-label isn't worth flagging. Now I'm shifting focus to the ChatComposer changes to check for consistency issues, starting with an addition there.

11. git_tools — isTeleportComposerLocked|teleportLockReason

💭 I want to check the composer input area's disabled prop handling around lines 3480-3560 to verify UI consistency—looking at how the prompt component handles disabled state for attachments and the send button, and whether other controls like attach, model picker, stash, and mic stay enabled while locked. I'll view lines 3300-3620 to get the full picture.

12. browse_code — apps/web/src/components/chat/ChatComposer.tsx

💭 I notice the composer just dims opacity when project selection is required, relying on the placeholder text rather than explicit disabled styling — that seems fine if the prompt editor handles disabled state itself. I want to verify ComposerPromptEditor actually blocks typing and shows the right cursor state when disabled, and confirm other input pathways like attachments are also blocked.

13. git_tools — disabled

14. git_tools — ComposerPromptEditor

15. git_tools — disabled

💭 I'm noting that disabling the editor when teleport-locked doesn't fully lock the UI — the attach-image button, mic, and other composer controls stay active, and keyboard shortcuts or paste handling might still work even though the editor itself is non-editable. That seems like a minor inconsistency rather than a major issue.

16. git_tools — addComposerDraftImages(|addComposerDraftImage(

💭 Shifting focus to CommandPalette.tsx now, checking that new items use the same ADDON_ICON_CLASS/ITEM_ICON_CLASS pattern as other views, and verifying whether the LoaderIcon with animate-spin matches other loading states there or if there's a shared spinner component I should use instead.

17. git_tools — animate-spin|Loader2Icon|LoaderIcon|Spinner

💭 Let me check how other loading indicators appear elsewhere in the palette, and search for existing animate-spin usage or a shared Spinner/Loader component I could reuse.

18. git_tools — animate-spin

💭 I'm checking the spinner component to see if it's the intended shared primitive, though many call sites use LoaderIcon with animate-spin directly, so this isn't a clear violation. I also notice CommandPalette uses a template string for the icon class instead of the cn() helper—minor, but worth checking against other usages in that file before moving on.

19. browse_code — apps/web/src/components/CommandPalette.tsx

💭 I'm checking for other places where ITEM_ICON_CLASS gets combined with additional classes, and confirming that projectFavicon is used correctly for import items. I'm also noting how CommandPalette handles keepOpen for status and disabled items. I want to verify whether CommandPaletteResults handles the disabled property on action items, so I'm checking the item type definition in CommandPalette.logic.ts.

20. git_tools — disabled|keepOpen

💭 Now I want to check how CommandPaletteResults renders disabled items — whether it applies pointer-events-none or otherwise prevents execution.

21. browse_code — apps/web/src/components/CommandPaletteResults.tsx

💭 Disabled items render as non-interactive divs, which correctly matches the retry status pattern where items become enabled once a run exists. I want to check whether there's an existing convention elsewhere in the palette for empty or loading states to make sure this stays consistent.

22. browse_code — apps/web/src/components/CommandPaletteResults.tsx

💭 I wonder if there's already a disabled-item pattern used elsewhere, like for loading placeholders, so I'm searching for existing "disabled: true" usage in CommandPalette.tsx.

23. git_tools — disabled: true

💭 The disabled palette item pattern looks consistent with the codebase. I'm now checking the import sessions status item behavior for error/interrupted cases, then moving to review the ChatView banner item, send guard, and the teleport native conflict hook for consistency. Looking at whether the new headline button recreates the shared Button primitive, it actually matches the existing inline text-link-with-icon idiom already used elsewhere, like the project breadcrumb button in ChatHeader — so it's not a real violation, though I notice it might be missing a pointer-coarse touch target consideration. Now I'm tracing environment routing through CommandPalette's teleport logic — the import, native session loading, and thread-binding checks all correctly resolve environments per-project. I'm now checking whether ChatView passes the correct environmentId into useTeleportNativeConflict, tracing back to how that variable is defined in ChatViewContent.

24. git_tools — const environmentId =

25. git_tools — environmentId,\n threadId: isServerThread|useTeleportNativeConflict({

26. browse_code — apps/web/src/components/ChatView.tsx

💭 I'm tracing where environmentId comes from in this hook, comparing it to how other environment-scoped hooks derive it from activeThread?.environmentId, versus teleport's route-level value. Since ChatView is scoped to a route environmentId and activeThread is looked up via scopeThreadRef(environmentId, threadId), these should align, so I want to verify the derivation in ChatView's props.

27. git_tools — environmentId

💭 Since ChatView's environmentId matches activeThread's, that part checks out. Now I need to trace where activeThread comes from within CommandPalette to see if there's a mismatch feeding the "Import this thread from native CLI" teleport logic.

28. browse_code — apps/web/src/components/CommandPalette.tsx

💭 I'm confirming the native CLI import action doesn't set keepOpen, so on failure the palette closes and relies on a toast to communicate the error—which seems acceptable. Now I'm turning to check ChatView's toast and refresh behavior. I want to verify there's no CSS inconsistency introduced, and I'm now examining whether the TeleportOutButton's icon-xs size visually matches neighboring header controls like ProjectScriptsControl, which use fixed width classes rather than the icon-xs preset—so I need to check the Button size definitions to confirm they align in height and width.

29. browse_code — apps/web/src/components/ui/button.tsx

💭 The size mappings look consistent with neighboring elements' heights. I want to double check the GitActionsControl header button size for consistency, and also verify what the data-toolbar-control themed CSS expects around those line ranges since earlier grep results didn't quite match what I saw in the file view.

30. git_tools — data-toolbar-control

💭 I'm checking whether the TeleportOutButton sits within the data-chat-header ancestor so it inherits the same theming as ProjectScriptsControl, which it appears to do. Now I'm looking at the hover styling rule that applies a background color to toolbar controls. For the busy/disabled state, TeleportOutButton stays hoverable even with aria-disabled and cursor-not-allowed, matching the existing pattern seen in GitActionsControl's disabled button around line 1711 — I want to double-check that button also carries the data-toolbar-control attribute to confirm consistency.

31. browse_code — apps/web/src/components/GitActionsControl.tsx

💭 I'm noting that the explicit type="button" on the Button inside TooltipTrigger's render is harmless since Base UI merges props, and other call sites like ProjectScriptsControl skip it without issue—just a minor inconsistency, not a real bug. Now I'm checking the icon's conditional className for the teleported-out state. The class doesn't accidentally match the base CSS's substring selectors for sizing or text-color overrides, so the icon still inherits the correct control color and default size regardless of state. Turning to the ChatComposer's collapsed mobile row, I want to verify whether isSendDisabled was properly folded into collapsedComposerPrimaryActionDisabled for the expand/send button when teleport-locked, so I'm going to look at the surrounding lines where that condition was modified.

32. browse_code — apps/web/src/components/chat/ChatComposer.tsx

💭 I'm realizing insertComposerTextAtEnd now blocks on any send-disabled state, not just teleport — including attachment upload errors or externally-set reasons like "Messages loading". That means dragging a file into the composer or other insertions could silently fail whenever the composer is disabled for these broader reasons. Checking the diff, I confirm isSendDisabled || is a newly added line at line 2803 in this PR, inside insertComposerTextAtEnd. It seems the intent was to block insertion during teleport-lock, but this condition is broader than that and may reintroduce the same class of bug fixed earlier at the paste/addToChat path via canMutateTeleportDraft. isSendDisabled gets set true for unrelated reasons too — image upload in progress, failed attachments, feedback sending, or messages still loading — so dragging a file mention into the composer during any of those states now fails with an error toast, mirroring the earlier inconsistency I flagged. I want to check the other callers of insertComposerTextAtEnd to see how widespread this impact is.

33. git_tools — insertComposerTextAtEnd

💭 Grep treats the file as binary because of a null byte, so I'll look for callers directly instead—checking drag handlers that use insertMentionAtEnd and the composer ref handle, then viewing the surrounding lines around 2830-2990 to find insertComposerTextAtEnd usage.

34. browse_code — apps/web/src/components/chat/ChatComposer.tsx

💭 I'm tracing where insertTextAtEnd gets exposed on the composer handle, likely for external callers like file-tree mentions, and checking consumers of it. I also notice the useImperativeHandle dependency array is missing isSendDisabled/sendDisabledReason, even though the closure now seems to depend on them. Given the existing pattern closes over several state values that are all correctly listed as deps (isConnecting, isComposerApprovalState, pendingUserInputs.length, etc.), the newly added isSendDisabled read breaks that convention since it's not in the deps array — this looks like a genuine stale-closure bug introduced by this change. But I need to check whether isSendDisabled implicitly tracks with activeThread, which is a dep — since teleport-lock state changes likely coincide with the activeThread object changing, which would force the handle to rebuild anyway. Given the ambiguity, I'll keep my focus on the over-broad isSendDisabled gate rather than staleness, and note that the teleport-specific predicate used elsewhere in this PR is ref-based so it avoids this deps issue. Let me trace through the actual consumers of insertTextAtEnd to ground the impact in something concrete.

35. git_tools — insertTextAtEnd

💭 I'm tracing the two confirmed consumers of this function: the terminal-link-to-draft insertion, and the type-to-focus behavior that inserts a typed character into the composer. I'm now considering whether the new isSendDisabled guard could interfere with that type-to-focus path when an image upload or feedback upload is in progress. Actually, checking applyPromptReplacement more closely — since setPrompt already refuses under teleport lock, an explicit guard using the ref-based canMutateTeleportDraft check is better than isTeleportComposerLocked, since it's already in the imperative-handle deps and avoids stale-closure issues. I'm confirming this is line 2803 in the diff, distinct from the earlier addToChat guard comment around line 2668, so it's not a duplicate flag.

36. browse_code — apps/web/src/components/chat/ChatComposer.tsx

💭 The author's own comment confirms the intent: other send-disable states like pending or failed uploads shouldn't block a second paste, which means the isSendDisabled gate at line 2803 directly contradicts that stated design. I'll draft an inline review suggestion replacing that gate with the teleport-specific mutation check, matching the existing indentation. Scanning the rest of the changed files for anything else worth flagging — the composer disabled-reason ordering, the teleport state module, and the pure logic helpers all look fine, and the conflict banner wiring checks out too. I'm confident in the single finding, and I'll fold in the stale-dependency issue for the imperative handle since the same fix (using the stable canMutateTeleportDraft function) resolves both problems together. I'll submit the review as a comment-level submission since the tool doesn't need an explicit event type, then wrap up with a failure-state summary reflecting the one finding.

37. modify_pr — One finding: a composer insertion guard was widened to every sendDisabledReason, which blocks ordinary draft input (file-tree mentions, terminal links, type-to-focus) while an image upload is in flight or the thread is still loading. Details inline.


Agent Credits: 1014 credits