feat(ui): redesign with modern styling and resource display

UI Improvements:
- Add gradient backgrounds to all components
- Add smooth hover/active animations with transforms
- Add subtle shadows and glow effects
- Add animated progress bar shimmer effect
- Add gradient logo with text fill

Button Redesign:
- Add gradient backgrounds and hover effects
- Add shadow on hover with lift animation
- Add btn-xs size for compact buttons
- Improve Kill All button styling

Session Cards:
- Show resource usage (memory, messages)
- Color-coded usage indicators (green/yellow/red)
- Add kill button per session
- Click to switch active session and load terminal

Process Management:
- Add Kill All Sessions button in panel header
- Add killAllSessions() and deleteSession() methods
- Add formatBytes() helper for display
- Add selectSession() to switch between sessions

Performance:
- Add requestAnimationFrame batching for terminal writes
- Smooth 60fps rendering with write batching

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-18 21:36:37 +01:00
co-authored by Claude Opus 4.5
parent 4712af433c
commit 09877c66b2
3 changed files with 506 additions and 83 deletions
+148 -6
View File
@@ -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 {
<span class="session-respawn-dot ${respawn.state === 'stopped' ? 'stopped' : ''}"></span>
<span class="session-respawn-state">${respawn.state.replace(/_/g, ' ')}</span>
<span class="session-respawn-actions">
<button class="btn btn-sm" onclick="app.stopRespawnForSession('${s.id}')">Stop</button>
<button class="btn btn-sm" onclick="event.stopPropagation(); app.stopRespawnForSession('${s.id}')">Stop</button>
</span>
</div>
` : `
@@ -940,26 +959,79 @@ class ClaudemanApp {
<span class="session-respawn-dot stopped"></span>
<span class="session-respawn-state">No respawn</span>
<span class="session-respawn-actions">
<button class="btn btn-sm" onclick="app.startRespawnForSession('${s.id}')">Start</button>
<button class="btn btn-sm" onclick="event.stopPropagation(); app.startRespawnForSession('${s.id}')">Start</button>
</span>
</div>
`;
// 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 = `
<div class="session-resources">
<div class="resource-item">
<span class="resource-label">Memory:</span>
<span class="resource-value ${memoryClass}">${this.formatBytes(totalMemory)}</span>
</div>
<div class="resource-item">
<span class="resource-label">Messages:</span>
<span class="resource-value">${msgCount}</span>
</div>
</div>
`;
return `
<div class="session-card" data-session="${s.id}">
<div class="session-card ${this.activeSessionId === s.id ? 'active' : ''}" data-session="${s.id}" onclick="app.selectSession('${s.id}')">
<div class="session-card-header">
<div class="session-status">
<span class="session-status-dot ${s.status}"></span>
<span>${s.id.slice(0, 8)}</span>
</div>
<span class="session-cost">$${(s.totalCost || 0).toFixed(4)}</span>
<div class="session-actions">
<span class="session-cost">$${(s.totalCost || 0).toFixed(4)}</span>
<button class="btn btn-xs btn-danger" onclick="event.stopPropagation(); app.deleteSession('${s.id}')" title="Kill session">✕</button>
</div>
</div>
${resourceHtml}
${respawnHtml}
</div>
`;
}).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
}