一键导入
hgraf-add-keyboard-shortcut
Add or change a global keyboard shortcut — shortcuts.ts registry, uiStore action, HelpOverlay entry, tests.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Add or change a global keyboard shortcut — shortcuts.ts registry, uiStore action, HelpOverlay entry, tests.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Understand and safely change heartbeat interval, stuck threshold, and timeout constants — client cadence, server detection math, frontend signal.
Add a new AnnotationKind — proto, storage, ingress RPC, optional downstream delivery to agents, frontend authoring UI, tests.
Add a new agent Capability — proto enum, client advertisement, server ingest, frontend gating in the control UI.
Add a new ControlKind — capability advertisement, client handler, server routing, ack semantics, frontend button, and tests — all the way through.
Add a new drift kind end-to-end. Goldfive owns the detector + enum; harmonograf reflects the kind through the intervention aggregator and the UI timeline.
Script a complex multi-turn FakeLlm scenario — function calls, tool chains, drift events, side-effect hooks, partial responses — for deterministic adk test coverage.
| name | hgraf-add-keyboard-shortcut |
| description | Add or change a global keyboard shortcut — shortcuts.ts registry, uiStore action, HelpOverlay entry, tests. |
You're adding a new keybinding to the harmonograf console. All global shortcuts live in a single table (frontend/src/lib/shortcuts.ts) and are installed once via useGlobalShortcuts() at app mount. No component-local key handlers — everything goes through this one table so HelpOverlay can render the complete list.
frontend/src/lib/shortcuts.ts:1-35 — the ShortcutBinding interface, comboMatches, the mod+ (Cmd on macOS / Ctrl elsewhere) convention.shortcuts.ts:303-332 useGlobalShortcuts — the single global keydown listener that iterates bindings and runs the first matching handler.shortcuts.ts:77-301 defaultShortcuts — the full binding table; every key in the product is here.useUiStore actions, renderer actions on ganttRenderer, or the session picker.Conventions used in the existing table:
a annotate, s steer, f fit, l live, t thinking trajectory.j / k for sequential navigation (span next/prev).[ / ] for agent row navigation.g / shift+g for first/last.mod+k for palette-style actions (session picker).mod+= / mod++ / mod+- / mod+0 for zoom.shift+? for help.Avoid single modifiers like shift on their own. Avoid alt unless the binding is specifically for advanced users — alt conflicts with macOS special-character entry.
Check for collisions:
grep -n "combo: '" frontend/src/lib/shortcuts.ts
If the handler mutates UI state, add a method on useUiStore first (frontend/src/state/uiStore.ts). Do not call useUiStore.setState({...}) directly from inside a shortcut handler unless the change is a simple field flip — that breaks the invariant that uiStore actions are the public API.
Example uiStore action:
toggleMinimap(): void {
set((s) => ({ minimapVisible: !s.minimapVisible }));
},
shortcuts.ts:79 inside defaultShortcuts():
{
id: 'toggle-minimap',
description: 'Toggle Gantt minimap',
combo: 'm',
handler: () => ui().toggleMinimap(),
},
id: unique stable string, used by HelpOverlay and tests.description: one line for the help screen — be action-oriented ("Pan 10% left", not "Arrow left").combo: lowercase, key names from KeyboardEvent.key — arrowleft, escape, (literal space), shift+?.handler: fires on exact match. comboMatches checks modifier exclusivity — mod+k does NOT match plain k.If the shortcut should open the drawer on a specific tab (like the t → Trajectory binding), add an openDrawerOnX(spanId) action on uiStore mirroring openDrawerOnTrajectory, then call it from the handler:
{
id: 'thinking-tab',
description: 'Open drawer on Thinking tab',
combo: 'shift+t',
handler: () => {
const s = ui();
if (!s.currentSessionId) return;
const spanId = s.selectedSpanId ?? listAllSpans(s.currentSessionId)[0]?.id ?? null;
if (spanId) s.openDrawerOnThinking(spanId);
},
},
The existing thinking-trajectory binding at shortcuts.ts:272 is the template.
For j/k-style traversal, use the existing neighborSpan / neighborAgent helpers at shortcuts.ts:54. They traverse getSessionStore(sessionId) in the same stable order the renderer uses. Don't re-implement traversal — the helpers keep ordering consistent across views.
The global listener (shortcuts.ts:307-320) suppresses most shortcuts while an input / textarea / contenteditable is focused. Escape and mod+k are explicit exceptions. If your new shortcut should also work while typing (rare — prefer not), add it to the exception list:
if (e.key !== 'Escape' && !(e.key.toLowerCase() === 'k' && (e.metaKey || e.ctrlKey)) && !myException) {
return;
}
Most new shortcuts should fall through normally.
frontend/src/components/shell/HelpOverlay.tsx reads defaultShortcuts() and renders the table. The new binding appears automatically. Verify by checking the HelpOverlay test for a snapshot of the rendered list.
frontend/src/__tests__/lib/shortcuts.test.ts:
it('m toggles minimap', () => {
const e = new KeyboardEvent('keydown', { key: 'm' });
document.dispatchEvent(e);
expect(useUiStore.getState().minimapVisible).toBe(true);
});
Use the existing test setup — install the global listener in a beforeEach, clean up in afterEach.
If HelpOverlay has a snapshot, update it (--update-snapshot).
cd frontend && pnpm test -- --run shortcuts HelpOverlay
cd frontend && pnpm typecheck
cd frontend && pnpm dev # manually press the combo on every view
mod+k on macOS is Cmd-K; on Linux it's Ctrl-K. comboMatches handles this, but don't use mod+w (Cmd-W closes the tab) or mod+s (Save page). Test on the actual OS you care about.mod+k and also k, the plain k binding runs whenever mod is not held. If the order matters, put the more specific binding first.' ' (literal space) is the combo for space. arrowleft, arrowright, escape, enter are the names. ? combined with shift should be shift+?, not shift+/ (even though that's the physical key). KeyboardEvent.key gives you the logical key.defaultShortcuts() runs once inside useEffect([]). If the handler closes over a variable, that variable is the initial value forever. Always call useUiStore.getState() inside the handler body, never outside.preventDefault() runs for every matched combo (shortcuts.ts:323). A shortcut that's too broad (e.g. plain f) will eat legitimate typing if the editable-field guard fails for any reason. Prefer letters that aren't bound for core typing (j/k/l/g/f/a/s/t are already consumed).useEffect returns a cleanup function that removes the listener. If HMR reloads shortcuts.ts without unbinding, you get stacked listeners. Symptom: one keypress fires the handler twice. Fix: always include the cleanup — the existing code does.