From dcc814f40c2247e60b4d9f412092e97c4b045be9 Mon Sep 17 00:00:00 2001 From: arkon Date: Tue, 12 May 2026 09:18:14 +0200 Subject: [PATCH] chore: version packages Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 10 ++++ CLAUDE.md | 3 +- README.md | 11 +++++ package-lock.json | 4 +- package.json | 2 +- src/web/public/constants.js | 2 +- src/web/routes/session-routes.ts | 84 ++++++++++++++++++-------------- 7 files changed, 75 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b09a78e0..8f69cbbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # aicodeman +## 0.6.7 + +### Patch Changes + +- - **fix(client): preserve inline rename input across tab re-renders** (#81) — Right-click → rename on a session tab no longer loses keystrokes when SSE traffic from sibling sessions triggers a tab re-render. Adds an `_inlineRenameActive` guard at the top of `renderSessionTabs()` and `_fullRenameSessionTabs()` so the in-progress input isn't destroyed mid-typing. Also fixes a latent double-fire of `finishRename` (blur + Enter could both invoke it). Drive-by: safer DOM child clearing in place of `innerHTML = ''`. + - **feat: hostname-aware window title** (#82) — The browser tab title is now `codeman:` instead of the bare `Codeman` literal, so users running Codeman on multiple hosts (laptop, dev box, NAS) can tell at a glance which tab points at which backend. New `--title-hostname ` CLI flag overrides the detected `os.hostname()` when it's noisy or you want a cosmetic name. The title is templated into the served HTML on first byte (with narrow HTML escaping), so it's correct from the first paint and works without JavaScript. Title-flash logic now respects the per-host title. + - **perf: larger terminal tail on tab switch** — `TERMINAL_TAIL_SIZE` raised from 128KB to 1MB. When switching back to a busy session tab you now get ~8× more scrollback restored immediately. + - **fix: preserve response text in Ink redraw stripping** — `stripInkRedrawBloat()` rewritten from a first-VPA approach to cluster-based detection. The previous algorithm assumed all VPA escapes after the first one belonged to a single redraw region and discarded everything in between, which silently lost 100KB+ of legitimate Claude response text once a render had occurred. The new approach groups VPAs into clusters separated by ≥8KB gaps and only collapses clusters spanning ≥32KB, so streamed response content between redraw bursts is preserved. + - **docs**: `CLAUDE.md` Additional Commands gains the `--title-hostname` row; `README.md` gets a "Hostname-Aware Window Title" subsection under Multi-Session Dashboard. + ## 0.6.6 ### Patch Changes diff --git a/CLAUDE.md b/CLAUDE.md index ce08b2f5..1515818c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -56,7 +56,7 @@ When user says "COM": CI runs `npm run check:lockfile` on every push/PR, so lockfile drift fails the build even if the `version-packages` script is bypassed. -**Version**: 0.6.6 (must match `package.json`) +**Version**: 0.6.7 (must match `package.json`) ## Project Overview @@ -77,6 +77,7 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph | Task | Command | |------|---------| | Dev with TLS | `npx tsx src/index.ts web --https` | +| Override window title hostname | `npx tsx src/index.ts web --title-hostname ` (default: `os.hostname()` — tab title renders as `codeman:`) | | Continuous typecheck | `tsc --noEmit --watch` | | Test coverage | `npm run test:coverage` | | Dead-code sweep | `npm run knip` (config in `knip.json`) | diff --git a/README.md b/README.md index 311afec8..bec7bc48 100644 --- a/README.md +++ b/README.md @@ -226,6 +226,17 @@ Run **20 parallel sessions** with full visibility — real-time xterm.js termina Every session runs inside **tmux** — sessions survive server restarts, network drops, and machine sleep. Auto-recovery on startup with dual redundancy. Ghost session discovery finds orphaned tmux sessions. Managed sessions are environment-tagged so the agent won't kill its own session. +### Hostname-Aware Window Title + +Running Codeman on multiple hosts (laptop, dev box, NAS)? The browser tab title is `codeman:` so you can tell which backend each tab points at without clicking in: + +```bash +codeman web # codeman: +codeman web --title-hostname dev-box # codeman:dev-box (manual override for noisy hostnames) +``` + +The title is templated into the served HTML on first byte, so it's correct from the very first paint and works without JavaScript. + ### Smart Token Management | Threshold | Action | Result | diff --git a/package-lock.json b/package-lock.json index 847d1421..2f4be212 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "aicodeman", - "version": "0.6.6", + "version": "0.6.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "aicodeman", - "version": "0.6.6", + "version": "0.6.7", "hasInstallScript": true, "license": "MIT", "workspaces": [ diff --git a/package.json b/package.json index c5072210..f24891e3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aicodeman", - "version": "0.6.6", + "version": "0.6.7", "description": "The missing control plane for AI coding agents - run 20 autonomous agents with real-time monitoring and session persistence", "type": "module", "main": "dist/index.js", diff --git a/src/web/public/constants.js b/src/web/public/constants.js index b977662c..db309f3a 100644 --- a/src/web/public/constants.js +++ b/src/web/public/constants.js @@ -54,7 +54,7 @@ const BROWSER_NOTIF_RATE_LIMIT_MS = 3000; // Rate limit for browser notificati const AUTO_CLOSE_NOTIFICATION_MS = 8000; // Auto-close browser notifications const THROTTLE_DELAY_MS = 100; // General UI throttle delay const TERMINAL_CHUNK_SIZE = 32 * 1024; // 32KB chunks for terminal buffer loading -const TERMINAL_TAIL_SIZE = 128 * 1024; // 128KB tail for initial load +const TERMINAL_TAIL_SIZE = 1024 * 1024; // 1MB tail for initial load (more scrollback on tab switch) const SYNC_WAIT_TIMEOUT_MS = 50; // Wait timeout for terminal sync const STATS_POLLING_INTERVAL_MS = 2000; // System stats polling diff --git a/src/web/routes/session-routes.ts b/src/web/routes/session-routes.ts index ee60211f..22766723 100644 --- a/src/web/routes/session-routes.ts +++ b/src/web/routes/session-routes.ts @@ -66,48 +66,60 @@ const LEADING_WHITESPACE_PATTERN = /^[\s\r\n]+/; /** * Strip redundant Ink spinner/status-bar redraw frames from the terminal buffer. - * Ink (Claude Code's TUI) uses absolute cursor positioning (CSI n d = VPA, CSI n;m H = CUP) - * to animate the spinner and update the status bar. During long thinking phases, these frames - * accumulate to 500KB+ of repeated overwrites to the same rows. When the buffer is tailed, - * only spinner frames are returned, making the terminal appear empty. + * Ink (Claude Code's TUI) uses absolute cursor positioning (CSI n d = VPA) to animate + * the spinner and update the status bar. During long thinking phases, these frames + * accumulate to 500KB+ of repeated overwrites to the same rows. * - * Strategy: find where absolute-positioned redraws begin (first VPA sequence), then keep - * only the last ~4KB of redraw frames (the final visual state) and discard the rest. + * Strategy: detect "redraw clusters" — dense runs of VPA escapes where each is within + * FRAME_GAP bytes of the previous (i.e. continuous rerendering of the same UI region). + * Collapse each big cluster down to just the bytes from its last VPA onwards (the final + * frame). Content *between* clusters (Claude's streamed response text) is preserved. + * + * Without clustering, a single first-VPA-finds-all approach would discard the entire + * conversation after Claude's first render — losing 100KB+ of legitimate scrollback. */ function stripInkRedrawBloat(buffer: string): string { - // Find where Ink's absolute-positioned redraws start (first CSI n d = VPA) // eslint-disable-next-line no-control-regex - const firstVPA = buffer.search(/\x1b\[\d+d/); - if (firstVPA === -1) return buffer; // No Ink redraws - - const contentPart = buffer.slice(0, firstVPA); - const redrawPart = buffer.slice(firstVPA); - - // If the redraw section is small (<16KB), not worth stripping - if (redrawPart.length < 16384) return buffer; - - // Find the last complete Ink frame by searching for where the VPA row - // number drops (cursor jumps back to viewport top for a new render cycle). - // Search the last 64KB — a single Ink frame with response content can be - // 10-20KB, so 4KB was too small and caused partial frames (blank gap). - const searchLen = Math.min(redrawPart.length, 65536); - const searchWindow = redrawPart.slice(-searchLen); - - // eslint-disable-next-line no-control-regex - const vpaRe = /\x1b\[(\d+)d/g; - let lastFrameStart = 0; - let prevRow = -1; - let match; - while ((match = vpaRe.exec(searchWindow)) !== null) { - const row = parseInt(match[1], 10); - // Row number dropped significantly — Ink started a new frame - if (prevRow > 0 && row < prevRow - 5) { - lastFrameStart = match.index; - } - prevRow = row; + const vpaRe = /\x1b\[\d+d/g; + const positions: number[] = []; + let m: RegExpExecArray | null; + while ((m = vpaRe.exec(buffer)) !== null) { + positions.push(m.index); } + if (positions.length < 10) return buffer; // Too few VPAs to be bloat - return contentPart + searchWindow.slice(lastFrameStart); + // Group consecutive VPAs into clusters separated by gaps > FRAME_GAP. + // Within a cluster, VPAs are close together (continuous rerenders). + // Between clusters, real terminal output (response text) lives. + const FRAME_GAP = 8 * 1024; // 8KB — one Ink frame is typically 1-4KB + const MIN_BLOAT_SIZE = 32 * 1024; // Only collapse clusters spanning >= 32KB + + const clusters: { start: number; end: number }[] = []; + let cs = positions[0]; + let ce = positions[0]; + for (let i = 1; i < positions.length; i++) { + if (positions[i] - ce <= FRAME_GAP) { + ce = positions[i]; + } else { + clusters.push({ start: cs, end: ce }); + cs = positions[i]; + ce = positions[i]; + } + } + clusters.push({ start: cs, end: ce }); + + // For each big cluster, replace [start..end] with the bytes from `end` onwards + // (which contains the last frame's content up to where the next cluster, or + // post-cluster content, begins). + const parts: string[] = []; + let cursor = 0; + for (const cl of clusters) { + if (cl.end - cl.start < MIN_BLOAT_SIZE) continue; + parts.push(buffer.slice(cursor, cl.start)); + cursor = cl.end; + } + parts.push(buffer.slice(cursor)); + return parts.join(''); } export function registerSessionRoutes(