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

44 KiB
Raw Blame History

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

{
  "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

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

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

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

// 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)

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

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

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

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

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

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

/* 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

/* 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

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

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

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

/**
 * 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

/**
 * 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

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

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

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

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

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