- Add optional globalDir parameter to resolveProject(), getDefaultProject(), setDefaultProject(), and clearDefaultProject() for test isolation - Remove ESLint suppression by using void operator for intentionally unused var - Update all tests to use isolated globalDir parameter - Complete tests for default project resolution
11 KiB
Task: KB-080 - Combined Dropdown and Text Entry Model Filter
Created: 2026-03-30 Size: M
Review Level: 2 (Plan and Code)
Assessment: This is a UI component refactor that replaces separate filter input + native select with a unified combobox pattern. Changes are localized to ModelSelectorTab but require careful UX handling for keyboard navigation and accessibility. Score: 5/8 — Blast radius: 1, Pattern novelty: 2, Security: 0, Reversibility: 2
Mission
Replace the separate filter text input and native HTML select dropdown in the ModelSelectorTab with a unified "combobox" component. When the user clicks the dropdown, it opens to show all models with a text input at the top for filtering. The user can type to filter models while the dropdown remains open, then click to select. This provides a cleaner, more intuitive model selection experience.
The component must maintain all existing functionality: provider grouping, "Use default" option, model filtering by provider/name/ID, selection state, keyboard navigation, and accessibility.
Dependencies
- None
Context to Read First
packages/dashboard/app/components/ModelSelectorTab.tsx— The component to modify. Currently uses separate filter inputs and native select elements.packages/dashboard/app/utils/modelFilter.ts— Existing filter logic that should be reused.packages/dashboard/app/api.ts—ModelInfointerface andfetchModels()API.packages/dashboard/app/components/__tests__/ModelSelectorTab.test.tsx— Existing tests to update/extend.packages/dashboard/app/styles.css— Search for.model-selector-*CSS classes.
File Scope
packages/dashboard/app/components/ModelSelectorTab.tsx(major refactor)packages/dashboard/app/components/__tests__/ModelSelectorTab.test.tsx(update tests for new component)packages/dashboard/app/styles.css(update CSS classes for new combobox pattern)
Steps
Step 1: Create Combobox Component
- Create an internal
ModelComboboxcomponent withinModelSelectorTab.tsx(or separate file if complex) - Component props interface:
value: string(provider/id combo like "anthropic/claude-sonnet-4-5" or "" for default)onChange: (value: string) => voidmodels: ModelInfo[]disabled?: booleanplaceholder?: stringlabel: stringid: string
- Combobox UI structure:
- Trigger button showing current selection (model name or "Use default")
- Dropdown panel containing:
- Text input for filtering (at top, auto-focused when opened)
- Results count indicator
- Clear button (×) when filter has text
- Scrollable list of models grouped by provider
- "Use default" option at top
- "No results" message when filter matches nothing
- Keyboard navigation:
- ArrowDown/ArrowUp to navigate options
- Enter to select highlighted option
- Escape to close dropdown
- Tab to move to next field (close dropdown)
- Click outside to close dropdown
- Use existing
filterModels()utility for filtering logic
Artifacts:
packages/dashboard/app/components/ModelSelectorTab.tsx(modified with new combobox component)
Step 2: Refactor ModelSelectorTab to Use Combobox
- Remove separate filter state variables (
executorFilter,validatorFilter) - Remove
filteredExecutorModelsandfilteredValidatorModelsuseMemo hooks - Remove
executorModelsByProviderandvalidatorModelsByProvideruseMemo hooks - Replace the
<input>+<select>pairs with the newModelComboboxcomponent for both executor and validator - Keep the current selection display (the model badges showing current saved values)
- Keep the "Save" and "Reset" buttons and their handlers
- Keep all loading, error, and empty states
- Ensure the combobox is disabled during
isSavingstate
Artifacts:
packages/dashboard/app/components/ModelSelectorTab.tsx(refactored to use combobox)
Step 3: Update CSS Styles
- Remove or deprecate
.model-selector-filterrelated styles (old separate filter input) - Add new CSS classes for combobox:
.model-combobox— container.model-combobox-trigger— clickable trigger button.model-combobox-dropdown— dropdown panel.model-combobox-search— filter input inside dropdown.model-combobox-list— scrollable options list.model-combobox-option— individual option row.model-combobox-optgroup— provider group header.model-combobox-option--highlighted— keyboard navigation highlight.model-combobox-option--selected— selected state.model-combobox-no-results— empty state
- Match existing visual style (colors from CSS variables, border-radius, etc.)
- Ensure dropdown has proper z-index and positioning
- Style the "Use default" option distinctly (muted text)
- Style provider group headers (muted, uppercase, smaller font)
Artifacts:
packages/dashboard/app/styles.css(new combobox styles added)
Step 4: Update Tests
- Update existing tests that rely on native
<select>elements to use new combobox interactions:- Replace
screen.getByLabelText()for selects with combobox trigger - Replace
user.selectOptions()with combobox open + click option pattern
- Replace
- Add new tests for combobox behavior:
- Opening dropdown shows all models
- Typing in search input filters models
- Keyboard navigation (arrow keys, enter, escape)
- Clicking outside closes dropdown
- Selecting a model updates the value
- "Use default" option works
- Ensure
filterModelsutility is still being used correctly - All existing test cases must pass with new component
Artifacts:
packages/dashboard/app/components/__tests__/ModelSelectorTab.test.tsx(updated tests)
Step 5: Testing & Verification
ZERO test failures allowed. Full test suite as quality gate.
- Run
pnpm testto verify all tests pass - Run
pnpm buildto verify no TypeScript errors - Manual verification checklist (test in browser):
- Combobox opens when clicked
- Filter input is auto-focused when dropdown opens
- Typing filters models in real-time
- Clear button (×) clears filter
- Arrow keys navigate options
- Enter selects highlighted option
- Escape closes dropdown without selection
- Clicking outside closes dropdown
- Provider grouping is visually clear
- "Use default" option appears at top
- Selected model shows in trigger button
- Saving persists the selection
- Works in both light and dark themes
Step 6: Documentation & Delivery
- Update component comments if needed explaining the combobox pattern
- Create changeset file for the UI improvement:
cat > .changeset/combined-model-dropdown.md << 'EOF' --- "@dustinbyrne/kb": patch --- Improved model selector UX with combined dropdown and text entry for easier filtering and selection. EOF - Verify no out-of-scope changes were made
Documentation Requirements
Must Update:
- None (code is self-documenting with clear component structure)
Check If Affected:
- Any dashboard user documentation mentioning model selection (unlikely at this stage)
Completion Criteria
- All steps complete
- All tests passing (
pnpm test) - Build passes (
pnpm build) - Combobox works identically for both executor and validator model selection
- No visual regressions in other parts of the dashboard
- Accessibility maintained (keyboard navigation, ARIA labels where appropriate)
Git Commit Convention
Commits at step boundaries. All commits include the task ID:
- Step completion:
feat(KB-080): complete Step N — description - Bug fixes:
fix(KB-080): description - Tests:
test(KB-080): description
Example commits:
feat(KB-080): complete Step 1 — create combobox componentfeat(KB-080): complete Step 2 — refactor ModelSelectorTab to use comboboxfeat(KB-080): complete Step 3 — add combobox CSS stylestest(KB-080): update tests for combobox componentfeat(KB-080): complete Step 5 — all tests passing
Do NOT
- Install external UI libraries (create the combobox using native React/DOM)
- Change the
ModelInfointerface or API responses - Modify
filterModels()utility (reuse it as-is) - Remove or change the "Model Configuration" tab structure in TaskDetailModal
- Change how model overrides are saved/loaded (only the UI changes)
- Add animations that slow down the interaction
- Change the filter algorithm behavior (keep multi-word AND logic)
- Break KB-081 (ensure the component structure allows for future icon additions)
Implementation Notes
Combobox Pattern Details
The combobox should follow this interaction pattern:
-
Closed State: Shows a button/trigger displaying current selection (model name) or "Use default"
-
Open State: Dropdown appears below trigger with:
- Search input at top (auto-focused, placeholder: "Filter models…")
- Results count (e.g., "3 models")
- Clear button (×) when filter has text
- Scrollable list:
- "Use default" at top (always visible)
- Provider groups with headers ("anthropic", "openai", etc.)
- Model names under each provider
- "No models match 'filter'" message when empty
-
Filtering: As user types in search input,
filterModels()is called and list updates in real-time -
Selection: Clicking an option or pressing Enter on highlighted option:
- Sets the value ("provider/modelId" format)
- Calls
onChange - Closes dropdown
-
Keyboard Navigation:
- ArrowDown: Open dropdown (if closed) or move highlight down
- ArrowUp: Move highlight up
- Enter: Select highlighted option
- Escape: Close dropdown without selection
- Tab: Close dropdown, move focus
Reference Implementation Approach
Look at the existing dep-dropdown pattern in InlineCreateCard for similar dropdown behavior:
dep-dropdownCSS class for dropdown containerdep-dropdown-searchfor search inputdep-dropdown-itemfor selectable items- Position absolute below trigger
State Management
Keep combobox state internal to the component:
isOpen: boolean— dropdown visibilityhighlightedIndex: number— for keyboard navigationlocalFilter: string— search text (only while dropdown is open)
When dropdown closes, reset localFilter to empty string.
Data Flow
ModelSelectorTab (task.modelProvider, task.modelId)
↓
ModelCombobox value="anthropic/claude-sonnet-4-5"
↓ (user selects)
onChange("openai/gpt-4o")
↓
ModelSelectorTab setExecutorProvider/ModelId
↓ (user clicks Save)
updateTask API call