Skip to content

fix(shortcuts): clear modifier-hint badges when main consumes a Cmd/Ctrl chord#1412

Open
brennanb2025 wants to merge 3 commits intomainfrom
brennanb2025/cmd-in-tui
Open

fix(shortcuts): clear modifier-hint badges when main consumes a Cmd/Ctrl chord#1412
brennanb2025 wants to merge 3 commits intomainfrom
brennanb2025/cmd-in-tui

Conversation

@brennanb2025
Copy link
Copy Markdown
Contributor

@brennanb2025 brennanb2025 commented May 4, 2026

Summary

Holding Cmd over a terminal pane and pressing +/- left the Cmd+N worktree number badges stuck on-screen even after Cmd was released. Same shape applies to any main-intercepted chord — the renderer's useModifierHint hook never saw the chord's non-modifier keydown, and macOS frequently drops the subsequent Cmd keyup after a prevented chord.

Approach

Rather than scatter dispatchClearModifierHints() across every main-forwarded IPC handler (the pre-existing pattern, which already had a gap in the Cmd+N early-return branch and was fragile-by-design), introduce a single authoritative signal:

  • New IPC ui:shortcutConsumed — main emits once per intercepted chord.
  • Emit points: createMainWindow.ts before-input-event after preventDefault() (covers all 8 action types in the allowlist in one place), zoom-changed fallback, all CmdOrCtrl-accelerated menu callbacks (onOpenSettings, onZoomIn/Out/Reset, onToggleLeft/RightSidebar, Export as PDF).
  • Single subscriber in useIpcEvents dispatches CLEAR_MODIFIER_HINTS_EVENT.
  • Removed all 9 scattered dispatchClearModifierHints() calls from forwarded-IPC handlers — they're now redundant.

Net result: main is the sole authority on "I consumed a chord"; the renderer reacts in one place. Any future main-intercepted shortcut gets hint-clearing automatically.

Files

  • New: src/main/window/emit-consumed-shortcut.ts — 7-line helper documenting why the signal exists.
  • src/main/window/createMainWindow.ts — emit after preventDefault() and in zoom-changed.
  • src/main/index.ts — emit in menu-accelerator callbacks.
  • src/main/menu/register-app-menu.ts — emit in Export as PDF click.
  • src/preload/index.ts + src/preload/api-types.ts — expose onShortcutConsumed.
  • src/renderer/src/hooks/useIpcEvents.ts — single subscriber; 9 scattered dispatches removed.

Tests

  • Rewrote useIpcEvents.test.ts "shortcut hint clearing" suite to verify the new single-subscriber architecture (positive: onShortcutConsumed dispatches clear; regression guard: individual handlers no longer dispatch).
  • Updated createMainWindow.test.ts send-count/index assertions for the paired ui:shortcutConsumed send.
  • tc:web + tc:node clean. Relevant test files pass (38/38 across the four affected suites).

Manual test plan

  1. Open a worktree with ≥ 2 visible worktrees in the sidebar.
  2. Focus a terminal pane. Hold Cmd for 750 ms — number badges appear on worktree cards.
  3. Press - while holding Cmd — font zooms out, badges still visible (correct; Cmd still held).
  4. Release Cmd — badges disappear immediately. (Previously they stuck until clicking/blurring.)
  5. Repeat with +, 0, ,, J, P, 1–9, Alt+Arrow, menu-clicked Settings/Zoom items. All clear correctly on Cmd release.

Made with Orca 🐋

brennanb2025 and others added 3 commits May 4, 2026 15:11
…trl chord

Holding Cmd over a terminal pane and pressing +/- left the Cmd+N number
badges stuck on worktrees even after releasing Cmd. Root cause: main's
before-input-event preventDefault()s the zoom chord, so the '-'/'+'
keydown never reaches the renderer and useModifierHint's "other key
while modifier held" branch can't self-clear. The subsequent Cmd keyup
is also frequently dropped on macOS after a prevented chord.

Instead of scattering dispatchClearModifierHints() calls across every
forwarded-IPC handler (fragile; one gap already existed in the Cmd+N
early-return path), introduce a single 'ui:shortcutConsumed' IPC. Main
emits it once per intercepted chord (before-input-event, zoom-changed,
native menu accelerators). One renderer subscriber clears the overlay.
All 9 previously scattered dispatches are now redundant and removed.

Tests updated to verify the single-subscriber architecture, plus a
regression guard that individual handlers don't re-introduce the old
scattered pattern.

Co-authored-by: Orca <help@stably.ai>
… forwards

Without this, the stuck-badge regression the PR fixes still reproduces
when focus is inside a webview guest: browser-guest-ui.ts forwards 5+
shortcut IPCs (worktreeHistoryNavigate, toggleWorktreePalette,
openQuickOpen, openNewWorkspace, jumpToWorktreeIndex, plus the
browser-specific Cmd+T/W/L/R/F, Cmd+Shift+B/[/], Cmd+Alt+[/],
Ctrl+PageUp/Down) but never called emitConsumedShortcut.

Consolidate emit via an IIFE-thunk so every matched branch emits once,
before the action send, honoring the main->renderer ordering contract.
Also tighten createMainWindow.ts zoom-changed ordering and update the
JSDoc on emitConsumedShortcut to accurately describe trigger conditions
and FIFO delivery.

Co-authored-by: Orca <help@stably.ai>
…edShortcut

Every forwarded browser-guest chord now sends ui:shortcutConsumed
immediately before the action IPC, so the 9-call sequence is now 18
calls alternating shortcutConsumed/action.

Co-authored-by: Orca <help@stably.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant