diff --git a/CLAUDE.md b/CLAUDE.md index 2d19f86a..f8be7235 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,7 +19,9 @@ Claudeman is a Claude Code session manager with a web interface and autonomous R **Tech Stack**: TypeScript (ES2022/NodeNext, strict mode), Node.js, Fastify, Server-Sent Events, node-pty -**Requirements**: Node.js 18+, Claude CLI (`claude`) installed and available in PATH +**Key Dependencies**: fastify (REST API), node-pty (PTY spawning), ink/react (TUI), xterm.js (web terminal) + +**Requirements**: Node.js 18+, Claude CLI (`claude`) in PATH, GNU Screen (`apt install screen` / `brew install screen`) ## First-Time Setup @@ -90,6 +92,33 @@ screen -X -S quit # Graceful quit pkill -f "SCREEN.*claudeman" # Force kill all claudeman screens ``` +## CLI Commands + +```bash +claudeman session [s] # Manage Claude sessions + start # Start new session + stop # Stop session + list [ls] # List all + logs # View output + +claudeman task [t] # Manage tasks + add # Add task + list [ls] # List tasks + status # Task details + remove [rm] # Remove task + clear # Clear completed/failed + +claudeman ralph [r] # Control Ralph loop + start # Start loop + stop # Stop loop + status # Show status + +claudeman web # Start web interface +claudeman tui # Start TUI +claudeman status # Overall status +claudeman reset # Reset all state +``` + ## Architecture ### Key Files @@ -385,7 +414,7 @@ TUI uses React JSX (`jsxImportSource: react`) for Ink components. - **API endpoint**: Add types in `types.ts`, route in `server.ts:buildServer()`, use `createErrorResponse()` for errors - **SSE event**: Emit via `broadcast()` in server.ts, handle in `app.js:handleSSEEvent()` switch - **Session event**: Add to `SessionEvents` interface in `session.ts`, emit via `this.emit()`, subscribe in server.ts, handle in frontend -- **New test file**: Create `test/.test.ts`, pick unique port (next available: 3122+), add to port allocation comment above +- **New test file**: Create `test/.test.ts`, pick unique port (next available: 3127+), add to port allocation comment above ### API Error Codes @@ -439,12 +468,17 @@ Long-running sessions are supported with automatic trimming: | Buffer | Max Size | Trim To | |--------|----------|---------| -| Terminal | 5MB | 4MB | -| Text output | 2MB | 1.5MB | +| Terminal | 2MB | 1.5MB | +| Text output | 1MB | 768KB | | Messages | 1000 | 800 | | Line buffer | 64KB | (flushed every 100ms) | | Respawn buffer | 1MB | 512KB | +**Performance optimizations:** +- Tab switch uses `tail=256KB` for fast initial load, then chunked writes +- Large buffers written in 64KB chunks via `requestAnimationFrame` to avoid UI jank +- Truncation indicator shown when earlier output is cut + ## API Routes Quick Reference | Method | Endpoint | Description | diff --git a/src/session.ts b/src/session.ts index 177687a6..705ed161 100644 --- a/src/session.ts +++ b/src/session.ts @@ -31,17 +31,17 @@ export { withTimeout }; // Buffer Size Constants // ============================================================================ -/** Maximum terminal buffer size in characters (5MB) */ -const MAX_TERMINAL_BUFFER_SIZE = 5 * 1024 * 1024; +/** Maximum terminal buffer size in characters (2MB) - reduced from 5MB for better render performance */ +const MAX_TERMINAL_BUFFER_SIZE = 2 * 1024 * 1024; -/** When trimming terminal buffer, keep the most recent portion (4MB) */ -const TERMINAL_BUFFER_TRIM_SIZE = 4 * 1024 * 1024; +/** When trimming terminal buffer, keep the most recent portion (1.5MB) */ +const TERMINAL_BUFFER_TRIM_SIZE = 1.5 * 1024 * 1024; -/** Maximum text output buffer size (2MB) - ANSI-stripped text */ -const MAX_TEXT_OUTPUT_SIZE = 2 * 1024 * 1024; +/** Maximum text output buffer size (1MB) - ANSI-stripped text */ +const MAX_TEXT_OUTPUT_SIZE = 1 * 1024 * 1024; -/** When trimming text output, keep the most recent portion (1.5MB) */ -const TEXT_OUTPUT_TRIM_SIZE = 1.5 * 1024 * 1024; +/** When trimming text output, keep the most recent portion (768KB) */ +const TEXT_OUTPUT_TRIM_SIZE = 768 * 1024; /** Maximum number of Claude JSON messages to keep in memory */ const MAX_MESSAGES = 1000; diff --git a/src/web/public/app.js b/src/web/public/app.js index 80c3a22d..8165a9dd 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -207,6 +207,47 @@ class ClaudemanApp { } } + /** + * Write large buffer to terminal in chunks to avoid UI jank. + * Uses requestAnimationFrame to spread work across frames. + * @param {string} buffer - The full terminal buffer to write + * @param {number} chunkSize - Size of each chunk (default 64KB for smooth 60fps) + * @returns {Promise} - Resolves when all chunks written + */ + chunkedTerminalWrite(buffer, chunkSize = 64 * 1024) { + return new Promise((resolve) => { + if (!buffer || buffer.length === 0) { + resolve(); + return; + } + + // For small buffers, write directly + if (buffer.length <= chunkSize) { + this.terminal.write(buffer); + resolve(); + return; + } + + let offset = 0; + const writeChunk = () => { + if (offset >= buffer.length) { + resolve(); + return; + } + + const chunk = buffer.slice(offset, offset + chunkSize); + this.terminal.write(chunk); + offset += chunkSize; + + // Schedule next chunk on next frame + requestAnimationFrame(writeChunk); + }; + + // Start writing + requestAnimationFrame(writeChunk); + }); + } + setupEventListeners() { // Use capture to handle before terminal document.addEventListener('keydown', (e) => { @@ -772,14 +813,21 @@ class ClaudemanApp { } // Load terminal buffer for this session + // Use tail mode for faster initial load (256KB is enough for recent visible content) try { - const res = await fetch(`/api/sessions/${sessionId}/terminal`); + const tailSize = 256 * 1024; + const res = await fetch(`/api/sessions/${sessionId}/terminal?tail=${tailSize}`); const data = await res.json(); this.terminal.clear(); this.terminal.reset(); if (data.terminalBuffer) { - this.terminal.write(data.terminalBuffer); + // Show truncation indicator if buffer was cut + if (data.truncated) { + this.terminal.write('\x1b[90m... (earlier output truncated for performance) ...\x1b[0m\r\n\r\n'); + } + // Use chunked write for large buffers to avoid UI jank + await this.chunkedTerminalWrite(data.terminalBuffer); } // Send resize and Ctrl+L to trigger Claude to redraw at correct size diff --git a/src/web/server.ts b/src/web/server.ts index 04841a0b..dde671ca 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -505,8 +505,11 @@ export class WebServer extends EventEmitter { }); // Get session terminal buffer (for reconnecting) + // Query params: + // tail= - Only return last N bytes (faster initial load) this.app.get('/api/sessions/:id/terminal', async (req) => { const { id } = req.params as { id: string }; + const query = req.query as { tail?: string }; const session = this.sessions.get(id); if (!session) { @@ -534,9 +537,21 @@ export class WebServer extends EventEmitter { .replace(/\x0c/g, '') .replace(/^[\s\r\n]+/, ''); + // Optionally truncate to last N bytes for faster initial load + const tailBytes = query.tail ? parseInt(query.tail, 10) : 0; + const fullSize = cleanBuffer.length; + let truncated = false; + + if (tailBytes > 0 && cleanBuffer.length > tailBytes) { + cleanBuffer = cleanBuffer.slice(-tailBytes); + truncated = true; + } + return { terminalBuffer: cleanBuffer, status: session.status, + fullSize, + truncated, }; });