Files
SuperCharged-Claude-Code-Up…/docs/plans/2026-01-21-file-editor-chat-ui-redesign.md
uroma 9e445bf653 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 <noreply@anthropic.com>
2026-01-21 08:02:19 +00:00

1460 lines
44 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 => `
<div class="picker-item" data-value="${item.label || item.name}">
<span class="picker-label">${item.label || item.name}</span>
<span class="picker-description">${item.description || ''}</span>
</div>
`).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 = `
<div class="session-picker-content">
<h2>Select a Session</h2>
<div class="picker-tabs">
<button class="tab active" data-tab="recent">Recent</button>
<button class="tab" data-tab="projects">Projects</button>
<button class="tab" data-tab="new">+ New Session</button>
</div>
<div class="picker-content">
<div id="recent-sessions" class="tab-content active">
<!-- Recent sessions list -->
</div>
<div id="projects-list" class="tab-content">
<!-- Projects list -->
</div>
<div id="new-session" class="tab-content">
<input type="text" placeholder="Session name or project..." />
<button class="btn-primary">Create Session</button>
</div>
</div>
</div>
`;
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 = `
<span class="token-input">${this.inputTokens.toLocaleString()} in</span>
<span class="token-output">${this.outputTokens.toLocaleString()} out</span>
<span class="token-cost">$${this.totalCost.toFixed(4)}</span>
`;
}
}
```
#### 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 => `
<div class="tab ${tab.id === this.activeTab ? 'active' : ''}" data-tab-id="${tab.id}">
<span class="tab-name">${tab.name}</span>
${tab.dirty ? '<span class="tab-dirty">●</span>' : ''}
<button class="tab-close">×</button>
</div>
`).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 => `
<div class="attachment-chip" data-id="${a.id}">
${a.type === 'file' ? `📄 ${a.name}` : ''}
${a.type === 'image' ? `<img src="${a.preview}" />` : ''}
${a.type === 'pasted' ? `📋 ${a.label}` : ''}
<button class="chip-remove">×</button>
</div>
`).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-