--- phase: 06-chat-ui-redesign plan: 01 type: execute wave: 1 depends_on: [] files_modified: - tauri-app/src-tauri/src/model_files.rs - tauri-app/src/components/ModelSelectorPill.tsx - tauri-app/src/components/Sidebar.tsx - tauri-app/src/App.tsx - tauri-app/src/components/ChatView.tsx autonomous: true requirements: [UI-01] must_haves: truths: - "scan_models returns only GGUF files at the root of the models directory (no subdirectory traversal)" - "Model selector dropdown appears inside the chat input area (both hero and regular input)" - "Model selector is absent from the Sidebar" - "Selecting a model in the input pill updates the active model" artifacts: - path: "tauri-app/src-tauri/src/model_files.rs" provides: "scan_model_dir_top_level function replaces recursive scan" contains: "scan_model_dir_top_level" - path: "tauri-app/src/components/ModelSelectorPill.tsx" provides: "Reusable model selector dropdown pill for input area" exports: ["ModelSelectorPill", "ModelSelectorPillProps"] - path: "tauri-app/src/components/Sidebar.tsx" provides: "Sidebar without model-section block" key_links: - from: "tauri-app/src/App.tsx" to: "tauri-app/src/components/ChatView.tsx" via: "new props: models, localModels, selectedModel, onModelChange, onModelSwap, isSwapping, status" pattern: "models=\\{models\\}" - from: "tauri-app/src/components/ChatView.tsx" to: "tauri-app/src/components/ModelSelectorPill.tsx" via: "import and render inside .input-box" pattern: " Fix the Rust model scan to return only top-level GGUF files, create a ModelSelectorPill component, move the model selector out of the Sidebar and into the ChatView input area. Purpose: The model selector must live in the chat input bubble (Manus AI pattern), not the sidebar. The scan bug returns unintended nested models from HuggingFace cache dirs. Output: `scan_model_dir_top_level` Rust function, `ModelSelectorPill.tsx` component, Sidebar cleaned of model-section, ChatView wired with model selector in both input boxes. @$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md @$HOME/.config/opencode/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/06-chat-ui-redesign/06-RESEARCH.md @tauri-app/src-tauri/src/model_files.rs @tauri-app/src/components/Sidebar.tsx @tauri-app/src/components/ChatView.tsx @tauri-app/src/App.tsx From tauri-app/src/components/Sidebar.tsx (lines 13-22) — props that move to ChatView: ```typescript interface SidebarProps { models: ModelInfo[]; // <- these 6 props move to ChatView localModels: RuntimeModelFile[]; selectedModel: string; onModelChange: (v: string) => void; onModelSwap?: (path: string) => void; isSwapping?: boolean; status: RuntimeStatus; // ... non-model props stay in Sidebar } ``` From tauri-app/src/components/ChatView.tsx (lines 18-28) — current ChatView Props: ```typescript interface Props { db: DB; chatId: number | null; server: string; apiKey: string; model: string; onChatCreated: (id: number) => void; onChatUpdated: () => void; onOpenSidebar?: () => void; onDegradationDetected?: () => void; } // New props to add: models, localModels, selectedModel, onModelChange, onModelSwap, isSwapping, status ``` From tauri-app/src/components/Sidebar.tsx (lines 56-71) — deduplication logic to extract: ```typescript // Merge API models + local scanned models, dedupe by id, filter isInChatSelector const allModels: ModelOption[] = [ ...models .filter((m) => m.id !== "" && m.id !== "local") .map((m) => ({ id: m.id, label: getDisplayName(m.id) })), ...localModels.map((m) => ({ id: m.path, label: getDisplayName(m.path) })), ].filter((m, i, arr) => arr.findIndex((x) => x.id === m.id) === i); const selectedLabel = getDisplayName(selectedModel) || "No model"; ``` New ModelSelectorPill interface (to create): ```typescript interface ModelSelectorPillProps { models: ModelInfo[]; localModels: RuntimeModelFile[]; selectedModel: string; onModelChange: (id: string) => void; onModelSwap?: (path: string) => void; isSwapping?: boolean; status: RuntimeStatus; } ``` Rust scan_models Tauri command signature (model_files.rs): ```rust #[tauri::command] pub fn scan_models(dir: String) -> Result, String> // Currently calls scan_model_dir() -> scan_model_dir_recursive() // Must change to call scan_model_dir_top_level() ``` Task 1: Fix Rust model scan to top-level only + update test tauri-app/src-tauri/src/model_files.rs Add a new `scan_model_dir_top_level(path: &Path) -> Result, String>` function that reads only the immediate children of `path` (depth=1) — no recursion. Only collects entries where `p.is_file()` and extension is `"gguf"` (case-insensitive). Sort by name. Keep `scan_model_dir_recursive` in place (used by existing tests and potentially future use) — do NOT delete it. Update `scan_model_dir()` to call `scan_model_dir_top_level` instead of `scan_model_dir_recursive` — OR bypass `scan_model_dir()` entirely and have the `scan_models` Tauri command call `scan_model_dir_top_level` directly. Update the `scan_model_dir_finds_gguf_in_subdirectories` test (line 196): rename it to `scan_model_dir_returns_only_top_level_gguf` and update the assertion so nested models are NOT in the result — only `["top-level.gguf"]`. Add a new test `scan_model_dir_recursive_still_finds_nested` that exercises `scan_model_dir_recursive` directly to ensure the recursive function still works. Verify: `cargo test -p commands` (or `cargo test` in `tauri-app/src-tauri/`) passes with no failures. cd tauri-app/src-tauri && cargo test scan_model 2>&1 | tail -10 All `scan_model*` tests pass. `scan_models` Tauri command uses top-level scan. Nested GGUFs are NOT returned by default. Task 2: Create ModelSelectorPill + wire into ChatView + clean Sidebar tauri-app/src/components/ModelSelectorPill.tsx, tauri-app/src/components/Sidebar.tsx, tauri-app/src/components/ChatView.tsx, tauri-app/src/App.tsx **Step A — Create `ModelSelectorPill.tsx`:** Create `tauri-app/src/components/ModelSelectorPill.tsx`. Extract the deduplication logic from `Sidebar.tsx` lines 56-71 into this component (or call a shared util). The component renders a compact pill button showing the selected model's display name (`getDisplayName`). Clicking opens a dropdown listbox (reuse the same HTML pattern as `Sidebar.tsx` lines 136-168: `model-dropdown-menu` with `model-dropdown-item` entries). Use `getDisplayName` from `lib/modelAlias`. Emit `onModelChange(id)` when item selected; if item is a local model path, also call `onModelSwap(path)`. Show a spinner/disabled state when `isSwapping` is true. Style: `bg-[#262626] text-[#9CA3AF] rounded-md px-2 py-1 text-sm flex items-center gap-1.5` for the pill trigger. Dropdown: absolute positioned, `bg-[#1E1E1E] border border-[#333333]`. Props: `ModelSelectorPillProps` as defined in the interface block above. **Step B — Clean Sidebar.tsx:** In `Sidebar.tsx`, delete the `model-section` div block (lines ~131-170) and remove from `SidebarProps`: `models`, `localModels`, `selectedModel`, `onModelChange`, `onModelSwap`, `isSwapping`. Remove the `status` prop if it was only used for model selector logic. Remove the deduplication logic (lines 56-69) and the `handleModelSelect` handler. Remove `ModelInfo` import if no longer used. **Step C — Update App.tsx:** In `App.tsx`, stop passing the 6 model props to ``. Add them as new props to ``: `models={models}`, `localModels={localModels}`, `selectedModel={selectedModel}`, `onModelChange={setSelectedModel}`, `onModelSwap={handleModelSwap}`, `isSwapping={isSwapping}`, `status={status}`. **Step D — Update ChatView.tsx:** Add the 6 model props to ChatView's `Props` interface. Import `ModelSelectorPill`. Render `` inside the input area in TWO places: 1. Hero input block (around line 657) — inside `.input-box`, before the textarea, on its own row or inline left. 2. Regular input block (around line 860-875) — same position inside `.input-box`. Do NOT change any existing chat logic, message handling, or streaming logic. Pitfall to avoid: Do NOT use `useModelSelector` hook — just pass props directly. Do NOT create a context. Keep it simple prop-drilling. cd tauri-app && npm run build 2>&1 | tail -20 TypeScript build passes. ModelSelectorPill renders in both hero and regular input areas. Sidebar has no model-section. App.tsx routes model props to ChatView. - `cargo test scan_model` passes — top-level scan verified by updated test - `npm run build` passes — no TypeScript errors - Sidebar.tsx no longer contains `model-section` - ChatView.tsx imports and renders `ModelSelectorPill` in both input contexts - App.tsx passes model props to ChatView (not Sidebar) - Top-level model scan: `scan_models("./models")` returns only `.gguf` files directly in the folder — NOT files from subdirectories - Model selector visible in chat input pill (hero screen and regular screen) - No model selector in the sidebar - TypeScript and Rust compile without errors After completion, create `.planning/phases/06-chat-ui-redesign/06-01-SUMMARY.md`