feat: improve Monitor panel UX and preserve session names

- 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 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-19 04:22:41 +01:00
co-authored by Claude Opus 4.5
parent 34c86488dd
commit ea9618e281
7 changed files with 55 additions and 8 deletions
+14 -2
View File
@@ -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<ScreenSession> {
async createScreen(sessionId: string, workingDir: string, mode: 'claude' | 'shell', name?: string): Promise<ScreenSession> {
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[] = [];
+13 -2
View File
@@ -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;
+1
View File
@@ -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 {
+6 -1
View File
@@ -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 = '&#x25BC;'; // Down arrow when open
} else {
// Stop stats collection when panel is closed
await fetch('/api/screens/stats/stop', { method: 'POST' });
if (toggleBtn) toggleBtn.innerHTML = '&#x25B2;'; // Up arrow when closed
}
}
@@ -1487,7 +1492,7 @@ class ClaudemanApp {
<div class="process-item">
<span class="process-mode ${modeClass}">${screen.mode}</span>
<div class="process-info">
<div class="process-name">${this.escapeHtml(screen.screenName)}</div>
<div class="process-name">${this.escapeHtml(screen.name || screen.screenName)}</div>
<div class="process-meta">
<span class="process-stat memory">${stats.memoryMB}MB</span>
<span class="process-stat cpu">${stats.cpuPercent}%</span>
+2 -2
View File
@@ -135,8 +135,8 @@
<div class="monitor-panel-header">
<span>Monitor</span>
<div class="monitor-panel-actions">
<button class="btn-toolbar btn-sm" onclick="app.reconcileScreens()" title="Check for dead screens">Reconcile</button>
<button class="btn-close" onclick="app.toggleMonitorPanel()">&times;</button>
<button class="btn-icon-sm" onclick="app.reconcileScreens()" title="Refresh screens">&#x21BB;</button>
<button class="btn-icon-sm" onclick="app.toggleMonitorPanel()" title="Toggle panel" id="monitorToggleBtn">&#x25BC;</button>
</div>
</div>
<div class="monitor-panel-body">
+16
View File
@@ -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;
}
+3 -1
View File
@@ -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
});