# 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-