209 lines
9.9 KiB
Markdown
209 lines
9.9 KiB
Markdown
---
|
|
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: "<ModelSelectorPill"
|
|
- from: "tauri-app/src-tauri/src/model_files.rs"
|
|
to: "scan_model_dir_top_level"
|
|
via: "scan_models Tauri command calls top-level scanner"
|
|
pattern: "scan_model_dir_top_level"
|
|
---
|
|
|
|
<objective>
|
|
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.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.config/opencode/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<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
|
|
</context>
|
|
|
|
<interfaces>
|
|
<!-- Key contracts the executor needs. Extracted from codebase. -->
|
|
|
|
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<Vec<ModelFile>, String>
|
|
// Currently calls scan_model_dir() -> scan_model_dir_recursive()
|
|
// Must change to call scan_model_dir_top_level()
|
|
```
|
|
</interfaces>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Fix Rust model scan to top-level only + update test</name>
|
|
<files>tauri-app/src-tauri/src/model_files.rs</files>
|
|
<action>
|
|
Add a new `scan_model_dir_top_level(path: &Path) -> Result<Vec<ModelFile>, 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.
|
|
</action>
|
|
<verify>
|
|
<automated>cd tauri-app/src-tauri && cargo test scan_model 2>&1 | tail -10</automated>
|
|
</verify>
|
|
<done>All `scan_model*` tests pass. `scan_models` Tauri command uses top-level scan. Nested GGUFs are NOT returned by default.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Create ModelSelectorPill + wire into ChatView + clean Sidebar</name>
|
|
<files>
|
|
tauri-app/src/components/ModelSelectorPill.tsx,
|
|
tauri-app/src/components/Sidebar.tsx,
|
|
tauri-app/src/components/ChatView.tsx,
|
|
tauri-app/src/App.tsx
|
|
</files>
|
|
<action>
|
|
**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 `<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:
|
|
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.
|
|
</action>
|
|
<verify>
|
|
<automated>cd tauri-app && npm run build 2>&1 | tail -20</automated>
|
|
</verify>
|
|
<done>TypeScript build passes. ModelSelectorPill renders in both hero and regular input areas. Sidebar has no model-section. App.tsx routes model props to ChatView.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `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)
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- 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
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/06-chat-ui-redesign/06-01-SUMMARY.md`
|
|
</output>
|