Files

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
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
true
UI-01
truths artifacts key_links
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
path provides contains
tauri-app/src-tauri/src/model_files.rs scan_model_dir_top_level function replaces recursive scan scan_model_dir_top_level
path provides exports
tauri-app/src/components/ModelSelectorPill.tsx Reusable model selector dropdown pill for input area
ModelSelectorPill
ModelSelectorPillProps
path provides
tauri-app/src/components/Sidebar.tsx Sidebar without model-section block
from to via pattern
tauri-app/src/App.tsx tauri-app/src/components/ChatView.tsx new props: models, localModels, selectedModel, onModelChange, onModelSwap, isSwapping, status models={models}
from to via pattern
tauri-app/src/components/ChatView.tsx tauri-app/src/components/ModelSelectorPill.tsx import and render inside .input-box <ModelSelectorPill
from to via pattern
tauri-app/src-tauri/src/model_files.rs scan_model_dir_top_level scan_models Tauri command calls top-level scanner scan_model_dir_top_level
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.

<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()
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 <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. 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)

<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>
After completion, create `.planning/phases/06-chat-ui-redesign/06-01-SUMMARY.md`