Three follow-up fixes to the inline rename input introduced in #81:
1. IME composition guard. Pressing Enter to confirm a Chinese pinyin
candidate (or any IME composition) was committing the half-composed
text as the session name. Skip the keydown handler when isComposing
is true or when keyCode is the legacy 229 sentinel that older
Safari/Edge versions report on the Enter that triggers compositionend.
2. Ghost tab on mid-rename deletion. If a session was deleted via SSE
while its tab was being renamed, the render-skip flag suppressed
_renderSessionTabs() and the orphaned <input> stayed on screen until
blur — at which point the rename PUT 404'd against the dead session.
Replace the boolean _inlineRenameActive with a _activeRename
{sessionId, cancel} object so _cleanupSessionData can abort an
in-flight rename targeting the deleted session, and finishRename
skips the API call when the session is gone.
3. Stuck-flag risk. Move the settle-once guard into a closure-local
`settled` boolean so blur / Enter / Escape / external cancel all
converge to a single idempotent path. Register _activeRename only
after the input is fully wired so a throw earlier in setup can't
strand state.
Adds test/inline-rename.test.ts with 7 Playwright tests that drive
startInlineRename via page.evaluate() against a stubbed session and
synthetic .tab-name node — no real PTY/tmux needed, runs in ~1.3s.
Also fixes test/mobile/helpers/server.ts which imported the WebServer
via a path one directory short of the repo root, breaking the entire
mobile test suite under the main vitest config.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the inline session-rename input is open, any incoming SSE event
that triggers renderSessionTabs() (a sibling session updating, a hook
firing, a status change) destroys the input element mid-keystroke and
the user loses what they were typing.
Add a _inlineRenameActive flag that:
- guards the two render paths (renderSessionTabs and
_fullRenderSessionTabs) so they bail out early while a rename is
in progress;
- is set true when the inline input mounts (session-ui.js);
- is cleared in finishRename, which then explicitly calls
renderSessionTabs to restore the normal tab structure.
Also add a re-entrance guard at the top of finishRename so the blur
event and the Enter keydown do not both fire it (was a latent
double-call).
Drive-by: replace tabName.innerHTML = "" with explicit child removal.
The preceding textContent = "" already clears the element; this avoids
an innerHTML write on a node that takes user-supplied content on the
next line.
Follow-up to the inline-rename feature cherry-picked from #60.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: restore clear message separation + proper table layout in response viewer
* fix: capture Claude CLI's real session ID + robust ANSI/CLI-chrome stripping in response viewer fallback
Session constructor seeded _claudeSessionId with Codeman's session.id as a
placeholder, and the message-driven update was gated on !_claudeSessionId —
meaning Claude CLI's actual session UUID was never adopted. This broke
/api/sessions/:id/last-response JSONL lookups, silently falling through to
the terminal-buffer path whose ANSI regex missed \x1b[>c / \x1b[>q queries.
- session.ts: update _claudeSessionId whenever a message's session_id differs
from current (covers placeholder and stale-resume cases)
- app.js: extract _cleanTerminalBuffer with proper CSI regex (param bytes
0x30-0x3F now covers > ? < =) plus a chrome filter for status bar,
progress bar, spinner, shell prompt, and hint lines
* fix: wrap regular code blocks on mobile, keep ASCII diagrams rigid with scroll hint
* feat: add per-block wrap toggle on ASCII-diagram code blocks
* fix: wrap by default, pin toggle button outside scroll container
* fix: narrow diagram detection to box-drawing + block elements only
* feat: show last-response viewer eye icon on desktop too
The response viewer button was mobile-only via a display:none default with a
mobile.css override. Flip the default to inline-flex and drop the override so
the eye icon appears in the header on every form factor — desktop users get
the same quick "Last Response" pane as mobile.
* fix(response-viewer): restore HTML sanitizer + fix undefined `src` in _renderMarkdown
- `_renderMarkdown` referenced an undefined `src` (should be `text`),
causing a ReferenceError on every markdown render. The try/catch
swallowed it, so the new table-wrap and ASCII-diagram features
never actually ran — output silently fell through to plain-text.
app.js is excluded from ESLint, so this wasn't caught at lint time.
- `_sanitizeHtml` was removed when refactoring the response viewer,
leaving `marked.parse()` output going straight into `innerHTML`
without sanitization (XSS regression vs. master). Restored the
helper and re-applied it before any post-processing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Teigen <teigen@TeigendeMac-mini.local>
Co-authored-by: arkon <arkon.85@hotmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
POST /api/clipboard with { text } broadcasts to all connected browsers
via SSE. Browser attempts navigator.clipboard.writeText, falling back
to a modal with manual copy button if blocked.
Enables remote clipboard workflows: CLI tools can push text to the
user's browser clipboard across the network.
- Active tab: bright green border with color-matched glow per session color
- Tab number badges (1-9) showing Alt+N shortcut hints
- Badges update on tab reorder and re-render
Add marked.js (39KB) for rich text display in the response viewer.
Renders headings, code blocks, lists, tables, blockquotes, and
inline formatting with dark theme styling.
Falls back to escaped plain text if marked.js fails to load.
Claude Code's Ink framework uses alternate screen buffer + VPA cursor
positioning, resulting in near-zero xterm.js scrollback on mobile.
Instead of fighting terminal scrollback, this adds a native scrollable
overlay that reads structured responses from Claude's JSONL transcripts.
- New API: GET /api/sessions/:id/last-response reads transcript JSONL
- ?context=full returns full conversation thread (user + assistant)
- Fallback to terminal buffer with ANSI stripping if no transcript
- Response viewer panel: bottom sheet with native iOS/Android scroll
- "More" button loads full conversation context as threaded view
- Eye icon in header bar (mobile only), no toolbar space impact
Previously, restoreMuxSessions() only created Session objects without
attaching PTY processes. Sessions stayed at pid=null until the client
manually selected them, causing terminals to appear "closed" after deploy.
Now the server calls startInteractive() for each recovered session during
startup, so all sessions resume capturing output immediately. The frontend
auto-attach condition is also relaxed from (pid===null && status==='idle')
to (pid===null && !_ended) as a safety net for edge cases.
Terminal flicker:
- Skip buffer-recovered/clear-terminal events during active buffer load
to prevent competing clear+rewrite cycles (app.js)
- Move viewport+scrollback clear inside dimension-change guard so resize
without actual SIGWINCH doesn't blank the terminal (terminal-ui.js)
- Sync _lastResizeDims on explicit resize to prevent redundant clears
CJK input rewrite (input-cjk.js):
- Use InputEvent.inputType to distinguish insertText (final) from
insertCompositionText (tentative) — fixes Chinese punctuation and
English text being swallowed during Android IME composition
- Remove isComposing guard on Enter so it always sends
- Phantom character (U+200B) keeps textarea non-empty so Android
long-press backspace generates continuous deleteContentBackward
events at the keyboard's native repeat rate
CJK input settings:
- Add "CJK Input" toggle in Settings > Input (index.html, settings-ui.js)
- Store as device-specific setting (cjkInputEnabled), not synced to server
- Replace INPUT_CJK_FORM env var dependency with user-controlled setting
(env var still works as server override)
Mobile layout:
- Fix welcome screen overflow on phones by constraining .welcome-content
to calc(100vw - 1.5rem) (mobile.css)
- Move xterm helper textarea on-screen for touch devices to fix iOS
keyboard input (styles.css)
- Focus terminal synchronously in user-gesture context for iOS Safari
keyboard activation (session-ui.js, app.js)
- Refocus terminal on tap (not scroll) in touch handler (terminal-ui.js)
Tailed terminal buffers contain multiple CUP-positioned Ink frames from
different time points. When replayed in xterm, old frames at viewport
positions not covered by the latest frame persist as ghost content
(e.g. duplicate "bypass permissions" bars). After buffer load, send
Ctrl+L via the session input API to trigger a full Ink redraw, which
overwrites all stale frame content with the correct current state.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two fixes for the tab-switching corruption bug:
1. _finishBufferLoad() now discards queued SSE events instead of flushing
them. The loaded API buffer is the source of truth — queued events
overlap with it, and flushing them writes duplicate Ink cursor-up
redraws that corrupt the terminal display (garbled text, wrong cursor
positions).
2. Skip stale cache write for busy sessions. When a session is actively
working, the cache is always outdated — writing it first and then
rewriting with the fresh API buffer caused a jarring double-render
flash. Now busy sessions get a single clean clear+write transition.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
chunkedTerminalWrite used requestAnimationFrame to write buffer chunks across
frames but had no cancellation. When switching tabs, old session's remaining
chunks continued writing stale data into the new session's terminal, causing
visual artifacts and garbled content.
- Add _chunkedWriteGen generation counter to abort in-flight chunked writes
- Bump gen early in selectSession() and SSE reconnect to immediately cancel
- Guard finish() so aborted writes don't flush SSE queue for wrong session
- Add fitAddon.fit() before buffer writes to sync terminal dimensions
- Add fitAddon.fit() in sendResize() to ensure local/server dim parity
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New SSE event orchestrator:planProgress streams phase/detail updates
from the planner to the frontend in real-time. The panel now shows
a scrollable log of planning steps instead of just "Generating plan..."
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a new autonomous loop that accepts high-level goals, generates
phased execution plans via AI, and executes them step-by-step with
verification gates between phases.
Core components:
- OrchestratorLoop: state machine (idle→planning→approval→executing→verifying→completed)
- OrchestratorPlanner: plan generation via PlanOrchestrator, Kahn's algorithm phase grouping
- OrchestratorVerifier: phase verification (strict/moderate/lenient modes)
- Prompt templates for phase execution, team delegation, verification, replanning
API (10 endpoints):
- POST start/approve/reject/pause/resume/stop
- GET status/plan
- POST phase/:id/skip, phase/:id/retry
Frontend: orchestrator-panel.js with SSE-driven state, phase progress, task tracking
Tests: 22 tests (18 route + 4 unit), all passing. Typecheck/lint/format clean.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Split the monolithic app.js (~12.5K lines) into 6 focused mixin modules
that extend CodemanApp.prototype via Object.assign:
- terminal-ui.js — terminal setup, rendering pipeline, controls
- respawn-ui.js — respawn banner, countdown, presets, run summary
- ralph-panel.js — Ralph state panel, fix_plan, plan versioning
- settings-ui.js — app settings, visibility, web push, tunnel/QR, help
- panels-ui.js — subagent panel, teams, insights, file browser, log viewer
- session-ui.js — quick start, session options, case settings
Fix deferred script init ordering: wrap CodemanApp instantiation in
DOMContentLoaded so all defer'd mixin modules execute their
Object.assign before the constructor runs. Without this, init() calls
methods like applyHeaderVisibilitySettings() that don't exist yet.
Guard missing cleanupWizardDragging() call in subagent-windows.js.
Update build.mjs to minify/hash all new modules. Update CLAUDE.md
with new frontend architecture and load order.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Enable NODE_COMPILE_CACHE in systemd service and npm start for 10-20% faster cold starts
- Lazy-load xterm-addon-webgl.min.js (244KB) only on desktop — mobile never downloads it
- Add <link rel="preload"> hints for critical scripts (xterm, constants, app) in <head>
- Replace per-session tmux subprocess calls with single batch `list-panes -a` call
(N*2+1+M execSync calls → 1 for reconcileSessions)
- Fix CLAUDE.md frontend module count (10 → 11)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Close WebSocket when session exits (exit event listener) to prevent
orphaned listeners and stale writes to dead PTY
- Add readyState guard in onTerminal to stop buffering after socket closes
- Simplify heartbeat: remove redundant alive flag, use pongTimeout only
- Add exponential backoff reconnection on unexpected WS close (skip for
server rejections 4004/4008/4009)
- Clear CJK textarea on session switch to prevent wrong-session input
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
WebSocket route: add socket error handler to prevent process crashes, enforce
per-session connection limit (max 5), track/decrement counts on close.
CJK input: add destroy() method with proper listener cleanup, guard against
double-init, add maxlength/aria-label to textarea, use language-neutral
placeholder, explicitly clear cjkActive on hide.
install.sh: fix update() to use $BRANCH and $REPO_URL instead of hardcoded
origin/master — fork users were silently switched back to master on update.
README: fix broken markdown table (paragraph concatenated into last cell),
add CODEMAN_NODE_VERSION to env var table.
Tests: add 8 new test cases for batch coalescing, flush threshold, unknown
message types, connection limit, heartbeat, readyState guards. Import
MAX_INPUT_LENGTH from config, add connectWs timeout, replace setTimeout
with vi.waitFor in cleanup test.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a dedicated textarea below the terminal for CJK (Korean/Japanese/Chinese)
IME input. xterm.js intercepts IME composition events, preventing composed
characters from displaying correctly. This textarea bypasses xterm entirely
by using native browser IME handling — text accumulates until Enter, then
sends to PTY in one shot.
- Always-visible textarea below terminal (inside .terminal-wrap flex column)
- focus/blur sets window.cjkActive flag to block xterm onData
- Enter sends textarea.value + \r to PTY, Escape clears
- Arrow keys, Ctrl+C/D/L/Z, Tab, Backspace pass through to PTY when empty
- attachCustomKeyEventHandler suppresses xterm key handling during composition
- INPUT_CJK_FORM=ON|OFF env var toggle (default: off, passed via SSE init)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace per-keystroke HTTP POST + SSE terminal output with a single
bidirectional WebSocket connection for dramatically lower input latency.
The existing SSE+POST paths remain fully functional as fallback.
Server-side: ws-routes.ts provides /ws/sessions/:id/terminal with 8ms
micro-batching and 16KB flush threshold. Each batch is wrapped in
DEC 2026 synchronized update markers so xterm.js renders atomically —
Ink's DA capability negotiation fails through the PTY→server→WS proxy
chain, so without server-injected markers, cursor-up redraws flicker.
Frontend: _connectWs/_disconnectWs manage per-session WS lifecycle.
Input and resize use WS fast path with HTTP POST fallback. SSE terminal
events are suppressed when WS is active to prevent double rendering.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Change showSubagents default from true to false so the subagent
panel doesn't auto-show on page load. Users can still enable it
via Settings.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
- Default terminal font 14px → 12px, min 10px → 8px
- Save tab metadata to localStorage on every render
- Restore ended sessions as dimmed tabs after page refresh
- Ended tabs show "Session ended" message on click
- Filter empty sessions from history API (check for conversation content)
- Add --resume fallback to new session if resume fails (prevents dead panes)
- Pass resumeSessionId through respawnPane for dead pane recovery
- Persist respawn presets and runMode to server settings (cross-device sync)
- Fix mobile touch handling for Recent Sessions dropdown (DOM API + touch CSS)
selectSession() was always calling /interactive for restored idle
sessions regardless of mode. Shell sessions now correctly call /shell.
Also add loadHistorySessions and resumeHistorySession to frontend.
Green pulsing dot in the desktop header shows when Cloudflare tunnel is active.
Clicking opens a dropdown panel with tunnel URL, remote client count, auth
sessions, and start/stop/QR/revoke controls. Detects tunnel clients via
Cf-Connecting-Ip header to exclude local connections from the count.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two hardcoded `256 * 1024` tail sizes in app.js bypassed the
TERMINAL_TAIL_SIZE constant (128KB), causing stale cached browsers
to fetch 256KB buffers even after the constant was reduced to prevent
WebGL GPU stalls. Also adds a self-recovery timer that reloads the
terminal buffer after client-side data drops, preventing permanent
display corruption.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add SSE padding to backpressure drain write for tunnel clients
- Remove dead SessionTerminal from broadcast padding check
- Trim whitespace in SSE session filter query params
- Remove unused _bufferLazyTerminalData scaffolding code
- Skip WebGL on tablets too, not just phones (canvas fallback)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1. Session-scoped SSE subscriptions: server filters events by session ID,
clients can subscribe via ?sessions=id1,id2 (backwards-compatible)
2. Lazy xterm.js for subagent windows: terminals created on restore,
disposed on minimize — saves ~3.75MB DOM at 50 agents
3. Targeted badge updates: badge count changes update the <span> directly
instead of rebuilding the entire session tab sidebar (O(1) vs O(n))
4. Conditional SSE padding: 8KB Cloudflare padding only on session:terminal
and session:needsRefresh, not every event (~70% bandwidth reduction)
5. Canvas renderer on mobile: skip WebGL addon on mobile devices to reduce
GPU pressure and prevent context loss on weaker mobile GPUs
All 5 implemented in parallel via isolated git worktrees, merged conflict-free.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add three layers of protection against oversized terminal.write() calls
that freeze Chrome's main thread:
1. SSE entry cap: _onSessionTerminal drops data when total queued bytes
(pendingWrites + flickerFilterBuffer) exceeds 128KB. Server sends
session:needsRefresh to recover dropped content.
2. Flush cap: flushPendingWrites splits at DEC 2026 sync segment
boundaries with 64KB per-frame budget. Excess segments deferred to
next requestAnimationFrame. Segment-level splitting preserves Ink
redraw atomicity (no flicker).
3. Reduced tail size: initial buffer fetch reduced to 128KB (from 256KB)
to limit data volume during tab switch.
Re-enable WebGL renderer — root cause was unbounded terminal.write()
volume, not the GPU renderer itself.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause was NOT WebGL — breadcrumbs showed 141KB single-frame
terminal.write() calls freezing Chrome for 2+ minutes even with the
canvas renderer. During heavy Ink output, multiple SSE terminal events
accumulate between animation frames and flush all at once.
Fix: split flushPendingWrites at DEC 2026 sync segment boundaries with
a 48KB per-frame budget. Each segment is a complete Ink redraw, so
splitting between them preserves atomicity (no flicker). Excess
segments are deferred to the next requestAnimationFrame.
Also re-enable WebGL since it was not the cause — the flush cap
protects both renderers equally.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: xterm.js WebGL addon performs synchronous GPU ReadPixels
calls during terminal.write(). When heavy terminal output floods in
(~1MB/4s from active Claude sessions), single-frame writes of 70-105KB
block Chrome's main thread for 10+ seconds, triggering "page
unresponsive" dialogs. This happens both during tab switches (buffer
load + live SSE data competing) and during normal use (Ink redraw
bursts).
Fix: disable WebGL by default, use canvas renderer instead. Canvas
handles the same workloads without GPU stalls. Re-enable with ?webgl
URL param for testing.
Also: gate live SSE terminal writes during the entire selectSession()
buffer load sequence (not just during chunkedTerminalWrite), preventing
live data from competing with historical buffer restoration.
Crash investigation data (from server-side breadcrumb collection):
- 27 flushes totaling 937KB in 4 seconds preceded crash
- Two 105KB and one 104KB single-frame flushes observed
- Crash occurred when user switched tabs during output flood
- Tab froze for 2m54s before recovering
- Backend always stable (0 crashes); pure frontend GPU issue
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The --drop:console esbuild flag was silently stripping all diagnostic
console.* calls from production builds, making crash investigation
impossible. Remove it temporarily for debugging.
Add server-side crash breadcrumb collection:
- Frontend writes breadcrumbs to localStorage AND POSTs to /api/crash-diag
- Server stores latest breadcrumbs in memory, readable via GET /api/crash-diag
- Granular breadcrumbs in selectSession: CACHE_WRITE, FETCH_START,
FETCH_DONE, REWRITE, FOCUS, SELECT_DONE
- 2s heartbeat beacon so breadcrumbs survive tab freezes
- text/plain content-type parser for navigator.sendBeacon compatibility
Initial findings from breadcrumbs:
- Crash happens during selectSession() for sessions with large buffers
- Pattern: cached buffer exists from prior visit + live SSE data arriving
- Backend is always stable (0 crashes); this is a pure frontend issue
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add global error handlers, long task detection, WebGL context tracking,
and performance timing to identify root cause of intermittent Chrome
"page unresponsive" freezes during session switching and typing.
Diagnostics:
- window error/unhandledrejection handlers ([CRASH-DIAG] prefix)
- PerformanceObserver for long tasks (>200ms main thread blocks)
- WebGL context loss/restore tracking on all canvases
- ?nowebgl URL param to disable WebGL renderer for testing
- Timing on flushPendingWrites, chunkedTerminalWrite, selectSession
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bug 1: Every incoming SSE terminal event reset the 50ms flush timer, not
just cursor-up events. During active Claude runs the timer never fired,
accumulating MBs in flickerFilterBuffer that froze Chrome on flush.
Fix: only reset timer on cursor-up events; add 256KB safety valve.
Bug 2: Shell sessions emit cursor-up on every keystroke for readline
prompt redraws, triggering the flicker filter and delaying feedback.
Fix: skip cursor-up filter for shell mode; disable local echo overlay.
Based on PR #31 by @SGudbrandsson.
Co-Authored-By: Sigurður Guðbrandsson <SGudbrandsson@users.noreply.github.com>
Stop toggling WebGL renderer off/on around large buffer writes in
chunkedTerminalWrite(). The dispose+loadAddon cycle caused visible
re-render flashes and (before the deferred fix) synchronous GPU stalls
from ReadPixels blocking the main thread. Instead, keep WebGL active
and rely on 32KB chunked writes to keep per-frame render work under
~5ms.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Disable WebGL renderer during large buffer loads (>32KB) and fall back to
canvas, which handles bulk ANSI writes without synchronous GPU ReadPixels
calls. Re-enable WebGL after the buffer load completes so live terminal
streaming still benefits from GPU acceleration.
Also reduce chunk size from 128KB to 32KB and use chunked writes for
cached buffer restores instead of synchronous terminal.write().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>