Skip to content

Surface-aware keyboard shortcuts and Dashboard shortcut set #417

Description

@BorisTyshkevich

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

  1. Application
    • Open Dashboard — G, then D
  2. SQL Browser
    • Run query
    • Format active document
    • Save query
    • Share query
    • SQL editor mode
    • Spec editor mode
    • Undo
    • Redo
    • Open reference for symbol
  3. Schema tree — database · table · column
    • existing mouse gestures
  4. General
    • Show this dialog — ?
    • Close dialog — Esc

Dashboard dialog structure

  1. Dashboard
    • Refresh all tiles, only when a Dashboard viewer exists
    • View mode
    • Edit mode
  2. Application
    • Open SQL Browser — G, then W
  3. 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:

  1. Return when defaultPrevented is already true.
  2. Give the active dialog, menu, pane, or overlay first opportunity to handle the key.
  3. Handle Esc according to overlay/pane precedence.
  4. Handle ? contextually on either ready application surface.
  5. Validate route readiness and projected workspace identity.
  6. Resolve the active surface.
  7. Dispatch Workbench commands for SQL Browser.
  8. Dispatch Dashboard commands for Dashboard.
  9. 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:

G  then  D

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    inboxFiled mid-task; not yet triaged into the roadmap

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions