From 7add243f39411457d8af4bbbd499089cde81dfd5 Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 24 Jan 2026 03:49:19 +0100 Subject: [PATCH] feat: blinking tab alerts for sessions needing attention - Red blink for action-required events (permission_prompt, elicitation_dialog) - Yellow blink for idle sessions (idle_prompt) - Alert clears when user clicks the tab or session starts working again - Only blinks non-active tabs (active tab is already visible) Co-Authored-By: Claude Opus 4.5 --- src/web/public/app.js | 31 ++++++++++++++++++++++++++++++- src/web/public/styles.css | 19 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/web/public/app.js b/src/web/public/app.js index ff9f4d7a..01645367 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -370,6 +370,9 @@ class ClaudemanApp { this.ralphStates = new Map(); // Map this.ralphStatePanelCollapsed = true; // Default to collapsed + // Tab alert states: Map + this.tabAlerts = new Map(); + // Terminal write batching this.pendingWrites = ''; this.writeFrameScheduled = false; @@ -832,6 +835,7 @@ class ClaudemanApp { const session = this.sessions.get(data.id); if (session) { session.status = 'busy'; + this.tabAlerts.delete(data.id); this.renderSessionTabs(); this.sendPendingCtrlL(data.id); } @@ -1121,6 +1125,10 @@ class ClaudemanApp { this.eventSource.addEventListener('hook:idle_prompt', (e) => { const data = JSON.parse(e.data); const session = this.sessions.get(data.sessionId); + if (data.sessionId && data.sessionId !== this.activeSessionId) { + this.tabAlerts.set(data.sessionId, 'idle'); + this.renderSessionTabs(); + } this.notificationManager?.notify({ urgency: 'warning', category: 'hook-idle', @@ -1134,6 +1142,10 @@ class ClaudemanApp { this.eventSource.addEventListener('hook:permission_prompt', (e) => { const data = JSON.parse(e.data); const session = this.sessions.get(data.sessionId); + if (data.sessionId && data.sessionId !== this.activeSessionId) { + this.tabAlerts.set(data.sessionId, 'action'); + this.renderSessionTabs(); + } this.notificationManager?.notify({ urgency: 'critical', category: 'hook-permission', @@ -1147,6 +1159,10 @@ class ClaudemanApp { this.eventSource.addEventListener('hook:elicitation_dialog', (e) => { const data = JSON.parse(e.data); const session = this.sessions.get(data.sessionId); + if (data.sessionId && data.sessionId !== this.activeSessionId) { + this.tabAlerts.set(data.sessionId, 'action'); + this.renderSessionTabs(); + } this.notificationManager?.notify({ urgency: 'critical', category: 'hook-elicitation', @@ -1266,6 +1282,16 @@ class ClaudemanApp { tab.classList.remove('active'); } + // Update alert class + const alertType = this.tabAlerts.get(id); + const wantAction = alertType === 'action'; + const wantIdle = alertType === 'idle'; + const hasAction = tab.classList.contains('tab-alert-action'); + const hasIdle = tab.classList.contains('tab-alert-idle'); + if (wantAction && !hasAction) { tab.classList.add('tab-alert-action'); tab.classList.remove('tab-alert-idle'); } + else if (wantIdle && !hasIdle) { tab.classList.add('tab-alert-idle'); tab.classList.remove('tab-alert-action'); } + else if (!alertType && (hasAction || hasIdle)) { tab.classList.remove('tab-alert-action', 'tab-alert-idle'); } + // Update status indicator const statusEl = tab.querySelector('.tab-status'); if (statusEl && !statusEl.classList.contains(status)) { @@ -1320,8 +1346,10 @@ class ClaudemanApp { const mode = session.mode || 'claude'; const taskStats = session.taskStats || { running: 0, total: 0 }; const hasRunningTasks = taskStats.running > 0; + const alertType = this.tabAlerts.get(id); + const alertClass = alertType === 'action' ? ' tab-alert-action' : alertType === 'idle' ? ' tab-alert-idle' : ''; - parts.push(`
+ parts.push(`
${mode === 'shell' ? 'sh' : ''} ${this.escapeHtml(name)} @@ -1350,6 +1378,7 @@ class ClaudemanApp { if (this.activeSessionId === sessionId) return; this.activeSessionId = sessionId; + this.tabAlerts.delete(sessionId); this.renderSessionTabs(); // Check if this is a restored session that needs to be attached diff --git a/src/web/public/styles.css b/src/web/public/styles.css index 395938e1..f30d4754 100644 --- a/src/web/public/styles.css +++ b/src/web/public/styles.css @@ -182,6 +182,25 @@ body { color: var(--green); } +/* Tab alert animations */ +.session-tab.tab-alert-action { + animation: tab-blink-red 1s infinite; +} + +.session-tab.tab-alert-idle { + animation: tab-blink-yellow 1.5s infinite; +} + +@keyframes tab-blink-red { + 0%, 100% { background: transparent; border-color: transparent; } + 50% { background: rgba(239, 68, 68, 0.15); border-color: var(--red); } +} + +@keyframes tab-blink-yellow { + 0%, 100% { background: transparent; border-color: transparent; } + 50% { background: rgba(234, 179, 8, 0.12); border-color: var(--yellow); } +} + @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; }