Skip to content

Offer view-scoped actions in the quick palette - #2268

Closed
andrewkchan wants to merge 5 commits into
get-bb:mainfrom
andrewkchan:bb/quick-palette-view-actions-thr_6uabvx9wey
Closed

Offer view-scoped actions in the quick palette#2268
andrewkchan wants to merge 5 commits into
get-bb:mainfrom
andrewkchan:bb/quick-palette-view-actions-thr_6uabvx9wey

Conversation

@andrewkchan

Copy link
Copy Markdown
Collaborator

What was wrong

The quick palette could only list app commands, so everything without a stable
AppCommandId behind it was unreachable from the keyboard: switching to a
named model, opening a particular thread, starting a thread in another project,
opening a workspace in a target that is not the preferred one. AppCommandId
is a closed enum the server owns, so those actions cannot become commands —
which left the palette a searchable view of the shortcuts a user already had.

What changed

Views can now contribute rows.

  • palette-registry.ts — a provider registry plus
    useRegisterPaletteActions. A component registers a function, not an
    array, and the palette calls it when it opens, handing it the element that
    was focused beforehand. That is what makes scoping possible: every composer
    mounts a model picker and every split pane a thread view, so a static array
    would produce duplicate rows with no way to tell which surface owns them. A
    provider that throws is contained.
  • useNavigationPaletteActions.ts (mounted by AppLayout) — a new thread
    in any project, and recent threads by title. Reads the sidebar bootstrap
    AppLayout already holds, so no extra query and no second realtime
    subscription. Capped at 15 threads and excludes the current one.
  • ModelReasoningPicker.tsx — the focused composer's other models,
    reasoning efforts, and providers, by name. Scoped with the picker's existing
    ownsModelPickerCycleChord, which already resolves which of the mounted
    pickers a chord addresses from the focused pane, the caret's composer, and
    whether the picker is open. Reusing it keeps the palette and the cycle chords
    from drifting apart. This is also why the cycle commands are palette-hidden:
    they move one step against a picker the palette is covering up, whereas these
    rows name the destination.
  • ThreadDetailView.tsx — one row per configured workspace open target,
    beside the workspace.openPreferred command that only ever opens the
    preferred one. Focused pane only.
  • docs/configuration.md — what the palette lists beyond commands.

Rows that would do nothing are dropped throughout: the current model, effort,
and provider; options the picker would refuse; the thread already on screen.

No wire change, no new plugin API. HOST_DAEMON_PROTOCOL_VERSION untouched.

How you verified

10 tests: 9 unit and 1 integration.

  • CommandPalette.test.tsx — a registered provider's row appears beside the
    app commands, receives the pre-open focused element, and runs. This covers
    registration and target passthrough end to end, so the registry's unit tests
    cover only what it cannot: that a provider reads current props through a ref
    rather than a stale closure, and that one throwing provider does not take the
    palette down with it.
  • useNavigationPaletteActions.test.tsx — project rows include the personal
    project (a separate field from projects), recency ordering and title
    fallback, the cap plus current-thread exclusion, and the routes each row
    navigates to.
  • palette-composer-actions.test.ts — which options become rows.

pnpm exec turbo run typecheck and lint clean for @bb/app (0 errors); the
app suite passes 3255. Each of the five commits typechecks on its own.

One pre-existing failure is unrelated and reproduces on clean main:
PluginIcon.test.tsx walks plugins/ and throws ENOENT on a stray empty
plugins/monaco/ directory in the local checkout.

AGENT GENERATED: by Claude Opus 5

andrewkchan and others added 5 commits August 21, 2026 15:47
The palette could only list app commands, which excludes everything without a
stable command id behind it: switching to a named model, opening a particular
thread. Those are the rows that make a palette worth opening.

Add a provider registry and `useRegisterPaletteActions`. A component registers
a function rather than an array, and the palette calls it when it opens: the
same question app commands already answer, asked of a view. That matters for
surfaces that mount more than once -- every composer has a model picker, every
split pane a thread view -- because the provider receives the element focused
before the palette opened and can decline exactly as a command handler scoped
to an event target does. A provider that throws is contained; it must not cost
the user the palette.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Start a thread in any project by name, or reopen a recent thread by title,
both landing where the sidebar's own affordances land. Registered by AppLayout,
which already holds the sidebar bootstrap, so this adds no query and no second
realtime subscription.

Thread rows are capped at fifteen and exclude the thread already on screen: an
empty palette should be a short list of what is worth doing next, not a thread
list, and reopening the current thread does nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Choose a model, reasoning effort, or provider by name. This is the thing the
cycle chords cannot do -- they move one step against a picker the palette is
covering up -- which is why those commands are palette-hidden.

Rows come from the picker the cycle chords would have moved:
`ownsModelPickerCycleChord` already resolves which of the mounted pickers a
chord addresses, given the focused pane, the caret's composer, and whether the
picker is open, so the palette reuses it rather than inventing a second answer.
The current value and options the picker would refuse are dropped, since both
would be rows that do nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
One row per configured target, beside the `workspace.openPreferred` command
that only ever opens the preferred one. Scoped to the focused pane: each pane
mounts this view over its own workspace, so unscoped rows would be
indistinguishable duplicates pointing at different paths.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant