From ea9618e281aedea3c33931281c7254223409f81c Mon Sep 17 00:00:00 2001 From: arkon Date: Mon, 19 Jan 2026 04:22:41 +0100 Subject: [PATCH] feat: improve Monitor panel UX and preserve session names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Monitor panel now opens by default on webapp start - Replace "Reconcile" button with refresh icon (↻) - Change close button to toggle (▼/▲) for collapse/expand - Preserve session display names when saving screen info - Use stored name when restoring sessions (no more "Restored:" prefix) - Show session name in Monitor panel instead of screen name - Kill screen session when closing a tab Co-Authored-By: Claude Opus 4.5 --- src/screen-manager.ts | 16 ++++++++++++++-- src/session.ts | 15 +++++++++++++-- src/types.ts | 1 + src/web/public/app.js | 7 ++++++- src/web/public/index.html | 4 ++-- src/web/public/styles.css | 16 ++++++++++++++++ src/web/server.ts | 4 +++- 7 files changed, 55 insertions(+), 8 deletions(-) diff --git a/src/screen-manager.ts b/src/screen-manager.ts index 2f6f17c5..cbbc8088 100644 --- a/src/screen-manager.ts +++ b/src/screen-manager.ts @@ -48,7 +48,7 @@ export class ScreenManager extends EventEmitter { } // Create a new GNU screen session - async createScreen(sessionId: string, workingDir: string, mode: 'claude' | 'shell'): Promise { + async createScreen(sessionId: string, workingDir: string, mode: 'claude' | 'shell', name?: string): Promise { const screenName = `claudeman-${sessionId.slice(0, 8)}`; // Create screen in detached mode with the appropriate command @@ -84,7 +84,8 @@ export class ScreenManager extends EventEmitter { createdAt: Date.now(), workingDir, mode, - attached: false + attached: false, + name }; this.screens.set(sessionId, screen); @@ -150,6 +151,17 @@ export class ScreenManager extends EventEmitter { return this.screens.get(sessionId); } + // Update screen display name + updateScreenName(sessionId: string, name: string): boolean { + const screen = this.screens.get(sessionId); + if (!screen) { + return false; + } + screen.name = name; + this.saveScreens(); + return true; + } + // Reconcile screens - find orphaned/dead screens async reconcileScreens(): Promise<{ alive: string[]; dead: string[] }> { const alive: string[] = []; diff --git a/src/session.ts b/src/session.ts index 3807df83..dce56eb6 100644 --- a/src/session.ts +++ b/src/session.ts @@ -306,7 +306,7 @@ export class Session extends EventEmitter { // If screen wrapping is enabled, create a screen session first if (this._useScreen && this._screenManager) { try { - this._screenSession = await this._screenManager.createScreen(this.id, this.workingDir, 'claude'); + this._screenSession = await this._screenManager.createScreen(this.id, this.workingDir, 'claude', this._name); console.log('[Session] Created screen session:', this._screenSession.screenName); // Wait a moment for screen to fully start @@ -424,7 +424,7 @@ export class Session extends EventEmitter { // If screen wrapping is enabled, create a screen session first if (this._useScreen && this._screenManager) { try { - this._screenSession = await this._screenManager.createScreen(this.id, this.workingDir, 'shell'); + this._screenSession = await this._screenManager.createScreen(this.id, this.workingDir, 'shell', this._name); console.log('[Session] Created screen session:', this._screenSession.screenName); // Wait a moment for screen to fully start @@ -767,6 +767,17 @@ export class Session extends EventEmitter { this._status = 'stopped'; this._currentTaskId = null; + // Kill the associated screen session if any + if (this._screenSession && this._screenManager) { + try { + await this._screenManager.killScreen(this.id); + console.log('[Session] Killed screen session:', this._screenSession.screenName); + } catch (err) { + console.error('[Session] Failed to kill screen session:', err); + } + this._screenSession = null; + } + if (this.rejectPromise) { this.rejectPromise(new Error('Session stopped')); this.resolvePromise = null; diff --git a/src/types.ts b/src/types.ts index 852016c4..fc5d2e18 100644 --- a/src/types.ts +++ b/src/types.ts @@ -214,6 +214,7 @@ export interface ScreenSession { workingDir: string; mode: 'claude' | 'shell'; attached: boolean; // Whether webserver is attached + name?: string; // Session display name (tab name) } export interface ProcessStats { diff --git a/src/web/public/app.js b/src/web/public/app.js index a8e381ab..30346634 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -30,6 +30,8 @@ class ClaudemanApp { this.loadState(); this.loadQuickStartCases(); this.setupEventListeners(); + // Show monitor panel by default + this.toggleMonitorPanel(); } initTerminal() { @@ -1361,6 +1363,7 @@ class ClaudemanApp { async toggleMonitorPanel() { const panel = document.getElementById('monitorPanel'); + const toggleBtn = document.getElementById('monitorToggleBtn'); panel.classList.toggle('open'); if (panel.classList.contains('open')) { @@ -1368,9 +1371,11 @@ class ClaudemanApp { await this.loadScreens(); await fetch('/api/screens/stats/start', { method: 'POST' }); this.renderTaskPanel(); + if (toggleBtn) toggleBtn.innerHTML = '▼'; // Down arrow when open } else { // Stop stats collection when panel is closed await fetch('/api/screens/stats/stop', { method: 'POST' }); + if (toggleBtn) toggleBtn.innerHTML = '▲'; // Up arrow when closed } } @@ -1487,7 +1492,7 @@ class ClaudemanApp {
${screen.mode}
-
${this.escapeHtml(screen.screenName)}
+
${this.escapeHtml(screen.name || screen.screenName)}
${stats.memoryMB}MB ${stats.cpuPercent}% diff --git a/src/web/public/index.html b/src/web/public/index.html index d041d904..46bb0f91 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -135,8 +135,8 @@
Monitor
- - + +
diff --git a/src/web/public/styles.css b/src/web/public/styles.css index 4fcf2134..34e0f15b 100644 --- a/src/web/public/styles.css +++ b/src/web/public/styles.css @@ -661,6 +661,22 @@ body { color: var(--text); } +.btn-icon-sm { + background: transparent; + border: none; + color: var(--text-dim); + font-size: 1rem; + cursor: pointer; + padding: 0.25rem; + line-height: 1; + border-radius: 4px; +} + +.btn-icon-sm:hover { + color: var(--text); + background: var(--bg-hover); +} + .form-row { margin-bottom: 0.75rem; } diff --git a/src/web/server.ts b/src/web/server.ts index e179ac5a..f1fd99e3 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -141,6 +141,8 @@ export class WebServer extends EventEmitter { } session.name = body.name || ''; + // Also update the screen name if this session has a screen + this.screenManager.updateScreenName(id, session.name); this.broadcast('session:updated', session.toDetailedState()); return { success: true, name: session.name }; }); @@ -1063,7 +1065,7 @@ export class WebServer extends EventEmitter { id: screen.sessionId, // Preserve the original session ID workingDir: screen.workingDir, mode: screen.mode, - name: `Restored: ${screen.screenName}`, + name: screen.name || screen.screenName, screenManager: this.screenManager, useScreen: true });