Repository navigation
Commit 9cb4e29
fix(app-shell): the command palette matches navigation entries on word prefixes and substrings (objectui#11812) (#11883)
Fixes #11812
Clause-②: no
## What changes
The ⌘K palette's navigation groups (Objects, Dashboards, Pages, Reports,
Switch App) and its theme and full-search commands were matched by
cmdk's default subsequence scorer: `CommandDialog` mounts cmdk's
`Command` with no `filter`, so every item goes through `commandScore`.
The palette now matches those entries itself and renders only the
matches.
- **One module-private matcher**, `matchesPaletteQuery` in
`packages/app-shell/src/chrome/paletteMatch.ts` (not re-exported). An
entry matches when the query, ignoring case, starts a word of its label
or machine name (`field` finds "Field Zoo", `zoo` finds
`showcase_field_zoo`), or, from three characters on, is a contiguous
substring of one of them (`view` finds "Task Overview"). A hyphen and a
space read alike, which is cmdk's own normalisation. No typo tolerance.
- **Han, Hiragana and Katakana** are written without spaces, so each of
their characters starts a word: `大屏` and `屏` both find "Command Center
(大屏)" (Zone 2 item 4, pinned).
- **`CommandPalette`** filters each navigation group, the app list, the
three theme commands and the full-search command through it, and renders
a group, with its separator, only when something in it matched.
- **No cmdk `value` changes.** Each rendered entry carries its label and
name in its value, so the trimmed query is a contiguous run of that
value and cmdk's scorer, which still ranks what is rendered and decides
when `CommandEmpty` shows, accepts every entry the palette rendered. "No
results" now shows exactly when no entry matched and the record search
found nothing.
- **Record hits are untouched**: the same item, value (the query
embedded), group and order.
### The mechanism, measured (Zone 2 item 1: confirmed)
cmdk 1.1.1's `commandScore`, run on the showcase entries' values:
| query | value | score |
|---|---|---|
| `zzzz` | `object Field Zoo showcase_field_zoo` | 9.900e-3 |
| `zzzz` | `page New Project (Wizard) showcase_new_project_wizard` |
9.900e-3 |
| `ingest` | `object In-Progress Tasks showcase_task` | 2.305e-2 |
| `ingest` | `object Cascading Select showcase_cascade` | 2.853e-3 |
| `ingest` | `page Page Authoring showcase_start_here` | 2.271e-2 |
| `ingest` | `page Styling (ADR-0065) showcase_styling_gallery` |
2.246e-2 |
| `wayne` | `page Styling (ADR-0065) showcase_styling_gallery` |
1.386e-4 |
These are exactly the false hits the card's QA pass read, so the scorer
alone explains them. `zzzz` scores above zero because the scorer may
skip a query letter that repeats the next one (a 0.1 factor each time),
so the two `z`s of "Field Zoo ... zoo" absorb four.
### Route taken (Zone 2 item 2)
- **Taken:** the palette matches, and renders only the matches, inside
`CommandPalette.tsx`. Neither `forceMount` nor a query carried in the
value was needed, because every entry the matcher accepts already scores
above zero in cmdk. `forceMount` was read in cmdk 1.1.1's source and set
aside: a force-mounted item that cmdk scores 0 is rendered but not
counted in `filtered.count`, so "No results" could show beside it.
- **Not taken:** composing `Dialog` and `Command` from
`@object-ui/components` with a `filter`. Both are exported, but that
copies `CommandDialog`'s composition (the visually-hidden title and
description of ADR-0054 C4, the cmdk class string) out of the
Shadcn-synced `ui/command.tsx` into app-shell, and a `filter` sees only
a value string, so record hits would need a kind marker in their value
to stay exempt. A `filter` passthrough on `CommandDialog` would be a new
prop on a published component (a Clause-② stop) in `ui/**` (AGENTS.md
rule 7). `CommandDialog` is not edited; the `overlay:command-palette`
test id and the accessible name are untouched (pinned by the empty-query
control).
### Theme and full-search commands (Zone 2 item 3: they follow the rule)
They are matched by the same rule over the words they were already
matched by (`theme light`, `theme dark`, `theme system`, `search all
results full page`). Under the old scorer scattered letters reached them
too: `hsy` scored 1.515e-1 on `theme system`, `tht` 1.686e-1 on `theme
light`.
### Behaviour changes a reviewer should know
- **Initials no longer match.** `fz` found "Field Zoo" and `sap` found
the full-search command (8.003e-1) through scattered word starts;
neither is a word prefix or a substring, so neither matches now. This is
the card's *Expected*.
- **The group's type word no longer matches.** `object`, `page`,
`dashboard`, `report` and `app` stay at the head of each cmdk value but
are not a matching term: `page` now finds "Page Authoring" and "Page
Variables", not every page. Matching is on what the entry is called, its
label and machine name, per the triage direction.
## Real-browser reading (Chromium 141, 1440×900)
The console's Vite dev server served a temporary probe page, deleted
afterwards and never committed, that mounts the real
`CommandPaletteProvider` (opened by `?palette=1`) and the real
`CommandPalette` under `I18nProvider` in `en`, over the showcase app's
whole navigation (its 50 leaf entries, read from objectstack
`examples/app-showcase`, `ShowcaseApp.navigation`), with a `searchAll`
stub holding three records (Ingest pipeline, Wayne Enterprises, 华宁科技).
Queries were typed key by key. *Before* is `CommandPalette.tsx` at this
branch's base `5d69133`; *after* is `a76ebb5`.
| query | before | after |
|---|---|---|
| `zzzz` | Objects: Field Zoo · Pages: New Project (Wizard) | "No
results found." |
| `ingest` | Task: Ingest pipeline · Objects: In-Progress Tasks,
Cascading Select · Pages: Page Authoring, Styling (ADR-0065) | Task:
Ingest pipeline |
| `wayne` | Account: Wayne Enterprises · Pages: Styling (ADR-0065) |
Account: Wayne Enterprises |
| `field` | Objects: Field Zoo | Objects: Field Zoo |
| `大屏` | Pages: Command Center (大屏) | Pages: Command Center (大屏) |
No page errors in either run. The showcase backend was not booted: the
sister objectstack checkout is not built in this container, so the
record hits come from the stub.
## Tests (at `a76ebb5`)
- `paletteMatch.test.ts` (new, 13 cases): the seven scattered pairs from
the card do not match; word prefixes of the label and of the machine
name match at any length; a mid-word substring matches from three
characters on and not before; phrases, hyphen and space, case; Han and
kana substrings; blank query and missing terms.
- `CommandPalette.navMatch-11812.test.tsx` (new, 7 cases), through the
real `CommandPaletteProvider` and cmdk: `zzzz` shows no entry and "No
results found." once the record search settles; `ingest` shows its
record hit and only the entry whose machine name contains it; `wayne`
shows Wayne Enterprises and no page; `field`, `view`, `大屏` and `屏` each
show their entry; apps, theme and full-search commands follow the rule.
Controls: the record hit's item is the one the palette always rendered
(`record wayne Wayne Enterprises Account showcase_account a1`), and an
empty query shows every entry, app and command.
- The existing palette suites (`CommandPalette.navLabelInherits-9868`,
`CommandPalette.searchLabelFls-10500`) pass unchanged: no cmdk value
moved.
### Ablation (direction: red, as expected)
`node scripts/ablation-replace.mjs` (objectstack's tool, run from this
worktree) replaced the `matches` helper with one that accepts every
entry (anchor 1 → 0, blob `77b858975656` → `0168da5f2e1f`), so cmdk's
default scorer decides again, and ran the DOM suite: **5 failed, 2
passed**. The five pins went red with the card's own readings (`zzzz`:
`['Field Zoo', 'New Project (Wizard)']`; `ingest`: In-Progress Tasks,
Cascading Select, Page Authoring and Styling (ADR-0065) beside Replay
Queue; `wayne`: Styling (ADR-0065)); the two controls stayed green.
Restore: blob back to `77b858975656` = HEAD, `git diff HEAD` empty,
marker count 0. The suite imports `./CommandPalette` from source, so no
`dist` sat between the mutation and the run.
## Gates (all at `a76ebb5`)
| command | exit | verdict line |
|---|---|---|
| `pnpm exec vitest run packages/app-shell/` | 0 | Test Files 1097
passed, 1 skipped (1098) · Tests 10803 passed, 9 skipped |
| `pnpm --filter @object-ui/app-shell type-check` (script name echoed;
`tsconfig.test.json` lists all four touched files) | 0 | `tsc --noEmit
&& tsc -p tsconfig.test.json` |
| `pnpm --filter @object-ui/app-shell build` | 0 | dist completeness: 1
package(s) complete |
| `pnpm exec eslint` on the 4 touched files | 0 | 0 errors; 13 warnings,
all in `CommandPalette.tsx`, rule-for-rule the same 13 as on base
`5d69133` |
| `pnpm check:control-bytes` | 0 | OK |
| `pnpm check:test-path-roots` | 0 | OK |
| `pnpm check:changeset-claims` | 0 | No pending changeset names a file
this change touches |
| `pnpm check:pending-changeset-literals` | 0 | No test source names a
pending changeset |
| `pnpm check:new-line-citations` | 0 | 0 new citation(s) |
| `pnpm check:i18n-keys` | 0 | Every in-scope call-site key resolves
against the en pack |
| `pnpm check:vi-mock-specifiers` · `check:vi-mock-inherit` ·
`check:vi-mock-override-shape` | 0 | OK |
| `pnpm check:unreferenced-sources` · `check:self-import` ·
`check:phantom-deps` · `check:esm-specifiers` | 0 | OK |
| `node scripts/check-changeset-presence.mjs` ·
`check-changeset-no-major.mjs` | 0 | 1 changeset declared; no `major` |
The eslint run is a narrowing, not the repo-wide `pnpm lint` (CI's):
eslint's own config globs `**/*.{ts,tsx}` cover all four files; its
`--format json` output lists 4 files, none ignored; and
`eslint.config.js` enables no type-aware linting (no `projectService` or
`parserOptions.project`), so this diff cannot move the verdict of a file
it does not touch. Locale packs and package exports are unchanged, so
the i18n pack gates and `check:readme-exports` do not apply. NOT
MEASURED locally: `check:node-esm-load` (its load leg builds the tree),
left to CI.
The public surface is unchanged: the built `dist/index.d.ts` and
`dist/chrome/index.d.ts` do not reach `paletteMatch.d.ts`; no export,
prop, type member or language-pack key is added. Patch changeset:
`.changeset/11812-palette-nav-match.md`.
`main` has moved to `04c8f56` since the base; none of those commits
touches `packages/app-shell/src/chrome/`, `ui/command.tsx`,
`useRecordSearch` or `CommandPaletteProvider`, and `git merge-tree`
against it is clean, so the branch was not merged.
## Acceptance notes
- The theme commands are matched by their English words in every locale,
as before: a zh viewer typing `主题` finds none of them, because their
translated labels were never in their values. Unchanged here; noted, not
filed. Carrier: none.
- The record search (`useRecordSearch`, the server call) and the
`?palette=1` deep link are untouched, per the triage ruling.
Session: `https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8`
(dispatched by the `domain:ui` seat 3 PM).
---
_Generated by [Claude
Code](https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 4373484 commit 9cb4e29
5 files changed
Lines changed: 491 additions & 42 deletions
File tree
- .changeset
- packages/app-shell/src/chrome
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
Lines changed: 238 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
0 commit comments