Skip to content

Commit 849575d

Browse files
alicodingclaude
andcommitted
Add per-view hotkeys, in-window-only by explicit decision (task #9)
New capability, not previously named anywhere in SPEC.md. Cmd+1 through Cmd+5 jump straight to a top-level view (Composition/Configure/ Activity/Runs/Spec, matching the sidebar's own order) via a plain keydown listener in App.tsx calling the existing useAppStore setView -- no new navigation mechanism, reuses exactly what the sidebar's own nav links already call. Deliberately in-window-only, not a real OS-level golang.design/x/hotkey registration the way per-workflow and summon hotkeys use (§3.4/§3.7): registering these globally would mean checking each combo against TriggerService's own claimed-combo conflict space, the same bidirectional check the summon hotkey already goes through -- a bigger design surface this pass intentionally didn't take on. The safer, reversible default named directly in the session goal that built this. Active regardless of which element has focus -- matches browsers'/ Slack's own Cmd+1-9 tab-switching precedent (Cmd+digit isn't a combo real typing produces, so there's no need to scope it away from text inputs). Verified directly, not assumed: a real e2e test confirms a hotkey fires correctly even while a text field has focus. Verified end-to-end via Playwright against the real server-mode backend (view-hotkeys.spec.ts, run twice): all five hotkeys navigate correctly from a cold start, checked against real existing markers per view (Composition/Configure's own tablist aria-labels, Activity/ Runs' own h1 headings, Spec's own capability-index testid) rather than new testids added just for this; a bare digit key without Cmd does nothing. Full frontend check suite (tsc, eslint, boundaries, vitest) clean. Documented in docs/SPEC.md §3.7. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FYwojT8GdUbYSoggbvEFft
1 parent 7fe6ec9 commit 849575d

2 files changed

Lines changed: 83 additions & 0 deletions

File tree

frontend/e2e/view-hotkeys.spec.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { test, expect } from '@playwright/test'
2+
3+
// Real Go bindings over HTTP (Wails3 server mode), not mocks -- same
4+
// setup as the rest of this suite. Exercises task #9's in-window Cmd+1
5+
// through Cmd+5 view hotkeys (App.tsx), each checked against a real,
6+
// already-existing marker unique to that view rather than a new testid
7+
// added just for this test.
8+
9+
test('Cmd+1 through Cmd+5 jump to their view from anywhere else in the app', async ({ page }) => {
10+
await page.goto('/')
11+
12+
// Start on Composition (the default landing view) and confirm each
13+
// hotkey lands somewhere else first, so a false positive (already
14+
// being on the target view) can't hide a broken hotkey.
15+
await expect(page.getByRole('tablist', { name: 'Composition' })).toBeVisible()
16+
17+
await page.keyboard.press('Meta+2')
18+
await expect(page.getByRole('tablist', { name: 'Configure' })).toBeVisible()
19+
20+
await page.keyboard.press('Meta+3')
21+
await expect(page.getByRole('heading', { name: 'Activity', level: 1 })).toBeVisible()
22+
23+
await page.keyboard.press('Meta+4')
24+
await expect(page.getByRole('heading', { name: 'Runs', level: 1 })).toBeVisible()
25+
26+
await page.keyboard.press('Meta+5')
27+
await expect(page.getByTestId('capability-index')).toBeVisible()
28+
29+
await page.keyboard.press('Meta+1')
30+
await expect(page.getByRole('tablist', { name: 'Composition' })).toBeVisible()
31+
})
32+
33+
test('A view hotkey works while a text field has focus, matching browser tab-switching precedent', async ({ page }) => {
34+
await page.goto('/')
35+
await page.getByTestId('new-workflow').click()
36+
await page.getByLabel('Label').click()
37+
38+
await page.keyboard.press('Meta+3')
39+
await expect(page.getByRole('heading', { name: 'Activity', level: 1 })).toBeVisible()
40+
})
41+
42+
test('Plain digit keys without Cmd do not navigate', async ({ page }) => {
43+
await page.goto('/')
44+
await expect(page.getByRole('tablist', { name: 'Composition' })).toBeVisible()
45+
46+
await page.keyboard.press('3')
47+
await expect(page.getByRole('tablist', { name: 'Composition' })).toBeVisible()
48+
await expect(page.getByRole('heading', { name: 'Activity', level: 1 })).toHaveCount(0)
49+
})

frontend/src/app/App.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import SettingsView from "../views/SettingsView";
1111
import PlaceholderView from "../views/PlaceholderView";
1212
import { CompositionService, CapabilitiesService } from "../../bindings/github.com/alicoding/mill";
1313
import { useAppStore, viewFor, viewsEqual, statusVariant } from "../shared/store";
14+
import type { View } from "../shared/store";
1415
import { COLOR_MODE_STORAGE_KEY, SIDEBAR_OPEN_STORAGE_KEY } from "./theme";
1516
import { CAPABILITY_ICON, SPEC_ICON } from "./navIcon";
1617
import styles from "./App.module.css";
@@ -47,6 +48,39 @@ function App() {
4748
// worth chasing further for a dev-convenience ribbon -- see SPEC.md.
4849
const [loadedAt] = useState(() => new Date().toLocaleTimeString());
4950

51+
// Per-view hotkeys (task #9, docs/SPEC.md §3.7) -- Cmd+1 through
52+
// Cmd+5 jump straight to a top-level view, matching the sidebar's own
53+
// order (Composition/Configure lead, Activity/Runs follow, Spec is
54+
// always last). Deliberately in-window-only, not a global OS-level
55+
// hotkey: this reuses plain browser keydown handling, the reversible/
56+
// safer default named directly in the session goal that built this,
57+
// distinct from TriggerService's real OS-level golang.design/x/hotkey
58+
// registration (§3.4) that per-workflow and summon hotkeys use --
59+
// registering these globally too would mean checking them against
60+
// TriggerService's own claimed-combo conflict space, a bigger design
61+
// surface this pass deliberately doesn't take on. Matches
62+
// browsers'/Slack's own Cmd+1-9 tab-switching precedent: active
63+
// regardless of which element has focus, not scoped away from text
64+
// inputs, since Cmd+digit isn't a combo real typing produces.
65+
useEffect(() => {
66+
const VIEW_HOTKEYS: Record<string, View> = {
67+
'1': { kind: 'composition' },
68+
'2': { kind: 'configure' },
69+
'3': { kind: 'activity' },
70+
'4': { kind: 'runs' },
71+
'5': { kind: 'spec' },
72+
};
73+
const onKeyDown = (e: KeyboardEvent) => {
74+
if (!e.metaKey || e.ctrlKey || e.altKey || e.shiftKey) return;
75+
const target = VIEW_HOTKEYS[e.key];
76+
if (!target) return;
77+
e.preventDefault();
78+
setView(target);
79+
};
80+
window.addEventListener('keydown', onKeyDown);
81+
return () => window.removeEventListener('keydown', onKeyDown);
82+
}, [setView]);
83+
5084
// Icon-rail collapse (narrow persistent strip, not full hide/show) is a
5185
// well-established pattern -- but Primer genuinely ships none of its
5286
// mechanics. Checked exhaustively, not assumed: grepped @primer/react's

0 commit comments

Comments
 (0)