From 9e445bf653cc81cfeac01108a752d6ca869213a6 Mon Sep 17 00:00:00 2001 From: uroma Date: Wed, 21 Jan 2026 08:02:19 +0000 Subject: [PATCH] docs: Add comprehensive design document for file editor & chat UI redesign This document outlines the complete plan for implementing: - CodeMirror 6 file editor with syntax highlighting - Enhanced chat input with attachments, history, slash commands - Hybrid session flow (OpenCode + CodeNomad patterns) - Mobile-first responsive design - Conduit-copy integrations (session forking, token tracking) Key features: - Phase-by-phase implementation plan (5 phases, 3 weeks) - Component specifications with code examples - API endpoints required for new features - Testing strategy with unit/integration tests - Proof verification checkpoints - Mobile device testing matrix The design incorporates best practices from: - CodeServer (CodeMirror 6 integration) - CodeNomad (sophisticated prompt input) - OpenCode (session management) - Conduit-Copy (multi-agent orchestration) Co-Authored-By: Claude Sonnet 4.5 --- ...2026-01-21-file-editor-chat-ui-redesign.md | 1459 +++++++++++++++++ 1 file changed, 1459 insertions(+) create mode 100644 docs/plans/2026-01-21-file-editor-chat-ui-redesign.md 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-