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
Add app icons, update manifest with icon entries, and add app-shell
caching to service worker for offline/instant startup.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Show Codeman logo on mobile as compact home button (was hidden)
- Add "Show More" button for history sessions (initial 4, expand all)
- Deduplicate by projectKey instead of workingDir (lossy decode fix)
- Fix project key decoding: handle '_' encoded as '-' with look-ahead
- Pre-validate resumeSessionId before passing to Claude CLI
- Apply content validation to all session files regardless of size
Filter readdir and linked-case names through /^[a-zA-Z0-9_-]+$/ before
returning them from GET /api/cases. Prevents XSS via maliciously-named
directories reaching frontend inline onclick handlers where escapeHtml
is insufficient (HTML-decoded back to quotes before JS execution).
Also fix misleading "Drag or use arrows" hint (no drag-and-drop exists).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a "Manage" tab to the create-case modal with up/down reorder
buttons and delete for each case. Linked cases are unlinked (folder
preserved); CASES_DIR cases are permanently deleted.
Backend:
- DELETE /api/cases/:name — unlink or delete
- PUT /api/cases/order — persist ordering to settings.json
- GET /api/cases now respects saved caseOrder
Frontend:
- Third "Manage" tab in createCaseModal with case list
- Delete button in mobile case picker bottom sheet
- SSE events: case:deleted, case:order-changed
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.
Three fixes from the WIP flicker branch that were lost during master merges:
1. Move viewport clear (\x1b[3J\x1b[H\x1b[2J) inside the dimension-change
guard so it only fires when cols/rows actually change. Previously every
resize event cleared the screen even at identical dimensions, causing
visible flicker with no subsequent Ink redraw to repaint.
2. Sync _lastResizeDims in sendResize() so restoreTerminalSize() doesn't
trigger a redundant viewport clear on the next throttledResize tick.
3. Add didScroll tracking to touch events — tap (no scroll) now refocuses
xterm's hidden textarea, fixing mobile keyboard input routing after
tapping the terminal area.
Mobile users cannot press Shift+Tab on virtual keyboards. Add a ⇧Tab
button that sends the escape sequence (\x1b[Z) to the PTY, enabling
mode switching on mobile devices.
Also fix accessory bar overflow on narrow screens by making it
horizontally scrollable with hidden scrollbar.
Allow users to leave the default model unset, so sessions use whatever
the Claude CLI defaults to rather than forcing a specific model.
- Add empty-value "Default (CLI default)" option to the model dropdown
- Treat empty string as undefined when passing model to Session
- Apply consistently across session creation, quick-start, and Ralph
Thank you for the clean fix! The root cause analysis in the PR description was excellent — the strict Zod schema rejecting modelConfig during the GET-then-PUT pattern was a subtle bug.
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>
xterm.js DOM renderer doesn't populate .xterm-viewport's scroll area (the div
is empty, scrollHeight === clientHeight), so native CSS scrolling via
touch-action:pan-y and overflow-y:scroll had nothing to scroll. Desktop worked
only because the wheel handler called terminal.scrollLines() directly.
- Replace split mobile/desktop touch handlers with unified JS-driven handler
that converts touch deltas to terminal.scrollLines() calls (with pixel
accumulation for slow swipes and momentum scrolling)
- Change touch-action from pan-y to none on terminal elements so browser
doesn't fight the JS handler
- Remove now-unnecessary xterm-viewport position/overflow/z-index overrides
and iOS -webkit-overflow-scrolling rules
- Fix _shrinkPaddingToFit() arithmetic (was adding gap instead of subtracting)
- Minor: add route-helpers.ts to CLAUDE.md, fix sse-events.ts comment count
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
iOS Safari scrolls the document to bring xterm's hidden textarea into
view when the user types, pushing the entire UI off-screen. Fix with:
- CSS position:fixed on .app when keyboard is visible
- window.scroll listener to reset scroll position as safety net
- scroll reset in onKeyboardShow before and after fit/resize
Co-Authored-By: Claude Opus 4.6 (1M context) <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>