You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by ku...@apache.org on 2026/09/05 11:16:58 UTC
(maka) branch main updated: fix(desktop): keep the Agent Graph panel visibly alive while operators run (#4514)
This is an automated email from the ASF dual-hosted git repository.
likun666661 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/maka.git
The following commit(s) were added to refs/heads/main by this push:
new 03b5a2cc5a fix(desktop): keep the Agent Graph panel visibly alive while operators run (#4514)
03b5a2cc5a is described below
commit 03b5a2cc5a76897b919e691ee4f242fa2e7c9e36
Author: 小七 <gg...@gmail.com>
AuthorDate: Sat Sep 5 19:16:53 2026 +0800
fix(desktop): keep the Agent Graph panel visibly alive while operators run (#4514)
* fix(desktop): keep the Agent Graph panel visibly alive while operators run
A fan-out graph in its early window shows a static header (`Running · 0/7
settled`) and seven static dots, so the panel reads as frozen even though
scheduling is correct and child sessions are working (#3878).
Three presentation-layer liveness signals, no protocol change:
- Running and runnable operator dots pulse (CSS only, disabled under
prefers-reduced-motion).
- The header shows a subtle spinner for as long as the snapshot status is
active, waiting, or closing.
- A per-epoch elapsed clock (`· 00:23`) trails the settled counter. The
client snapshot carries no start timestamp, so the clock measures this
view's observation of the live graph and resets when the selection
moves to another epoch or the graph settles; it is aria-hidden because
status and progress are already announced.
Fixes #3878
Generated-by: GLM-5.3-Flash (ZCode)
* fix(desktop): address review on the Agent Graph liveness change
Per review by Astro-Han on #4514:
- The stopwatch now runs from the selected epoch's `createdAt` instead of
the mount time, so the header reports how long the graph has run, not
how long this view has been watching it. `AgentGraphLiveStatus` takes
`startedAt` and keeps only the 1 Hz ticker; `liveSinceRef`, `resetKey`,
and the reset branch are gone.
- `GRAPH_LIVE_STATUSES` moves to `agent-graph-panel-visibility.ts` as
`isAgentGraphLive`, shared by the heartbeat gate and `stopAvailable` so
"can stop" and "shows as running" cannot drift; covered in the
visibility suite (14 tests).
- The heartbeat spinner is `aria-hidden` (it duplicated the adjacent
visible status text on a `role="status"` element), the progress line is
an inline flex with `gap` so the spinner has real spacing, the dead
`flex: 0 0 auto` rule is removed, and the per-file reduced-motion block
is dropped in favour of the global rule in `base.css` which also covers
the e2e reduced-motion fixture path.
Verified: ui 320/320 (5 live-status tests updated for the `startedAt`
shape), desktop typecheck 0 errors, renderer architecture check green
against the merge base, surface inventory ok, format and ASF headers
clean.
Generated-by: GLM-5.3-Flash (ZCode)
* test(desktop): cover isAgentGraphLive and give the graph story a realistic epoch
Adds the visibility-suite coverage for the shared live-status gate and
gives the story fixture a realistic epoch createdAt (hardcoded 1 rendered
a five-digit hour count under the new epoch-origin stopwatch).
Generated-by: GLM-5.3-Flash (ZCode)
* fix(desktop): answer the liveness complaint with pulse and spinner only
Epoch 1 - the first graph a Session fans out - has no real start
timestamp: its createdAt is a sentinel 0 synthesized by the coordinator
and the sqlite store, and a remote Host clock cannot be correlated with
the Desktop wall clock. Treating that value as a stopwatch origin
rendered `496781:08:56` on the exact first-fan-out path the issue
describes, so the clock was wrong on the headline case.
Drop the clock and keep the two signals that work: the heartbeat
spinner (aria-hidden; the adjacent text already announces the status)
gated by isAgentGraphLive, and the progress line. This collapses
AgentGraphLiveStatus back into the panel, removes the component and its
tests from @maka/ui, and reverts the story fixture to the production
sentinel shape. The clock returns as its own change once the Host sends
a real start time or an elapsed value from its own clock domain.
Generated-by: GLM-5.3-Flash (ZCode)
* fix(desktop): stop the heartbeat when the snapshot read has failed, hide it under reduced motion
Per review on #4514: a failed snapshot read leaves the last known status
in place, so graphLive now also requires !error - a spinner asserting
liveness while liveness is unknown is the false signal the panel exists
to avoid. The heartbeat is hidden under both reduced-motion paths (the
base.css global freezes Astryx's ring mid-spin, which reads as broken);
the maka-agent-graph-heartbeat class stays as the rule's hook.
Generated-by: GLM-5.3-Flash (ZCode)
* fix(desktop): freeze the status-dot pulse when the snapshot read has failed
Per review on #4514: the spinner heartbeat is gated by !error but the
operator dot pulse kept running off the stale snapshot, so the two
liveness signals answered to two different authorities. The panel now
exposes data-error and the pulse animation freezes under it, same as
the spinner.
Generated-by: GLM-5.3-Flash (ZCode)
* fix(desktop): share the live gate with status dots
Per review on #4514: the header heartbeat and operator-dot pulse now use the same graphLive authority. The panel exposes data-live, and running/runnable dots only animate under that shared live gate.
Generated-by: GLM-5.3-Flash (ZCode)
---
.../__tests__/agent-graph-panel-visibility.test.ts | 13 ++++++++
.../src/renderer/agent-graph-panel-visibility.ts | 11 +++++++
apps/desktop/src/renderer/agent-graph-panel.tsx | 14 +++++++-
apps/desktop/src/renderer/styles/agent-graph.css | 37 ++++++++++++++++++++++
4 files changed, 74 insertions(+), 1 deletion(-)
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<AgentGraphPanelStatus>([
+ '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 b3c2b79f9a..d2eb5c7d74 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,
@@ -252,6 +253,8 @@ export function AgentGraphPanel(props: {
stopState.rootSessionId === props.rootSessionId && stopState.graphId === selectedGraphId;
const stopPending = stopFeedbackMatchesSelection && stopState.pending;
const stopError = stopFeedbackMatchesSelection && stopState.error;
+ // One liveness judgment gates both animated signals.
+ const graphLive = !error && snapshot !== undefined && isAgentGraphLive(snapshot.status);
useEffect(() => {
setSnapshot(undefined);
@@ -399,7 +402,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 &&
@@ -412,6 +415,7 @@ export function AgentGraphPanel(props: {
className="maka-agent-graph-panel"
aria-label={copy.title}
data-collapsed={collapsed ? 'true' : 'false'}
+ data-live={graphLive ? 'true' : 'false'}
>
<header className="maka-agent-graph-heading">
<div className="maka-agent-graph-heading-copy">
@@ -442,6 +446,14 @@ export function AgentGraphPanel(props: {
) : null}
{snapshot ? (
<span className="maka-agent-graph-progress">
+ {graphLive ? (
+ <Spinner
+ size="sm"
+ shade="subtle"
+ className="maka-agent-graph-heartbeat"
+ aria-hidden="true"
+ />
+ ) : null}
{copy.status(snapshot.status)} ·{' '}
{copy.progress(
progress.settled,
diff --git a/apps/desktop/src/renderer/styles/agent-graph.css b/apps/desktop/src/renderer/styles/agent-graph.css
index cd4e0640a9..e24734583e 100644
--- a/apps/desktop/src/renderer/styles/agent-graph.css
+++ b/apps/desktop/src/renderer/styles/agent-graph.css
@@ -102,6 +102,14 @@
color: var(--muted-foreground);
}
+/* Inline flex so the heartbeat spinner stays on the status line and spacing is
+ gap-driven; the element also holds plain text nodes. */
+.maka-agent-graph-progress {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-1);
+}
+
.maka-agent-graph-empty {
margin: var(--space-2) 0 0;
}
@@ -143,11 +151,27 @@
background: var(--muted-foreground);
}
+@keyframes maka-agent-graph-dot-pulse {
+ 0%,
+ 100% {
+ opacity: 1;
+ }
+
+ 50% {
+ opacity: 0.35;
+ }
+}
+
.maka-agent-graph-operators li[data-status="running"] .maka-agent-graph-status-dot,
.maka-agent-graph-operators li[data-status="runnable"] .maka-agent-graph-status-dot {
background: var(--status-running);
}
+.maka-agent-graph-panel[data-live="true"] .maka-agent-graph-operators li[data-status="running"] .maka-agent-graph-status-dot,
+.maka-agent-graph-panel[data-live="true"] .maka-agent-graph-operators li[data-status="runnable"] .maka-agent-graph-status-dot {
+ animation: maka-agent-graph-dot-pulse 1.6s ease-in-out infinite;
+}
+
.maka-agent-graph-operators li[data-status="completed"] .maka-agent-graph-status-dot {
background: var(--success);
}
@@ -223,3 +247,16 @@
display: none;
}
}
+
+/* Under reduced motion the base.css global freezes Astryx's ring mid-spin,
+ and a motionless ring reads as broken — hide it and let the pulse's static
+ dot carry the state. Both reduce paths get the same treatment. */
+@media (prefers-reduced-motion: reduce) {
+ .maka-agent-graph-heartbeat {
+ display: none;
+ }
+}
+
+[data-maka-reduced-motion="true"] .maka-agent-graph-heartbeat {
+ display: none;
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org