mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 20:49:41 +02:00
perf: optimize terminal buffer rendering for smoother tab switches
- Reduce buffer limits (terminal: 5MB→2MB, text: 2MB→1MB) to decrease render payload - Add chunkedTerminalWrite() that writes in 64KB chunks via requestAnimationFrame to avoid UI jank - Add tail mode to /api/sessions/:id/terminal endpoint (?tail=256KB) for faster initial load on tab switch - Show truncation indicator when earlier output is cut - Update CLAUDE.md with new buffer limits and performance notes Fixes jittery behavior when switching tabs with large (2MB+) buffers. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 <name> 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 <id> # Stop session
|
||||
list [ls] # List all
|
||||
logs <id> # View output
|
||||
|
||||
claudeman task [t] # Manage tasks
|
||||
add <prompt> # Add task
|
||||
list [ls] # List tasks
|
||||
status <id> # Task details
|
||||
remove [rm] <id> # 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/<name>.test.ts`, pick unique port (next available: 3122+), add to port allocation comment above
|
||||
- **New test file**: Create `test/<name>.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 |
|
||||
|
||||
+8
-8
@@ -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;
|
||||
|
||||
+50
-2
@@ -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<void>} - 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
|
||||
|
||||
@@ -505,8 +505,11 @@ export class WebServer extends EventEmitter {
|
||||
});
|
||||
|
||||
// Get session terminal buffer (for reconnecting)
|
||||
// Query params:
|
||||
// tail=<bytes> - 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,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user