PacketBenchDocs

Keyboard shortcuts#

PacketBench binds keys on several surfaces at once: an app-shell listener on window, a per-view listener, a per-pane listener, and — when you opt in — three accelerators registered with the operating system. Most of the time only one of them can plausibly want a keypress. The interesting cases are the ones where two can, and those are decided by explicit checks in the code rather than by listener order.

This page lists every binding PacketBench ships, grouped by the surface that owns it, and then states the precedence rules that decide which surface consumes a key when more than one is listening.

Ordering. Within each table, rows appear in source order — the order the owning handler tests them — so the table reads in the same sequence the code executes. The view-switch table follows declaration order in src/lib/routeRegistry.ts. Nothing here is alphabetical.

Global#

Bound on window at the app shell, so they are live on every view. src/hooks/useGlobalShortcuts.ts:29-88 owns the first four; src/hooks/useAgentTabHoists.ts:37-70 and src/hooks/useSideChatHotkey.ts:11-24 own the rest, each in its own listener.

Keys Action Yields to Source
Ctrl+K Toggle the command palette Terminals and every editable control — but only while the palette is closed. While it is open, Ctrl+K always closes it src/hooks/useGlobalShortcuts.ts:39-44
Esc Close the command palette Nothing. Deliberately unguarded — the palette owns focus whenever it is open src/hooks/useGlobalShortcuts.ts:48-52
Esc Cancel an in-flight dictation recording Terminals only. Editable controls keep the cancel, because they are the usual dictation target src/hooks/useGlobalShortcuts.ts:57-66
Ctrl+Shift+chord Switch view — see the next table Terminals, and any open ui/Modal. Not text fields: they do not bind Ctrl+Shift chords src/hooks/useGlobalShortcuts.ts:70-83
Ctrl/Cmd+N On Agents, clear the active conversation and expose the launch composer. Everywhere else, create a new auto-named workspace Inputs, textareas and contenteditable regions src/hooks/useAgentTabHoists.ts:38-53
Ctrl/Cmd+Shift+O Cycle the global transcript view mode: Summary → Normal → Verbose Inputs, textareas and contenteditable regions src/hooks/useAgentTabHoists.ts:61-69
Ctrl/Cmd+; Toggle the side-chat overlay. Ctrl/Cmd+: is accepted too, for layouts that report the shifted glyph Nothing — this listener has no focus guard src/hooks/useSideChatHotkey.ts:12-20
Note

The transcript-mode cycler used to sit on Ctrl+Shift+V. That chord is also a plausible push-to-talk binding, and both handlers fired on one press — so the transcript view mode flipped every time dictation started. It was moved to Ctrl+Shift+O, which is otherwise unbound (src/hooks/useAgentTabHoists.ts:26-30).

View switching#

Every view-switch chord is declared once, in the route registry, and every consumer — the left rail, the status strip, the command palette — derives its label from the same row. Adding a shell route without a registry entry is a compile error (src/lib/routeRegistry.ts:104-110).

Keys Physical key matched Goes to Source
Ctrl+Shift+W KeyW Workspace src/lib/routeRegistry.ts:125
Ctrl+Shift+1 Digit1 Agents src/lib/routeRegistry.ts:138
Ctrl+Shift+6 Digit6 PacketCode (ACP) src/lib/routeRegistry.ts:152
Ctrl+Shift+2 Digit2 Flight Deck src/lib/routeRegistry.ts:165
Ctrl+Shift+3 Digit3 Issues src/lib/routeRegistry.ts:180
Ctrl+Shift+4 Digit4 Session History src/lib/routeRegistry.ts:221
Ctrl+Shift+D KeyD Dictation src/lib/routeRegistry.ts:238
Ctrl+Shift+5 Digit5 Settings src/lib/routeRegistry.ts:259

Three shell routes carry no chord and are reachable only from the rail or the palette: Memory (src/lib/routeRegistry.ts:182), Git Hosts (:197) and the Welcome screen (:106). That is a deliberate absence, not an oversight — the number row ran out at six and the registry makes an unbound route a one-line row rather than a missing case.

Workspace and terminal panes#

Keys Action Notes Source
Esc Exit pane zoom The lowest-priority Escape consumer in the app. Only the visible, active workspace's container listens src/components/workspace/WorkspaceMosaicContainer.tsx:98-131
y Approve a terminal approval prompt — writes y\n into the PTY Bare key, no modifier src/hooks/useApprovalShortcuts.ts:80-82
n Deny — writes n\n Bare key, no modifier src/hooks/useApprovalShortcuts.ts:83-85
Esc Abort — writes \x03 (Ctrl-C) Bare key, no modifier src/hooks/useApprovalShortcuts.ts:86-88
Ctrl/Cmd+S Save the file open in the Editor pane Bound on window per mounted EditorPane, not on the textarea src/components/editor/EditorPane.tsx:86-94
/ Resize the right dock by 8 px, or 32 px with Shift Only while the dock's splitter handle has focus src/components/layout/RightDock.tsx:198-206

The bare y / n / Esc approval keys are bound on window by every pane that is waiting, so a single press reaches all of them and preventDefault cannot stop a sibling. Rather than fight that, each handler decides whether the press is addressed to it, in this order (src/hooks/useApprovalShortcuts.ts:36-50):

Panes waiting Who answers
Exactly one That pane, focused or not
Several, one active Only the active pane
Several, none active Nobody — click a pane, or use its on-screen buttons
Warning

The ownership rule exists because the earlier version had none: every waiting pane bound the same window keys, so one y wrote y\n into every waiting agent's stdin — silently approving actions in panes nobody had looked at.

The composer#

One handler serves both composer variants — the chat composer and the launch composer (src/components/agents/composer/buildComposerKeyboardHandler.ts:51-188). Rows are in the order the handler tests them, which is also their precedence.

Keys Action Condition Source
Esc Dismiss the / slash-command popover Slash popover open buildComposerKeyboardHandler.ts:69-73
/ Move the slash highlight Slash popover open buildComposerKeyboardHandler.ts:74-83
Enter / Tab Pick the highlighted slash command, or dismiss if the list is empty Slash popover open buildComposerKeyboardHandler.ts:84-93
Esc Dismiss the @ file-mention popover Mention popover open buildComposerKeyboardHandler.ts:98-102
/ Move the mention highlight Mention popover open buildComposerKeyboardHandler.ts:103-112
Enter / Tab Insert the highlighted path, or dismiss if the list is empty Mention popover open buildComposerKeyboardHandler.ts:113-121
Recall the previous prompt, shell style Chat composer only; no popover open, no modifier, and the composer is empty or the caret is at position 0 buildComposerKeyboardHandler.ts:146-155
Walk forward through recalled prompts As above, and history recall is active buildComposerKeyboardHandler.ts:156-163
Esc Leave history recall and clear the composer As above, and history recall is active buildComposerKeyboardHandler.ts:164-170
Shift+Tab Cycle the mode chip: default → plan → manual → deny → yolo Chat composer only buildComposerKeyboardHandler.ts:173-177, cycle order at src/components/agents/composer/slashCommandHandlers.ts:25
Ctrl+Enter Send buildComposerKeyboardHandler.ts:179-183
Enter Send Without Shift. Shift+Enter falls through and inserts a newline buildComposerKeyboardHandler.ts:184-187
Tab Deliberately not handled outside the popovers, so it keeps native focus navigation buildComposerKeyboardHandler.ts:40-42

While either popover is open, Enter and Tab are always swallowed. A half-typed @query whose directory scan has not landed, or an unmatched /xyz, is never sent as a literal message by the same keystroke that was meant to complete it; an empty list dismisses the popover so the next Enter submits.

Agent approvals and review#

These listen on document, not window, and each is gated by a per-tile focus flag so only one conversation tile in a workspace mosaic answers a press.

Keys Action Arming condition Source
y Allow the head-of-queue permission once A permission is pending, the palette is closed, this tile holds keyboard scope, and focus is not in an editable control src/components/agents/chat/PendingApprovalsSection.tsx:81-102
n Deny it As above src/components/agents/chat/PendingApprovalsSection.tsx:81-102
y Apply the head-of-queue pending edit An edit is pending, no permission is pending, and this tile holds keyboard scope src/components/agents/review/ReviewBar.tsx:74-94
n Reject it As above src/components/agents/review/ReviewBar.tsx:74-94
Esc Close the expanded review surface Not while focus is in an input, textarea, select or contenteditable, and not if the press was already handled src/components/agents/review/ReviewSurface.tsx:119-132
Ctrl/Cmd+Enter Submit an edited user message While editing a transcript message src/components/agents/chat/MessageList.tsx:410-417
Esc Cancel that edit As above src/components/agents/chat/MessageList.tsx:414-416
Enter / Esc Confirm / cancel a permission prompt's inline field src/components/agents/PermissionPrompt.tsx:378-383
Esc Close the side-chat overlay Overlay open src/components/agents/SideChatOverlay.tsx:46-52
Enter Ask the side chat Without Shift src/components/agents/SideChatOverlay.tsx:63-67
F2 Rename a conversation or an adoptable session row in the agent sidebar Row focused src/components/agents/AgentSidebar.tsx:432-440, :592-598, :611-617

The edit handler stays passive while any permission is pending (PendingApprovalsSection.tsx:73, ReviewBar.tsx:72): both bind y and n, so permissions are answered first and only then do the same two keys mean apply/reject.

Dictation#

Three accelerators are registered with the operating system, and only when Settings → Integrations & Data → Dictation → Global dictation shortcuts is on. PacketBench unregisters only what it successfully registered itself; an existing OS or application binding is reported as a conflict, never taken over (src/hooks/useDictationGlobalShortcuts.ts:43-53, :147-153).

Keys Action Scope Rebindable Source
Ctrl/Cmd+Alt+Space Hold to record, release to transcribe OS-global Yes; must include a modifier src/types/dictation.ts:193
Ctrl/Cmd+Alt+R Start or stop recording OS-global Yes; must include a modifier src/types/dictation.ts:194
Ctrl/Cmd+Shift+D Open the Dictation view OS-global No — fixed src/types/dictation.ts:195
Esc Cancel and discard the recording In-app only No — fixed src/hooks/useGlobalShortcuts.ts:57-66
Ctrl+V Not a binding you press: it is the synthetic keystroke PacketBench sends to the foreground application when auto-paste is on See Dictation & analytics

All three accelerators are stored as @tauri-apps/plugin-global-shortcut strings using CommandOrControl, which resolves to Cmd on macOS and Ctrl elsewhere (src/components/views/tools/KeyboardShortcutsCard.tsx:271-277). They must all differ from each other, including the fixed open shortcut (src/types/dictation.ts:253-265).

The toggle carries a 400 ms repeat guard. Win32 RegisterHotKey delivers repeated WM_HOTKEY messages while a key is held with no intervening release, so an unguarded toggle would start a capture and stop it again on the next repeat, transcribing a fraction of a second (src/hooks/useDictationGlobalShortcuts.ts:35-41).

Modals, palettes and lists#

Keys Action Notes Source
Esc Close the top-most dialog Skipped while an unbreakable operation is in flight, when the caller opts out, mid-IME composition, or when an inner layer already handled the press src/components/ui/Modal.tsx:82-101
/ Move the command-palette selection src/components/common/CommandPalette.tsx:147-152
Enter Run the selected palette command src/components/common/CommandPalette.tsx:153-157
Esc Close a dropdown or popover Each owns its own Escape and marks the press handled src/components/ui/Dropdown.tsx:133, src/components/ui/Popover.tsx:124
/ / / Move between segmented-control options src/components/ui/SegmentedControl.tsx:48-53
Ctrl+Enter Launch a Flight from the launch dialog Ctrl only — see Gaps below src/components/flights/LaunchAsyncFlightModal.tsx:505-510
Enter Send a follow-up to an attempt Without Shift src/components/flights/AttemptTile.tsx:179-184
Enter / Space Activate a flight card Cards are role="button" src/components/views/FlightsView.tsx:582, :641
Enter / Esc Commit / cancel an inline issue field edit Assignee, title, criteria src/components/issues/IssueDetail.tsx:411-418, :585-588
Enter Add an acceptance criterion New-issue form src/components/issues/NewIssueForm.tsx:294-297
Ctrl/Cmd+R Re-run the code-quality analyser Code Quality module view only; no typing guard src/components/views/QualityView.tsx:167-171
Ctrl/Cmd+F Focus and select the quality filter box Code Quality module view only src/components/views/QualityView.tsx:172-179

Rebinding#

Settings → Integrations & Data → Keyboard Shortcuts owns the global dictation accelerators and nothing else. Every other binding on this page is in-app and is not editable.

Control Behaviour Source
Pencil icon Enters capture. The row focuses itself and swallows keystrokes until a valid combination arrives src/components/views/tools/KeyboardShortcutsCard.tsx:156-195
Modifier-only press Ignored — capture waits for a real key KeyboardShortcutsCard.tsx:163-170
Esc during capture Cancels, leaving the binding unchanged KeyboardShortcutsCard.tsx:176-179
A bare key with no modifier Rejected with "Shortcut must include at least one modifier (Ctrl/Alt/Shift/Cmd)." — otherwise a could become push-to-talk KeyboardShortcutsCard.tsx:182-186
Clicking away Cancels capture on blur KeyboardShortcutsCard.tsx:206-209
Rotate icon Appears only when the binding differs from its default; restores it KeyboardShortcutsCard.tsx:235-244

Capture listens on the row's own DOM node in the capture phase, not on window, so a simultaneous interaction elsewhere in Settings cannot rebind the shortcut by accident (KeyboardShortcutsCard.tsx:153-155).

Precedence#

Four mechanisms decide who consumes a key. They are listed here in the order they take effect.

The handled flag unwinds one layer at a time#

Every window-level handler returns early on e.defaultPrevented (src/hooks/useGlobalShortcuts.ts:34, src/components/ui/Modal.tsx:92, src/components/workspace/WorkspaceMosaicContainer.tsx:107, src/hooks/useApprovalShortcuts.ts:69). An inner surface that has already acted — xterm, an open dropdown, an inline field editor, the palette's own list — marks the press handled, and one press unwinds exactly one layer.

The flag alone is not enough, and the code says why. Listeners fire in registration order, and the zoom-exit listener registers when a pane is zoomed, which is before any dialog opened afterwards registers its own. Zoom therefore ran first, called preventDefault, and Modal then bailed on exactly the flag zoom had just set: zooming a pane and opening any dialog meant Esc closed the zoom and left the dialog open (src/components/workspace/WorkspaceMosaicContainer.tsx:88-97). The explicit checks in the next two sections are what actually order the layers.

Focus classification decides who yields#

src/lib/keyboardTarget.ts is the single definition of "the user is typing", and every global listener routes its guard through it rather than re-deriving a tag check.

Predicate True when the event originated in Source
isTerminalTarget .xterm, .xterm-helper-textarea, or [data-dictation-pty-session] src/lib/keyboardTarget.ts:28, :38-41
isEditableTarget A terminal, or an INPUT / TEXTAREA / SELECT, or any contenteditable region src/lib/keyboardTarget.ts:48-55

xterm renders a focus-holding <textarea> inside its container, so a focused terminal trips the tag check on its own; the container lookup additionally covers keydowns raised on the viewport or canvas before that textarea takes focus (src/lib/keyboardTarget.ts:21-28).

Binding Guard Consequence
Ctrl+K isEditableTarget, and only while the palette is closed Suppressed in a terminal and in every text field. In a terminal this is the point: Ctrl+K is readline's kill-line, and a user killing a line at their shell was getting the palette dropped over the terminal
Ctrl+Shift+chord isTerminalTarget Suppressed in a terminal only. Text fields do not bind Ctrl+Shift chords, so navigation stays available while typing in a composer
Esc (dictation cancel) isTerminalTarget Suppressed in a terminal, where Escape belongs to vim, readline's meta prefix or a TUI's back action. Editable controls keep the cancel
y / n / Esc (pane approvals) INPUT / TEXTAREA check Never steals a literal keystroke from a field, which covers terminals via xterm's focus holder

Escape has a fixed ladder#

More surfaces want Esc than want any other key. From highest priority to lowest:

Rank Consumer Condition Source
1 Whatever already handled the press defaultPrevented set by an inner layer All handlers
2 A focused terminal The PTY owns every key, Escape included src/components/workspace/WorkspaceMosaicContainer.tsx:113
3 The command palette Unguarded — it owns focus whenever it is open src/hooks/useGlobalShortcuts.ts:48-52
4 The top-most ui/Modal Ordered by React nesting depth, then registration order src/components/ui/Modal.tsx:96, src/lib/modalStack.ts:32-55
5 An active dictation recording Skipped inside a terminal src/hooks/useGlobalShortcuts.ts:57-66
6 The review surface, if the review belongs to the zoomed pane Scoped by conversation id src/components/workspace/WorkspaceMosaicContainer.tsx:114-126
7 Pane zoom Only the visible, active workspace's container listens src/components/workspace/WorkspaceMosaicContainer.tsx:99-104

Rank 6 is scoped for a reason worth knowing: reviewStore.open is a global flag that nothing resets on a view change, so an unscoped read left Esc permanently dead after opening a review in Agents and switching to Workspace — with the un-exitable zoom and the on-screen "Press Esc" hint both still showing (src/components/workspace/WorkspaceMosaicContainer.tsx:114-121).

Important

A focused terminal outranks pane zoom, so Esc will not un-zoom a shell. The tile's own zoom button is the way out, and the on-screen hint reads "Press Esc — or the tile's zoom button — to exit" (src/components/workspace/WorkspaceMosaicContainer.tsx:300).

Nested dialogs order by React nesting depth first and registration order second. Depth has to lead: React runs child effects before parent effects, so a dialog rendered inside another dialog's children registers first, and raw ordering would get that case exactly backwards (src/lib/modalStack.ts:15-18).

Chords match the physical key, not the character#

resolveViewHotkey compares KeyboardEvent.code against the registry's code field before it falls back to the shifted glyph (src/lib/routeRegistry.ts:356-365). Physical Digit1 resolves to Agents on AZERTY, QWERTZ and Dvorak, where the same position reports & or 1 rather than !. The shifted-glyph map survives only as a fallback for events that do not carry code, and only for the number-row chords — the mnemonic letter chords never had glyph entries (src/lib/viewHotkeys.ts:16-22).

Chords also yield to an open dialog, which focus classification alone would not catch: switching view unmounts the outgoing view, and takes any open dialog and its half-typed form down with it (src/hooks/useGlobalShortcuts.ts:77-79, src/lib/modalStack.ts:57-59).

Two tiles, one keypress#

Approval keys are bound at document level, so every mounted conversation tile would answer a single press. Exactly one handler may be live per tile, and it is armed by keyboardScopeActive: undefined means there is no pane context — the standalone Agents view — and the handler arms as before; defined means it arms only for the focused tile (src/components/agents/chat/PendingApprovalsSection.tsx:36-43, src/components/workspace/ConversationTile.tsx:244). The cards themselves moved into the transcript; the handler deliberately did not move with them (src/components/agents/chat/InlineApprovals.tsx:42-53).

Gaps and asymmetries#

These are real, present in 0.13.1, and documented rather than smoothed over.

What Detail
The palette is Ctrl-only useGlobalShortcuts.ts:39 tests e.ctrlKey and never e.metaKey, so Cmd+K does not open the palette on macOS. Ctrl+K does
View chords are Ctrl-only resolveViewHotkey requires e.ctrlKey (src/lib/routeRegistry.ts:357). There is no Cmd equivalent
The Flight launch chord is Ctrl-only LaunchAsyncFlightModal.tsx:506 tests e.ctrlKey alone, and the dialog footer advertises "Ctrl+Enter to launch". The composer's send chord has the same shape (buildComposerKeyboardHandler.ts:179), but there a bare Enter already submits, so the asymmetry only bites in the dialog
The side-chat toggle has no focus guard useSideChatHotkey.ts:12-20 binds Ctrl/Cmd+; on window with no isEditableTarget check, so it fires while you are typing in a composer or a terminal
The Code Quality shortcuts have no typing guard QualityView.tsx:167-179 binds Ctrl/Cmd+R and Ctrl/Cmd+F on window with no editable check. They are only live while that module view is mounted
Ctrl+Shift+D is bound twice It is the Dictation route chord (src/lib/routeRegistry.ts:238) and the fixed OS-global open accelerator (src/types/dictation.ts:195). Both open the Dictation view, so the collision is invisible in practice — but with global shortcuts on, the OS handler runs even when PacketBench is not focused
No in-app rebinding Only the two dictation capture shortcuts can be changed. Everything else is compiled in
Note

Settings states the Cmd-on-macOS substitution for push-to-talk only. It applies to the toggle as well: both defaults are stored as CommandOrControl accelerators (src/types/dictation.ts:193-194), and the code is authoritative.