Skip to content

fix(skills): the Settings list and the composer picker match a query word by word (F5) - #289

Merged
Broccolito merged 1 commit into
mainfrom
fix/chip-a781334f
Sep 12, 2026
Merged

fix(skills): the Settings list and the composer picker match a query word by word (F5)#289
Broccolito merged 1 commit into
mainfrom
fix/chip-a781334f

Conversation

@Broccolito

Copy link
Copy Markdown
Collaborator

QA finding F5's third and fourth copies. SkillsView (Settings → Skills) and BottomMenuSkillSelection (the composer's skill menu) each filtered installed skills by asking whether the whole lowercased query occurred inside one field — so a phrase naming two installed skills found neither, and a one-letter query found every row whose prose happened to hold that letter.

Measured on origin/main (5a404ec), before the change

The new tests were written first and run against unmodified main: 5 failed, 32 passed.

query surface before after
R scripting ggplot visualization (3 rows: ggplot, pdf, r-scripting) Settings → Skills 0 rows — the empty state r-scripting, then ggplot
same composer picker 0 rowsUnable to find role="menuitemcheckbox" r-scripting, then ggplot
ggplot alone either 1 row 1 row
R (2 rows: markdown-render, r-scripting) either 2 of 2 rows r-scripting only
R, "Enable all" composer picker Enable all (2) Enable all (1)

That last row is why the letter leak is not just a long list: "Enable all" writes every row the filter left on screen, so a leaking filter is a bulk write over rows the user never asked about.

After the change, the same suites are green: 71 passed across src/components/skills/ + BottomMenuSkillSelection.test.tsx; 246 passed including src/components/baam/; 155 passed across the eleven downstream suites that mount ChatInput, App routing or Hub.

The change

Neither surface got a matcher. They got a field list: the new ui/desktop/src/components/skills/searchCatalog.ts imports baam/search.ts — the port of crates/biorouter/src/catalog_search.rs — and says only what text of an installed row is searched and what a match there is worth. These two pickers, the Browse modals and the model's skills__searchSkills now read the same words the same way.

baam/search.ts was deliberately not moved. The matcher is shared by import; only its directory name is now narrower than its callers. A rename would rewrite four BAAM files, and the header that has to stay accurate — the one warning that a rule change is a change to three files, landing/marketplace-search.js included — belongs to that file.

Decision 1 — what a member name is worth on a bundle row

A bundle ROW has no counterpart in Rust: search_fields (agents/skills_extension.rs) ranks individual skills, so a member's name is the Name field of a different entry. Here the package is the row — in the composer only the package can be toggled — so member names must be searchable or a package is unfindable by what it contains.

They are searched at Weight.Label, argued rather than ported: on a bundle row a member's name is not what the row is called, it is a label saying what the row contains — the role a tag plays on a marketplace card.

The weight is load-bearing, and the scores are asserted rather than just the order. For the query ggplot, measured on this tree: a skill of its own by that name scores 39, a package merely containing a member called ggplot scores 36. At Name both score 39 and the tie falls to catalog order — which lists every bundle before every single skill, so the package would silently win every such query. I flipped the weight to Name once to check the assertion can fail: it goes red with expected [ 39, 39 ] to deeply equal [ 39, 36 ].

A single skill's fields are left exactly as Rust has them — name → Name, description → Prose, bundle → Label — so this PR changes how the installed catalog is matched, not which fields are read.

Decision 2 — SkillsView was throwing the ranking away

It re-grouped by provenance (Biorouter / per-extension / other-agent / project), which discards the rank: a Biorouter skill matching one word of the query would sit above a project skill matching all of them. Under a query it is now one ranked "Matches (n)" list, copying BrowseSkillsModal down to the heading; with an empty query the provenance headings are unchanged.

Deletability moved from the group to the row with it — a Matches list mixes provenances, and an extension-supplied skill must still offer no Delete. A test asserts exactly that inside the Matches list.

Deliberately not changed

Gates

npm run lint:check exit 0 (tsc --noEmit, eslint --max-warnings 0, themes, 332 contrast assertions, token mirrors) and npx prettier --check clean on src/components/skills/ and src/components/bottom_menu/.

Closes chips task_a781334f and task_359d7a24, which describe these same two surfaces from either side.

🤖 Generated with Claude Code

…word by word (F5)

QA finding F5's third and fourth copies. Both surfaces filtered installed
skills by asking whether the WHOLE lowercased query occurred inside one
field, so a phrase naming two installed skills found neither, and a
one-letter query found every row whose prose held that letter.

Measured on this tree before the change, over the rows the two pickers
render: `R scripting ggplot visualization` listed 0 of 3 rows on both
surfaces while `ggplot` alone listed 1; `R` listed 2 of 2 rows, one of
them `markdown-render`, which holds the letter twice and means nothing
by it. In the composer that second case is a bulk write, not just a long
list: "Enable all" writes every row the filter left on screen, and it
read "Enable all (2)".

Both now go through `skills/searchCatalog.ts`, which is not a matcher —
it is the field list. The rules stay in `baam/search.ts`, the port of
`crates/biorouter/src/catalog_search.rs`, so these two pickers, the
Browse modals and the model's own `skills__searchSkills` read the same
words the same way.

Two decisions the shared matcher could not settle, both recorded in the
new file and pinned by its tests:

- A bundle ROW has no counterpart in Rust, where a member's name is the
  Name field of its own entry. Member names are searched at Label: on a
  bundle row a member's name is not what the row is called, it is a
  label saying what the row contains. Measured for the query `ggplot`, a
  skill of its own by that name scores 39 against a package containing a
  member of that name at 36. At Name both score 39 and the tie falls to
  catalog order, which lists every bundle first — so the package would
  win every such query. Flipping the weight was run once: the assertion
  goes red with [39, 39].

- SkillsView grouped by provenance, which discards the rank. Under a
  query it is now one ranked "Matches (n)" list, as BrowseSkillsModal
  does; browsing keeps the headings. Deletability moved from the group
  to the row with it, since a Matches list mixes provenances and an
  extension-supplied skill must still offer no Delete.

Rust is untouched: the field weights for a single skill are exactly
`search_fields` in agents/skills_extension.rs.
@Broccolito
Broccolito merged commit 27c0327 into main Sep 12, 2026
16 checks passed
@Broccolito
Broccolito deleted the fix/chip-a781334f branch September 12, 2026 18:29
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