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 { 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 { groupSessionsByProject } from '../utils/status.js';
|
||||||
import { Sidebar } from './Sidebar.js';
|
import { Sidebar } from './Sidebar.js';
|
||||||
import { SessionCard } from './SessionCard.js';
|
import { SessionCard } from './SessionCard.js';
|
||||||
@@ -20,6 +20,9 @@ export function App() {
|
|||||||
const [sseConnected, setSseConnected] = useState(false);
|
const [sseConnected, setSseConnected] = useState(false);
|
||||||
const [deadSessionsCollapsed, setDeadSessionsCollapsed] = useState(true);
|
const [deadSessionsCollapsed, setDeadSessionsCollapsed] = useState(true);
|
||||||
const [spawnModalOpen, setSpawnModalOpen] = useState(false);
|
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
|
// Background conversation refresh with error tracking
|
||||||
const refreshConversationSilent = useCallback(async (sessionId, projectDir, agent = 'claude') => {
|
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
|
// Clean up conversation cache for sessions that no longer exist
|
||||||
setConversations(prev => {
|
setConversations(prev => {
|
||||||
const activeIds = Object.keys(prev).filter(id => newSessionIds.has(id));
|
const activeIds = Object.keys(prev).filter(id => newSessionIds.has(id));
|
||||||
@@ -303,6 +334,25 @@ export function App() {
|
|||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [fetchState, sseConnected]);
|
}, [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
|
// Group sessions by project
|
||||||
const projectGroups = groupSessionsByProject(sessions);
|
const projectGroups = groupSessionsByProject(sessions);
|
||||||
|
|
||||||
@@ -353,6 +403,9 @@ export function App() {
|
|||||||
const handleSpawnResult = useCallback((result) => {
|
const handleSpawnResult = useCallback((result) => {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
showToast(`${result.agentType} agent spawned for ${result.project}`, 'success');
|
showToast(`${result.agentType} agent spawned for ${result.project}`, 'success');
|
||||||
|
if (result.spawnId) {
|
||||||
|
pendingSpawnIdsRef.current.add(result.spawnId);
|
||||||
|
}
|
||||||
} else if (result.error) {
|
} else if (result.error) {
|
||||||
showToast(result.error, 'error');
|
showToast(result.error, 'error');
|
||||||
}
|
}
|
||||||
@@ -419,7 +472,8 @@ export function App() {
|
|||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<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)}
|
onClick=${() => setSpawnModalOpen(true)}
|
||||||
>
|
>
|
||||||
+ New Agent
|
+ New Agent
|
||||||
@@ -427,6 +481,13 @@ export function App() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</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">
|
<main class="px-6 pb-6 pt-6">
|
||||||
${loading ? html`
|
${loading ? html`
|
||||||
<div class="glass-panel animate-fade-in-up flex items-center justify-center rounded-2xl py-24">
|
<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}
|
onFetchConversation=${fetchConversation}
|
||||||
onRespond=${respondToSession}
|
onRespond=${respondToSession}
|
||||||
onDismiss=${dismissSession}
|
onDismiss=${dismissSession}
|
||||||
|
isNewlySpawned=${newlySpawnedIds.has(session.session_id)}
|
||||||
/>
|
/>
|
||||||
`)}
|
`)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user