diff --git a/src/session.ts b/src/session.ts index 4402dfba..57cb6e7a 100644 --- a/src/session.ts +++ b/src/session.ts @@ -959,9 +959,8 @@ export class Session extends EventEmitter { } ); - // Set claudeSessionId immediately since we passed --session-id to Claude - // The mux manager passes --session-id ${sessionId} to Claude - this._claudeSessionId = this.id; + // Set claudeSessionId — when resuming, the Claude conversation ID is the resumed one. + this._claudeSessionId = this._resumeSessionId || this.id; } catch (spawnErr) { console.error('[Session] Failed to spawn PTY for mux attachment:', spawnErr); this.emit('error', `Failed to attach to mux session: ${spawnErr}`); @@ -1046,9 +1045,8 @@ export class Session extends EventEmitter { } } - // Set the claudeSessionId immediately since we passed --session-id - // This ensures subagent matching works without waiting for JSON messages - this._claudeSessionId = this.id; + // Set claudeSessionId — when resuming, the Claude conversation ID is the resumed one. + this._claudeSessionId = this._resumeSessionId || this.id; this._pid = this.ptyProcess.pid; console.log('[Session] Interactive PTY spawned with PID:', this._pid); diff --git a/src/web/public/app.js b/src/web/public/app.js index 13ab3812..27fc0907 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -63,7 +63,7 @@ * @class CodemanApp * @globals {CodemanApp} app - Singleton instance (also on window.app) * - * @dependency constants.js (SSE_EVENTS, timing constants, escapeHtml, extractSyncSegments, DEC sync markers) + * @dependency constants.js (SSE_EVENTS, timing constants, escapeHtml, DEC_SYNC_STRIP_RE) * @dependency mobile-handlers.js (MobileDetection, KeyboardHandler, SwipeHandler) * @dependency voice-input.js (VoiceInput, DeepgramProvider) * @dependency notification-manager.js (NotificationManager class) @@ -600,7 +600,7 @@ class CodemanApp { }, fontFamily: '"Fira Code", "Cascadia Code", "JetBrains Mono", "SF Mono", Monaco, monospace', // Use smaller font on mobile to fit more columns (prevents wrapping of Claude's status line) - fontSize: MobileDetection.getDeviceType() === 'mobile' ? 10 : 12, + fontSize: MobileDetection.getDeviceType() === 'mobile' ? 10 : 14, lineHeight: 1.2, cursorBlink: false, cursorStyle: 'block', @@ -1181,35 +1181,41 @@ class CodemanApp { } } + /** + * Fetch and deduplicate history sessions (up to 2 per dir, max `limit` total). + * @returns {Promise} deduplicated session list, sorted by lastModified desc + */ + async _fetchHistorySessions(limit = 12) { + const res = await fetch('/api/history/sessions'); + const data = await res.json(); + const sessions = data.sessions || []; + if (sessions.length === 0) return []; + + const byDir = new Map(); + for (const s of sessions) { + if (!byDir.has(s.workingDir)) byDir.set(s.workingDir, []); + byDir.get(s.workingDir).push(s); + } + const items = []; + for (const [, group] of byDir) { + items.push(...group.slice(0, 2)); + } + items.sort((a, b) => new Date(b.lastModified) - new Date(a.lastModified)); + return items.slice(0, limit); + } + async loadHistorySessions() { const container = document.getElementById('historySessions'); const list = document.getElementById('historyList'); if (!container || !list) return; try { - const res = await fetch('/api/history/sessions'); - const data = await res.json(); - const sessions = data.sessions || []; - if (sessions.length === 0) { + const display = await this._fetchHistorySessions(12); + if (display.length === 0) { container.style.display = 'none'; return; } - // Deduplicate: keep only the most recent session per workingDir - const byDir = new Map(); - for (const s of sessions) { - if (!byDir.has(s.workingDir)) byDir.set(s.workingDir, []); - byDir.get(s.workingDir).push(s); - } - - // Flatten: show up to 2 most recent per dir, max 12 total - const items = []; - for (const [, group] of byDir) { - items.push(...group.slice(0, 2)); - } - items.sort((a, b) => new Date(b.lastModified) - new Date(a.lastModified)); - const display = items.slice(0, 12); - // Build DOM safely (no innerHTML with user data) list.replaceChildren(); for (const s of display) { @@ -4263,28 +4269,12 @@ class CodemanApp { container.innerHTML = '
Loading...
'; try { - const res = await fetch('/api/history/sessions'); - const data = await res.json(); - const sessions = data.sessions || []; - - if (sessions.length === 0) { + const display = await this._fetchHistorySessions(10); + if (display.length === 0) { container.innerHTML = '
No history
'; return; } - // Deduplicate: up to 2 per dir, max 10 total - const byDir = new Map(); - for (const s of sessions) { - if (!byDir.has(s.workingDir)) byDir.set(s.workingDir, []); - byDir.get(s.workingDir).push(s); - } - const items = []; - for (const [, group] of byDir) { - items.push(...group.slice(0, 2)); - } - items.sort((a, b) => new Date(b.lastModified) - new Date(a.lastModified)); - const display = items.slice(0, 10); - // Build items using DOM API for reliable mobile touch handling container.replaceChildren(); for (const s of display) { @@ -5211,13 +5201,13 @@ class CodemanApp { } increaseFontSize() { - const current = this.terminal.options.fontSize || 12; + const current = this.terminal.options.fontSize || 14; this.setFontSize(Math.min(current + 2, 24)); } decreaseFontSize() { - const current = this.terminal.options.fontSize || 12; - this.setFontSize(Math.max(current - 2, 8)); + const current = this.terminal.options.fontSize || 14; + this.setFontSize(Math.max(current - 2, 10)); } setFontSize(size) { @@ -5233,7 +5223,7 @@ class CodemanApp { const saved = localStorage.getItem('codeman-font-size'); if (saved) { const size = parseInt(saved, 10); - if (size >= 8 && size <= 24) { + if (size >= 10 && size <= 24) { this.terminal.options.fontSize = size; document.getElementById('fontSizeDisplay').textContent = size; } diff --git a/src/web/public/constants.js b/src/web/public/constants.js index 7543c2e2..44e6c9a0 100644 --- a/src/web/public/constants.js +++ b/src/web/public/constants.js @@ -2,13 +2,12 @@ * @fileoverview Shared constants, utility functions, and SSE event type registry for all frontend modules. * * This is the first script loaded in index.html. Every other frontend module depends on the - * globals defined here: timing constants, Z-index layers, DEC 2026 sync markers, respawn - * preset definitions, the SSE_EVENTS registry, and shared utilities (escapeHtml, extractSyncSegments, + * globals defined here: timing constants, Z-index layers, respawn + * preset definitions, the SSE_EVENTS registry, and shared utilities (escapeHtml, * getEventCoords, scheduleBackground, urlBase64ToUint8Array). * * @globals {function} urlBase64ToUint8Array - VAPID key conversion for Web Push * @globals {function} scheduleBackground - scheduler.postTask wrapper (background priority) - * @globals {function} extractSyncSegments - DEC 2026 terminal sync marker parser * @globals {function} getEventCoords - Unified mouse/touch coordinate extractor * @globals {function} escapeHtml - XSS-safe HTML escaping * @globals {object} SSE_EVENTS - Centralized SSE event type constants (~73 event types) @@ -79,14 +78,8 @@ function scheduleBackground(fn) { else { requestAnimationFrame(fn); } } -// DEC mode 2026 - Synchronized Output (xterm.js 6.0+ handles natively) -// Wrap terminal writes with these markers to prevent partial-frame flicker. -// Terminal buffers all output between markers and renders atomically. -// Supported by: WezTerm, Kitty, Ghostty, iTerm2 3.5+, Windows Terminal, VSCode terminal -// xterm.js 6.0+ supports DEC 2026 natively. Constants kept for reference/stripping. -const DEC_SYNC_START = '\x1b[?2026h'; -const DEC_SYNC_END = '\x1b[?2026l'; -// Pre-compiled regex for stripping DEC 2026 markers (single pass instead of two replaceAll calls) +// DEC mode 2026 marker stripping — xterm.js 6.0 handles sync natively, +// but server-sent terminal buffers may still contain markers from Claude CLI. const DEC_SYNC_STRIP_RE = /\x1b\[\?2026[hl]/g; // Built-in respawn configuration presets @@ -303,59 +296,6 @@ function getEventCoords(e) { return { clientX: e.clientX, clientY: e.clientY }; } -/** - * Process data containing DEC 2026 sync markers. - * Strips markers and returns segments that should be written atomically. - * Each returned segment represents content between SYNC_START and SYNC_END. - * Content outside sync blocks is returned as-is. - * - * @param {string} data - Raw terminal data with potential sync markers - * @returns {string[]} - Array of content segments to write (markers stripped) - */ -function extractSyncSegments(data) { - const segments = []; - let remaining = data; - - while (remaining.length > 0) { - const startIdx = remaining.indexOf(DEC_SYNC_START); - - if (startIdx === -1) { - // No more sync blocks, return rest as-is - if (remaining.length > 0) { - segments.push(remaining); - } - break; - } - - // Content before sync block (if any) - if (startIdx > 0) { - segments.push(remaining.slice(0, startIdx)); - } - - // Find matching end marker - const afterStart = remaining.slice(startIdx + DEC_SYNC_START.length); - const endIdx = afterStart.indexOf(DEC_SYNC_END); - - if (endIdx === -1) { - // No end marker found - sync block continues in next chunk - // Include the start marker so it can be handled when more data arrives - segments.push(remaining.slice(startIdx)); - break; - } - - // Extract synchronized content (without markers) - const syncContent = afterStart.slice(0, endIdx); - if (syncContent.length > 0) { - segments.push(syncContent); - } - - // Continue with content after end marker - remaining = afterStart.slice(endIdx + DEC_SYNC_END.length); - } - - return segments; -} - // HTML escape utility (shared by NotificationManager, CodemanApp, and ralph-wizard.js) const _htmlEscapeMap = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; const _htmlEscapePattern = /[&<>"']/g; diff --git a/src/web/server.ts b/src/web/server.ts index 03b85581..ccbd8574 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -123,13 +123,6 @@ import { INACTIVITY_TIMEOUT_MS, } from '../config/server-timing.js'; -// DEC mode 2026 - Synchronized Output -// When terminal supports this, it buffers all output between start/end markers -// and renders atomically, eliminating partial-frame flicker from Ink redraws. -// Supported by: WezTerm, Kitty, Ghostty, iTerm2 3.5+, Windows Terminal, VSCode terminal -// DEC 2026 sync markers no longer added server-side — xterm.js 6.0 handles -// them natively and Claude CLI already emits its own markers via Ink. - // SSE padding for Cloudflare tunnel buffer flushing. // Cloudflare quick tunnels buffer small SSE responses, causing lag for real-time events. // Appending SSE comment padding (ignored by EventSource) forces the proxy to flush.