diff --git a/docs/plans/2026-01-21-file-editor-chat-ui-redesign.md b/docs/plans/2026-01-21-file-editor-chat-ui-redesign.md
new file mode 100644
index 00000000..657d4d0d
--- /dev/null
+++ b/docs/plans/2026-01-21-file-editor-chat-ui-redesign.md
@@ -0,0 +1,1459 @@
+# File Editor & Chat UI Redesign - Comprehensive Design Document
+
+**Date:** 2026-01-21
+**Author:** Claude (Sonnet 4.5)
+**Status:** Design Phase - Pre-Implementation
+
+---
+
+## Executive Summary
+
+### Problem Statement
+
+The current Claude Code Web IDE has three critical issues that block effective development:
+
+1. **File Editor Non-Functional**
+ - File content displays as "undefined" for non-HTML files
+ - Edit button calls non-existent `editFile()` function
+ - No syntax highlighting or code navigation
+ - No tab-based multi-file editing
+
+2. **Chat Input Too Basic**
+ - Single-line textarea with no expansion
+ - No attachment support (files, images)
+ - No slash commands or file mentions
+ - No draft persistence
+ - No history navigation
+
+3. **Session Flow Broken**
+ - Auto-session creation fails silently
+ - Race condition between input and session creation
+ - No session forking capability
+ - No multi-session management
+
+### Solution Overview
+
+Implement a complete redesign incorporating best practices from three reference projects:
+
+- **CodeMirror 6** (from code-server): Professional code editor with syntax highlighting
+- **CodeNomad UI Patterns**: Sophisticated prompt input with attachments
+- **OpenCode Session Management**: Explicit session browser and forking
+- **Conduit-Copy Architecture**: Multi-agent orchestration and token tracking
+
+**Design Philosophy:** Speed and simplicity - minimize friction for developers who want to get from idea → code as quickly as possible.
+
+---
+
+## Technical Architecture
+
+### 1. CodeMirror 6 Integration
+
+#### Why CodeMirror 6?
+
+- **Modern Architecture**: Extension-based, not monolithic
+- **Lightweight**: ~100KB vs Monaco's ~2MB
+- **Mobile-Friendly**: Touch-optimized, works on all devices
+- **Framework Agnostic**: Works with vanilla JS (no React/Vue required)
+- **Proven**: Used by CodeMirror, VS Code Web, CodeServer
+
+#### Package Structure
+
+```json
+{
+ "dependencies": {
+ "@codemirror/state": "^6.4.0",
+ "@codemirror/view": "^6.23.0",
+ "@codemirror/basic-setup": "^0.20.0",
+ "@codemirror/lang-javascript": "^6.2.1",
+ "@codemirror/lang-python": "^6.1.3",
+ "@codemirror/lang-html": "^6.4.6",
+ "@codemirror/lang-css": "^6.2.1",
+ "@codemirror/lang-json": "^6.0.1",
+ "@codemirror/lang-markdown": "^6.2.0",
+ "@codemirror/commands": "^6.3.3",
+ "@codemirror/search": "^6.5.5",
+ "@codemirror/autocomplete": "^6.12.0",
+ "@codemirror/lint": "^6.4.2",
+ "@codemirror/panel": "^6.10.0",
+ "@codemirror/gutter": "^6.5.0",
+ "@codemirror/fold": "^6.5.0",
+ "@lezer/highlight": "^1.2.0"
+ }
+}
+```
+
+#### Extension Configuration
+
+```javascript
+import { EditorState, Compartment } from '@codemirror/state';
+import { EditorView, keymap, highlightSpecialChars, drawSelection } from '@codemirror/view';
+import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
+import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
+import { autocompletion, completionKeymap } from '@codemirror/autocomplete';
+import { bracketMatching, codeFolding, foldGutter } from '@codemirror/language';
+import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
+import { rectangularSelection } from '@codemirror/commands';
+import { crosshairCursor } from '@codemirror/view';
+import { highlightSelectionMatches } from '@codemirror/search';
+import { oneDark } from '@codemirror/theme-one-dark';
+
+// Language support
+import { javascript } from '@codemirror/lang-javascript';
+import { python } from '@codemirror/lang-python';
+import { html } from '@codemirror/lang-html';
+import { css } from '@codemirror/lang-css';
+import { json } from '@codemirror/lang-json';
+import { markdown } from '@codemirror/lang-markdown';
+
+// State compartments for dynamic reconfiguration
+const languageCompartment = new Compartment();
+const themeCompartment = new Compartment();
+const tabSizeCompartment = new Compartment();
+
+function createEditorState(filePath, content) {
+ const extension = getExtension(filePath);
+
+ return EditorState.create({
+ doc: content,
+ extensions: [
+ lineNumbers(),
+ highlightSpecialChars(),
+ history(),
+ foldGutter(),
+ drawSelection(),
+ dropCursor(),
+ codeFolding(),
+ bracketMatching(),
+ closeBrackets(),
+ autocompletion(),
+ rectangularSelection(),
+ crosshairCursor(),
+ highlightActiveLine(),
+ highlightSelectionMatches(),
+ keymap.of([
+ ...closeBracketsKeymap,
+ ...defaultKeymap,
+ ...searchKeymap,
+ ...historyKeymap,
+ ...completionKeymap
+ ]),
+ oneDark,
+ languageCompartment.of(extension),
+ EditorView.updateListener.of((update) => {
+ if (update.docChanged) {
+ markUnsaved();
+ }
+ }),
+ EditorView.theme({
+ "&": { height: "100%" },
+ ".cm-scroller": { overflow: "auto" },
+ ".cm-content": { padding: "12px 0" },
+ ".cm-line": { padding: "0 4px" }
+ })
+ ]
+ });
+}
+
+function getExtension(filePath) {
+ const ext = filePath.split('.').pop().toLowerCase();
+
+ const languageMap = {
+ 'js': javascript(),
+ 'jsx': javascript({ jsx: true }),
+ 'ts': javascript({ typescript: true }),
+ 'tsx': javascript({ typescript: true, jsx: true }),
+ 'py': python(),
+ 'html': html(),
+ 'htm': html(),
+ 'css': css(),
+ 'scss': css(),
+ 'json': json(),
+ 'md': markdown(),
+ 'markdown': markdown()
+ };
+
+ return languageMap[ext] || javascript();
+}
+```
+
+### 2. Enhanced Chat Input Architecture
+
+#### Component Structure
+
+```
+chat-input-container/
+├── textarea (auto-expanding 2-15 lines)
+├── attachment-chips (horizontal scroll)
+├── unified-picker (autocomplete menu)
+├── action-bar (send, attach, voice input)
+└── draft-storage (localStorage per session)
+```
+
+#### State Management
+
+```javascript
+const ChatInputState = {
+ // Current input text
+ value: '',
+
+ // Attachments
+ attachments: [
+ { id: 1, type: 'file', name: 'src/index.js', content: '...' },
+ { id: 2, type: 'image', data: 'data:image/png;base64,...' },
+ { id: 3, type: 'pasted', label: 'pasted #1', content: '...' }
+ ],
+
+ // Cursor position for unified picker
+ cursorPosition: 0,
+
+ // Trigger character positions (@ for files, / for commands)
+ triggers: {
+ at: null, // Position of @ character
+ slash: null // Position of / character
+ },
+
+ // Draft storage
+ drafts: new Map(), // sessionId -> draft content
+
+ // History
+ history: [],
+ historyIndex: -1,
+
+ // Mode
+ mode: 'chat', // 'chat', 'native', 'terminal'
+
+ // Shell mode prefix (!)
+ shellMode: false
+};
+```
+
+#### Auto-Expand Logic
+
+```javascript
+function autoExpandTextarea(textarea) {
+ const minLines = 2;
+ const maxLines = 15;
+ const lineHeight = 24; // pixels
+
+ textarea.style.height = 'auto';
+ const newHeight = textarea.scrollHeight;
+
+ const minHeight = lineHeight * minLines;
+ const maxHeight = lineHeight * maxLines;
+
+ if (newHeight < minHeight) {
+ textarea.style.height = `${minHeight}px`;
+ } else if (newHeight > maxHeight) {
+ textarea.style.height = `${maxHeight}px`;
+ textarea.style.overflowY = 'auto';
+ } else {
+ textarea.style.height = `${newHeight}px`;
+ }
+}
+```
+
+#### Attachment System
+
+```javascript
+// File attachment
+async function attachFile(file) {
+ const content = await file.text();
+
+ const attachment = {
+ id: Date.now(),
+ type: 'file',
+ name: file.name,
+ size: file.size,
+ content: content,
+ mimeType: file.type
+ };
+
+ ChatInputState.attachments.push(attachment);
+ renderAttachmentChips();
+}
+
+// Image attachment (drag & drop or paste)
+async function attachImage(dataUrl) {
+ const attachment = {
+ id: Date.now(),
+ type: 'image',
+ data: dataUrl,
+ preview: dataUrl
+ };
+
+ ChatInputState.attachments.push(attachment);
+ renderAttachmentChips();
+}
+
+// Long paste detection (>150 chars or >3 lines)
+function handlePaste(event) {
+ const pastedText = (event.clipboardData || window.clipboardData).getData('text');
+ const lines = pastedText.split('\n').length;
+ const chars = pastedText.length;
+
+ if (chars > 150 || lines > 3) {
+ event.preventDefault();
+
+ const attachment = {
+ id: Date.now(),
+ type: 'pasted',
+ label: `pasted #${ChatInputState.attachments.filter(a => a.type === 'pasted').length + 1}`,
+ content: pastedText,
+ chars: chars,
+ lines: lines
+ };
+
+ ChatInputState.attachments.push(attachment);
+ renderAttachmentChips();
+ }
+}
+```
+
+#### Unified Picker (@files, /commands)
+
+```javascript
+function showUnifiedPicker(trigger, position) {
+ let items = [];
+
+ if (trigger === '@') {
+ // File picker
+ items = getFileTreeItems();
+ } else if (trigger === '/') {
+ // Command picker
+ items = [
+ { label: 'help', description: 'Show available commands' },
+ { label: 'clear', description: 'Clear conversation history' },
+ { label: 'save', description: 'Save current session' },
+ { label: 'export', description: 'Export conversation' },
+ { label: 'fork', description: 'Fork this session' },
+ { label: 'agent', description: 'Switch AI agent' }
+ ];
+ }
+
+ const picker = document.createElement('div');
+ picker.className = 'unified-picker';
+ picker.innerHTML = items.map(item => `
+
+ ${item.label || item.name}
+ ${item.description || ''}
+
+ `).join('');
+
+ // Position picker near cursor
+ const textarea = document.getElementById('chat-input');
+ const coords = getCursorCoordinates(textarea, position);
+ picker.style.left = `${coords.x}px`;
+ picker.style.top = `${coords.y + 24}px`;
+
+ document.body.appendChild(picker);
+
+ // Handle selection
+ picker.addEventListener('click', (e) => {
+ const selectedItem = e.target.closest('.picker-item');
+ if (selectedItem) {
+ insertAtCursor(`${trigger}${selectedItem.dataset.value}`, position);
+ picker.remove();
+ }
+ });
+}
+```
+
+#### Draft Persistence
+
+```javascript
+// Save draft on every change
+function saveDraft() {
+ const sessionId = getCurrentSessionId();
+ if (!sessionId) return;
+
+ const draft = {
+ value: ChatInputState.value,
+ attachments: ChatInputState.attachments,
+ timestamp: Date.now()
+ };
+
+ ChatInputState.drafts.set(sessionId, draft);
+ localStorage.setItem(`chat-drafts`, JSON.stringify([...ChatInputState.drafts]));
+}
+
+// Load draft on session switch
+function loadDraft(sessionId) {
+ const drafts = JSON.parse(localStorage.getItem('chat-drafts') || '[]');
+ const draft = drafts.find(([id]) => id === sessionId);
+
+ if (draft) {
+ const [_, draftData] = draft;
+ ChatInputState.value = draftData.value;
+ ChatInputState.attachments = draftData.attachments || [];
+ renderAttachmentChips();
+ document.getElementById('chat-input').value = draftData.value;
+ }
+}
+
+// Clear draft after sending
+function clearDraft(sessionId) {
+ ChatInputState.drafts.delete(sessionId);
+ localStorage.setItem(`chat-drafts`, JSON.stringify([...ChatInputState.drafts]));
+}
+```
+
+#### History Navigation
+
+```javascript
+// Navigate with ↑↓ arrows
+document.getElementById('chat-input').addEventListener('keydown', (e) => {
+ if (e.key === 'ArrowUp' && ChatInputState.historyIndex < ChatInputState.history.length - 1) {
+ e.preventDefault();
+ ChatInputState.historyIndex++;
+ const text = ChatInputState.history[ChatInputState.history.length - 1 - ChatInputState.historyIndex];
+ document.getElementById('chat-input').value = text;
+ ChatInputState.value = text;
+ } else if (e.key === 'ArrowDown' && ChatInputState.historyIndex > 0) {
+ e.preventDefault();
+ ChatInputState.historyIndex--;
+ const text = ChatInputState.history[ChatInputState.history.length - 1 - ChatInputState.historyIndex];
+ document.getElementById('chat-input').value = text;
+ ChatInputState.value = text;
+ }
+});
+
+// Save to history after sending
+function saveToHistory(text) {
+ if (!text.trim()) return;
+ ChatInputState.history.push(text);
+ ChatInputState.historyIndex = -1;
+ localStorage.setItem('chat-history', JSON.stringify(ChatInputState.history));
+}
+```
+
+### 3. Hybrid Session Flow
+
+#### Session State Machine
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ User Opens Chat │
+└──────────────────────┬──────────────────────────────────────┘
+ │
+ ▼
+ ┌────────────────────────┐
+ │ Check URL Parameters │
+ │ ?session=XYZ │──Yes──▶ Load Existing Session
+ │ ?project=ABC │
+ └────────────────────────┘
+ │ No
+ ▼
+ ┌────────────────────────┐
+ │ Show Session Picker │
+ │ - Recent Sessions │
+ │ - Start New Session │
+ └────────────────────────┘
+ │
+ ├─────────────▶ Load Existing Session
+ │
+ ▼
+ Create New Session
+ │
+ ▼
+ ┌────────────────────────┐
+ │ Zero-Friction Entry │
+ │ - Auto-focus input │
+ │ - Type & send │
+ └────────────────────────┘
+ │
+ ▼
+ ┌────────────────────────┐
+ │ Auto-Create on Send │
+ │ (if no session) │
+ └────────────────────────┘
+ │
+ ▼
+ ┌────────────────────────┐
+ │ Session Active │
+ │ - Send messages │
+ │ - Attach files │
+ │ - Fork session │
+ └────────────────────────┘
+```
+
+#### Session Picker Modal
+
+```javascript
+function showSessionPicker() {
+ const modal = document.createElement('div');
+ modal.className = 'session-picker-modal';
+
+ modal.innerHTML = `
+
+
Select a Session
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ document.body.appendChild(modal);
+
+ // Load sessions
+ loadRecentSessions();
+ loadProjectsList();
+
+ // Handle selection
+ modal.addEventListener('click', (e) => {
+ const sessionItem = e.target.closest('.session-item');
+ if (sessionItem) {
+ loadSession(sessionItem.dataset.sessionId);
+ modal.remove();
+ }
+ });
+}
+```
+
+#### Auto-Session Creation
+
+```javascript
+async function ensureSession() {
+ // Check if we already have a session
+ if (attachedSessionId) {
+ return attachedSessionId;
+ }
+
+ // Check URL for session parameter
+ const urlParams = new URLSearchParams(window.location.search);
+ const sessionParam = urlParams.get('session');
+ if (sessionParam) {
+ attachedSessionId = sessionParam;
+ await loadSession(sessionParam);
+ return attachedSessionId;
+ }
+
+ // Check URL for project parameter
+ const projectParam = urlParams.get('project');
+ if (projectParam) {
+ currentProjectName = projectParam;
+
+ // Find existing session for this project
+ const sessions = await fetchSessions();
+ const projectSession = sessions.find(s => s.metadata?.project === projectParam);
+
+ if (projectSession) {
+ attachedSessionId = projectSession.id;
+ await loadSession(projectSession.id);
+ return attachedSessionId;
+ }
+ }
+
+ // Auto-create session if needed
+ const projectName = currentProjectName || 'Untitled';
+ const newSession = await createSession({
+ type: 'chat',
+ source: 'web-ide',
+ project: projectName
+ });
+
+ attachedSessionId = newSession.id;
+ return attachedSessionId;
+}
+
+// Call before sending message
+async function sendMessage() {
+ await ensureSession();
+
+ const message = {
+ sessionId: attachedSessionId,
+ content: ChatInputState.value,
+ attachments: ChatInputState.attachments,
+ mode: ChatInputState.mode,
+ timestamp: Date.now()
+ };
+
+ // Send to backend
+ await fetch('/claude/api/claude/chat', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(message)
+ });
+
+ // Save to history and clear
+ saveToHistory(ChatInputState.value);
+ ChatInputState.value = '';
+ ChatInputState.attachments = [];
+ clearDraft(attachedSessionId);
+}
+```
+
+#### Session Forking
+
+```javascript
+async function forkSession(sessionId, messageId = null) {
+ // Clone session up to message N
+ const originalSession = await fetchSession(sessionId);
+ const forkPoint = messageId || originalSession.messages.length;
+
+ const forkedSession = await createSession({
+ type: 'chat',
+ source: 'fork',
+ parentSessionId: sessionId,
+ forkPoint: forkPoint,
+ metadata: originalSession.metadata
+ });
+
+ // Copy messages up to fork point
+ const messagesToCopy = originalSession.messages.slice(0, forkPoint);
+ for (const msg of messagesToCopy) {
+ await appendMessage(forkedSession.id, msg);
+ }
+
+ return forkedSession;
+}
+
+// UI: Fork button in chat
+function addForkButton(messageId) {
+ const messageEl = document.querySelector(`[data-message-id="${messageId}"]`);
+ const forkBtn = document.createElement('button');
+ forkBtn.className = 'fork-btn';
+ forkBtn.textContent = '🍴 Fork from here';
+ forkBtn.addEventListener('click', () => forkSession(attachedSessionId, messageId));
+
+ messageEl.querySelector('.message-actions').appendChild(forkBtn);
+}
+```
+
+### 4. Mobile-First Responsive Design
+
+#### Breakpoint System
+
+```css
+/* Mobile First Approach */
+/* Base styles: < 640px (mobile phones) */
+
+/* Small devices: 640px - 1024px (tablets) */
+@media (min-width: 640px) {
+ /* Tablet-specific adjustments */
+}
+
+/* Large devices: > 1024px (desktop) */
+@media (min-width: 1024px) {
+ /* Desktop-specific adjustments */
+}
+```
+
+#### Layout Specifications
+
+**Mobile (< 640px)**:
+- File tree: Hidden hamburger menu, slide-in drawer
+- Editor: Full width, no sidebar
+- Chat: Full width, input expands to 4 lines max
+- Tabs: Horizontal scroll, show max 3 before scrolling
+- Touch targets: Minimum 44×44px
+
+**Tablet (640px - 1024px)**:
+- File tree: Collapsible sidebar (drawer)
+- Editor: Split view 70/30
+- Chat: Side-by-side with editor (toggle)
+- Input: Expands to 10 lines max
+- Tabs: Show max 5 before scrolling
+
+**Desktop (> 1024px)**:
+- File tree: Always-visible sidebar
+- Editor: Full workspace with resizable panels
+- Chat: Dedicated panel with optional split view
+- Input: Expands to 15 lines max
+- Tabs: Show all with scroll if needed
+
+#### Touch-Optimized Interactions
+
+```css
+/* Touch targets */
+.touch-target {
+ min-width: 44px;
+ min-height: 44px;
+ padding: 12px;
+}
+
+/* Swipe gestures for mobile */
+.mobile-drawer {
+ transform: translateX(-100%);
+ transition: transform 0.3s ease;
+}
+
+.mobile-drawer.open {
+ transform: translateX(0);
+}
+
+/* Pull-to-refresh */
+.pull-to-refresh {
+ position: relative;
+ overflow: hidden;
+}
+
+.pull-to-refresh::before {
+ content: '↓ Pull to refresh';
+ position: absolute;
+ top: -40px;
+ left: 50%;
+ transform: translateX(-50%);
+ opacity: 0;
+ transition: all 0.3s ease;
+}
+
+.pull-to-refresh.pulling::before {
+ top: 10px;
+ opacity: 1;
+}
+```
+
+### 5. Conduit-Copy Integrations
+
+#### Token Usage Tracking
+
+```javascript
+// Token counter component
+class TokenCounter {
+ constructor() {
+ this.inputTokens = 0;
+ this.outputTokens = 0;
+ this.costPerMillionInput = 3.0;
+ this.costPerMillionOutput = 15.0;
+ }
+
+ update(input, output) {
+ this.inputTokens += input;
+ this.outputTokens += output;
+ this.render();
+ }
+
+ get totalCost() {
+ const inputCost = (this.inputTokens / 1_000_000) * this.costPerMillionInput;
+ const outputCost = (this.outputTokens / 1_000_000) * this.costPerMillionOutput;
+ return inputCost + outputCost;
+ }
+
+ render() {
+ const el = document.getElementById('token-counter');
+ el.innerHTML = `
+ ${this.inputTokens.toLocaleString()} in
+ ${this.outputTokens.toLocaleString()} out
+ $${this.totalCost.toFixed(4)}
+ `;
+ }
+}
+```
+
+#### Multi-Agent Abstraction
+
+```javascript
+// Agent configuration
+const agents = {
+ claude: {
+ name: 'Claude Sonnet',
+ model: 'claude-sonnet-4-5',
+ costInput: 3.0,
+ costOutput: 15.0
+ },
+ claudeOpus: {
+ name: 'Claude Opus',
+ model: 'claude-opus-4-5',
+ costInput: 15.0,
+ costOutput: 75.0
+ },
+ codex: {
+ name: 'Codex',
+ model: 'gpt-4-codex',
+ costInput: 10.0,
+ costOutput: 30.0
+ }
+};
+
+// Switch agent
+async function switchAgent(agentKey) {
+ const agent = agents[agentKey];
+ const session = await fetchSession(attachedSessionId);
+
+ session.metadata.agent = agentKey;
+ session.metadata.model = agent.model;
+
+ await updateSession(attachedSessionId, { metadata: session.metadata });
+
+ showToast(`Switched to ${agent.name}`, 'info');
+}
+```
+
+#### Git Worktree Integration
+
+```javascript
+// Create worktree for new session
+async function createSessionWorktree(sessionId, projectName) {
+ const worktreePath = `/worktrees/${sessionId.substring(0, 8)}`;
+
+ await fetch('/claude/api/git/worktree', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ sessionId,
+ projectName,
+ path: worktreePath
+ })
+ });
+
+ return worktreePath;
+}
+```
+
+---
+
+## Component Specifications
+
+### File Editor Component
+
+```javascript
+/**
+ * File Editor with CodeMirror 6
+ * Supports: Multi-file tabs, syntax highlighting, split view
+ */
+class FileEditor {
+ constructor(container) {
+ this.container = container;
+ this.editors = new Map(); // tabId -> EditorView
+ this.activeTab = null;
+ this.tabs = [];
+ }
+
+ async openFile(filePath, content) {
+ const tabId = filePath;
+
+ // Check if already open
+ if (this.editors.has(tabId)) {
+ this.activateTab(tabId);
+ return;
+ }
+
+ // Create new tab
+ const tab = {
+ id: tabId,
+ path: filePath,
+ name: filePath.split('/').pop(),
+ dirty: false
+ };
+
+ this.tabs.push(tab);
+
+ // Create CodeMirror instance
+ const state = createEditorState(filePath, content);
+ const editor = new EditorView({
+ state: state,
+ parent: this.container.querySelector('.editor-content')
+ });
+
+ this.editors.set(tabId, editor);
+ this.renderTabs();
+ this.activateTab(tabId);
+ }
+
+ activateTab(tabId) {
+ // Hide all editors
+ this.editors.forEach((editor, id) => {
+ editor.dom.style.display = id === tabId ? 'block' : 'none';
+ });
+
+ this.activeTab = tabId;
+ this.renderTabs();
+ }
+
+ closeTab(tabId) {
+ const editor = this.editors.get(tabId);
+ editor.destroy();
+
+ this.editors.delete(tabId);
+ this.tabs = this.tabs.filter(t => t.id !== tabId);
+
+ if (this.activeTab === tabId && this.tabs.length > 0) {
+ this.activateTab(this.tabs[0].id);
+ }
+
+ this.renderTabs();
+ }
+
+ async saveFile(tabId) {
+ const editor = this.editors.get(tabId);
+ const content = editor.state.doc.toString();
+
+ await fetch(`/claude/api/file/${encodeURIComponent(tabId)}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ content })
+ });
+
+ const tab = this.tabs.find(t => t.id === tabId);
+ tab.dirty = false;
+ this.renderTabs();
+ }
+
+ markDirty(tabId) {
+ const tab = this.tabs.find(t => t.id === tabId);
+ if (tab) {
+ tab.dirty = true;
+ this.renderTabs();
+ }
+ }
+
+ renderTabs() {
+ const tabsContainer = this.container.querySelector('.editor-tabs');
+ tabsContainer.innerHTML = this.tabs.map(tab => `
+
+ ${tab.name}
+ ${tab.dirty ? '●' : ''}
+
+
+ `).join('');
+
+ // Tab click handlers
+ tabsContainer.querySelectorAll('.tab').forEach(tabEl => {
+ tabEl.addEventListener('click', (e) => {
+ if (!e.target.classList.contains('tab-close')) {
+ this.activateTab(tabEl.dataset.tabId);
+ }
+ });
+
+ tabEl.querySelector('.tab-close').addEventListener('click', (e) => {
+ e.stopPropagation();
+ this.closeTab(tabEl.dataset.tabId);
+ });
+ });
+ }
+}
+```
+
+### Enhanced Chat Input Component
+
+```javascript
+/**
+ * Enhanced Chat Input
+ * Supports: Attachments, history, slash commands, file mentions, draft persistence
+ */
+class ChatInput {
+ constructor(container) {
+ this.container = container;
+ this.textarea = container.querySelector('textarea');
+ this.attachments = [];
+ this.drafts = new Map();
+ this.history = [];
+ this.historyIndex = -1;
+
+ this.initialize();
+ }
+
+ initialize() {
+ // Auto-expand
+ this.textarea.addEventListener('input', () => {
+ autoExpandTextarea(this.textarea);
+ this.saveDraft();
+ this.checkTriggers();
+ });
+
+ // Paste handling
+ this.textarea.addEventListener('paste', (e) => this.handlePaste(e));
+
+ // Image paste
+ this.textarea.addEventListener('paste', (e) => this.handleImagePaste(e));
+
+ // History navigation
+ this.textarea.addEventListener('keydown', (e) => {
+ if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
+ this.navigateHistory(e);
+ } else if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ this.send();
+ }
+ });
+
+ // Attachment buttons
+ this.container.querySelector('.btn-attach-file').addEventListener('click', () => this.attachFile());
+ this.container.querySelector('.btn-attach-image').addEventListener('click', () => this.attachImage());
+ this.container.querySelector('.btn-send').addEventListener('click', () => this.send());
+
+ // Load draft
+ this.loadDraft();
+ }
+
+ checkTriggers() {
+ const value = this.textarea.value;
+ const cursorPos = this.textarea.selectionStart;
+
+ // Check for @ trigger
+ const atMatch = value.substring(0, cursorPos).match(/@(\w*)$/);
+ if (atMatch) {
+ this.showPicker('@', cursorPos - atMatch[0].length);
+ }
+
+ // Check for / trigger
+ const slashMatch = value.substring(0, cursorPos).match(/\/(\w*)$/);
+ if (slashMatch) {
+ this.showPicker('/', cursorPos - slashMatch[0].length);
+ }
+ }
+
+ handlePaste(e) {
+ const pastedText = (e.clipboardData || window.clipboardData).getData('text');
+ const lines = pastedText.split('\n').length;
+ const chars = pastedText.length;
+
+ if (chars > 150 || lines > 3) {
+ e.preventDefault();
+ this.addAttachment({
+ type: 'pasted',
+ label: `pasted #${this.attachments.filter(a => a.type === 'pasted').length + 1}`,
+ content: pastedText
+ });
+ }
+ }
+
+ handleImagePaste(e) {
+ const items = e.clipboardData.items;
+ for (const item of items) {
+ if (item.type.startsWith('image/')) {
+ e.preventDefault();
+ const file = item.getAsFile();
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ this.addAttachment({
+ type: 'image',
+ data: e.target.result,
+ preview: e.target.result
+ });
+ };
+ reader.readAsDataURL(file);
+ }
+ }
+ }
+
+ addAttachment(attachment) {
+ attachment.id = Date.now();
+ this.attachments.push(attachment);
+ this.renderAttachments();
+ }
+
+ removeAttachment(id) {
+ this.attachments = this.attachments.filter(a => a.id !== id);
+ this.renderAttachments();
+ }
+
+ renderAttachments() {
+ const container = this.container.querySelector('.attachment-chips');
+ container.innerHTML = this.attachments.map(a => `
+
+ ${a.type === 'file' ? `📄 ${a.name}` : ''}
+ ${a.type === 'image' ? `

` : ''}
+ ${a.type === 'pasted' ? `📋 ${a.label}` : ''}
+
+
+ `).join('');
+
+ container.querySelectorAll('.chip-remove').forEach(btn => {
+ btn.addEventListener('click', (e) => {
+ this.removeAttachment(parseInt(e.target.parentElement.dataset.id));
+ });
+ });
+ }
+
+ showPicker(trigger, position) {
+ // Show unified picker (implementation in architecture section)
+ }
+
+ saveDraft() {
+ const sessionId = getCurrentSessionId();
+ if (!sessionId) return;
+
+ const draft = {
+ value: this.textarea.value,
+ attachments: this.attachments,
+ timestamp: Date.now()
+ };
+
+ this.drafts.set(sessionId, draft);
+ localStorage.setItem(`chat-drafts`, JSON.stringify([...this.drafts]));
+ }
+
+ loadDraft() {
+ const sessionId = getCurrentSessionId();
+ if (!sessionId) return;
+
+ const drafts = JSON.parse(localStorage.getItem('chat-drafts') || '[]');
+ const draft = drafts.find(([id]) => id === sessionId);
+
+ if (draft) {
+ const [_, draftData] = draft;
+ this.textarea.value = draftData.value;
+ this.attachments = draftData.attachments || [];
+ this.renderAttachments();
+ autoExpandTextarea(this.textarea);
+ }
+ }
+
+ clearDraft() {
+ const sessionId = getCurrentSessionId();
+ this.drafts.delete(sessionId);
+ localStorage.setItem(`chat-drafts`, JSON.stringify([...this.drafts]));
+ }
+
+ navigateHistory(e) {
+ if (e.key === 'ArrowUp' && this.historyIndex < this.history.length - 1) {
+ e.preventDefault();
+ this.historyIndex++;
+ this.textarea.value = this.history[this.history.length - 1 - this.historyIndex];
+ } else if (e.key === 'ArrowDown' && this.historyIndex > 0) {
+ e.preventDefault();
+ this.historyIndex--;
+ this.textarea.value = this.history[this.history.length - 1 - this.historyIndex];
+ }
+ }
+
+ async send() {
+ const content = this.textarea.value.trim();
+ if (!content && this.attachments.length === 0) return;
+
+ // Ensure session exists
+ await ensureSession();
+
+ const message = {
+ sessionId: attachedSessionId,
+ content: content,
+ attachments: this.attachments,
+ timestamp: Date.now()
+ };
+
+ // Send to backend
+ await fetch('/claude/api/claude/chat', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(message)
+ });
+
+ // Save to history
+ this.history.push(content);
+ this.historyIndex = -1;
+ localStorage.setItem('chat-history', JSON.stringify(this.history));
+
+ // Clear input
+ this.textarea.value = '';
+ this.attachments = [];
+ this.renderAttachments();
+ this.clearDraft();
+ autoExpandTextarea(this.textarea);
+ }
+}
+```
+
+---
+
+## API Endpoints Required
+
+### Session Management
+
+```javascript
+// Fork session
+POST /claude/api/claude/sessions/:id/fork
+{
+ messageId: 123 // Fork point (optional)
+}
+Response: {
+ id: 'new-session-id',
+ parentSessionId: 'original-id',
+ forkPoint: 123
+}
+
+// Update session metadata
+PUT /claude/api/claude/sessions/:id
+{
+ metadata: {
+ agent: 'claude',
+ project: 'my-project'
+ }
+}
+```
+
+### Git Worktree
+
+```javascript
+// Create worktree
+POST /claude/api/git/worktree
+{
+ sessionId: 'session-id',
+ projectName: 'project-name',
+ path: '/worktrees/session-id'
+}
+Response: {
+ path: '/worktrees/session-id',
+ created: true
+}
+```
+
+### Token Tracking
+
+```javascript
+// Get token usage
+GET /claude/api/claude/sessions/:id/tokens
+Response: {
+ inputTokens: 15000,
+ outputTokens: 5000,
+ totalCost: 0.12
+}
+```
+
+---
+
+## Implementation Priority
+
+### Phase 1: Core File Editor (Week 1)
+- Install CodeMirror 6 packages
+- Create editor component with state management
+- Implement file loading/saving
+- Add tab support for multiple files
+- Add keyboard shortcuts (Ctrl+S, Ctrl+W)
+
+### Phase 2: Enhanced Chat Input (Week 1-2)
+- Implement expandable textarea
+- Add attachment system (files, images, pasted text)
+- Implement unified picker (@files, /commands)
+- Add draft persistence per session
+- Implement history navigation
+
+### Phase 3: Session Flow Fixes (Week 2)
+- Fix URL parameter parsing (?project=)
+- Implement session picker modal
+- Fix auto-session creation
+- Implement session forking
+- Add multi-session tabs
+
+### Phase 4: Mobile Responsiveness (Week 2-3)
+- Implement breakpoint system
+- Touch-optimize all interactions
+- Test on mobile devices
+- Implement hamburger menu for file tree
+- Adaptive layout for all screen sizes
+
+### Phase 5: Conduit Integrations (Week 3) - OPTIONAL
+- Implement session forking UI
+- Add token usage counter
+- Create multi-agent abstraction
+- Implement git worktree integration
+- Add import/export functionality
+
+---
+
+## Testing Strategy
+
+### Unit Tests
+
+```javascript
+// File Editor Tests
+describe('FileEditor', () => {
+ test('opens file and creates editor', () => {
+ const editor = new FileEditor(container);
+ editor.openFile('/test.js', 'console.log("test")');
+ expect(editor.editors.size).toBe(1);
+ });
+
+ test('marks file as dirty on edit', () => {
+ const editor = new FileEditor(container);
+ editor.openFile('/test.js', 'console.log("test")');
+ editor.markDirty('/test.js');
+ expect(editor.tabs[0].dirty).toBe(true);
+ });
+});
+
+// Chat Input Tests
+describe('ChatInput', () => {
+ test('auto-expands textarea', () => {
+ const input = new ChatInput(container);
+ input.textarea.value = 'line 1\nline 2\nline 3\nline 4';
+ input.textarea.dispatchEvent(new Event('input'));
+ expect(parseInt(input.textarea.style.height)).toBeGreaterThan(50);
+ });
+
+ test('detects long paste', () => {
+ const input = new ChatInput(container);
+ const longText = 'a'.repeat(200);
+ input.handlePaste({ preventDefault: jest.fn(), clipboardData: { getData: () => longText } });
+ expect(input.attachments.length).toBe(1);
+ expect(input.attachments[0].type).toBe('pasted');
+ });
+});
+```
+
+### Integration Tests
+
+```javascript
+// Session Flow Tests
+describe('Session Flow', () => {
+ test('auto-creates session on send', async () => {
+ attachedSessionId = null;
+ await sendMessage();
+ expect(attachedSessionId).toBeTruthy();
+ });
+
+ test('forks session at message', async () => {
+ const forked = await forkSession('session-123', 5);
+ expect(forked.parentSessionId).toBe('session-123');
+ expect(forked.messages.length).toBe(5);
+ });
+});
+```
+
+### Mobile Device Testing Matrix
+
+| Device | Screen Size | OS | Browser | Test Coverage |
+|--------|-------------|----|---------|---------------|
+| iPhone SE | 375×667 | iOS 17 | Safari | All features |
+| iPhone 14 | 390×844 | iOS 17 | Safari | All features |
+| Samsung Galaxy S23 | 360×780 | Android 14 | Chrome | All features |
+| iPad Mini | 744×1133 | iPadOS 17 | Safari | All features |
+| iPad Pro | 1024×1366 | iPadOS 17 | Safari | All features |
+| Desktop | 1920×1080 | Windows 11 | Chrome | All features |
+
+### Proof Verification Checkpoints
+
+**Checkpoint 1: File Editor**
+- ✅ Can load files from file tree
+- ✅ Syntax highlighting works for JS, Python, HTML, CSS, JSON, MD
+- ✅ Can edit and save files
+- ✅ Tab switching works
+- ✅ Keyboard shortcuts work (Ctrl+S, Ctrl+W, Ctrl+Tab)
+- ✅ Mobile responsive (works on phone)
+
+**Checkpoint 2: Chat Input**
+- ✅ Textarea auto-expands (2-15 lines)
+- ✅ Can attach files (drag & drop)
+- ✅ Can attach images (paste)
+- ✅ Long pastes become chips
+- ✅ @ mentions show file picker
+- ✅ / commands show autocomplete
+- ✅ Draft persists on refresh
+- ✅ History navigation works (↑↓)
+- ✅ Shell mode works (! prefix)
+
+**Checkpoint 3: Session Flow**
+- ✅ Session picker shows on startup
+- ✅ Can load existing session
+- ✅ Can create new session
+- ✅ Auto-creates session on first send
+- ✅ Session forking works
+- ✅ Multi-session tabs work
+- ✅ URL parameters work (?session=, ?project=)
+
+**Checkpoint 4: Mobile**
+- ✅ File tree accessible via hamburger menu
+- ✅ Editor usable on small screen
+- ✅ Chat input works on mobile
+- ✅ All touch targets ≥44×44px
+- ✅ Swipe gestures work
+- ✅ No horizontal scrolling
+
+**Checkpoint 5: Conduit**
+- ✅ Session forking UI accessible
+- ✅ Token counter shows real-time cost
+- ✅ Can switch agents
+- ✅ Worktree creation works
+- ✅ Import/export functions work
+
+---
+
+## Proof of Success Criteria
+
+### File Editor
+- Can load any file from the file tree
+- Syntax highlighting works for all supported languages
+- Can edit files with full CodeMirror capabilities (autocomplete, bracket matching, etc.)
+- Can save files (Ctrl+S or toolbar button)
+- Tab-based multi-file editing works
+- Mobile: File tree in hamburger menu, editor takes full width
+- **Verification:** Test loading 5 different file types, edit, save, verify changes persisted
+
+### Chat Input
+- Textarea expands from 2 to 15 lines based on content
+- Can attach files via drag & drop or button
+- Can paste images, they show as preview chips
+- Pastes >150 chars or >3 lines become "pasted #N" chips
+- Type @ to see file picker
+- Type / to see command autocomplete
+- Draft persists when switching sessions
+- Press ↑↓ to navigate message history
+- Type ! to enter shell mode
+- Mobile: All features work, input max 4 lines, touch targets 44px minimum
+- **Verification:** Test all attachment types, paste long text, verify draft persists after refresh
+
+### Session Flow
+- Opening chat shows session picker (recent, projects, new)
+- Can select existing session to load
+- Can create new session with name
+- If no session, first message auto-creates one
+- Clicking 🍴 Fork on message creates new session branched from that point
+- Multi-session tabs show across top, can switch between sessions
+- URL ?session=123 loads that session directly
+- URL ?project=myapp creates session in that project context
+- **Verification:** Create session, send messages, fork at message 3, verify forked session has messages 1-3
+
+### Mobile
+- All features usable on iPhone SE (375px width)
+- File tree: Hamburger menu (mobile), drawer (tablet), sidebar (desktop)
+- Editor: Full width (mobile), 70/30 split (tablet), resizable panels (desktop)
+- Chat: Full width, input max 4 lines (mobile), side-by-side (desktop)
+- Tabs: Horizontal scroll, max 3 visible before scrolling (mobile)
+- All buttons ≥44×44px for touch
+- No horizontal scrolling on any page
+- **Verification:** Test on real device, navigate all features, take screenshots
+
+### Conduit
+- Each message shows token count (in/out)
+- Total session cost displayed in header
+- Fork button appears on hover for each message
+- Agent switcher in settings
+- Can create git worktree for session
+- Export session to JSON
+- **Verification:** Send 10 messages, verify token count matches expected, fork session, verify worktree created
+
+---
+
+## Conclusion
+
+This design document provides a comprehensive blueprint for transforming the Claude Code Web IDE into a professional, mobile-first development environment. By incorporating best practices from CodeMirror 6, CodeNomad, OpenCode, and Conduit-Copy, we will deliver a smooth, frictionless coding experience that works seamlessly across all devices.
+
+**Key Success Metrics:**
+1. File editor functional in 1 week
+2. Chat input enhanced in 1-2 weeks
+3. Session flow fixed in 2 weeks
+4. Mobile fully responsive in 2-3 weeks
+5. All features tested and verified in 3 weeks
+
+**Next Steps:**
+1. ✅ Design document complete (this document)
+2. Commit design document to git
+3. Set up git worktree for implementation
+4. Select optimal agents for each phase
+5. Begin Phase 1: File Editor implementation
+
+---
+
+**Appendix: Reference Links**
+
+- CodeMirror 6: https://codemirror.net/
+- CodeServer: https://github.com/coder/code-server
+- CodeNomad: https://github.com/NeuralNomadsAI/CodeNomad
+- OpenCode: https://github.com/anomalyco/opencode
+- Conduit-Copy: https://github.com/roman-ryzenadvanced/conduit-copy-