Skip to content

fix(web): pass terminal focus to thread hints

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

UI Consistency: 1 issue found

apps/web/src/components/Sidebar.tsx — The new thread-jump hint call passes only context: { terminalFocus: isTerminalFocused() }, omitting terminalOpen and modelPickerOpen that the component's own keydown dispatcher (lines 3261-3268) supplies. Default keybindings bind modelPicker.jump.N to mod+N with when modelPickerOpen, so with the model picker open the hint layer still advertises ⌘1..9 badges while those keys select a model — the same "hint promises a shortcut that does something else" defect this PR fixes for the terminal. LegacySidebar.tsx avoids the divergence by feeding one shared sidebarShortcutContext to both paths. Inline comment posted with a self-contained suggested fix.

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.

Verification performed:

  • Traced shouldShowThreadJumpHintsForModifiers / findEffectiveShortcutForCommand / resolveContext in apps/web/src/keybindings.ts to confirm that omitted context keys default to false, so when-scoped bindings that shadow mod+1..9 are ignored during hint computation.
  • Confirmed shipped defaults in packages/shared/src/keybindings.ts register modelPicker.jump.N on mod+${N} with when: "modelPickerOpen", and that these entries come after the thread.jump.* defaults, so they win the conflict claim only when modelPickerOpen is true.
  • Compared both call sites: Sidebar.tsx keydown handler (full context) vs. new hint call (partial context); LegacySidebar.tsx builds sidebarShortcutContext once with terminalFocus/terminalOpen/modelPickerOpen and reuses it.
  • Checked reactivity of the new render-time isTerminalFocused() read: useShortcutModifierState installs capture-phase window keydown/keyup listeners, so focus state is re-read on each modifier transition (including while xterm.js has focus). No finding there.
  • The new test in apps/web/src/keybindings.test.ts correctly covers both terminal-focus branches; no styling, Tailwind, CSS-ownership, scroll/mask, or shared-primitive changes are present in this diff.

🔍 Agent activity — 1 tool call

💭 I need to verify that isTerminalFocused is properly defined and imported in both sidebar components, and confirm the context option structure is correct. I need to examine the keybindings file around lines 100-330 and how it relates to the sidebar, then check that LegacySidebar line where terminal focus is being set inside what might be a memoized object—if it's being called during render without triggering a re-render when focus changes, that could explain why the hints aren't updating properly. This is a behavioral change affecting keyboard handling, not UI styling, so it falls outside the scope of the UI consistency check even though the modified files are TypeScript/TSX. The real issue is that isTerminalFocused() gets called during render without reactivity, which means if terminal focus changes between renders, the hint visibility could become stale and out of sync. However, since hints only display when a modifier key is held (which triggers a re-render through the shortcutModifiers state), this might not be a practical problem. I need to trace through Sidebar.tsx around line 3300 to understand how the focus state is computed and whether focus changes actually trigger re-renders. I should also check whether sidebarShortcutContext is used elsewhere in LegacySidebar beyond just the hints at line 3414—particularly in functions like buildThreadJumpLabelMap or command resolution with when clauses—since the change from hardcoded terminalFocus: false to the actual focus value could affect behavior in those places too. I'm noticing that the render-time context in Sidebar.tsx now only includes terminalFocus, while terminalOpen and modelPickerOpen are missing—even though they're present in the keydown handler context. This matters because findEffectiveShortcutForCommand uses context to evaluate when clauses, so if thread.jump bindings have conditions like !terminalOpen, the hints might display incorrectly when the terminal is actually open. Though this is technically a pre-existing issue since context wasn't being passed at all before the diff. But LegacySidebar does pass the full context including terminalOpen, so there's now an inconsistency between the two versions. The PR modified that exact line and could have included terminalOpen—this feels like a potential regression. I should check whether the default keybindings for thread.jump actually have when clauses that reference terminalOpen. Looking at whether there's an actual regression here: the main issue is that isTerminalFocused() gets called during render but isn't reactive. The shortcutModifiers state from useShortcutModifierState() does trigger re-renders when modifier keys change, so hints correctly hide when Cmd is held while chat is focused. But if the user clicks into the terminal while still holding Cmd, there's no re-render to update the hints visibility since the focus change itself doesn't trigger a subscription. There's also a potential mismatch: Sidebar v2 drops terminalOpen and modelPickerOpen from the hint context, but the keydown handler and LegacySidebar include them. If any jump bindings have when clauses referencing terminalOpen, the hints and actual behavior would diverge. I need to check the terminalFocus implementation and the default keybindings to see if this is actually a problem. I'm checking whether buildThreadJumpLabelMap in LegacySidebar needs to account for terminalFocus in addition to terminalOpen, since hints visibility is now controlled by that flag. The threadJumpLabelByKey function currently uses buildThreadJumpLabelMap with only terminalOpen, which might be an inconsistency I need to address in the Sidebar component. The check is really about behavioral constraints and primitives rather than UI consistency, so this diff doesn't fit the intended scope. I'm considering whether the actual bug is that shouldShowThreadJumpHintsForModifiers hardcodes the terminalFocus flag in a generic keybinding helper instead of letting when clauses handle it, though the comment does explain the reasoning. The context defaults and wrapper functions seem fine, so I'm looking at the LegacySidebar change around line 3400 as another possible issue. Actually, I think I've found it — in LegacySidebar, terminalFocus was previously set to false during render-time context, which affects how findEffectiveShortcutForCommand resolves bindings for labels. If terminalFocus were true, terminal-scoped bindings would become active and could shadow the thread.jump shortcut, resulting in no hint. This change might be the intended bug, but I need to check if the original code had a comment explaining why terminalFocus was false there. The sidebarShortcutContext in LegacySidebar is only referenced in a couple of places for terminal state checks, so the earlier change to terminalFocus is actually necessary since the helper function now reads that value from context to gate the behavior properly. There's a potential reactivity issue though: isTerminalFocused() reads document.activeElement during render, which is impure and could behave inconsistently with React 18's concurrent rendering. More importantly, if the user holds Cmd while the terminal closes and focus shifts back to the composer, the hints would stay hidden until the modifier state changes. I should check if there's already a reactive hook like useTerminalFocus or a focus store that other components use—if so, calling isTerminalFocused() during render would be inconsistent with the codebase pattern. I need to examine how shortcutModifierState.ts handles state updates—specifically whether it's only responding to keyboard events or if it also accounts for blur and focus scenarios. The keyboard listeners are attached to the window with capture enabled, so they fire regardless of where focus is, and each modifier press checks the terminal focus state fresh. But there's a mismatch: the Sidebar's hint context only passes terminalFocus, while the keydown handler also includes terminalOpen and modelPickerOpen, creating an inconsistency between what determines hint eligibility and what actually gets dispatched. This means hints can advertise shortcuts that are shadowed by custom bindings scoped to conditions like when terminalOpen, since the hint layer computes with incomplete context and doesn't see those conditions. The fix is to extract a shared context builder that both the keydown handler and hint computation use, ensuring they evaluate the same set of conditions when determining which shortcuts are available. But there's also a concrete issue with modelPickerOpen: if the default keybindings include model picker jump commands on mod+1..9 with when modelPickerOpen, then the sidebar hints would still show thread jump hints even when the model picker is open and those shortcuts are actually shadowed. LegacySidebar already accounts for this by including modelPickerOpen in its context, so I should check whether the defaults actually have these bindings. I need to check the server defaults file to verify the keybinding configuration for the model picker, specifically the modelPicker.jump.N command on mod+N and its conditional clause. So the issue is that the default keybindings for the model picker are registered after the thread ones in the array, and when iterating from the end to find effective shortcuts, mod+1 gets claimed by modelPicker.jump.1 when the picker is open, leaving thread.jump.1 without an effective binding and hiding its hints. The fix involved updating the Sidebar component to pass the terminal focus context alongside the platform information. However, the patch still omits modelPickerOpen from that context object, so even with the model picker open and Cmd held, the sidebar still shows thread jump hints when it shouldn't — this is the exact bug pattern being addressed. Before the diff, the context was empty too, so this was pre-existing, but the patch directly modifies that line and introduces partial context while the sibling keydown handler builds the full context correctly. This is a consistency issue worth flagging: the hint layer and dispatch layer are diverging, whereas LegacySidebar handles it properly. I should also check whether routeTerminalOpen creates any conflicting thread.jump bindings when the terminal is open but not focused. During a React render triggered by something else while modifiers are held, the value could flip, but that's actually the desired freshness. I'll add an inline comment on the Sidebar.tsx suggestion to include terminalOpen and modelPickerOpen in the context object, matching what the dispatch context and LegacySidebar use — both imports are already available in the file.

1. modify_pr — One finding: the new jump-hint context in Sidebar.tsx is partial, so hints can still advertise ⌘1..9 while the default modelPicker.jump.* bindings own those keys. See the inline comment.


Agent Credits: 96 credits