From 0245f61c373e004466c3eed2f9e521daae458a39 Mon Sep 17 00:00:00 2001 From: arkon Date: Mon, 19 Jan 2026 04:47:48 +0100 Subject: [PATCH] feat: add option to keep screen alive when closing tab When clicking (x) on a session tab, users now choose: - Hide Tab (Keep Screen): removes tab but screen session stays alive - Close & Kill Screen: terminates both tab and screen session Co-Authored-By: Claude Opus 4.5 --- src/session.ts | 9 ++++++--- src/web/public/app.js | 14 ++++++++++---- src/web/public/index.html | 7 ++++--- src/web/public/styles.css | 18 ++++++++++++++++++ src/web/server.ts | 4 +++- 5 files changed, 41 insertions(+), 11 deletions(-) diff --git a/src/session.ts b/src/session.ts index dd6034af..694409db 100644 --- a/src/session.ts +++ b/src/session.ts @@ -764,7 +764,7 @@ export class Session extends EventEmitter { }); } - async stop(): Promise { + async stop(killScreen: boolean = true): Promise { // Clear activity timeout to prevent memory leak if (this.activityTimeout) { clearTimeout(this.activityTimeout); @@ -808,8 +808,8 @@ export class Session extends EventEmitter { this._status = 'stopped'; this._currentTaskId = null; - // Kill the associated screen session if any - if (this._screenSession && this._screenManager) { + // Kill the associated screen session if requested + if (this._screenSession && this._screenManager && killScreen) { try { await this._screenManager.killScreen(this.id); console.log('[Session] Killed screen session:', this._screenSession.screenName); @@ -817,6 +817,9 @@ export class Session extends EventEmitter { console.error('[Session] Failed to kill screen session:', err); } this._screenSession = null; + } else if (this._screenSession && !killScreen) { + console.log('[Session] Keeping screen session alive:', this._screenSession.screenName); + this._screenSession = null; // Detach but don't kill } if (this.rejectPromise) { diff --git a/src/web/public/app.js b/src/web/public/app.js index 8a5034e5..2532b0c7 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -563,9 +563,9 @@ class ClaudemanApp { } } - async closeSession(sessionId) { + async closeSession(sessionId, killScreen = true) { try { - await fetch(`/api/sessions/${sessionId}`, { method: 'DELETE' }); + await fetch(`/api/sessions/${sessionId}?killScreen=${killScreen}`, { method: 'DELETE' }); this.sessions.delete(sessionId); this.terminalBuffers.delete(sessionId); @@ -582,6 +582,12 @@ class ClaudemanApp { } this.renderSessionTabs(); + + if (killScreen) { + this.showToast('Session closed and screen killed', 'success'); + } else { + this.showToast('Tab hidden, screen still running', 'info'); + } } catch (err) { this.showToast('Failed to close session', 'error'); } @@ -607,12 +613,12 @@ class ClaudemanApp { document.getElementById('closeConfirmModal').classList.remove('active'); } - async confirmCloseSession() { + async confirmCloseSession(killScreen = true) { const sessionId = this.pendingCloseSessionId; this.cancelCloseSession(); if (sessionId) { - await this.closeSession(sessionId); + await this.closeSession(sessionId, killScreen); } } diff --git a/src/web/public/index.html b/src/web/public/index.html index 46bb0f91..0d0aa185 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -268,12 +268,13 @@ -
+
+ + -
diff --git a/src/web/public/styles.css b/src/web/public/styles.css index b0559e4a..767a433e 100644 --- a/src/web/public/styles.css +++ b/src/web/public/styles.css @@ -800,6 +800,24 @@ body { border-top: 1px solid var(--border); } +.form-actions-stack { + flex-direction: column; + align-items: stretch; +} + +.form-actions-stack button { + width: 100%; +} + +.btn-warning { + background: var(--yellow); + color: #000; +} + +.btn-warning:hover { + background: #d4a107; +} + /* Modal */ .modal { display: none; diff --git a/src/web/server.ts b/src/web/server.ts index f1fd99e3..40267cfe 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -149,6 +149,8 @@ export class WebServer extends EventEmitter { this.app.delete('/api/sessions/:id', async (req): Promise => { const { id } = req.params as { id: string }; + const query = req.query as { killScreen?: string }; + const killScreen = query.killScreen !== 'false'; // Default to true const session = this.sessions.get(id); if (!session) { @@ -162,7 +164,7 @@ export class WebServer extends EventEmitter { this.respawnControllers.delete(id); } - await session.stop(); + await session.stop(killScreen); this.sessions.delete(id); this.terminalBatches.delete(id); this.broadcast('session:deleted', { id });