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