mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-07 16:09:43 +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
|
**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
|
## First-Time Setup
|
||||||
|
|
||||||
@@ -90,6 +92,33 @@ screen -X -S <name> quit # Graceful quit
|
|||||||
pkill -f "SCREEN.*claudeman" # Force kill all claudeman screens
|
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
|
## Architecture
|
||||||
|
|
||||||
### Key Files
|
### 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
|
- **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
|
- **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
|
- **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
|
### API Error Codes
|
||||||
|
|
||||||
@@ -439,12 +468,17 @@ Long-running sessions are supported with automatic trimming:
|
|||||||
|
|
||||||
| Buffer | Max Size | Trim To |
|
| Buffer | Max Size | Trim To |
|
||||||
|--------|----------|---------|
|
|--------|----------|---------|
|
||||||
| Terminal | 5MB | 4MB |
|
| Terminal | 2MB | 1.5MB |
|
||||||
| Text output | 2MB | 1.5MB |
|
| Text output | 1MB | 768KB |
|
||||||
| Messages | 1000 | 800 |
|
| Messages | 1000 | 800 |
|
||||||
| Line buffer | 64KB | (flushed every 100ms) |
|
| Line buffer | 64KB | (flushed every 100ms) |
|
||||||
| Respawn buffer | 1MB | 512KB |
|
| 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
|
## API Routes Quick Reference
|
||||||
|
|
||||||
| Method | Endpoint | Description |
|
| Method | Endpoint | Description |
|
||||||
|
|||||||
+8
-8
@@ -31,17 +31,17 @@ export { withTimeout };
|
|||||||
// Buffer Size Constants
|
// Buffer Size Constants
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/** Maximum terminal buffer size in characters (5MB) */
|
/** Maximum terminal buffer size in characters (2MB) - reduced from 5MB for better render performance */
|
||||||
const MAX_TERMINAL_BUFFER_SIZE = 5 * 1024 * 1024;
|
const MAX_TERMINAL_BUFFER_SIZE = 2 * 1024 * 1024;
|
||||||
|
|
||||||
/** When trimming terminal buffer, keep the most recent portion (4MB) */
|
/** When trimming terminal buffer, keep the most recent portion (1.5MB) */
|
||||||
const TERMINAL_BUFFER_TRIM_SIZE = 4 * 1024 * 1024;
|
const TERMINAL_BUFFER_TRIM_SIZE = 1.5 * 1024 * 1024;
|
||||||
|
|
||||||
/** Maximum text output buffer size (2MB) - ANSI-stripped text */
|
/** Maximum text output buffer size (1MB) - ANSI-stripped text */
|
||||||
const MAX_TEXT_OUTPUT_SIZE = 2 * 1024 * 1024;
|
const MAX_TEXT_OUTPUT_SIZE = 1 * 1024 * 1024;
|
||||||
|
|
||||||
/** When trimming text output, keep the most recent portion (1.5MB) */
|
/** When trimming text output, keep the most recent portion (768KB) */
|
||||||
const TEXT_OUTPUT_TRIM_SIZE = 1.5 * 1024 * 1024;
|
const TEXT_OUTPUT_TRIM_SIZE = 768 * 1024;
|
||||||
|
|
||||||
/** Maximum number of Claude JSON messages to keep in memory */
|
/** Maximum number of Claude JSON messages to keep in memory */
|
||||||
const MAX_MESSAGES = 1000;
|
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() {
|
setupEventListeners() {
|
||||||
// Use capture to handle before terminal
|
// Use capture to handle before terminal
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
@@ -772,14 +813,21 @@ class ClaudemanApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load terminal buffer for this session
|
// Load terminal buffer for this session
|
||||||
|
// Use tail mode for faster initial load (256KB is enough for recent visible content)
|
||||||
try {
|
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();
|
const data = await res.json();
|
||||||
|
|
||||||
this.terminal.clear();
|
this.terminal.clear();
|
||||||
this.terminal.reset();
|
this.terminal.reset();
|
||||||
if (data.terminalBuffer) {
|
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
|
// 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)
|
// 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) => {
|
this.app.get('/api/sessions/:id/terminal', async (req) => {
|
||||||
const { id } = req.params as { id: string };
|
const { id } = req.params as { id: string };
|
||||||
|
const query = req.query as { tail?: string };
|
||||||
const session = this.sessions.get(id);
|
const session = this.sessions.get(id);
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
@@ -534,9 +537,21 @@ export class WebServer extends EventEmitter {
|
|||||||
.replace(/\x0c/g, '')
|
.replace(/\x0c/g, '')
|
||||||
.replace(/^[\s\r\n]+/, '');
|
.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 {
|
return {
|
||||||
terminalBuffer: cleanBuffer,
|
terminalBuffer: cleanBuffer,
|
||||||
status: session.status,
|
status: session.status,
|
||||||
|
fullSize,
|
||||||
|
truncated,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user