Skip to content

fix(web): skip thread shortcut when settlement is unavailable

MacroscopeApp / Macroscope - UI Consistency failed Aug 24, 2026 in 3m 55s

UI Consistency: 1 issue found

  • apps/web/src/routes/_chat.tsx: the new thread.settle shortcut derives the thread's settled state from threadChangeRequestSnapshotsAtom (the sidebar partition's rule), while the settle affordances for the same thread (ChatView's parked-thread banner and the chat-header thread action menu) derive it from live VCS status via resolveDisplayedThreadPr. Because only Sidebar rows write that snapshot map (LegacySidebar never does, and unmounted rows don't), a merged-PR auto-settled thread reads as not settled in the shortcut, so mod+shift+s settles instead of un-settling while the on-screen banner says the thread is settled.

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.

Methodology

  • Reviewed the full merge-base..head diff; only apps/web/src/routes/_chat.tsx falls in the check's include patterns with behavioral changes (the remaining web change is a test file, plus shared/contracts keybinding registration and docs).
  • No shared-primitive, Tailwind, theme, CSS, or scroll/virtualization surface is touched by this PR, so those rule sets produced no findings.
  • Cross-checked the other settle surfaces for the same target thread: apps/web/src/components/ChatView.tsx (activeThreadChangeRequest via resolveDisplayedThreadPr, activeThreadSettled, parked-thread banner with the Un-settle button), apps/web/src/components/chat/ChatHeader.tsx -> apps/web/src/hooks/useThreadActionMenu.ts (isSettled gated by supports.settlement), and apps/web/src/components/Sidebar.tsx (partition rule at the snapshot map).
  • Verified snapshot ownership: setThreadChangeRequestSnapshot is called only from Sidebar's thread rows; LegacySidebar has no writer, and _chat.tsx itself tracks legacySidebarEnabled.
  • Confirmed the capability gate flagged in the previous run is now present (readEnvironmentSupportsSettlement at line 127), so that finding was not repeated.
  • Precedent used for the suggested fix is in the same file: the preview.toggle handler dispatches into ChatView through the preview action bus specifically to avoid duplicating an arbitration rule.

🔍 Agent activity — 28 tool calls

💭 Checking the diff, I see the earlier concern about a missing environment-support check is actually addressed — the guard clause is already there. Now I want to look at the changed file more closely to confirm this stays consistent with how other settle surfaces like the toast handle things. I should check _chat.tsx and useThreadActionMenu/Sidebar to see how they wire this up.

1. git_tools — stackedThreadToast

2. git_tools — effectiveSettled

💭 I notice _chat.tsx around line 189 references effectiveSettled with the active thread shell, and there's an added handler near line 123 in the diff. Let me open the file to see its current state.

3. browse_code — apps/web/src/routes/_chat.tsx

💭 I want to compare this with other settle surfaces like useThreadActionMenu.ts and Sidebar.tsx, checking how they handle the settle behavior, canSettle gating, success toasts, and whether they rely on a useNowMinute hook instead of manually computing the current time. I'll look at useThreadActionMenu.ts around line 100-260 to see this.

4. browse_code — apps/web/src/hooks/useThreadActionMenu.ts

5. browse_code — apps/web/src/hooks/useNowMinute.ts

💭 I should also check whether the menu gating logic in buildThreadActionMenuItems blocks settle when a thread is running, so I'm looking at settleThread in useThreadActions.ts to see how it determines eligibility.

6. browse_code — apps/web/src/hooks/useThreadActions.ts

7. browse_code — apps/web/src/components/threadActionMenu.logic.ts

💭 When settle is blocked, settleThread returns a ThreadSettleBlockedError and the shortcut path shows "Failed to settle thread" with that error's message—matching how useThreadActionMenu's settle case handles it via failureToast. I want to check the actual text of that blocked-error message and see how the Sidebar surface handles this same case.

8. git_tools — ThreadSettleBlockedError|ThreadSettlementUnsupportedError

💭 I want to check consistency here: the shortcut derives settled using effectiveSettled with the changeRequest from threadChangeRequestSnapshotsAtom, which should match how ChatView.tsx computes the activeThreadShell settled state around line 4331. Let me look at that section of ChatView to see if they share a common derivation helper.

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

💭 I'm noticing the new shortcut derives the change request by checking whether the snapshot's branch matches the thread's branch, rather than reusing the existing resolveDisplayedThreadPr logic that also accounts for gitStatus and terminal retention on branch mismatch. I want to compare this against how the sidebar computes the same value to see if they're consistent.

10. git_tools — resolveDisplayedThreadPr

11. browse_code — apps/web/src/components/Sidebar.tsx

💭 The new shortcut's changeRequest derivation lines up with the sidebar's partition logic, which is the authoritative classification for the shelf. I want to check whether settling emits a toast with undo capability, and whether the sidebar or other surfaces navigate away after settling, plus whether the shortcut mirrors that same behavior. useThreadActions confirms settle stays silent since it's a high-frequency lifecycle action — no toast needed there. Now I'm wondering whether the command palette also has a settle entry, and whether keybindings typically surface as hint labels next to commands like preview.toggle or chat.new, so I should check how the repo maps keybindings to command palette shortcut displays to keep this new command consistent.

12. git_tools — preview.toggle

13. git_tools — thread.settle|thread.pin|thread.snooze

💭 I'm also wondering whether keybinding shortcuts get displayed anywhere with human-readable labels, like in a Settings > Keybindings screen, since a missing label for the new command would show the raw command id instead of a friendly name. I want to search for a command label/description mapping used by that UI.

14. git_tools — chat.newLocal

💭 I'm checking shortcutLabelForCommand for shortcut hints, and looking into KeybindingsSettings.logic.ts to see if there's a label/description map keyed by command that would need a new "thread.settle" entry.

15. browse_code — apps/web/src/components/settings/Keybindings*

16. git_tools — shortcutLabelForCommand

💭 There's a test at line 374 referencing a shortcut label that wasn't in the diff I saw, which is odd since the diff only showed two hunks around lines 145 and 195. I should check the actual file content around lines 360-380 to see if this test already existed or if the diff was condensed.

17. git_tools — f035a0f..5d19de1

💭 That test case referencing "thread.settle" at line 374 isn't part of the diff, so I want to look at lines 366-378 in the head version to understand the surrounding context.

18. browse_code — apps/web/src/keybindings.test.ts

💭 The line numbers from grep don't match what's actually at that location in the file, so I suspect grep ran against a different revision than the one I'm viewing. I'll set that aside and instead check whether there's a UI surface that should display the shortcut hint — looking at whether ContextMenuItem supports an accelerator field, and whether CommandPaletteResults would need a shortcutCommand: "thread.settle" entry for consistency with other actions.

19. git_tools — shortcutCommand

20. git_tools — [Ss]ettle

💭 The command palette doesn't have a settle action, so no hint needed there. I'm now checking whether the chat header shows a settle button or pill with tooltip that might display the shortcut label, looking at ChatHeader and the composer banner for "Settle" text.

21. git_tools — Settle|Un-settle|un-settle

💭 I'm noticing ChatView already has its own settle/un-settle logic for the active thread, tied to an activeThreadSettled memo that mirrors the sidebar's partition rule using the same shell and cap. This raises the question of whether the new shortcut's snapshot-derivation duplicates that classification unnecessarily, and which rule should actually be authoritative for the active thread.

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

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

💭 I'm now checking whether the shortcut's toggle direction stays consistent with other surfaces showing the same thread—specifically the parked-thread banner and the chat header menu, both of which derive their changeRequest from gitStatus-aware logic rather than the sidebar-partition rule the new shortcut uses. I want to see if these two approaches could produce different results, so I'm going to look at how resolveDisplayedThreadPr is implemented.

24. browse_code — apps/web/src/components/ThreadStatusIndicators.tsx

💭 The snapshot map is populated by Sidebar/ChatView from live gitStatus, so it's effectively the live PR for the matching branch, and the sidebar partition rule applies exactly that logic to decide whether a thread's PR shows or is nulled out. The new code replicates this correctly, matching the surface that determines whether a thread lands in the Settled shelf. But I need to check whether the snapshot is populated only when a Sidebar row or ChatView is actually mounted. If the legacy sidebar is active or the thread row isn't rendered, the snapshot could be missing, causing the changeRequest to read as null and the auto-settle-on-merge classification to diverge from what the sidebar displays — meaning the shortcut might settle a thread the sidebar already considers settled. Since settling is idempotent that's mostly harmless, but if the user expected an un-settle action instead, this could cause unexpected behavior; I want to check whether ChatView writes the snapshot for the active thread.

25. git_tools — setThreadChangeRequestSnapshot

💭 Only the Sidebar rows actually write snapshots per-row, while ChatView just reads from the atom without writing. That means the snapshot for the active thread only exists if its sidebar row is rendered, which could be a problem since collapsed or paged-out rows might not mount, leaving the snapshot missing. With legacy sidebar enabled, snapshots may never get written at all, but ChatView's banner still works fine since it reads gitStatus directly rather than depending on the snapshot map. The real inconsistency: for a thread whose PR auto-settles on merge but whose sidebar row isn't rendered (legacy sidebar, collapsed shelf, or paging), the shortcut computes settled=false while the banner shows "This thread is settled" with an Un-settle option — so pressing mod+shift+S would settle it again instead of un-settling, a genuine divergence from what's visibly on screen when the shortcut fires. I'm weighing whether this counts as an in-scope finding, since the check is framed around shared components, Tailwind ownership, and visual/layout consistency rather than behavioral logic — this PR doesn't touch styling or primitives at all, so the toggle-direction divergence is really a behavior-consistency risk between two on-screen surfaces rather than a component-system issue, and I'm unsure it falls within the stated remit even though "preserve interaction" language could stretch to cover it. I'm tracing whether ChatView's route thread row is reliably rendered when the settled shelf is collapsed, since the divergence claim only holds if the snapshot mechanism is actually present for that thread in the sidebar. Checking whether LegacySidebar writes snapshots at all -- it doesn't seem to call setThreadChangeRequestSnapshot, so with legacy sidebar enabled, threadChangeRequestSnapshotsAtom stays empty and the shortcut can't detect auto-settle-on-merge. That means in legacy mode the shortcut would incorrectly settle a thread that ChatView's banner already reports as settled, since the banner reads gitStatus directly rather than relying on the snapshot atom. I'm also wondering about stale snapshots when a sidebar row unmounts (e.g. sidebar collapsed on mobile) -- unmounting probably doesn't clear the snapshot, leaving it stale but likely harmless. I want to check whether ChatHeader's useThreadActionMenu receives the same gitStatus-derived changeRequest from ChatView, to confirm there's a consistent precedent for how changeRequest flows into these components.

26. git_tools — changeRequest={|changeRequest,

💭 I'm confirming that the chat-header thread action menu relies on ChatView's live-derived activeThreadChangeRequest via resolveDisplayedThreadPr, whereas the new shortcut reads the raw threadChangeRequestSnapshotsAtom map directly through the sidebar-partition rule—these are different data sources that could diverge. If legacy sidebar is disabled or the row isn't mounted, the snapshot goes missing, causing the shortcut to think a thread is unsettled while the header menu and composer banner both indicate it's settled—so pressing the shortcut would explicitly settle a thread that's already flagged as settled, inverting the expected toggle behavior. The cleanest fix is to have the shortcut derive its settled state from the same authoritative source ChatView uses, rather than re-deriving it from the sidebar snapshot map, and I'm now checking whether _chat.tsx has any existing bus or hook (like dispatchPreviewAction) to reach into ChatView's classification logic. I want to verify that previewOpen's comment about routing through ChatView's onTogglePreview action bus really does mirror this same pattern—there's precedent in this exact file for dispatching through a bus instead of duplicating a rule ChatView already owns. That gives me a solid, in-file consistency argument for flagging this as an inline comment on lines 132-144 of _chat.tsx. I also want to confirm my assumption that legacy sidebar mode truly lacks the snapshot writer by searching for changeRequestSnapshot and threadChangeRequestSnapshotsAtom usage there.

27. git_tools — ChangeRequestSnapshot|threadChangeRequestSnapshotsAtom

💭 I've confirmed that only Sidebar rows write the snapshot map, not LegacySidebar, so with the legacy sidebar enabled the shortcut always sees changeRequest as null—meaning an auto-settled merged PR thread reads as unsettled, and mod+shift+S settles it explicitly instead of un-settling it. Meanwhile ChatView's banner and header menu, which derive from gitStatus, both indicate the thread is already settled and offer an "Un-settle" action, creating a real inconsistency between the two UI paths. The fix should avoid re-deriving classification separately—instead it should resolve the change request the same way ChatView/ChatHeader do via resolveDisplayedThreadPr against live VCS status, or route the toggle through ChatView's dispatch so there's one authoritative source of truth. Since this spans multiple hunks and files, I can't offer an inline diff suggestion, so I'll flag it as a comment on line 139 (effectiveSettled) with start_line 132 in the added _chat.tsx lines, distinct from my earlier comment about capability gating.

28. modify_pr — One finding: the new thread.settle shortcut re-derives the active thread's settled state from the sidebar's snapshot map, so its toggle direction can disagree with the settle affordances rendered for the same thread.


Agent Credits: 204 credits