Skip to content

Commit 193ad26

Browse files
alicodingclaude
andauthored
feat: inline hotkey hints (goal 0015 remainder) + real Close-tabs bindings (#24)
Owner's Chrome-"Search Tabs"-inspired ask: show a command's bound shortcut inline, next to the action itself, reading from the SAME registry Settings' Keyboard Shortcuts already reads from (never a second hardcoded copy that can drift on rebind). - app/HotkeyHint.tsx: resolveHotkeyLabel/useCommandBinding/<HotkeyHint>, the one shared O(1) resolution every inline hint in the app now uses (shared/commands.ts default merged with store.ts keybindingOverrides). - shared/commands.ts: two new REAL, rebindable commands -- tab.closeOthers (⌘⌥W) and tab.closeAll (⌘⇧W), Safari's own combos for the identical actions, wired into WorkTabShell's tab-overflow menu alongside their hints -- not just a display pattern. - QuickPanel's "Open Settings" row and CommandPalette's own pre-existing duplicated ShortcutHint logic both now render through the same shared component (fixes a real staleness bug: QuickPanel hardcoded "⌘,"). - Also includes view.home (⌘0, "Go to Home") filling the one view missing a Cmd+N jump -- Composition/Configure/Activity/Review all had one already. Proofs: HotkeyHint.test.ts (resolution logic: default/override/unbound cases), e2e/hotkey-hint.spec.ts (the dropdown shows both hints, the shortcuts actually close tabs, and a Settings rebind updates the same hint live -- the single-source-of-truth requirement). Goal 0015 stays OPEN: the workflow's-own-Hotkey-TRIGGER half of the Acceptance sentence (distinct from this app-level command-shortcut half) remains unbuilt, named honestly in the goal file and BACKLOG.md rather than claiming full closure. Claude-Session: https://claude.ai/code/session_018pkViCNAuZp2vBv2K9AbUh Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 498c42f commit 193ad26

13 files changed

Lines changed: 415 additions & 57 deletions

docs/SPEC.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2613,7 +2613,22 @@ findings) and the build rationale are in
26132613
detail* the Acceptance sentence's "showed them the hotkey" half
26142614
names — and a ⌘?/⌘/ alias (blocked on the 1:1 command↔binding
26152615
registry shape) both stay recorded in the goal file, not silently
2616-
dropped. `workflow.run`'s default moved
2616+
dropped. **Update (2026-08-12, session 2)**: the COMMAND-shortcut
2617+
half of that inline-hotkey-hint gap (as opposed to the still-open
2618+
per-workflow-TRIGGER half above) is now built and shared, not
2619+
per-surface: `app/HotkeyHint.tsx` (`resolveHotkeyLabel`/
2620+
`useCommandBinding`/`<HotkeyHint commandId="..." />`) is the one
2621+
place every inline shortcut chip resolves a command's live binding
2622+
(default merged with any Settings override) — CommandPalette's own
2623+
command rows now render through it too (was a duplicated local
2624+
`ShortcutHint` + `effectiveBinding` computation before), as does a
2625+
hardcoded `"⌘,"` in `QuickPanel.tsx`'s "Open Settings" row that this
2626+
replaced (a real staleness bug: it would have silently ignored a
2627+
rebind). Two new commands, `tab.closeOthers` (⌘⌥W) and `tab.closeAll`
2628+
(⌘⇧W, both Safari's own combos for the identical actions), got real
2629+
default bindings — not just a display label — wired into
2630+
`WorkTabShell.tsx`'s tab-overflow menu alongside their hints.
2631+
`workflow.run`'s default moved
26172632
off an initial ⌘R pick to ⌘↩ by owner decision, once implementation
26182633
surfaced a real collision the original research missed: macOS's
26192634
`DefaultApplicationMenu()` installs View > Reload on Cmd+R
@@ -2659,7 +2674,10 @@ findings) and the build rationale are in
26592674
(Composition/Configure/Activity/Spec, matching the sidebar order,
26602675
down from an original five once Runs stopped being a top-level view —
26612676
§7's Update). Historical bullet, left as originally written; the
2662-
keymap-system entry above is the current shape.
2677+
keymap-system entry above is the current shape. **Update
2678+
(2026-08-12)**: `view.home` (⌘0, `commands.ts`) fills the one real
2679+
gap this left — Composition/Configure/Activity/Review all had a
2680+
Cmd+N jump but Home never did.
26632681
- **Window/tab/filter state persistence** — window position/size/
26642682
maximized state is Go-side (`settingsservice.go`'s
26652683
`LoadWindowGeometry`/`WatchWindowGeometry`, persisted via

docs/goals/0015-summon-quick-invoke.md

Lines changed: 69 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ runs a workflow" run path is real and built there today; the
6464
inline-hotkey-hint and frecency/pins halves of this acceptance bar are
6565
still the unbuilt remainder this goal file tracks, for either surface.
6666

67+
**Note (2026-08-12)**: frecency landed session 1; the command-shortcut
68+
half of the inline-hotkey-hint bar landed session 2 (both below). What
69+
remains against this Acceptance sentence's literal wording is narrower
70+
now: a workflow row's own Hotkey-TRIGGER combo (as opposed to an
71+
app-level command's shortcut, which is done) — see "Still open" below.
72+
6773
## Remainder delivered 2026-08-12 — three of four items, into the Quick Panel
6874

6975
Investigated and confirmed DoR-met (the usage substrate, the RPCs, and
@@ -114,16 +120,67 @@ backend surface):
114120
the main window's Configure > Lists tab is visible with the seeded
115121
"Example: Country codes" row).
116122

117-
## Still open — not touched this session, named so they're not silently dropped
123+
## Inline-hotkey-hint remainder delivered 2026-08-12 (session 2) — the COMMAND-shortcut half
124+
125+
Owner's fresh ask (with screenshots): Chrome's own "Search Tabs"
126+
dropdown shows a bound shortcut inline next to the action, near where
127+
the user actually clicks it, not just in a central reference list.
128+
Built as one shared, O(1) single-source-of-truth piece rather than a
129+
per-surface copy:
130+
131+
- `app/HotkeyHint.tsx`: `resolveHotkeyLabel(commandId, overrides)` (a
132+
plain function, unit-tested in `HotkeyHint.test.ts` without needing
133+
`@testing-library/react`, which this repo doesn't have installed),
134+
`useCommandBinding` (the same resolution as a store-subscribed hook),
135+
and `<HotkeyHint commandId="..." />` (renders the chip, or nothing
136+
for an unbound command — never a placeholder). All three read the
137+
exact same `shared/commands.ts` default + `store.ts`
138+
`keybindingOverrides` merge `KeyboardShortcutsSection.tsx`'s Settings
139+
list already used via `effectiveBinding` — a rebind in Settings is
140+
reflected everywhere without a second hardcoded copy to drift.
141+
- **Two real commands + default bindings**, not just a display
142+
pattern: `tab.closeOthers` (⌘⌥W, Safari's own "Close Other Tabs"
143+
combo) and `tab.closeAll` (⌘⇧W, Safari's "Close Window" combo,
144+
repurposed the same way `tab.close` already repurposed plain ⌘W) —
145+
both wired into `app/WorkTabShell.tsx`'s tab-overflow menu, both
146+
rebindable in Settings like every other command. Proven end-to-end
147+
in `e2e/hotkey-hint.spec.ts`: the menu shows both hints, the real
148+
shortcuts actually close tabs, and rebinding `tab.closeOthers` in
149+
Settings updates the SAME hint the overflow menu renders (the
150+
single-source-of-truth requirement, not a screenshot-alike).
151+
- Applied to `app/QuickPanel.tsx`'s "Open Settings" row, replacing a
152+
literal hardcoded `"⌘,"` string that would have silently gone stale
153+
the moment `settings.open` was ever rebound — a real, if minor, bug
154+
this closes. `app/CommandPalette.tsx`'s own pre-existing command-row
155+
shortcut rendering (built in the 2026-08-11 core, before this
156+
session) was refactored onto the same shared component too, removing
157+
its independent, duplicated `ShortcutHint` + `effectiveBinding`
158+
computation (was one of two near-identical copies this session found
159+
and consolidated, `QuickPanel.tsx`'s "Open Settings" hardcode being
160+
the other).
161+
- Deliberately NOT built: a hint on QuickPanel's per-workflow "Run"
162+
rows — no real `commands.ts` entry exists for "run this specific
163+
workflow" (each is dispatched by ID, not a named command), and
164+
inventing one to have something to hang a hint on would be exactly
165+
the "don't invent per-workflow bindings that don't exist" trap this
166+
session's own scope named up front.
167+
168+
## Still open — the WORKFLOW-trigger half, distinct from the above
118169

119-
- **Inline hotkey hint per workflow row** (the ⌘K palette's own
120-
documented simplification from the 2026-08-11 core build —
121-
`app/CommandPalette.tsx`'s workflow-row comment: shows the trigger
122-
NodeType label, e.g. "Hotkey trigger," not the live armed/hotkey-
123-
combo detail `TriggerRowLabel.tsx` owns). Not part of this session's
124-
investigated-DoR-met scope; still the literal "showed them the
125-
hotkey they'll use instead next time" half of this goal's Acceptance
126-
sentence.
170+
- **Inline hotkey hint per workflow's own Hotkey TRIGGER** (still the
171+
⌘K palette's documented simplification from the 2026-08-11 core
172+
build — `app/CommandPalette.tsx`'s workflow-row comment: shows the
173+
trigger NodeType label, e.g. "Hotkey trigger," not the live
174+
armed/hotkey-combo detail `TriggerRowLabel.tsx` owns). This is a
175+
DIFFERENT registry than the command-shortcut work above — a
176+
workflow's own trigger hotkey (`TriggerService`, per-workflow,
177+
claimed via `TriggerService.ClaimedCombos`) rather than an app-level
178+
command (`shared/commands.ts`). Still not built, still the literal
179+
"showed them the hotkey they'll use instead next time" reading of
180+
this goal's Acceptance sentence when "the row" is a workflow, not a
181+
command — building it would need the workflow's own live trigger
182+
combo fetched and formatted the way `TriggerRowLabel.tsx` already
183+
does for the canvas, ported into the palette/panel's row-rendering.
127184
- **⌘?/⌘/ multi-binding alias** — needs a command to carry more than
128185
one `KeyCombo` (today's registry is 1:1, `shared/commands.ts`'s
129186
`defaultBinding: KeyCombo | null`); recorded as a BACKLOG.md
@@ -135,5 +192,6 @@ backend surface):
135192

136193
This goal file stays OPEN (not archived) until the three items above
137194
are picked up — none of them block the palette/panel being genuinely
138-
useful today, but the Acceptance sentence isn't fully true until the
139-
first one lands.
195+
useful today (the command-shortcut half above is real and shipped),
196+
but the Acceptance sentence's workflow-row reading isn't fully true
197+
until the first one lands.

docs/goals/BACKLOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ live-review material, interleaved during owner reviews, not a lane.**
159159
8. [x] [0013 — Canonical type system](archive/0013-canonical-type-system.md) — COMPLETE 2026-08-10 (typedfield leaf pkg; all 4 vocabularies converged incl. openapispec Phase 3; the #1 kernel investment)
160160
9. [x] [0011 — Lists maturation](archive/0011-lists-maturation.md) — DELIVERED 2026-08-12 (harvested from a parallel owner session + reconciled onto main: typed Columns/Rows against ADR-0029's canonical typedfield, system-managed audit columns w/ Expired-excluded-by-default, `list-search` node w/ go-edlib fuzzy matching, in-place legacy-List migration; CSV import + full per-run dataset snapshot named-deferred)
161161
10. [x] [0014 — Home dashboard / value mirror](archive/0014-home-dashboard.md) — delivered 2026-08-10 (Recharts, industry-decided metric semantics, editable minutes-saved, default landing)
162-
11. [ ] [0015 — Summon quick-invoke](0015-summon-quick-invoke.md) — CORE delivered 2026-08-11 (⌘K palette: commands with inline shortcuts, workflow run, tab jump/close; delegated build); PHASE 2 delivered same day (ADR-0033: the summon hotkey opens a dedicated floating Quick Panel — frameless, floats over fullscreen, Esc/blur dismiss, focus-yield; supersedes "summon opens the main window"). REMAINDER delivered 2026-08-12, into the Quick Panel: frecency sort (frequency-only, `app/workflowFrecency.ts` off goal 0014's `HomeMetrics.mostUsed`), pending-review count (own window-local read+subscribe, `QuickPanel.tsx`), Configure entities as jumpable rows (`configure:<tab>` via new `app/useMillNavigate.ts` + `ConfigureView.initialTab`). Still open, named in the goal file, not silently dropped: the ⌘K palette's own inline-hotkey-per-workflow-row detail (a pre-existing documented simplification, untouched this session); pins/favorites and the ⌘?/⌘/ multi-binding alias — see their own tech-debt lines below (Standing section).
162+
11. [ ] [0015 — Summon quick-invoke](0015-summon-quick-invoke.md) — CORE delivered 2026-08-11 (⌘K palette: commands with inline shortcuts, workflow run, tab jump/close; delegated build); PHASE 2 delivered same day (ADR-0033: the summon hotkey opens a dedicated floating Quick Panel — frameless, floats over fullscreen, Esc/blur dismiss, focus-yield; supersedes "summon opens the main window"). REMAINDER delivered 2026-08-12 (session 1), into the Quick Panel: frecency sort (frequency-only, `app/workflowFrecency.ts` off goal 0014's `HomeMetrics.mostUsed`), pending-review count (own window-local read+subscribe, `QuickPanel.tsx`), Configure entities as jumpable rows (`configure:<tab>` via new `app/useMillNavigate.ts` + `ConfigureView.initialTab`). INLINE-HOTKEY-HINT (command half) delivered 2026-08-12 (session 2): `app/HotkeyHint.tsx` (`resolveHotkeyLabel`/`useCommandBinding`/`<HotkeyHint>`), the ONE place every inline shortcut chip reads `shared/commands.ts` + `keybindingOverrides` from now (also absorbed CommandPalette's and QuickPanel's own prior independent copies); two new real, rebindable commands `tab.closeOthers` (⌘⌥W) / `tab.closeAll` (⌘⇧W) wired into `WorkTabShell.tsx`'s tab-overflow menu; proven in `e2e/hotkey-hint.spec.ts` including a rebind-in-Settings-updates-the-hint-elsewhere case. Still open, named in the goal file, not silently dropped: the ⌘K palette/Quick Panel's own inline-hotkey-per-WORKFLOW-TRIGGER-row detail (a distinct, still-unbuilt registry — a workflow's own Hotkey trigger combo, not an app-level command's); pins/favorites and the ⌘?/⌘/ multi-binding alias — see their own tech-debt lines below (Standing section).
163163
12. [x] [0022 — Workflow view mode](archive/0022-workflow-view-mode.md) — delivered 2026-08-11 (row click → read-only canvas w/ Run+step-debug; Edit explicit in-place mode switch; breakpoint dot moved onto the node card, both modes; fixed a latent bug where a policy deny could hide a breakpoint's existence)
164164
13. [x] [0020 — Workflow breakpoints](archive/0020-workflow-breakpoints.md) — delivered 2026-08-11 (ADR-0031 full scope incl. step mode + MCP debug tools; delegated build; found+fixed the ExecuteOptions.WorkflowID never-set bug that silently disabled all workflow/instance-scoped guardrail rules at runtime)
165165

frontend/e2e/hotkey-hint.spec.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { test, expect } from './fixtures/server'
2+
3+
// docs/goals/0015-summon-quick-invoke.md's inline-hotkey-hint remainder:
4+
// the tab-overflow dropdown (app/WorkTabShell.tsx) now shows each
5+
// bindable item's live shortcut (app/HotkeyHint.tsx, data-testid
6+
// "hotkey-hint") next to it -- Chrome's own "Search Tabs" pattern, the
7+
// owner's explicit ask -- and "Close other tabs"/"Close all tabs" carry
8+
// REAL default bindings (⌘⌥W / ⌘⇧W, shared/commands.ts's
9+
// tab.closeOthers/tab.closeAll), not just a display-only label.
10+
11+
async function openTwoNewWorkflowTabs(page: import('@playwright/test').Page) {
12+
await page.goto('/')
13+
await page.getByRole('link', { name: 'Workflows' }).click()
14+
// Meta+n (workflow.new), not clicking the page's own "new-workflow"
15+
// button twice -- the first click opens a workflow-new tab that
16+
// covers the Workflows list panel (and its button) entirely, so a
17+
// second click on the same locator would hang waiting for an element
18+
// that's no longer visible. Cmd+N still works from inside an already-
19+
// open workflow-new tab (shared/commands.ts's isWorkflowsArea also
20+
// treats an active workflow-edit/-new tab as "in the Workflows area"),
21+
// matching e2e/keymap.spec.ts's own "Ctrl+Tab / Ctrl+Shift+Tab" test.
22+
await page.keyboard.press('Meta+n')
23+
await page.keyboard.press('Meta+n')
24+
await expect(page.getByRole('tab', { name: 'New workflow' })).toHaveCount(2)
25+
}
26+
27+
test('the tab-overflow dropdown shows Close-other/Close-all inline hints, and Cmd+Shift+W actually closes every open tab', async ({ page }) => {
28+
await openTwoNewWorkflowTabs(page)
29+
30+
const overflow = page.getByTestId('work-tab-overflow')
31+
await overflow.click()
32+
33+
const closeOthersItem = page.getByRole('menuitem', { name: 'Close other tabs' })
34+
const closeAllItem = page.getByRole('menuitem', { name: 'Close all tabs' })
35+
await expect(closeOthersItem).toBeVisible()
36+
await expect(closeAllItem).toBeVisible()
37+
// shared/keybinding.ts's formatCombo output for each command's real
38+
// default binding -- read live off shared/commands.ts + the store's
39+
// keybindingOverrides via app/HotkeyHint.tsx, not a hardcoded label.
40+
await expect(closeOthersItem.getByTestId('hotkey-hint')).toHaveText('⌘⌥W')
41+
await expect(closeAllItem.getByTestId('hotkey-hint')).toHaveText('⌘⇧W')
42+
43+
await page.keyboard.press('Escape')
44+
await expect(page.getByRole('menu')).toHaveCount(0)
45+
46+
// The shortcut is real, not just displayed: Cmd+Shift+W (tab.closeAll)
47+
// drops every open work tab, back to just the pinned Workflows page
48+
// tab -- the overflow button itself disappears too (shown only at 2+
49+
// work tabs).
50+
await page.keyboard.press('Meta+Shift+w')
51+
await expect(page.getByRole('tab')).toHaveCount(1)
52+
await expect(page.getByRole('tab', { name: 'Workflows' })).toBeVisible()
53+
await expect(overflow).toHaveCount(0)
54+
})
55+
56+
test('Cmd+Option+W (tab.closeOthers) closes every tab except the active one', async ({ page }) => {
57+
await openTwoNewWorkflowTabs(page)
58+
59+
// The second 'New workflow' tab opened is the active one (store.ts's
60+
// sameWorkTarget never reuses a workflow-new tab, matching keymap.spec.ts's
61+
// own "Ctrl+Tab / Ctrl+Shift+Tab" test) -- tab.closeOthers keeps it and
62+
// drops the first.
63+
await page.keyboard.press('Meta+Alt+w')
64+
await expect(page.getByRole('tab', { name: 'New workflow' })).toHaveCount(1)
65+
await expect(page.getByRole('tab', { name: 'New workflow' })).toHaveAttribute('aria-selected', 'true')
66+
67+
// Cleanup.
68+
await page.getByRole('button', { name: 'Close tab' }).click()
69+
await expect(page.getByRole('tab', { name: 'New workflow' })).toHaveCount(0)
70+
})
71+
72+
test('Settings: rebinding Close other tabs updates the SAME inline hint the tab-overflow dropdown renders (O(1) single source of truth)', async ({ page }) => {
73+
await openTwoNewWorkflowTabs(page)
74+
75+
const overflow = page.getByTestId('work-tab-overflow')
76+
await overflow.click()
77+
const closeOthersItem = page.getByRole('menuitem', { name: 'Close other tabs' })
78+
await expect(closeOthersItem.getByTestId('hotkey-hint')).toHaveText('⌘⌥W')
79+
await page.keyboard.press('Escape')
80+
81+
// Rebind tab.closeOthers off its ⌘⌥W default onto Ctrl+Shift+O (no
82+
// collision with any other command's default or a RESERVED_COMBOS
83+
// entry, shared/keybinding.ts).
84+
await page.getByRole('button', { name: 'Settings' }).click()
85+
const row = page.locator('[data-testid="keymap-row"][data-command-id="tab.closeOthers"]')
86+
await expect(row.getByTestId('keymap-row-combo')).toHaveText('⌘⌥W')
87+
await row.getByTestId('keymap-row-combo').click()
88+
await page.keyboard.press('Control+Shift+O')
89+
await expect(row.getByTestId('keymap-row-combo')).toHaveText('⌃⇧O')
90+
91+
// Back on Workflows, the tab-overflow dropdown's hint -- reading off
92+
// the exact same shared/store.ts keybindingOverrides Settings just
93+
// wrote -- reflects the new combo without any code path re-deriving
94+
// it independently.
95+
await page.getByRole('link', { name: 'Workflows' }).click()
96+
// Navigating via the sidebar resets activeWorkTabKey to null
97+
// (shared/store.ts's setView -- switching sections always drops back
98+
// to the pinned page tab) -- click into one of the still-open work
99+
// tabs to have a real "active" tab again, the same precondition
100+
// WorkTabShell's own "Close other tabs" item requires (its
101+
// `disabled={activeWorkTabKey === null}`).
102+
await page.getByRole('tab', { name: 'New workflow' }).first().click()
103+
await overflow.click()
104+
await expect(closeOthersItem.getByTestId('hotkey-hint')).toHaveText('⌃⇧O')
105+
await page.keyboard.press('Escape')
106+
107+
// And the rebound combo is the one that's actually live now -- not
108+
// just displayed -- while the old default no longer does anything.
109+
await page.keyboard.press('Control+Shift+O')
110+
await expect(page.getByRole('tab', { name: 'New workflow' })).toHaveCount(1)
111+
112+
// Cleanup: reset the override and close the remaining tab, so this
113+
// doesn't leak into any other spec sharing this worker's settings file
114+
// (.claude/rules/testing.md's within-file/within-worker discipline).
115+
await page.getByRole('button', { name: 'Settings' }).click()
116+
const rowAgain = page.locator('[data-testid="keymap-row"][data-command-id="tab.closeOthers"]')
117+
await rowAgain.getByTestId('keymap-row-reset').click()
118+
await expect(rowAgain.getByTestId('keymap-row-combo')).toHaveText('⌘⌥W')
119+
120+
await page.getByRole('link', { name: 'Workflows' }).click()
121+
await page.getByRole('button', { name: 'Close tab' }).click()
122+
await expect(page.getByRole('tab', { name: 'New workflow' })).toHaveCount(0)
123+
})

frontend/src/app/CommandPalette.module.css

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,9 @@
11
/* Command palette (docs/goals/0015-summon-quick-invoke.md). Primer
22
design tokens only, per .claude/rules/frontend.md -- no bespoke
3-
colors/spacing. */
4-
5-
.shortcut {
6-
display: inline-flex;
7-
align-items: center;
8-
padding: 0 var(--base-size-6, 6px);
9-
border: 1px solid var(--borderColor-default);
10-
border-radius: var(--borderRadius-medium);
11-
background: var(--bgColor-muted);
12-
color: var(--fgColor-muted);
13-
font-size: var(--text-body-size-small);
14-
font-family: var(--fontStack-monospace, monospace);
15-
white-space: nowrap;
16-
}
3+
colors/spacing. The former local `.shortcut` chip class moved to
4+
app/HotkeyHint.module.css (docs/goals/0015's inline-hotkey-hint
5+
remainder) -- shared with QuickPanel.tsx and WorkTabShell.tsx now,
6+
not redefined per file. */
177

188
.empty {
199
padding: var(--base-size-16, 16px);

0 commit comments

Comments
 (0)