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>
1460 lines
44 KiB
Markdown
1460 lines
44 KiB
Markdown
# 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-
|