diff --git a/src/web/public/app.js b/src/web/public/app.js
index fda0564e..39e2f4c6 100644
--- a/src/web/public/app.js
+++ b/src/web/public/app.js
@@ -15,9 +15,28 @@ class ClaudemanApp {
this.respawnStatus = {}; // Respawn status per session
this.respawnEnabled = false; // Whether respawn is enabled for new sessions (disabled by default)
+ // Terminal write batching for performance
+ this.pendingWrites = '';
+ this.writeFrameScheduled = false;
+
this.init();
}
+ // Batch terminal writes using requestAnimationFrame for smooth rendering
+ batchTerminalWrite(data) {
+ this.pendingWrites += data;
+ if (!this.writeFrameScheduled) {
+ this.writeFrameScheduled = true;
+ requestAnimationFrame(() => {
+ if (this.pendingWrites && this.terminal) {
+ this.terminal.write(this.pendingWrites);
+ this.pendingWrites = '';
+ }
+ this.writeFrameScheduled = false;
+ });
+ }
+ }
+
init() {
this.initTerminal();
this.connectSSE();
@@ -195,10 +214,10 @@ class ClaudemanApp {
this.renderSessions();
});
- // Handle raw terminal data from PTY
+ // Handle raw terminal data from PTY - use batched writes for performance
this.eventSource.addEventListener('session:terminal', (e) => {
const data = JSON.parse(e.data);
- this.terminal.write(data.data);
+ this.batchTerminalWrite(data.data);
});
this.eventSource.addEventListener('session:output', (e) => {
@@ -932,7 +951,7 @@ class ClaudemanApp {
${respawn.state.replace(/_/g, ' ')}
-
+
` : `
@@ -940,26 +959,79 @@ class ClaudemanApp {
No respawn
-
+
`;
+ // Resource usage display
+ const bufferStats = s.bufferStats || {};
+ const terminalSize = bufferStats.terminalBufferSize || 0;
+ const textSize = bufferStats.textOutputSize || 0;
+ const msgCount = bufferStats.messageCount || 0;
+ const totalMemory = terminalSize + textSize;
+
+ // Color coding based on usage
+ const memoryPercent = bufferStats.maxTerminalBuffer ?
+ (terminalSize / bufferStats.maxTerminalBuffer * 100) : 0;
+ const memoryClass = memoryPercent > 80 ? 'high' : memoryPercent > 50 ? 'medium' : 'low';
+
+ const resourceHtml = `
+
+
+ Memory:
+ ${this.formatBytes(totalMemory)}
+
+
+ Messages:
+ ${msgCount}
+
+
+ `;
+
return `
-
+
+ ${resourceHtml}
${respawnHtml}
`;
}).join('');
}
+ // Select a session to view its terminal
+ selectSession(sessionId) {
+ this.activeSessionId = sessionId;
+ this.renderSessions();
+
+ // Load the session's terminal buffer
+ this.loadSessionTerminal(sessionId);
+ }
+
+ // Load terminal buffer for a session
+ async loadSessionTerminal(sessionId) {
+ try {
+ const res = await fetch(`/api/sessions/${sessionId}/terminal`);
+ const data = await res.json();
+ if (data.terminalBuffer) {
+ this.terminal.clear();
+ this.terminal.writeln(`\x1b[90m--- Session ${sessionId.slice(0, 8)} terminal ---\x1b[0m`);
+ this.terminal.write(data.terminalBuffer);
+ }
+ } catch (err) {
+ console.error('Failed to load terminal buffer:', err);
+ }
+ }
+
async stopRespawnForSession(sessionId) {
try {
const res = await fetch(`/api/sessions/${sessionId}/respawn/stop`, {
@@ -974,6 +1046,76 @@ class ClaudemanApp {
}
}
+ // Kill all sessions
+ async killAllSessions() {
+ const count = this.sessions.size;
+ if (count === 0) {
+ this.terminal.writeln('\x1b[90mNo sessions to kill\x1b[0m');
+ return;
+ }
+
+ if (!confirm(`Kill all ${count} session(s)? This will stop all running Claude processes.`)) {
+ return;
+ }
+
+ this.terminal.writeln(`\x1b[1;33m⚠ Killing ${count} session(s)...\x1b[0m`);
+
+ try {
+ const res = await fetch('/api/sessions', {
+ method: 'DELETE'
+ });
+ const data = await res.json();
+ if (data.success) {
+ this.terminal.writeln(`\x1b[1;32m✓ Killed ${data.data.killed} session(s)\x1b[0m`);
+ this.sessions.clear();
+ this.activeSessionId = null;
+ this.respawnStatus = {};
+ this.hideRespawnBanner();
+ this.renderSessions();
+ } else {
+ throw new Error(data.error);
+ }
+ } catch (err) {
+ this.terminal.writeln(`\x1b[1;31m❌ Error: ${err.message}\x1b[0m`);
+ }
+ }
+
+ // Kill a single session
+ async deleteSession(sessionId) {
+ if (!confirm(`Kill session ${sessionId.slice(0, 8)}?`)) {
+ return;
+ }
+
+ try {
+ const res = await fetch(`/api/sessions/${sessionId}`, {
+ method: 'DELETE'
+ });
+ const data = await res.json();
+ if (data.success) {
+ this.terminal.writeln(`\x1b[1;32m✓ Session ${sessionId.slice(0, 8)} killed\x1b[0m`);
+ this.sessions.delete(sessionId);
+ if (this.activeSessionId === sessionId) {
+ this.activeSessionId = null;
+ }
+ delete this.respawnStatus[sessionId];
+ this.renderSessions();
+ } else {
+ throw new Error(data.error);
+ }
+ } catch (err) {
+ this.terminal.writeln(`\x1b[1;31m❌ Error: ${err.message}\x1b[0m`);
+ }
+ }
+
+ // Format bytes for display
+ formatBytes(bytes) {
+ if (bytes === 0) return '0 B';
+ const k = 1024;
+ const sizes = ['B', 'KB', 'MB', 'GB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
+ }
+
updateSessionOutput(sessionId, text) {
// Sessions list shows brief info, main terminal shows full output
}
diff --git a/src/web/public/index.html b/src/web/public/index.html
index 2cbecccb..b6c44a8f 100644
--- a/src/web/public/index.html
+++ b/src/web/public/index.html
@@ -242,9 +242,14 @@