Command palette: full-app coverage, rich controls and teleport
The Ctrl+Shift+P master command
palette is Material Design 3's full-screen search view. That is the
accelerator the application menu actually registers for the
command-palette item, and since the MD3 shell landed it is
also what the header's palette chip prints, because the chip is a
read-through of the binding rather than a second copy of it — the two
cannot drift. This is a deliberate, documented departure from the shared
product contract, which binds every palette to
Ctrl+Shift+F: in this application that
chord has always been Show in Explorer (Finder on
macOS), the binding GitHub Desktop ships upstream, and taking it away
from the file browser broke a habit users brought with them. So the
desktop app alone keeps Ctrl+Shift+F on the repository folder and opens
the palette on Ctrl+Shift+P; the Pages site and the documentation hub,
which have no repository folder, follow the contract and open their
palettes on Ctrl+Shift+F. Pull, which briefly sat on Ctrl+Shift+P, is
Ctrl+Shift+L. The palette covers the entire app below the title bar
rather than floating as a small card, and its rows are no longer just
names to dispatch — a row that is a setting renders the setting's live
control inline, and choosing any row teleports to the place in the app
where that feature actually lives.
Behaviour
The card
centres itself with left, never with
transform
The medium and compact cards are positioned by
left: max(margin, calc(50vw - half the card)). They used to
be centred with left: 50% plus
transform: translateX(-50%), and that lasted exactly one
frame: the Dialog component keeps every floating dialog on screen by
writing an inline transform: translate(x, y) on drag and on
resize, an inline transform replaces the stylesheet's rather than adding
to it, and the entrance keyframes own transform while they
play. So the resize observer measured the card at left: 50%
with no centring shift and at the keyframes' opening scale of 0.82, the
clamp wrote a correction for that box, and the correction stuck. At a
1280×800 window the palette sat 71px past the right edge with its close
button, regex-builder button and appearance toggle unreachable; the real
built app measured it at translateX(-168.8px) where
-440px was intended. Centring by left alone
leaves transform to the drag and clamp logic, and
command-palette-size-contract-test.ts fails on any
transform or left: 50% returning to the card
sizes.
In the same pass the results pane became its own inline-size container so the group chip hides when the pane is narrower than 600px (inside the medium card it is about 476px) rather than only when the window is narrow, a plain command's action zone no longer reserves the 132px an inline control needs (its Run button is always in the tree and holds its own width), the title, the place it lives and the search-terms line wrap instead of ending in an ellipsis, and the closed select has room for its longest option; a row whose text column would drop under 200px wraps its control beneath the text instead, and the location line keeps its anchor icon on the first line beside its text. The palette's own icon buttons (the appearance toggle and the apply button) and the dialog header's close button are 40px targets, the pointer minimum the layout audit holds every control to; the Run pill keeps at least 32px. Measured in the built app at 1280×800 before the change, "Show emojis in dialogs and message boxes", "TUI language, appearance, and notifications" and even "Language mode" lost their last words to the ellipsis.
Full-app surface
- The palette opens over the whole window
(
#command-palette.command-palette-full), leaving only the title bar (and its caption buttons) reachable. Content is constrained to a 1180px column on wide windows, per MD3's readable-width rule, instead of stretching edge to edge. - The layout is a docked search field (pill,
surface-container-highest), a results list on the left, a detail pane on the right, and a footer with the match count and keyboard hints. Below 1080px the detail pane drops out; below 720px the group chips drop too. Nothing clips.
Rich controls
Commands that represent settings declare a control in
the catalog (app/src/lib/command-palette-catalog.ts), and
the palette renders the control that matches the value:
| Control kind | Rendered as | Examples |
|---|---|---|
toggle |
Material switch | Dark theme, notifications, |
| confirm force push, | ||
| side-by-side diff | ||
entry |
Text box + Apply (✓), | Commit summary, |
| applies on Enter | clone-from-URL | |
number |
Numeric box with range | Playfulness 1–5, |
| hint | diff tab size 1–16 | |
choice |
Select | Language mode (English / |
| Cantonese / bilingual) |
- Values are live:
Apppasses acontrolValuesmap read from real app state, andonControlChangewrites through the same dispatcher paths the Settings panes use. Changing a control does not close the palette. - A control whose live value is unknown renders disabled rather than pretending a default is the current setting.
- Out-of-range numbers are clamped to the declared bounds; empty entries are not applied. One-shot entries (clone URL) hand off to their dialog and close the palette; persistent entries (commit summary) keep showing the value.
Teleport — "take me to where it lives"
- Every command resolves a home
(
resolvePaletteHome): a Settings tab plus an anchored row, a toolbar item, a sidebar rail tab, the commit box, or — by default — the dialog the command itself opens. - Clicking a row or pressing Enter teleports: the
owning surface is opened, the exact control is scrolled into view,
ringed with a two-pulse primary spotlight
(
.teleport-spotlight,app/styles/ui/_teleport.scss) and handed focus. Ctrl+Enter (or the row's Run button) executes the command instead. Destructive or network commands (push, force push, discard, remove repository) declare homes with no self-opener, so teleporting to them can never fire them. - Targets resolve through the symbolic registry in
app/src/lib/teleport-targets.ts— structural hooks the app already renders ([data-toolbar-item-id="sync"],#history-tab) plus explicitdata-teleport-targetanchors added to settings rows. The DOM lookup polls for up to 4s so a surface that is still animating in can be found; if the surface never appears, a non-blocking notification says so instead of failing silently. - The detail pane names the home ("Where it lives — Toolbar", "Settings › Appearance") with Go there / Run buttons, and each row carries a compact home label.
Accessibility
- The results list is a
listboxwired to the search box viaaria-controls/aria-activedescendant; each option's accessible name is "title — home — current value" so screen readers hear where the feature lives and what the setting currently is. - All controls are keyboard-reachable; arrows inside a text box move
the caret, not the highlighted row. The spotlight honours
prefers-reduced-motion(ring stays, pulse drops), as does the palette's hover-revealed Run action (always visible under reduced motion).
Configuration
- The appearance editor (density, icons, group chips, keyword line, random-per-repository) is unchanged and applies to the full-screen layout.
- Language modes and the per-language funny levels style every palette
string through the standard i18n resources
(
commandPalette.*,palette.*). - When School mode is active, the language-mode and playfulness rows are omitted from the palette. The renamed School mode row remains available so the user can reach its unlock control.
Failure modes
- Missing teleport target → non-blocking notification naming the surface that was not on screen; nothing is highlighted; no state is changed.
- Unknown control value → control renders disabled.
- Invalid or empty entry → not applied; the user's draft is kept.
Security considerations
Teleport selectors are a fixed compile-time registry — no user input is ever interpolated into a DOM query. Control writes go through the same dispatcher setters the Settings panes use, so no new mutation paths exist.
Verification
app/test/unit/command-palette-catalog-test.ts— controls declare valid shapes, every home resolves and localizes, destructive commands never open via self, every Preferences tab maps to a registered event, selector registry is unique and syntactically valid.app/test/unit/ui/command-palette-rich-test.tsx— live switch renders and writes without dismissal, entry applies on Enter, select writes choices, Enter/click teleport vs Ctrl+Enter run, rows announce their home, unknown values disable controls.app/test/unit/ui/filter-mode-surfaces-test.tsx— search modes, regex builder and appearance editor still pass against the full-screen palette.