Offer view-scoped actions in the quick palette - #2268
Closed
andrewkchan wants to merge 5 commits into
Closed
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was wrong
The quick palette could only list app commands, so everything without a stable
AppCommandIdbehind it was unreachable from the keyboard: switching to anamed model, opening a particular thread, starting a thread in another project,
opening a workspace in a target that is not the preferred one.
AppCommandIdis 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 plususeRegisterPaletteActions. A component registers a function, not anarray, 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 byAppLayout) — a new threadin any project, and recent threads by title. Reads the sidebar bootstrap
AppLayoutalready holds, so no extra query and no second realtimesubscription. 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 mountedpickers 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.openPreferredcommand that only ever opens thepreferred 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_VERSIONuntouched.How you verified
10 tests: 9 unit and 1 integration.
CommandPalette.test.tsx— a registered provider's row appears beside theapp 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 personalproject (a separate field from
projects), recency ordering and titlefallback, 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 typecheckandlintclean for@bb/app(0 errors); theapp 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.tsxwalksplugins/and throwsENOENTon a stray emptyplugins/monaco/directory in the local checkout.