Problem or Motivation
OpenWork's global command palette (⌘K / Ctrl+K, shipped in #42) lists every
registered action grouped by category, but it has no memory of what you
actually run. Every time you open it you start at the same static list and must
re-search — even for the command you ran ten seconds ago.
Every mature command-palette surface fixes this with a "Recently used"
section pinned to the top: VS Code's ⌘⇧P, Raycast, Linear's ⌘K, and Claude Code
Desktop's command menu all float your most-recent commands first so the actions
you use are one keystroke away. OpenWork's palette is the conspicuous exception.
Proposed Solution
Add a "Recently used" group at the top of the command palette:
- Each action run from the palette is appended to a recents list (most-recent
first), persisted in the renderer's localStorage (global, like a shell
history), de-duplicated (an action never appears twice), and capped at 6
entries.
- When the palette opens with an empty query, the recents render in a
dedicated "Recently used" group above the category groups.
- The recents list is filtered to actions that still exist, aren't excluded from
the palette, and can actually run right now (canExecute) — so it never
surfaces a stale or dead command.
- As soon as you type a query, the recents group is hidden — a search
should rank by relevance, not recency — and normal cmdk filtering takes over.
Clearing the query brings the recents back.
Feasibility
Classification: pure frontend — candidate. No qwen-code backend involvement.
- The palette already lists the centralized action registry and dispatches via
execute(); recording a recent is a one-line call in the palette's existing
runAction. No new IPC, no main-process change.
- Persistence reuses the existing
lib/local-storage.ts KEYS/get/set
helper (one new key, command-palette-recents), exactly as other renderer UI
prefs do.
- The ordering logic (
pushRecent: dedupe → prepend → cap) is a small pure
module, fully unit-testable without a DOM.
- The recents group reuses the same cmdk primitives (
CommandGroup/CommandItem
/CommandShortcut) the palette already renders. One new i18n key
(commands.recent) added to all 7 locales for the group heading.
Alternatives Considered
- Frecency (frequency + recency) ranking: richer, but heavier and harder to
reason about; a simple most-recent-first list matches shell/VS-Code behaviour
and can be layered up later.
- Showing recents even while searching: conflicts with cmdk's relevance
filtering and produces duplicate rows; hiding the group for non-empty queries
is the standard, cleaner behaviour.
- Per-workspace recents: a global history matches the palette's global,
cross-workspace action set and is simpler; can be scoped later if wanted.
Additional Context
Confirmed genuinely missing: CommandPalette.tsx builds its groups purely from
actionsByCategory with no persistence and no recents concept.
Acceptance criteria (CDP e2e assertion)
A new e2e/assertions/command-palette-recents.assert.ts drives the real built
app over CDP entirely in the draft (no-session) state — no seeded conversation
and no backend. The palette reads recents fresh from localStorage on each open,
so the assertion seeds them and walks the full lifecycle:
- Seed
localStorage['craft-command-palette-recents'] = ['app.toggleTheme'],
open the palette → a "Recently used" group renders exactly one row
(Toggle Theme), above the first category row.
- Type
sidebar → the recents group hides (zero recent rows) while normal
filtering still surfaces a Toggle Sidebar match; clearing the query brings
the recents group back.
- Run a new, not-yet-recent action (
Toggle Sidebar) from the palette, then
reopen it → the recents group now has two rows with Toggle Sidebar
first and Toggle Theme second.
The reopen step is load-bearing: reading the palette after running an action
proves it actually persists what you run and orders recents newest-first —
not merely that a seeded list can render. In addition, the pure pushRecent
module ships with unit tests covering dedupe, move-to-front, cap, and immutability.
Part of the autonomous desktop-feature loop (loop-bot).
Problem or Motivation
OpenWork's global command palette (⌘K / Ctrl+K, shipped in #42) lists every
registered action grouped by category, but it has no memory of what you
actually run. Every time you open it you start at the same static list and must
re-search — even for the command you ran ten seconds ago.
Every mature command-palette surface fixes this with a "Recently used"
section pinned to the top: VS Code's ⌘⇧P, Raycast, Linear's ⌘K, and Claude Code
Desktop's command menu all float your most-recent commands first so the actions
you use are one keystroke away. OpenWork's palette is the conspicuous exception.
Proposed Solution
Add a "Recently used" group at the top of the command palette:
first), persisted in the renderer's
localStorage(global, like a shellhistory), de-duplicated (an action never appears twice), and capped at 6
entries.
dedicated "Recently used" group above the category groups.
the palette, and can actually run right now (
canExecute) — so it neversurfaces a stale or dead command.
should rank by relevance, not recency — and normal cmdk filtering takes over.
Clearing the query brings the recents back.
Feasibility
Classification: pure frontend — candidate. No qwen-code backend involvement.
execute(); recording a recent is a one-line call in the palette's existingrunAction. No new IPC, no main-process change.lib/local-storage.tsKEYS/get/sethelper (one new key,
command-palette-recents), exactly as other renderer UIprefs do.
pushRecent: dedupe → prepend → cap) is a small puremodule, fully unit-testable without a DOM.
CommandGroup/CommandItem/
CommandShortcut) the palette already renders. One new i18n key(
commands.recent) added to all 7 locales for the group heading.Alternatives Considered
reason about; a simple most-recent-first list matches shell/VS-Code behaviour
and can be layered up later.
filtering and produces duplicate rows; hiding the group for non-empty queries
is the standard, cleaner behaviour.
cross-workspace action set and is simpler; can be scoped later if wanted.
Additional Context
Confirmed genuinely missing:
CommandPalette.tsxbuilds its groups purely fromactionsByCategorywith no persistence and no recents concept.Acceptance criteria (CDP e2e assertion)
A new
e2e/assertions/command-palette-recents.assert.tsdrives the real builtapp over CDP entirely in the draft (no-session) state — no seeded conversation
and no backend. The palette reads recents fresh from
localStorageon each open,so the assertion seeds them and walks the full lifecycle:
localStorage['craft-command-palette-recents'] = ['app.toggleTheme'],open the palette → a "Recently used" group renders exactly one row
(
Toggle Theme), above the first category row.sidebar→ the recents group hides (zero recent rows) while normalfiltering still surfaces a
Toggle Sidebarmatch; clearing the query bringsthe recents group back.
Toggle Sidebar) from the palette, thenreopen it → the recents group now has two rows with
Toggle Sidebarfirst and
Toggle Themesecond.The reopen step is load-bearing: reading the palette after running an action
proves it actually persists what you run and orders recents newest-first —
not merely that a seeded list can render. In addition, the pure
pushRecentmodule ships with unit tests covering dedupe, move-to-front, cap, and immutability.
Part of the autonomous desktop-feature loop (
loop-bot).