Problem
The shared application header shows the same Keyboard shortcuts button on both SQL Browser and Dashboard, but the help dialog is currently hardcoded for the Workbench. On Dashboard it lists Run query, Format, Save, Share, SQL/Spec editor modes, Undo/Redo, F1, and schema-tree gestures even though those commands do not apply there.
The global shortcut handler already fails closed outside a ready Workbench route, so Workbench actions do not execute on Dashboard. However, that same route gate also prevents ? from opening keyboard help on Dashboard. The result is inconsistent:
- the Dashboard header exposes a shortcut-help button;
- the button opens irrelevant Workbench content;
? does nothing on Dashboard;
- Dashboard has no keyboard commands of its own;
- the help content and dispatch behavior are maintained separately and can drift.
Implement a surface-aware shortcut model shared by SQL Browser and Dashboard.
Goals
- Keep Workbench-only commands disabled on Dashboard.
- Make the shared header shortcut button open help for the active surface.
- Make
? open the correct help dialog on either ready surface.
- Add a compact, safe Dashboard shortcut set.
- Add SQL Browser ↔ Dashboard keyboard navigation.
- Drive both dispatch and help rendering from one shortcut catalog.
- Preserve route-loading, workspace-identity, and stale-surface safety.
- Make the help dialog scroll-safe and accessible.
Final shortcut map
Mod means Command on macOS and Control on Windows/Linux.
SQL Browser
| Action |
Shortcut |
| Run query |
Mod+Enter |
| Format active document |
Mod+Shift+Enter |
| Save query |
Mod+S |
| Share query |
Mod+Shift+S |
| SQL editor mode |
Mod+Alt+1 |
| Spec editor mode |
Mod+Alt+2 |
| Undo |
Mod+Z |
| Redo |
Mod+Shift+Z |
| Open reference for symbol |
F1 |
| Open Dashboard |
G, then D |
| Show keyboard shortcuts |
? |
| Close dialog/pane/menu |
Esc |
Keep the existing schema-tree gesture section:
| Action |
Gesture |
| Expand / collapse |
Click |
| Insert into editor |
Double-click |
Insert DDL / col::type |
Shift-click |
Dashboard
| Action |
Shortcut |
| Refresh all tiles |
Mod+Enter |
| View mode |
Mod+Alt+1 |
| Edit mode |
Mod+Alt+2 |
| Open SQL Browser |
G, then W |
| Show keyboard shortcuts |
? |
| Close dialog/menu |
Esc |
Do not list Open Dashboard while already on Dashboard. Do not list Refresh all tiles when the workspace has no Dashboard document/viewer session.
Command semantics
Refresh all tiles
Mod+Enter means “execute the current surface”:
- SQL Browser: run the active query;
- Dashboard: run the current Dashboard refresh wave using the same viewer-session operation as the visible Refresh button.
Dashboard refresh may run while a Dashboard filter input is focused. It must not run while a modal, menu, or other keyboard-owning overlay is open.
The shortcut must invoke only the currently mounted Dashboard viewer session. A viewer belonging to a disposed or replaced surface must never receive a refresh.
Dashboard View/Edit
Mod+Alt+1 selects Dashboard View mode.
Mod+Alt+2 selects Dashboard Edit mode.
- Selecting the already-active mode is a no-op.
- Use the same route operations as the visible View/Edit control.
- View/Edit changes use the existing
replaceState route behavior and must not add redundant browser-history entries.
The same shortcut pair remains SQL/Spec mode selection while the active surface is SQL Browser.
Surface navigation chord
G, then D opens Dashboard from SQL Browser.
G, then W opens SQL Browser from Dashboard.
- Surface navigation uses the active workspace key resolved at dispatch time.
- Surface changes use normal application navigation (
pushState) so Back returns to the prior surface.
- Navigating to the already-active surface is a no-op.
- Opening Dashboard uses the same destination policy as the shared header Dashboard button: Dashboard edit mode.
Use a 1500 ms timeout for the second key.
Chord state resets on:
- successful dispatch;
- timeout;
- unmatched second key;
Escape;
- route or surface change;
- window blur;
- opening a modal/dialog/menu;
- application sign-out or transition to Login.
Pressing G again while the prefix is pending restarts the timeout.
Surface-aware help dialog
Replace the single static list with content derived from the active surface and current command availability.
SQL Browser dialog structure
- Application
- Open Dashboard —
G, then D
- SQL Browser
- Run query
- Format active document
- Save query
- Share query
- SQL editor mode
- Spec editor mode
- Undo
- Redo
- Open reference for symbol
- Schema tree — database · table · column
- General
- Show this dialog —
?
- Close dialog —
Esc
Dashboard dialog structure
- Dashboard
- Refresh all tiles, only when a Dashboard viewer exists
- View mode
- Edit mode
- Application
- Open SQL Browser —
G, then W
- General
- Show this dialog —
?
- Close dialog —
Esc
The shared header button remains visible on both surfaces and opens the surface-specific dialog.
Implementation direction
Shared shortcut catalog
Do not create a second independently hardcoded Dashboard dialog. Define one shortcut catalog that is consumed by both the dispatcher and the help renderer.
Suggested model:
type ShortcutSurface = 'workspace' | 'dashboard' | 'all';
type ShortcutId =
| 'run-query'
| 'format-document'
| 'save-query'
| 'share-query'
| 'sql-mode'
| 'spec-mode'
| 'dashboard-refresh'
| 'dashboard-view'
| 'dashboard-edit'
| 'open-workbench'
| 'open-dashboard'
| 'open-reference'
| 'open-help'
| 'close-overlay';
interface ShortcutDefinition {
id: ShortcutId;
label: string;
section: 'application' | 'workspace' | 'dashboard' | 'general';
surface: ShortcutSurface;
sequence: ShortcutSequence;
available?(context: ShortcutContext): boolean;
}
The exact types may differ, but the catalog must be the source of truth for:
- command labels;
- key sequences;
- section placement;
- surface visibility;
- current availability;
- platform-specific key rendering.
The help renderer must not document a command the active dispatcher cannot execute.
Dispatch organization
Refactor the global handler in this order:
- Return when
defaultPrevented is already true.
- Give the active dialog, menu, pane, or overlay first opportunity to handle the key.
- Handle
Esc according to overlay/pane precedence.
- Handle
? contextually on either ready application surface.
- Validate route readiness and projected workspace identity.
- Resolve the active surface.
- Dispatch Workbench commands for SQL Browser.
- Dispatch Dashboard commands for Dashboard.
- Process/reset the surface-navigation chord.
Do not keep the current Workbench-only route return above ?; Dashboard must be able to open contextual help while still rejecting Workbench actions.
Route readiness and identity guards
Every application shortcut must fail closed unless:
workspaceRouteStatus === 'ready';
- the route has a workspace key;
- the route workspace key matches the projected/current workspace key;
- the application is signed in;
- the active surface command belongs to the current surface generation.
During route loading, not-found, error, Login, or a workspace-key mismatch:
- do not call an action;
- do not call
preventDefault() for action shortcuts;
- clear any pending navigation chord.
Dashboard surface command port
Dashboard refresh belongs to the currently mounted viewer session, not to a permanently reusable global callback.
Register a route-local command port when Dashboard mounts, for example:
app.surfaceCommands = {
surface: 'dashboard',
generation: app.surfaceGeneration,
refresh: () => session.refresh(),
};
On Dashboard disposal or any surface transition:
- clear the command port;
- invalidate its generation;
- ensure pending callbacks cannot repaint or refresh the replacement surface.
Before dispatching refresh, verify that the registered surface and generation match the current route and mounted surface.
Use the existing surface-generation/lifecycle model rather than adding a parallel stale-operation mechanism.
Navigation actions
Surface and mode shortcuts must call the same route APIs as the visible controls:
app.navigateSqlRoute(...)
Required destinations:
// SQL Browser -> Dashboard
{ surface: 'dashboard', workspaceKey, mode: 'edit' }
// Dashboard -> SQL Browser
{ surface: 'workspace', workspaceKey }
// Dashboard -> View
{ surface: 'dashboard', workspaceKey, mode: 'view' }
// Dashboard -> Edit
{ surface: 'dashboard', workspaceKey, mode: 'edit' }
Do not mutate sqlRoute, location.search, or history directly inside the shortcut module.
Navigation chord controller
Implement the G prefix as explicit state, not scattered timestamp checks.
Suggested shape:
interface ShortcutChordState {
prefix: 'g' | null;
timer: number | null;
}
The controller should expose operations similar to:
begin(prefix);
consume(key);
reset();
dispose().
It must be testable with an injected timer/clock seam or fake timers.
An unmatched second key resets the chord and performs no application action. Do not reinterpret that second key as a new shortcut during the same event.
Typing and focus suppression
Plain-key commands (?, G, W, D) must not trigger while the event target is or is contained by:
input;
textarea;
select;
[contenteditable];
[role="textbox"];
- a CodeMirror editor/content element.
Mod+Enter remains available from Dashboard filter inputs so users can edit a filter and refresh without moving focus.
Native editor shortcuts such as Undo, Redo, F1, and text selection continue to be owned by CodeMirror or the focused control. Respect defaultPrevented so a key already consumed by an editor does not also trigger an application action.
Overlay ownership
When a modal, menu, confirmation dialog, picker, or other overlay is open:
- only commands owned by that overlay may execute;
Esc closes the topmost applicable overlay according to existing precedence;
- Workbench Run/Save and Dashboard Refresh/navigation must not execute behind it;
- opening the shortcut dialog clears a pending navigation chord.
Provide an explicit application state/port for “keyboard interaction is owned by an overlay” rather than relying solely on incidental DOM selectors where practical.
Platform-aware key labels
The handler already treats Meta and Control as the same modifier. The dialog should render the matching platform label:
- macOS:
⌘, ⌥, ⇧;
- Windows/Linux:
Ctrl, Alt, Shift.
Keep command definitions platform-neutral (mod, alt, shift, enter) and format them in the help renderer.
For chord sequences, render separate keycaps and a textual separator, for example:
Do not render a chord as one combined keycap.
Dialog layout and accessibility
The additional SQL Browser navigation row must not be omitted because the current dialog is tall. Make the dialog viewport-safe:
.modal-card {
max-height: calc(100dvh - 32px);
}
.modal-card-body {
overflow-y: auto;
}
Structure the modal with a fixed heading/footer and a scrollable body where appropriate.
Required accessibility behavior:
role="dialog";
aria-modal="true";
- heading connected with
aria-labelledby;
- focus moves into the dialog when it opens;
- keyboard focus remains within the modal while open;
Esc closes it;
- backdrop click continues to close it without closing on a card-originated gesture;
- focus returns to the previously focused element or the invoking shortcut button after close;
- shortcut sections use semantic headings or equivalent labelled groups;
<kbd> is used for individual keys;
- the close button has a clear accessible name.
The dialog remains idempotent while already open.
Suggested code boundaries
Likely areas:
src/ui/shortcuts.ts
- shared catalog;
- surface-aware dispatcher;
- chord controller;
- contextual dialog renderer.
src/ui/app-header.ts
- continue invoking one shared
openShortcuts action; no surface-specific branching should be needed here if the action resolves context at invocation time.
src/ui/dashboard.ts
- register/clear the current Dashboard refresh command port with surface generation.
src/ui/app.ts / src/ui/app.types.ts
- expose route navigation, active-surface generation/commands, and overlay keyboard ownership through narrow typed seams.
- shortcut and Dashboard unit tests
- cover catalog rendering, dispatch, lifecycle, routing, and chord behavior.
Keep the shortcut module on narrow contracts rather than importing the full App controller.
Tests
Contextual help
- SQL Browser help lists existing Workbench commands and
G then D.
- SQL Browser help retains schema-tree gestures.
- Dashboard help does not list Run query, Format, Save, Share, editor modes, F1, or schema gestures.
- Dashboard help lists Refresh, View, Edit, and
G then W.
- A workspace with no Dashboard document omits Refresh.
- The header button opens the correct dialog on both surfaces.
? opens the correct dialog on both surfaces.
? is ignored while typing.
- platform labels render Command/Option on macOS and Ctrl/Alt elsewhere.
- chord sequences render as separate keycaps.
Dashboard dispatch
- Meta+Enter refreshes the current Dashboard session.
- Ctrl+Enter refreshes the current Dashboard session.
- refresh works while a Dashboard filter input is focused.
- refresh does not run behind a modal/menu.
- refresh does not run with
defaultPrevented.
- refresh does not run during loading, not-found, error, Login, or workspace mismatch.
- refresh does not run after Dashboard disposal.
- a stale Dashboard generation cannot refresh a newly mounted Dashboard.
- an empty/missing Dashboard has no refresh command.
Mode navigation
- Meta/Ctrl+Alt+1 selects View.
- Meta/Ctrl+Alt+2 selects Edit.
- selecting the current mode is a no-op.
- View/Edit uses replacement history behavior.
- pending Dashboard operations from the old mode cannot repaint the replacement mode.
Surface navigation chord
G, then D opens Dashboard from SQL Browser.
G, then W opens SQL Browser from Dashboard.
- navigation preserves the current workspace key.
- Dashboard opens in edit mode.
- surface changes use push-history behavior.
- the already-active destination is a no-op.
- the chord is ignored in text inputs, selects, contenteditable elements, role=textbox elements, and CodeMirror.
- timeout clears the pending prefix.
- unmatched second key clears the prefix and performs no action.
- repeated
G restarts the timeout.
- Escape, window blur, route change, dialog opening, and sign-out clear the prefix.
- the chord cannot complete against a different workspace/surface generation than the one where it started.
Existing Workbench behavior
- all existing Workbench shortcut tests continue to pass.
- Workbench action shortcuts remain disabled on Dashboard.
- Run/Format/Save/Share/editor-mode shortcuts remain disabled during route loading and workspace mismatch.
- editor-owned keys continue to respect
defaultPrevented.
- existing Escape precedence for the docs pane and running-query cancellation is preserved.
Accessibility and layout
- dialog has
role=dialog, aria-modal, and a labelled heading.
- focus moves into the dialog and returns on close.
- focus cannot escape the modal with Tab/Shift+Tab.
- Escape and backdrop close behavior remain correct.
- tall SQL Browser content is scrollable at constrained viewport heights.
Acceptance criteria
- Dashboard never displays Workbench-only shortcut documentation.
- Workbench commands never dispatch on Dashboard.
- Dashboard commands never dispatch on SQL Browser.
? and the shared header button open contextual help on either surface.
Mod+Enter refreshes only the current mounted Dashboard viewer.
Mod+Alt+1/2 directly select Dashboard View/Edit using existing route semantics.
G D opens Dashboard from SQL Browser and is documented there.
G W returns to SQL Browser and is documented on Dashboard.
- All commands fail closed during route loading, errors, workspace mismatch, Login, overlay ownership, and stale surface generations.
- Help rendering and dispatch use the same shortcut definitions.
- The dialog is platform-aware, viewport-safe, keyboard accessible, and restores focus after close.
Non-goals
- User-configurable or persisted shortcut remapping.
- Replacing browser-reserved tab/window/address-bar shortcuts.
- Adding tile-selection, tile-reordering, or per-tile Dashboard shortcuts in this issue.
- Changing the visible Dashboard Refresh, View/Edit, or SQL Browser/Dashboard controls beyond adding shortcut integration and help discoverability.
Problem
The shared application header shows the same Keyboard shortcuts button on both SQL Browser and Dashboard, but the help dialog is currently hardcoded for the Workbench. On Dashboard it lists Run query, Format, Save, Share, SQL/Spec editor modes, Undo/Redo, F1, and schema-tree gestures even though those commands do not apply there.
The global shortcut handler already fails closed outside a ready Workbench route, so Workbench actions do not execute on Dashboard. However, that same route gate also prevents
?from opening keyboard help on Dashboard. The result is inconsistent:?does nothing on Dashboard;Implement a surface-aware shortcut model shared by SQL Browser and Dashboard.
Goals
?open the correct help dialog on either ready surface.Final shortcut map
Modmeans Command on macOS and Control on Windows/Linux.SQL Browser
Mod+EnterMod+Shift+EnterMod+SMod+Shift+SMod+Alt+1Mod+Alt+2Mod+ZMod+Shift+ZF1G, thenD?EscKeep the existing schema-tree gesture section:
col::typeDashboard
Mod+EnterMod+Alt+1Mod+Alt+2G, thenW?EscDo not list Open Dashboard while already on Dashboard. Do not list Refresh all tiles when the workspace has no Dashboard document/viewer session.
Command semantics
Refresh all tiles
Mod+Entermeans “execute the current surface”:Dashboard refresh may run while a Dashboard filter input is focused. It must not run while a modal, menu, or other keyboard-owning overlay is open.
The shortcut must invoke only the currently mounted Dashboard viewer session. A viewer belonging to a disposed or replaced surface must never receive a refresh.
Dashboard View/Edit
Mod+Alt+1selects Dashboard View mode.Mod+Alt+2selects Dashboard Edit mode.replaceStateroute behavior and must not add redundant browser-history entries.The same shortcut pair remains SQL/Spec mode selection while the active surface is SQL Browser.
Surface navigation chord
G, thenDopens Dashboard from SQL Browser.G, thenWopens SQL Browser from Dashboard.pushState) so Back returns to the prior surface.Use a 1500 ms timeout for the second key.
Chord state resets on:
Escape;Pressing
Gagain while the prefix is pending restarts the timeout.Surface-aware help dialog
Replace the single static list with content derived from the active surface and current command availability.
SQL Browser dialog structure
G, thenD?EscDashboard dialog structure
G, thenW?EscThe shared header button remains visible on both surfaces and opens the surface-specific dialog.
Implementation direction
Shared shortcut catalog
Do not create a second independently hardcoded Dashboard dialog. Define one shortcut catalog that is consumed by both the dispatcher and the help renderer.
Suggested model:
The exact types may differ, but the catalog must be the source of truth for:
The help renderer must not document a command the active dispatcher cannot execute.
Dispatch organization
Refactor the global handler in this order:
defaultPreventedis already true.Escaccording to overlay/pane precedence.?contextually on either ready application surface.Do not keep the current Workbench-only route return above
?; Dashboard must be able to open contextual help while still rejecting Workbench actions.Route readiness and identity guards
Every application shortcut must fail closed unless:
workspaceRouteStatus === 'ready';During route loading, not-found, error, Login, or a workspace-key mismatch:
preventDefault()for action shortcuts;Dashboard surface command port
Dashboard refresh belongs to the currently mounted viewer session, not to a permanently reusable global callback.
Register a route-local command port when Dashboard mounts, for example:
On Dashboard disposal or any surface transition:
Before dispatching refresh, verify that the registered surface and generation match the current route and mounted surface.
Use the existing surface-generation/lifecycle model rather than adding a parallel stale-operation mechanism.
Navigation actions
Surface and mode shortcuts must call the same route APIs as the visible controls:
Required destinations:
Do not mutate
sqlRoute,location.search, or history directly inside the shortcut module.Navigation chord controller
Implement the
Gprefix as explicit state, not scattered timestamp checks.Suggested shape:
The controller should expose operations similar to:
begin(prefix);consume(key);reset();dispose().It must be testable with an injected timer/clock seam or fake timers.
An unmatched second key resets the chord and performs no application action. Do not reinterpret that second key as a new shortcut during the same event.
Typing and focus suppression
Plain-key commands (
?,G,W,D) must not trigger while the event target is or is contained by:input;textarea;select;[contenteditable];[role="textbox"];Mod+Enterremains available from Dashboard filter inputs so users can edit a filter and refresh without moving focus.Native editor shortcuts such as Undo, Redo, F1, and text selection continue to be owned by CodeMirror or the focused control. Respect
defaultPreventedso a key already consumed by an editor does not also trigger an application action.Overlay ownership
When a modal, menu, confirmation dialog, picker, or other overlay is open:
Esccloses the topmost applicable overlay according to existing precedence;Provide an explicit application state/port for “keyboard interaction is owned by an overlay” rather than relying solely on incidental DOM selectors where practical.
Platform-aware key labels
The handler already treats Meta and Control as the same modifier. The dialog should render the matching platform label:
⌘,⌥,⇧;Ctrl,Alt,Shift.Keep command definitions platform-neutral (
mod,alt,shift,enter) and format them in the help renderer.For chord sequences, render separate keycaps and a textual separator, for example:
Do not render a chord as one combined keycap.
Dialog layout and accessibility
The additional SQL Browser navigation row must not be omitted because the current dialog is tall. Make the dialog viewport-safe:
Structure the modal with a fixed heading/footer and a scrollable body where appropriate.
Required accessibility behavior:
role="dialog";aria-modal="true";aria-labelledby;Esccloses it;<kbd>is used for individual keys;The dialog remains idempotent while already open.
Suggested code boundaries
Likely areas:
src/ui/shortcuts.tssrc/ui/app-header.tsopenShortcutsaction; no surface-specific branching should be needed here if the action resolves context at invocation time.src/ui/dashboard.tssrc/ui/app.ts/src/ui/app.types.tsKeep the shortcut module on narrow contracts rather than importing the full
Appcontroller.Tests
Contextual help
GthenD.GthenW.?opens the correct dialog on both surfaces.?is ignored while typing.Dashboard dispatch
defaultPrevented.Mode navigation
Surface navigation chord
G, thenDopens Dashboard from SQL Browser.G, thenWopens SQL Browser from Dashboard.Grestarts the timeout.Existing Workbench behavior
defaultPrevented.Accessibility and layout
role=dialog,aria-modal, and a labelled heading.Acceptance criteria
?and the shared header button open contextual help on either surface.Mod+Enterrefreshes only the current mounted Dashboard viewer.Mod+Alt+1/2directly select Dashboard View/Edit using existing route semantics.G Dopens Dashboard from SQL Browser and is documented there.G Wreturns to SQL Browser and is documented on Dashboard.Non-goals