fix: correct claudeSessionId for resumed sessions and clean up DEC sync dead code

Use resumeSessionId for Claude conversation ID when resuming sessions,
increase default font size to 14, extract shared history fetch logic,
and remove unused DEC 2026 sync constants/functions (xterm.js 6.0 handles natively).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-03-11 19:36:58 +01:00
co-authored by Claude Opus 4.6
parent abbc4b13fd
commit d02cddece6
4 changed files with 41 additions and 120 deletions
+4 -6
View File
@@ -959,9 +959,8 @@ export class Session extends EventEmitter {
} }
); );
// Set claudeSessionId immediately since we passed --session-id to Claude // Set claudeSessionId — when resuming, the Claude conversation ID is the resumed one.
// The mux manager passes --session-id ${sessionId} to Claude this._claudeSessionId = this._resumeSessionId || this.id;
this._claudeSessionId = this.id;
} catch (spawnErr) { } catch (spawnErr) {
console.error('[Session] Failed to spawn PTY for mux attachment:', spawnErr); console.error('[Session] Failed to spawn PTY for mux attachment:', spawnErr);
this.emit('error', `Failed to attach to mux session: ${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 // Set claudeSessionId — when resuming, the Claude conversation ID is the resumed one.
// This ensures subagent matching works without waiting for JSON messages this._claudeSessionId = this._resumeSessionId || this.id;
this._claudeSessionId = this.id;
this._pid = this.ptyProcess.pid; this._pid = this.ptyProcess.pid;
console.log('[Session] Interactive PTY spawned with PID:', this._pid); console.log('[Session] Interactive PTY spawned with PID:', this._pid);
+33 -43
View File
@@ -63,7 +63,7 @@
* @class CodemanApp * @class CodemanApp
* @globals {CodemanApp} app - Singleton instance (also on window.app) * @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 mobile-handlers.js (MobileDetection, KeyboardHandler, SwipeHandler)
* @dependency voice-input.js (VoiceInput, DeepgramProvider) * @dependency voice-input.js (VoiceInput, DeepgramProvider)
* @dependency notification-manager.js (NotificationManager class) * @dependency notification-manager.js (NotificationManager class)
@@ -600,7 +600,7 @@ class CodemanApp {
}, },
fontFamily: '"Fira Code", "Cascadia Code", "JetBrains Mono", "SF Mono", Monaco, monospace', 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) // 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, lineHeight: 1.2,
cursorBlink: false, cursorBlink: false,
cursorStyle: 'block', cursorStyle: 'block',
@@ -1181,35 +1181,41 @@ class CodemanApp {
} }
} }
/**
* Fetch and deduplicate history sessions (up to 2 per dir, max `limit` total).
* @returns {Promise<Array>} 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() { async loadHistorySessions() {
const container = document.getElementById('historySessions'); const container = document.getElementById('historySessions');
const list = document.getElementById('historyList'); const list = document.getElementById('historyList');
if (!container || !list) return; if (!container || !list) return;
try { try {
const res = await fetch('/api/history/sessions'); const display = await this._fetchHistorySessions(12);
const data = await res.json(); if (display.length === 0) {
const sessions = data.sessions || [];
if (sessions.length === 0) {
container.style.display = 'none'; container.style.display = 'none';
return; 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) // Build DOM safely (no innerHTML with user data)
list.replaceChildren(); list.replaceChildren();
for (const s of display) { for (const s of display) {
@@ -4263,28 +4269,12 @@ class CodemanApp {
container.innerHTML = '<div class="run-mode-hist-empty">Loading...</div>'; container.innerHTML = '<div class="run-mode-hist-empty">Loading...</div>';
try { try {
const res = await fetch('/api/history/sessions'); const display = await this._fetchHistorySessions(10);
const data = await res.json(); if (display.length === 0) {
const sessions = data.sessions || [];
if (sessions.length === 0) {
container.innerHTML = '<div class="run-mode-hist-empty">No history</div>'; container.innerHTML = '<div class="run-mode-hist-empty">No history</div>';
return; 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 // Build items using DOM API for reliable mobile touch handling
container.replaceChildren(); container.replaceChildren();
for (const s of display) { for (const s of display) {
@@ -5211,13 +5201,13 @@ class CodemanApp {
} }
increaseFontSize() { increaseFontSize() {
const current = this.terminal.options.fontSize || 12; const current = this.terminal.options.fontSize || 14;
this.setFontSize(Math.min(current + 2, 24)); this.setFontSize(Math.min(current + 2, 24));
} }
decreaseFontSize() { decreaseFontSize() {
const current = this.terminal.options.fontSize || 12; const current = this.terminal.options.fontSize || 14;
this.setFontSize(Math.max(current - 2, 8)); this.setFontSize(Math.max(current - 2, 10));
} }
setFontSize(size) { setFontSize(size) {
@@ -5233,7 +5223,7 @@ class CodemanApp {
const saved = localStorage.getItem('codeman-font-size'); const saved = localStorage.getItem('codeman-font-size');
if (saved) { if (saved) {
const size = parseInt(saved, 10); const size = parseInt(saved, 10);
if (size >= 8 && size <= 24) { if (size >= 10 && size <= 24) {
this.terminal.options.fontSize = size; this.terminal.options.fontSize = size;
document.getElementById('fontSizeDisplay').textContent = size; document.getElementById('fontSizeDisplay').textContent = size;
} }
+4 -64
View File
@@ -2,13 +2,12 @@
* @fileoverview Shared constants, utility functions, and SSE event type registry for all frontend modules. * @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 * 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 * globals defined here: timing constants, Z-index layers, respawn
* preset definitions, the SSE_EVENTS registry, and shared utilities (escapeHtml, extractSyncSegments, * preset definitions, the SSE_EVENTS registry, and shared utilities (escapeHtml,
* getEventCoords, scheduleBackground, urlBase64ToUint8Array). * getEventCoords, scheduleBackground, urlBase64ToUint8Array).
* *
* @globals {function} urlBase64ToUint8Array - VAPID key conversion for Web Push * @globals {function} urlBase64ToUint8Array - VAPID key conversion for Web Push
* @globals {function} scheduleBackground - scheduler.postTask wrapper (background priority) * @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} getEventCoords - Unified mouse/touch coordinate extractor
* @globals {function} escapeHtml - XSS-safe HTML escaping * @globals {function} escapeHtml - XSS-safe HTML escaping
* @globals {object} SSE_EVENTS - Centralized SSE event type constants (~73 event types) * @globals {object} SSE_EVENTS - Centralized SSE event type constants (~73 event types)
@@ -79,14 +78,8 @@ function scheduleBackground(fn) {
else { requestAnimationFrame(fn); } else { requestAnimationFrame(fn); }
} }
// DEC mode 2026 - Synchronized Output (xterm.js 6.0+ handles natively) // DEC mode 2026 marker stripping — xterm.js 6.0 handles sync natively,
// Wrap terminal writes with these markers to prevent partial-frame flicker. // but server-sent terminal buffers may still contain markers from Claude CLI.
// 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)
const DEC_SYNC_STRIP_RE = /\x1b\[\?2026[hl]/g; const DEC_SYNC_STRIP_RE = /\x1b\[\?2026[hl]/g;
// Built-in respawn configuration presets // Built-in respawn configuration presets
@@ -303,59 +296,6 @@ function getEventCoords(e) {
return { clientX: e.clientX, clientY: e.clientY }; 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) // HTML escape utility (shared by NotificationManager, CodemanApp, and ralph-wizard.js)
const _htmlEscapeMap = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }; const _htmlEscapeMap = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
const _htmlEscapePattern = /[&<>"']/g; const _htmlEscapePattern = /[&<>"']/g;
-7
View File
@@ -123,13 +123,6 @@ import {
INACTIVITY_TIMEOUT_MS, INACTIVITY_TIMEOUT_MS,
} from '../config/server-timing.js'; } 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. // SSE padding for Cloudflare tunnel buffer flushing.
// Cloudflare quick tunnels buffer small SSE responses, causing lag for real-time events. // 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. // Appending SSE comment padding (ignored by EventSource) forces the proxy to flush.