Skip to content

Surface recently-used commands in the Command Palette (⌘K) #56

Description

@DragonnZhang

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:

  1. 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.
  2. 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.
  3. 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions