9.9 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 06-chat-ui-redesign | 01 | execute | 1 |
|
true |
|
|
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.
<execution_context> @$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md @$HOME/.config/opencode/get-shit-done/templates/summary.md </execution_context>
@.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:
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:
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:
// 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):
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):
#[tauri::command]
pub fn scan_models(dir: String) -> Result<Vec<ModelFile>, String>
// Currently calls scan_model_dir() -> scan_model_dir_recursive()
// Must change to call scan_model_dir_top_level()
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.
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 <Sidebar>. Add them as new props to <ChatView>: 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 <ModelSelectorPill ...props /> inside the input area in TWO places:
- Hero input block (around line 657) — inside
.input-box, before the textarea, on its own row or inline left. - 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.
<success_criteria>
- Top-level model scan:
scan_models("./models")returns only.gguffiles 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 </success_criteria>