feat(dashboard): add Zellij unavailable warning banner
This commit is contained in:
@@ -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() {
|
||||
})()}
|
||||
</div>
|
||||
<button
|
||||
class="rounded-lg border border-active/40 bg-active/12 px-3 py-2 text-sm font-medium text-active transition-colors hover:bg-active/20"
|
||||
disabled=${!zellijAvailable}
|
||||
class="rounded-lg border border-active/40 bg-active/12 px-3 py-2 text-sm font-medium text-active transition-colors hover:bg-active/20 ${!zellijAvailable ? 'opacity-50 cursor-not-allowed' : ''}"
|
||||
onClick=${() => setSpawnModalOpen(true)}
|
||||
>
|
||||
+ New Agent
|
||||
@@ -427,6 +481,13 @@ export function App() {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
${!zellijAvailable && html`
|
||||
<div class="border-b border-attention/50 bg-attention/10 px-6 py-2 text-sm text-attention">
|
||||
<span class="font-medium">Zellij session not found.</span>
|
||||
${' '}Agent spawning is unavailable. Start Zellij with: <code class="rounded bg-attention/15 px-1.5 py-0.5 font-mono text-micro">zellij attach infra</code>
|
||||
</div>
|
||||
`}
|
||||
|
||||
<main class="px-6 pb-6 pt-6">
|
||||
${loading ? html`
|
||||
<div class="glass-panel animate-fade-in-up flex items-center justify-center rounded-2xl py-24">
|
||||
@@ -454,6 +515,7 @@ export function App() {
|
||||
onFetchConversation=${fetchConversation}
|
||||
onRespond=${respondToSession}
|
||||
onDismiss=${dismissSession}
|
||||
isNewlySpawned=${newlySpawnedIds.has(session.session_id)}
|
||||
/>
|
||||
`)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user