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