# Phase 6: Chat UI Redesign — Research **Researched:** 2026-04-02 **Domain:** React + Tauri desktop UI, chat interface, Tailwind CSS **Confidence:** HIGH (all findings verified directly from source code) --- ## Summary The project has a mature Tauri + React desktop app (`tauri-app/`) with a well-factored component structure. The chat screen (`ChatView.tsx`, 918 lines) and the sidebar (`Sidebar.tsx`) are the primary targets. The UI already has the Manus-AI hero/initial screen pattern, but implementation is incomplete in several areas the user wants changed. **Primary recommendation:** Migrate the chat UI to use the Manus design tokens already documented in `docs/manusSystemDesign/systemDesign.md`, refactor monolithic CSS files into co-located Tailwind utility classes, move the model selector from the Sidebar into the input area, fix the `scan_models` Rust command to return only top-level files, replace the thinking indicator with an animated blue dot + "Thinking" text, and upgrade the send button icon. Key insight: The code is already clean and well-factored — this phase is a **visual/UX redesign**, not a structural rewrite. Most logic is preserved, only UI surface changes. --- ## Standard Stack ### Core (already installed — verified from `package.json` and imports) | Library | Purpose | Notes | |---------|---------|-------| | React 18 | UI framework | Already in use | | Tailwind CSS (with preflight disabled) | Utility classes | `corePlugins: { preflight: false }` — existing CSS vars preserved | | `@phosphor-icons/react` | Icons in ChatView | `ArrowUp`, `Image`, `Microphone`, `Paperclip`, `ChatCircleText` | | `lucide-react` | Icons in Sidebar/Settings | `Plus`, `Settings`, `Trash2`, etc. | | `@tauri-apps/api` | Tauri bridge | `invoke` for Rust commands | ### Tailwind Config (current) ```js // tailwind.config.js — preflight DISABLED, CSS vars bridged theme: { extend: { colors: { "c-bg": "var(--bg)", "c-bg2": "var(--bg2)", // ... etc } } } ``` **Key constraint:** `preflight: false` means Tailwind utilities work but browser reset is NOT applied. Existing CSS-var-based hand-crafted styles still work. New Tailwind classes coexist with existing `.css` files. ### Icon Strategy - ChatView uses `@phosphor-icons/react` — use this for new send button icon - Sidebar uses `lucide-react` — use this for sidebar icons - **Send button change:** Replace `ArrowUp` with `PaperPlaneRight` or `ArrowCircleUp` from `@phosphor-icons/react` --- ## Architecture Patterns ### Current Project Structure (verified) ``` tauri-app/src/ ├── App.tsx # Root — owns chat state, model swap, view routing ├── App.css # App-level layout ├── index.css # :root CSS vars + @tailwind utilities ├── components/ │ ├── ChatView.tsx # 918 lines — MAIN TARGET │ ├── ChatView.css # 1458 lines — MAIN TARGET CSS │ ├── MessageBubble.tsx # 237 lines — message rendering │ ├── MessageBubble.css # message styles │ ├── Sidebar.tsx # 217 lines — MAIN TARGET │ ├── Sidebar.css # sidebar styles │ ├── Toast.tsx / Toast.css # toast notifications │ ├── chat-view-state.ts # pure logic: composerNotice, suggestedPrompts │ └── chat-view-state.test.ts # tests ├── features/ │ ├── settings/ │ │ ├── SettingsPage.tsx # 1200+ lines — SECONDARY TARGET │ │ └── SettingsPage.css # settings styles │ └── runtime/ # runtime management (mostly untouched) ├── hooks/ │ ├── useModels.ts # fetches /v1/models API │ └── ... └── lib/ ├── modelAlias.ts # Cooper/Iron/Steel alias system └── types.ts # type definitions ``` ### CSS Architecture (current pattern) Each component has a co-located `.css` file using CSS custom properties from `:root` in `index.css`. Tailwind utilities (`flex`, `truncate`, `w-full`, etc.) are used selectively in settings but not yet in chat components. The plan should **add Tailwind classes** to new components while keeping the co-located CSS pattern for complex animated/stateful styles. --- ## Component-by-Component Analysis ### 1. Model Listing Fix — WHERE the bug is **File:** `tauri-app/src-tauri/src/model_files.rs` **Function:** `scan_model_dir_recursive` (line 46–70) ```rust fn scan_model_dir_recursive(dir: &Path, models: &mut Vec) -> Result<(), String> { for entry in entries.flatten() { let path = entry.path(); if path.is_dir() { // Recurses into ALL non-hidden subdirectories — THIS IS THE ISSUE scan_model_dir_recursive(&path, models)?; } else if path.is_file() && path.extension()... == "gguf" { models.push(...) } } } ``` **Problem confirmed:** The Rust scanner recursively descends into all non-hidden subdirectories. When the user points `models_dir` at a HuggingFace cache (e.g., `~/.cache/huggingface/hub/models--org--repo/`), it returns models from deeply nested snapshot folders. **Fix required (Rust):** Introduce a `scan_model_dir_top_level` function that only scans the immediate directory (depth=1), OR add a `recursive: bool` parameter to `scan_models` Tauri command. The `scan_models` command should default to top-level only. **Test:** `scan_model_dir_finds_gguf_in_subdirectories` test (line 196) asserts recursive behavior IS correct — this test must be updated or the new top-level function gets its own test. ### 2. Model Selector — Current Location **Current location:** `Sidebar.tsx` lines 131–170 — a `
` in the sidebar containing a dropdown trigger + listbox popover. **Props flow:** `App.tsx` → `Sidebar` via `models`, `localModels`, `selectedModel`, `onModelChange`, `onModelSwap`, `isSwapping`, `status`. **Target location (per requirements):** Inside the chat input area (`ChatView.tsx`). The model selector should appear inside or just above the `.input-box` pill. **What moves:** - The dropdown UI (trigger + popover) moves to `ChatView.tsx` - `App.tsx` must pass `models`, `localModels`, `selectedModel`, `onModelChange`, `onModelSwap`, `isSwapping`, `status` as new props to `ChatView` - The `Sidebar` removes the `model-section` block **Deduplication logic** currently in `Sidebar.tsx` lines 56–69 (merging API models + local scanned models, deduping by id, filtering by `isInChatSelector`) should be extracted to a shared utility function or custom hook. ### 3. Hero Screen — Current State **Already implemented!** `ChatView.tsx` has `isInitialScreen = chatId === null && messages.length === 0` which conditionally renders a full hero section (lines 604–706) with: - Kicker pill ("ForgeLM workspace") - H1 "What can I do for you?" (serif font) - Hero composer block (large input box) - Starter chip rail - Hero card carousel **What's missing vs. Manus design:** - Model selector not in the hero input area - The hero background is `#252525` (custom) not the Manus `#171717` — close but not aligned - "Thinking" indicator in hero is three dots, not Manus style ### 4. Thinking Indicator — Current vs. Target **Current (ChatView.tsx lines 763–769):** ```tsx {generating && !streamReasoning && !streamContent && !streamThinkingOpen && (
)} ``` **Current CSS (ChatView.css lines 770–802):** ```css .thinking span { width: 8px; height: 8px; background: #9fd0ff; border-radius: 50%; animation: thinking-bounce 1.4s ease-in-out infinite; } ``` → Three bouncing light-blue dots. **Target (Manus style):** - Single animated pulsing blue dot (`#3B82F6`) - "Thinking" text inline - Pattern: `[● pulsing] Thinking` **Implementation:** Replace the three-span structure with a single dot + text: ```tsx
Thinking
``` ```css .thinking-dot { width: 8px; height: 8px; background: #3B82F6; border-radius: 50%; animation: thinking-pulse 1.5s ease-in-out infinite; } @keyframes thinking-pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.4; transform: scale(0.75); } } ``` ### 5. Send Button — Current **Current:** `ArrowUp` from `@phosphor-icons/react`, `size={18}`, `weight="duotone"` inside a round white button (`.send-btn`). The button says "Send " in the non-hero input (line 871) and just `` in the hero input (line 657). **Target:** A different icon. Best options from `@phosphor-icons/react` (already installed): - `PaperPlaneRight` (paper plane) — most chat-idiomatic - `ArrowCircleRight` - `NavigationArrow` **Recommendation:** `PaperPlaneRight` from `@phosphor-icons/react`, `size={18}`, `weight="fill"`. Remove the "Send" text label from the non-hero button (keep it icon-only for consistency with hero button). ### 6. Settings Screen — Current State `SettingsPage.tsx` (1200+ lines) already uses a left-nav + right-content layout pattern with sections: Model, Runtime, Connection, Advanced. The CSS uses `sp-*` BEM-style classes. It already follows a clean pattern. **Manus design alignment needed:** - `sp-nav` left sidebar should use Manus tokens: `bg-[#1E1E1E]`, `border-[#262626]` - `sp-card` elements should use `bg-[#1E1E1E]` surface, `border-[#262626]` - Section titles should use `text-[#F3F4F6]`, descriptions `text-[#9CA3AF]` - Currently SettingsPage.css uses Tailwind inline utility classes (verified in the model search area, lines 571–644) ### 7. Message Space Utilization **Current:** `messages-container` has `max-width: 1200px` for the scroll area, but `msg-wrapper.assistant` is `max-width: 100%` and `msg-wrapper.user` is `max-width: 800px`. Input is constrained to `max-width: 800px`. **Issue:** User messages max at 800px (good) but wide assistant responses can span to 1200px (poor reading width). Also the input area and messages aren't visually aligned. **Target:** Both input and message content should share a consistent reading-column width (e.g., `max-width: 768px` or `800px`). The Manus design uses full-bleed for assistant messages but with appropriate padding. --- ## Don't Hand-Roll | Problem | Don't Build | Use Instead | |---------|-------------|-------------| | Dropdown/popover positioning | Custom CSS position math | Use `position: absolute` + the existing pattern already in Sidebar's `model-dropdown-menu` | | Icon set | SVG sprites or custom SVGs | `@phosphor-icons/react` (already installed) | | Animation timing | Custom JS timing | CSS `@keyframes` with `animation` | | Model deduplication | New logic | Extract `Sidebar.tsx` lines 56–69 into `lib/modelUtils.ts` | --- ## Runtime State Inventory > N/A — this is a UI redesign phase with no renames, no string replacements, and no data migrations. No runtime state is affected. **Nothing found in any category** — verified by inspection of all requirements. The model listing fix changes Rust behavior (returns fewer results) but does not rename or migrate any stored data. --- ## Common Pitfalls ### Pitfall 1: Tailwind + CSS-var Conflict **What goes wrong:** Writing `bg-[#171717]` in JSX when the existing `.chat-view.initial-screen` CSS rule sets `background: #252525`. The CSS rule wins due to specificity. **Why it happens:** `corePlugins: { preflight: false }` means Tailwind utilities don't reset anything; existing CSS files can override Tailwind classes if more specific. **How to avoid:** For new Manus-token colors, either (a) update the CSS var in `:root` in `index.css` and use `bg-c-bg`, or (b) use `!bg-[#171717]` (important modifier) carefully. **Warning signs:** Colors look wrong despite correct class on the element. ### Pitfall 2: Model Selector Prop Drilling **What goes wrong:** Moving the model selector from Sidebar to ChatView requires passing `models`, `localModels`, `selectedModel`, `onModelChange`, `onModelSwap`, `isSwapping`, `status` into ChatView — which becomes a massive props list. **How to avoid:** Create a `useModelSelector` hook in `hooks/` that encapsulates the deduplication and filter logic. Pass only the minimal necessary props to ChatView. ### Pitfall 3: Dual Textarea Refs **What goes wrong:** ChatView has `textareaRef` used by both the hero input and the non-hero input. They are the same ref but only one is mounted at a time. If both render simultaneously (e.g., during state transitions), the ref points to the second one. **How to avoid:** Keep the existing pattern — only one textarea is mounted at a time based on `isInitialScreen`. Don't change this logic. ### Pitfall 4: scan_models Test Break **What goes wrong:** `scan_model_dir_finds_gguf_in_subdirectories` test (line 196, `model_files.rs`) explicitly asserts that nested models ARE found. Changing to top-level-only will break this test. **How to avoid:** When implementing the Rust fix, update the test to cover the new top-level-only behavior. Add a separate test for "nested models are NOT returned by default." ### Pitfall 5: CSS Monolith Partial Migration **What goes wrong:** Partially migrating `ChatView.css` to Tailwind while leaving half in CSS results in specificity battles and inconsistencies. **How to avoid:** Follow the project's existing pattern: keep co-located `.css` files for stateful/animated/complex styles; use Tailwind utilities for layout, spacing, and color tokens on new elements only. Don't wholesale migrate existing CSS. ### Pitfall 6: Model Selector in Input vs. Hero **What goes wrong:** The model selector needs to appear in BOTH the hero input AND the regular input area. These are two separate JSX blocks in ChatView. **How to avoid:** Extract a `` component that renders identically in both contexts, positioned inside the input box. --- ## Code Examples ### Manus Thinking Indicator (target pattern) ```tsx // Source: systemDesign.md + verified against current thinking-bounce pattern // Replace the 3-dot bounce with a single pulsing dot + text {generating && !streamReasoning && !streamContent && !streamThinkingOpen && (
)} ``` ```css .thinking-manus { display: flex; align-items: center; gap: 8px; padding: 8px 0; } .thinking-dot { width: 8px; height: 8px; background: #3B82F6; border-radius: 50%; flex-shrink: 0; animation: thinking-pulse 1.5s ease-in-out infinite; } .thinking-label { font-size: 0.875rem; color: #9CA3AF; font-weight: 400; } @keyframes thinking-pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.35; transform: scale(0.7); } } ``` ### Model Selector in Input Area (sketch) ```tsx // New: — inline in input box // position: inside .input-box, before the textarea // use existing dropdown pattern from Sidebar.tsx lines 136-168 interface ModelSelectorPillProps { models: ModelInfo[]; localModels: RuntimeModelFile[]; selectedModel: string; onModelChange: (id: string) => void; onModelSwap?: (path: string) => void; isSwapping?: boolean; } ``` ### Rust Top-Level Scan (sketch) ```rust // New function alongside existing scan_model_dir_recursive fn scan_model_dir_top_level(path: &Path) -> Result, String> { let mut models = Vec::new(); let entries = std::fs::read_dir(path).map_err(|e| e.to_string())?; for entry in entries.flatten() { let p = entry.path(); if p.is_file() && p.extension().map(|e| e.eq_ignore_ascii_case("gguf")).unwrap_or(false) { models.push(model_file_from_path(&p)); } } models.sort_by(|a, b| a.name.cmp(&b.name)); Ok(models) } // Update scan_models command to use top-level scan: #[tauri::command] pub fn scan_models(dir: String) -> Result, String> { let path = resolve_models_dir(if dir.trim().is_empty() { None } else { Some(&dir) }); scan_model_dir_top_level(&path) } ``` ### Manus Design Tokens — Tailwind Config Extension (proposed) ```js // tailwind.config.js — extend with Manus tokens as named colors theme: { extend: { colors: { // Existing CSS-var bridges "c-bg": "var(--bg)", // ... existing entries ... // NEW: Manus design tokens (can use directly) "manus-base": "#171717", "manus-surface": "#1E1E1E", "manus-elevated": "#262626", "manus-hover": "#333333", "manus-tx": "#F3F4F6", "manus-tx2": "#9CA3AF", "manus-tx3": "#52525B", "manus-blue": "#3B82F6", } } } ``` --- ## State of the Art | Area | Current Approach | Target Approach | Impact | |------|-----------------|-----------------|--------| | Thinking indicator | 3 bouncing dots (`.thinking span`) | Single pulsing dot + "Thinking" text | Matches Manus AI style | | Model selector location | Sidebar dropdown | Input area pill | User requirement | | Manus tokens | Custom CSS vars (`--bg: #0d1115`) | Manus tokens (`#171717`, `#1E1E1E`) | Visual alignment | | Model scan depth | Recursive (all subdirectories) | Top-level only | Bug fix | | Send button | ArrowUp + "Send" text | PaperPlaneRight icon only | Better UX | | Input width | 800px (input) / 1200px (messages) | Aligned reading column | Better space use | --- ## Environment Availability > Step 2.6: SKIPPED — this is a UI code change phase. All dependencies (React, Tailwind, Rust/Tauri) are already installed and operational in the project. No external services or new CLI tools required. --- ## Validation Architecture > `nyquist_validation` is `false` in `.planning/config.json` — skipping this section. --- ## Open Questions 1. **Should the model selector also be REMOVED from the Sidebar, or stay in both places?** - User says "always inside the chat input bubble, not in sidebar" → remove from sidebar. - Implication: The sidebar `model-section` block is deleted entirely. - Recommendation: Remove from sidebar. The `model-section` CSS class and HTML block in `Sidebar.tsx` lines 131–170 can be deleted. 2. **Top-level scan vs. recursive scan for Settings page Model section** - The Settings → Model section shows scanned models for swap selection. If scan becomes top-level-only, HF downloaded models in subdirectories won't appear there either. - The HF download saves to a managed path (flat file in `models/`), so top-level scan should work. - Recommendation: Make top-level the default for `scan_models` command. Keep recursive scan available as a separate function for future use. 3. **Manus `#171717` vs. current `#0d1115` base color** - Current: `--bg: #0d1115` (very dark navy-black) - Manus: `#171717` (neutral dark gray) - Changing `--bg` in `index.css` would affect the ENTIRE app. The hero screen already uses `#252525` as an override. - Recommendation: Update the CSS vars in `index.css` to align with Manus tokens as part of this phase. This is the right time — it's a design phase. 4. **Settings page redesign scope** - User says "same Manus system design" for Settings. - Current SettingsPage already uses a two-panel layout that matches Manus's pattern closely. - Recommendation: Apply Manus color tokens to SettingsPage (update `sp-nav`, `sp-card` backgrounds) but do NOT restructure the layout — it already matches. --- ## Sources ### Primary (HIGH confidence) - **Direct source read:** `tauri-app/src/components/ChatView.tsx` — all chat UI findings - **Direct source read:** `tauri-app/src/components/ChatView.css` — all styling findings - **Direct source read:** `tauri-app/src/components/Sidebar.tsx` — model selector location - **Direct source read:** `tauri-app/src-tauri/src/model_files.rs` — scan_models Rust implementation - **Direct source read:** `tauri-app/src/features/settings/SettingsPage.tsx` — settings structure - **Direct source read:** `tauri-app/tailwind.config.js` — Tailwind configuration - **Direct source read:** `tauri-app/src/index.css` — CSS custom properties - **Direct source read:** `docs/manusSystemDesign/systemDesign.md` — Manus design tokens ### Secondary (MEDIUM confidence) - **`@phosphor-icons/react`** icon names: verified from existing imports in ChatView.tsx --- ## Metadata **Confidence breakdown:** - Model listing bug: HIGH — read the Rust source directly; recursive call confirmed at line 58 - Model selector location: HIGH — read Sidebar.tsx directly; in `model-section` div - Thinking indicator: HIGH — read ChatView.tsx and ChatView.css directly - Settings screen: HIGH — read SettingsPage.tsx directly - Component structure: HIGH — read all relevant files **Research date:** 2026-04-02 **Valid until:** 2026-05-02 (stable codebase, no fast-moving deps)