From 5494d76a98d29a7197e1f89ade65d5a8fc4e3ba6 Mon Sep 17 00:00:00 2001 From: teernisse Date: Thu, 26 Feb 2026 17:08:37 -0500 Subject: [PATCH] feat(dashboard): add Zellij unavailable warning banner --- dashboard/components/App.js | 66 +++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/dashboard/components/App.js b/dashboard/components/App.js index edb9e27..d85efd5 100644 --- a/dashboard/components/App.js +++ b/dashboard/components/App.js @@ -1,5 +1,5 @@ import { html, useState, useEffect, useCallback, useMemo, useRef } from '../lib/preact.js'; -import { API_STATE, API_STREAM, API_DISMISS, API_DISMISS_DEAD, API_RESPOND, API_CONVERSATION, POLL_MS, fetchWithTimeout } from '../utils/api.js'; +import { API_STATE, API_STREAM, API_DISMISS, API_DISMISS_DEAD, API_RESPOND, API_CONVERSATION, API_HEALTH, POLL_MS, fetchWithTimeout } from '../utils/api.js'; import { groupSessionsByProject } from '../utils/status.js'; import { Sidebar } from './Sidebar.js'; import { SessionCard } from './SessionCard.js'; @@ -20,6 +20,9 @@ export function App() { const [sseConnected, setSseConnected] = useState(false); const [deadSessionsCollapsed, setDeadSessionsCollapsed] = useState(true); const [spawnModalOpen, setSpawnModalOpen] = useState(false); + const [zellijAvailable, setZellijAvailable] = useState(true); + const [newlySpawnedIds, setNewlySpawnedIds] = useState(new Set()); + const pendingSpawnIdsRef = useRef(new Set()); // Background conversation refresh with error tracking const refreshConversationSilent = useCallback(async (sessionId, projectDir, agent = 'claude') => { @@ -71,6 +74,34 @@ export function App() { } } + // Check for newly spawned sessions matching pending spawn IDs + if (pendingSpawnIdsRef.current.size > 0) { + const matched = new Set(); + for (const session of newSessions) { + if (session.spawn_id && pendingSpawnIdsRef.current.has(session.spawn_id)) { + matched.add(session.session_id); + pendingSpawnIdsRef.current.delete(session.spawn_id); + } + } + if (matched.size > 0) { + setNewlySpawnedIds(prev => { + const next = new Set(prev); + for (const id of matched) next.add(id); + return next; + }); + // Auto-clear highlight after animation duration (2.5s) + for (const id of matched) { + setTimeout(() => { + setNewlySpawnedIds(prev => { + const next = new Set(prev); + next.delete(id); + return next; + }); + }, 2500); + } + } + } + // Clean up conversation cache for sessions that no longer exist setConversations(prev => { const activeIds = Object.keys(prev).filter(id => newSessionIds.has(id)); @@ -303,6 +334,25 @@ export function App() { return () => clearInterval(interval); }, [fetchState, sseConnected]); + // Poll Zellij health status + useEffect(() => { + const checkHealth = async () => { + try { + const response = await fetchWithTimeout(API_HEALTH); + if (response.ok) { + const data = await response.json(); + setZellijAvailable(data.zellij_available); + } + } catch { + // Server unreachable - handled by state fetch error + } + }; + + checkHealth(); + const interval = setInterval(checkHealth, 30000); + return () => clearInterval(interval); + }, []); + // Group sessions by project const projectGroups = groupSessionsByProject(sessions); @@ -353,6 +403,9 @@ export function App() { const handleSpawnResult = useCallback((result) => { if (result.success) { showToast(`${result.agentType} agent spawned for ${result.project}`, 'success'); + if (result.spawnId) { + pendingSpawnIdsRef.current.add(result.spawnId); + } } else if (result.error) { showToast(result.error, 'error'); } @@ -419,7 +472,8 @@ export function App() { })()}