21 KiB
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)
// 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
ArrowUpwithPaperPlaneRightorArrowCircleUpfrom@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)
fn scan_model_dir_recursive(dir: &Path, models: &mut Vec<ModelFile>) -> 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 <div class="model-section"> 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.tsxmust passmodels,localModels,selectedModel,onModelChange,onModelSwap,isSwapping,statusas new props toChatView- The
Sidebarremoves themodel-sectionblock
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):
{generating && !streamReasoning && !streamContent && !streamThinkingOpen && (
<div className="msg-wrapper assistant">
<div className="thinking">
<span /><span /><span />
</div>
</div>
)}
Current CSS (ChatView.css lines 770–802):
.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:
<div className="thinking-manus">
<span className="thinking-dot" />
<span className="thinking-label">Thinking</span>
</div>
.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 <ArrowUp> in the hero input (line 657).
Target: A different icon. Best options from @phosphor-icons/react (already installed):
PaperPlaneRight(paper plane) — most chat-idiomaticArrowCircleRightNavigationArrow
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-navleft sidebar should use Manus tokens:bg-[#1E1E1E],border-[#262626]sp-cardelements should usebg-[#1E1E1E]surface,border-[#262626]- Section titles should use
text-[#F3F4F6], descriptionstext-[#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 <ModelSelectorPill> component that renders identically in both contexts, positioned inside the input box.
Code Examples
Manus Thinking Indicator (target pattern)
// Source: systemDesign.md + verified against current thinking-bounce pattern
// Replace the 3-dot bounce with a single pulsing dot + text
{generating && !streamReasoning && !streamContent && !streamThinkingOpen && (
<div className="msg-wrapper assistant">
<div className="thinking-manus">
<span className="thinking-dot" aria-hidden="true" />
<span className="thinking-label">Thinking</span>
</div>
</div>
)}
.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)
// New: <ModelSelectorPill> — 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)
// New function alongside existing scan_model_dir_recursive
fn scan_model_dir_top_level(path: &Path) -> Result<Vec<ModelFile>, 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<Vec<ModelFile>, 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)
// 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_validationisfalsein.planning/config.json— skipping this section.
Open Questions
-
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-sectionblock is deleted entirely. - Recommendation: Remove from sidebar. The
model-sectionCSS class and HTML block inSidebar.tsxlines 131–170 can be deleted.
-
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_modelscommand. Keep recursive scan available as a separate function for future use.
-
Manus
#171717vs. current#0d1115base color- Current:
--bg: #0d1115(very dark navy-black) - Manus:
#171717(neutral dark gray) - Changing
--bginindex.csswould affect the ENTIRE app. The hero screen already uses#252525as an override. - Recommendation: Update the CSS vars in
index.cssto align with Manus tokens as part of this phase. This is the right time — it's a design phase.
- Current:
-
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-cardbackgrounds) 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/reacticon 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-sectiondiv - 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)