Skip to content

feat(tool): ToolPicker for browsing, searching and selecting tools (CL-8483) - #45

Merged
TheGreatAxios merged 9 commits into
mainfrom
cl-8483-tool-picker
Sep 21, 2026
Merged

TheGreatAxios merged 9 commits into
mainfrom
cl-8483-tool-picker

Conversation

@TheGreatAxios

Copy link
Copy Markdown
Contributor

Summary

  • New ToolPicker component (src/ui/tool-picker.tsx): browses, searches and selects tools from a caller-supplied catalog, grouped by package
  • Controlled value/onChange supports single-select (default) and multi-select via multiple
  • Handles loading, empty, and per-item pending/disabled status states; keyboard navigation reuses useCommandPaletteNavigation
  • Exported via generated barrel/exports (bun run build)

Test plan

  • bun run typecheck
  • bun run lint
  • bun test (344 pass, including new tool-picker.test.tsx)
  • bun run build (contrast gate passes)
  • bun run dep-guard
  • Story: stories/primitives/tool-picker.stories.tsx — single select, multi select, loading, empty

@TheGreatAxios

Copy link
Copy Markdown
Contributor Author

Review

Verdict: approve. Controlled ToolPicker with tests for group/select/multi/disabled/search/loading/empty. No CI. No prior reviews. Nit: no keyboard tests; disabled items stay in the nav list and Enter no-ops via toggle.

@TheGreatAxios

Copy link
Copy Markdown
Contributor Author

Review (Draper + Emil)

Verdict: request changes (empty copy + Escape).

Grouping and controlled select are right.

  • Empty catalog and failed search share “No tools match.” Split like CommandPalette (no catalog vs no matches). Don’t render group.id as the visible title if a display name exists.
  • Combobox that swallows Escape should not; Escape ought to clear search or close the way CommandPalette does.

Empty-copy split in flight if the default is still that string.

@TheGreatAxios

Copy link
Copy Markdown
Contributor Author

Re-review

Empty catalog vs no matches; Escape clears query (c8a6e1b). bun test 346 pass. Group headings still use group.id.

@TheGreatAxios TheGreatAxios left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid. Reusing useCommandPaletteNavigation instead of a second keyboard implementation is the right move, the combobox → aria-activedescendant → option-id wiring is correct, and keeping disabled items visible is documented and defensible.

Two notes:

  1. ToolPicker does its own substring filtering while CommandPalette deliberately leaves matching/ranking to the caller. Defensible for a bounded local catalog — worth one line in the doc comment so the deviation reads as chosen, not missed.
  2. onKeyDown on the listbox div is dead — options aren't focusable, so it can't fire there. Harmless; can drop.

Merge.

@TheGreatAxios

Copy link
Copy Markdown
Contributor Author

Re-review

Group headings humanize slugs (slack → Slack). SHA 04fb3ed. bun test 347 pass.

Default empty copy matches CommandPalette: no catalog vs failed search.
Escape clears the query and does not swallow a second Escape.
Sonner replays active toasts onto the next Toaster subscriber, so leftover
confirmations from a prior case stacked (Expected 1, Received 2).
Slugs like slack and google-drive render as Slack / Google Drive. A
package string that is already a display name is kept. Drop the
uppercase shout. Escape and empty-copy split are unchanged.
@TheGreatAxios

Copy link
Copy Markdown
Contributor Author

Review pass — rebased onto post-prune main + fixes applied

Rebased after the prune (#55) and test co-location (#50): generated exports regenerated, removed a dead onKeyDown on the non-focusable listbox (navigation lives in the input).

Persona review findings — all resolved

  • Keyboard nav could strand the active row (greybeard, emil): useEffect now scrolls [data-active] into view (block: "nearest") on every activeId change; active rows that are disabled keep the visual highlight so aria-activedescendant and the screen agree.
  • Incomplete combobox semantics (emil): added aria-autocomplete="list", aria-busy={loading}, spellCheck={false}, and a configurable searchLabel; loading/empty/no-match wrappers carry role="presentation".
  • Heavy no-match state (draper): EmptyState replaced with a compact muted message — empty-catalog still gets the full EmptyState.
  • Orange pending badges (draper): orange is a scarce action accent; a pending tool is not actionable. All status badges now tone="neutral"; selected cards keep border-primary-emphasis bg-primary/10.
  • Craft: group headings get tracked uppercase micro-label styling; rows get active:brightness-95 press feedback.
  • Architecture note: ARCHITECTURE.md now documents why ToolPicker self-filters (bounded, already-fetched catalog; must distinguish empty catalog from no match) while CommandPalette delegates matching to its caller (possibly unbounded/server-backed space).

Validation

236 tests, typecheck, lint, dep-guard, full build, contrast gate — all green at 959c05c.

@TheGreatAxios
TheGreatAxios merged commit 598e340 into main Sep 21, 2026
1 check passed
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