Skip to content

Add a global command palette (⌘K / Ctrl+K)#42

Merged
DragonnZhang merged 3 commits into
mainfrom
loop/command-palette
Jul 1, 2026
Merged

Add a global command palette (⌘K / Ctrl+K)#42
DragonnZhang merged 3 commits into
mainfrom
loop/command-palette

Conversation

@DragonnZhang

Copy link
Copy Markdown
Collaborator

Closes #41

What & why

Every comparable desktop app ships a command palette — a keyboard-driven
overlay to search for and run any command (⌘K in Claude Code Desktop / Linear,
⌘⇧P in VS Code / Codex desktop). OpenWork already had all the pieces — a
centralized action registry with an execute(actionId) method, the cmdk
command primitives, and translated action labels — but they were only
surfaced in read-only reference views (the Keyboard Shortcuts dialog and the
Settings → Shortcuts page). There was no way to run an action by searching for
it.

This adds a global command palette opened with ⌘K / Ctrl+K (and from the app
menu). It:

  • lists every registry action, grouped by category, with its hotkey on the right,
  • filters as you type (cmdk's built-in fuzzy match over the translated label),
  • shows a “No results found” empty state,
  • on select, closes and calls the registry's execute() — so the action runs
    exactly as if its hotkey were pressed,
  • closes on Esc / selection, integrated with the existing modal stack.

Frontend-only. No backend / qwen-code change. No new i18n keys — the input
placeholder reuses commands.searchCommands, the empty state reuses
common.noResultsFound, labels reuse shortcuts.action.*, and group headings
reuse shortcuts.category.*.

Changes

  • actions/definitions.ts — new app.commandPalette action (mod+k).
  • components/CommandPalette.tsx — the palette. Self-contained: it registers
    its own open handler, owns its open state, and joins the modal stack for
    layered close. Mounted once in App.tsx (inside the action + modal providers).
  • actions/action-i18n.ts — extracted the action-ID → i18n-key map
    (ACTION_LABEL_KEYS) plus a category-key helper into a shared module, and reuse
    it from Settings → Shortcuts (pages/settings/ShortcutsPage.tsx) so the two
    surfaces can't drift.
  • components/AppMenu.tsx — a “Command Palette” entry for discoverability.
  • e2e/assertions/command-palette.assert.ts — new CDP assertion (below).
  • e2e/app.ts — give each launched app instance its own profile dir keyed on
    the unique debug port. Electron's single-instance lock is keyed on userData, so
    a shared dir made a second assertion's launch defer to the first instance and
    never get its own renderer target. Per-port dirs let multiple assertions run in
    one bun run e2e.

Verification (DoD)

  • Feature CDP assertion — ✅, and the full e2e suite is green (2/2):
🧪 Running 2 e2e assertion(s)
  • app boots and main window renders ... ✅ (3764ms)
  • command palette opens, filters, and runs an action ... ✅ (3639ms)
✅ 2/2 passed

The command-palette assertion drives the real built app over CDP through the
whole path: presses Ctrl/Cmd+K and asserts the palette opens with more than one
action row; types theme and asserts the list narrows (every visible row
contains the query, count shrinks) with Toggle Theme surviving; types a
no-match query and asserts the empty state with zero rows; clears and asserts
the full list is restored; then selects Toggle Theme and asserts the palette
closes and the app theme actually flips (documentElement darklight) —
proving the palette executes the action, not merely displays it.

  • bun run typecheck:all — introduces no new errors. packages/{core, shared, server-core, server, session-tools-core, ui} all pass; the only errors
    are 11 pre-existing ones in apps/electron unrelated to this change
    (auto-update.ts, a settings-default-thinking test tuple, and two test files
    importing vitest, which is in no package.json). None are in the files this PR
    touches.
  • bun test — the failing set is byte-for-byte identical to main
    (56 pre-existing failures: BrowserCDP, BrowserPaneManager, i18n locale-parity
    sorted checks, startWebuiHttpServer, resource-bundle, etc.). This change
    adds zero new failures.
  • Electron build — ✅. App boots — ✅ (app boots assertion).

Part of the autonomous desktop-feature loop (loop-bot).


Generated by Claude Code

Add a keyboard-driven command palette that lets you search for and run any
registered app action — the "run" surface that pairs with the read-only
Keyboard Shortcuts reference. Every comparable desktop app (Claude Code
Desktop, VS Code / Codex, Linear) ships one; OpenWork had the action
registry, cmdk primitives, and translated labels, but nothing tied them
together.

Frontend-only. The palette lists the centralized action registry grouped by
category, filters with cmdk's built-in fuzzy match, and dispatches the chosen
action via the registry's execute() — exactly as if its hotkey were pressed.
Opened with ⌘K / Ctrl+K or from the app menu. No backend / qwen-code change,
and no new i18n keys (reuses commands.*, common.noResultsFound, and the
existing shortcuts.action.* / shortcuts.category.* labels).

- action registry: new app.commandPalette action (mod+k)
- CommandPalette component (self-contained: registers its open handler, owns
  state, integrates with the modal stack)
- extract ACTION_LABEL_KEYS to a shared actions/action-i18n module and reuse
  it in the Shortcuts settings page
- AppMenu entry for discoverability
- e2e: command-palette CDP assertion (open → filter → empty → clear → run
  Toggle Theme and assert the theme flips)
- e2e harness: per-port isolated profile dirs so multiple assertions can each
  launch their own app instance without single-instance-lock collisions

Closes #41
…close

Two issues surfaced from testing the palette:

- It listed context-scoped actions whose handler is disabled in the palette's
  context (e.g. "Next Search Match" / "Previous Search Match", which need an
  active in-conversation search, and the navigator/panel focus actions).
  Selecting one was a dead no-op — clicking appeared to "do nothing". The
  palette now filters to actions the registry reports as executable right now
  (new ActionRegistry.canExecute), so only runnable commands show.

- It ran the selected action synchronously while the dialog was still tearing
  down, so actions that open a panel or move focus (e.g. Search) could be
  clobbered by the dialog's focus restoration. It now closes first and runs the
  action on the next tick, in the app's restored-focus context.

The e2e assertion additionally verifies a known context-scoped action
("Next Search Match") is absent from the palette.
@DragonnZhang

Copy link
Copy Markdown
Collaborator Author

@copilot resolve the merge conflicts in this pull request

@DragonnZhang

Copy link
Copy Markdown
Collaborator Author

@copilot resolve the merge conflicts in this pull request

# Conflicts:
#	docs/loop/feature-ledger.md
#	e2e/app.ts
@DragonnZhang DragonnZhang merged commit 8741a7a into main Jul 1, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Global command palette (⌘K / Ctrl+K) to search and run any command

2 participants