diff --git a/CLAUDE.md b/CLAUDE.md index 8fd01a4a..0de0090d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -80,7 +80,7 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph **CI**: `.github/workflows/ci.yml` runs `typecheck`, `lint`, `format:check` on push to master (Node 22). Tests excluded (they spawn tmux). -**Code style**: Prettier (`singleQuote: true`, `printWidth: 120`, `trailingComma: "es5"`). ESLint allows `no-console`, warns on `@typescript-eslint/no-explicit-any`. Does not lint `app.js` or `scripts/**/*.mjs`. +**Code style**: Prettier (`singleQuote: true`, `printWidth: 120`, `trailingComma: "es5"`). ESLint flat config (`eslint.config.js`) allows `no-console`, warns on `@typescript-eslint/no-explicit-any`. Ignores: `app.js`, `scripts/**/*.mjs`, `src/web/public/vendor/**`, `tools/**`, `remotion/**`. ## Common Gotchas @@ -108,9 +108,9 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph | **State** | `src/state-store.ts`, `src/run-summary.ts`, `src/session-lifecycle-log.ts` | | | **Infra** | `src/hooks-config.ts`, `src/push-store.ts`, `src/tunnel-manager.ts`, `src/image-watcher.ts`, `src/file-stream-manager.ts` | | | **Plan** | `src/plan-orchestrator.ts`, `src/prompts/*.ts`, `src/templates/claude-md.ts` | | -| **Web** | `src/web/server.ts`, `src/web/sse-events.ts`, `src/web/routes/*.ts` (13 modules), `src/web/ports/*.ts`, `src/web/middleware/auth.ts`, `src/web/schemas.ts` | | +| **Web** | `src/web/server.ts`, `src/web/sse-events.ts`, `src/web/routes/*.ts` (12 route modules + barrel), `src/web/ports/*.ts`, `src/web/middleware/auth.ts`, `src/web/schemas.ts` | | | **Frontend** | `src/web/public/app.js` ★ (~11.8K lines) + 10 JS modules (incl. `sw.js` service worker) | | -| **Types** | `src/types/index.ts` → 14 domain files | See `@fileoverview` in index.ts | +| **Types** | `src/types/index.ts` → 13 domain files | See `@fileoverview` in index.ts | ★ = Large file (>50KB). All files have `@fileoverview` JSDoc — read that before diving in. @@ -170,7 +170,7 @@ Frontend JS modules have `@fileoverview` with `@dependency`/`@loadorder` tags. L ### API Routes -~111 handlers across 13 route files in `src/web/routes/`: system (35), sessions (24), ralph (9), plan (8), respawn (7), cases (7), files (5), mux (5), scheduled (4), push (4), teams (2), hooks (1). Each file has `@fileoverview` with endpoint details. +~111 handlers across 12 route files in `src/web/routes/`: system (35), sessions (24), ralph (9), plan (8), respawn (7), cases (7), files (5), mux (5), scheduled (4), push (4), teams (2), hooks (1). Each file has `@fileoverview` with endpoint details. ## Adding Features @@ -179,7 +179,7 @@ Frontend JS modules have `@fileoverview` with `@dependency`/`@loadorder` tags. L - **Session setting**: Add to `SessionState`, include in `session.toState()`, call `persistSessionState()` - **Hook event**: Add to `HookEventType`, add hook in `hooks-config.ts:generateHooksConfig()`, update `HookEventSchema` - **Mobile feature**: Add to relevant singleton, guard with `MobileDetection.isMobile()` -- **New test**: Pick unique port (search `const PORT =`). Integration: ports 3099-3211. Route tests: `app.inject()` — see `test/routes/_route-test-utils.ts`. +- **New test**: Pick unique port (search `const PORT =`). Route tests use `app.inject()` (no port needed) — see `test/routes/_route-test-utils.ts`. **Validation**: Zod v4 (different API from v3). Define schemas in `schemas.ts`, use `.parse()`/`.safeParse()`. @@ -238,7 +238,6 @@ Key: `scripts/tmux-manager.sh` (safe tmux mgmt), `scripts/tunnel.sh` (tunnel sta ## Common Workflows **Bug investigation**: Dev server → reproduce in browser → check terminal + `~/.codeman/state.json`. -**API endpoint**: Types in `src/types/*.ts` → route in `src/web/routes/*-routes.ts` → SSE event if needed → handle in `app.js`. **Respawn changes**: Read `docs/respawn-state-machine.md` first. Use `MockSession` from `test/respawn-test-utils.ts`. ## Tunnel diff --git a/src/web/public/app.js b/src/web/public/app.js index 6b628eda..1d03a912 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -1817,14 +1817,24 @@ class CodemanApp { if (data.id === this.activeSessionId) { if (data.data.length > 32768) _crashDiag.log(`TERMINAL: ${(data.data.length/1024).toFixed(0)}KB`); - // Hard cap: track total bytes queued across ALL buffers (pendingWrites + - // flickerFilterBuffer + loadBufferQueue). When rAF is throttled (tab + // Hard cap: track total bytes queued in render buffers (pendingWrites + + // flickerFilterBuffer). When rAF is throttled (tab // backgrounded, GPU busy), data accumulates with no flush, reaching - // 889KB+ and freezing Chrome for minutes. Drop data beyond 96KB — - // the server sends session:needsRefresh to recover. + // 889KB+ and freezing Chrome for minutes. Drop data beyond 128KB and + // schedule a buffer reload to recover the display once the burst subsides. const queued = (this.pendingWrites?.reduce((s, w) => s + w.length, 0) || 0) + (this.flickerFilterBuffer?.length || 0); - if (queued > 131072) return; // 128KB — drop to prevent accumulation + if (queued > 131072) { // 128KB — drop to prevent accumulation + // Schedule a self-recovery: reload the full terminal buffer once the + // queue drains (debounced to avoid hammering the API during sustained bursts). + if (!this._clientDropRecoveryTimer) { + this._clientDropRecoveryTimer = setTimeout(() => { + this._clientDropRecoveryTimer = null; + this._onSessionNeedsRefresh(); + }, 2000); + } + return; + } this.batchTerminalWrite(data.data); } @@ -1835,8 +1845,7 @@ class CodemanApp { // so reload the buffer to recover from any display corruption. if (!this.activeSessionId || !this.terminal) return; try { - const tailSize = 256 * 1024; - const res = await fetch(`/api/sessions/${this.activeSessionId}/terminal?tail=${tailSize}`); + const res = await fetch(`/api/sessions/${this.activeSessionId}/terminal?tail=${TERMINAL_TAIL_SIZE}`); const data = await res.json(); if (data.terminalBuffer) { this.terminal.clear(); @@ -3532,6 +3541,7 @@ class CodemanApp { this._tabCompletionRetries = 0; this._tabCompletionBaseText = null; if (this._tabCompletionFallback) { clearTimeout(this._tabCompletionFallback); this._tabCompletionFallback = null; } + if (this._clientDropRecoveryTimer) { clearTimeout(this._clientDropRecoveryTimer); this._clientDropRecoveryTimer = null; } // Clean up pending terminal writes to prevent old session data from appearing in new session if (this.syncWaitTimeout) { @@ -3634,7 +3644,7 @@ class CodemanApp { // Load terminal buffer for this session // Show cached content instantly while fetching fresh data in background. - // Use tail mode for faster initial load (256KB is enough for recent visible content). + // Use tail mode for faster initial load (128KB is enough for recent visible content). // // Protect flushed state during buffer load: terminal.write() can trigger // xterm.js onData responses (DA, OSC, etc.) that would otherwise clear @@ -3664,8 +3674,7 @@ class CodemanApp { } _crashDiag.log('FETCH_START'); - const tailSize = 256 * 1024; - const res = await fetch(`/api/sessions/${sessionId}/terminal?tail=${tailSize}`); + const res = await fetch(`/api/sessions/${sessionId}/terminal?tail=${TERMINAL_TAIL_SIZE}`); if (selectGen !== this._selectGeneration) { if (this._isLoadingBuffer) this._finishBufferLoad(); this._restoringFlushedState = false; return; } const data = await res.json(); _crashDiag.log(`FETCH_DONE: ${data.terminalBuffer ? (data.terminalBuffer.length/1024).toFixed(0) + 'KB' : 'empty'} truncated=${data.truncated}`);