diff --git a/apps/desktop/src/main/__tests__/agent-graph-panel-visibility.test.ts b/apps/desktop/src/main/__tests__/agent-graph-panel-visibility.test.ts index 8a5e0bb041..23d81784ee 100644 --- a/apps/desktop/src/main/__tests__/agent-graph-panel-visibility.test.ts +++ b/apps/desktop/src/main/__tests__/agent-graph-panel-visibility.test.ts @@ -21,11 +21,24 @@ import { strict as assert } from 'node:assert'; import { describe, it } from 'node:test'; import { dismissAgentGraphPanel, + isAgentGraphLive, isAgentGraphPanelDismissible, reconcileAgentGraphPanelDismissals, shouldShowAgentGraphPanel, } from '../../renderer/agent-graph-panel-visibility.js'; +describe('isAgentGraphLive', () => { + it('treats in-flight statuses as live and settled ones as not', () => { + for (const status of ['active', 'waiting', 'closing'] as const) { + assert.equal(isAgentGraphLive(status), true, status); + } + for (const status of ['empty', 'stopped', 'failed', 'completed'] as const) { + assert.equal(isAgentGraphLive(status), false, status); + } + assert.equal(isAgentGraphLive(undefined), false); + }); +}); + describe('isAgentGraphPanelDismissible', () => { it('allows hiding a graph that no longer has active work', () => { assert.equal(isAgentGraphPanelDismissible('completed'), true); diff --git a/apps/desktop/src/renderer/agent-graph-panel-visibility.ts b/apps/desktop/src/renderer/agent-graph-panel-visibility.ts index dca3b68b18..c80584c94b 100644 --- a/apps/desktop/src/renderer/agent-graph-panel-visibility.ts +++ b/apps/desktop/src/renderer/agent-graph-panel-visibility.ts @@ -40,6 +40,17 @@ export function isAgentGraphPanelDismissible( return status !== undefined && DISMISSIBLE_STATUSES.has(status); } +const LIVE_STATUSES = new Set([ + 'active', + 'waiting', + 'closing', +]); + +/** A graph in one of these statuses can be stopped and must keep signaling liveness. */ +export function isAgentGraphLive(status: AgentGraphPanelStatus | undefined): boolean { + return status !== undefined && LIVE_STATUSES.has(status); +} + export function dismissAgentGraphPanel( dismissedBySession: AgentGraphPanelDismissals, sessionId: string, diff --git a/apps/desktop/src/renderer/agent-graph-panel.tsx b/apps/desktop/src/renderer/agent-graph-panel.tsx index 32e8a38810..0d9afe017d 100644 --- a/apps/desktop/src/renderer/agent-graph-panel.tsx +++ b/apps/desktop/src/renderer/agent-graph-panel.tsx @@ -33,6 +33,7 @@ import { EmptyState } from '@astryxdesign/core/EmptyState'; import { Spinner } from '@astryxdesign/core/Spinner'; import { dismissAgentGraphPanel, + isAgentGraphLive, isAgentGraphPanelDismissible, reconcileAgentGraphPanelDismissals, shouldShowAgentGraphPanel, @@ -204,6 +205,10 @@ export function AgentGraphPanel(props: { stopState.rootSessionId === props.rootSessionId && stopState.graphId === selectedGraphId; const stopPending = stopFeedbackMatchesSelection && stopState.pending; const stopError = stopFeedbackMatchesSelection && stopState.error; + // `error` must gate liveness: a failed snapshot read leaves the last known + // status in place, and a spinner asserting liveness while liveness is unknown + // is exactly the false signal this panel exists to avoid. + const graphLive = !error && snapshot !== undefined && isAgentGraphLive(snapshot.status); useEffect(() => { setSnapshot(undefined); @@ -350,7 +355,7 @@ export function AgentGraphPanel(props: { !loading && snapshot !== undefined && snapshot.graphId === selectedGraphId && - ['active', 'waiting', 'closing'].includes(snapshot.status); + isAgentGraphLive(snapshot.status); const dismissAvailable = selectedEpoch?.current === true && !loading && @@ -393,6 +398,14 @@ export function AgentGraphPanel(props: { ) : null} {snapshot ? ( + {graphLive ? ( +