Skip to content

Commit de374c2

Browse files
committed
fix(subagents): keep the pushed footer status still, not a frozen spinner
Review caught an animation that cannot animate. ui.setStatus stores a finished string and updateStatus only runs when the watched work changes, so a spinner frame there freezes on whatever event wrote it last — a hung UI is worse than an honest still marker. The strip owns the animated glyph because it owns a render loop. Also drops shared/ -> extensions/subagents/ internal import: shared code must not depend on one extension's internals. Refs #41
1 parent 183c86e commit de374c2

3 files changed

Lines changed: 27 additions & 23 deletions

File tree

extensions/shared/activity-status.test.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import assert from "node:assert/strict";
22
import test from "node:test";
3-
import { SPINNER_INTERVAL_MS } from "../subagents/src/ui/transcript.ts";
43
import {
54
formatActivityStatus,
65
hasActivity,
@@ -56,13 +55,12 @@ test("a settle without a timestamp is treated as unread", () => {
5655

5756
test("status text names its own view command", () => {
5857
assert.equal(
59-
formatActivityStatus(
60-
identityTheme,
61-
"subagents",
62-
{ running: 1, done: 2, failed: 0 },
63-
0,
64-
),
65-
"subagents: ⠋ 1 running · ✓ 2 done · /subagents to view",
58+
formatActivityStatus(identityTheme, "subagents", {
59+
running: 1,
60+
done: 2,
61+
failed: 0,
62+
}),
63+
"subagents: ● 1 running · ✓ 2 done · /subagents to view",
6664
);
6765
assert.equal(
6866
formatActivityStatus(identityTheme, "workflows", {
@@ -74,24 +72,24 @@ test("status text names its own view command", () => {
7472
);
7573
});
7674

77-
test("running work shares the spinner frame; strip visibility owns the hint", () => {
75+
test("the pushed footer status uses a still marker; strip visibility owns the hint", () => {
7876
const counts = { running: 1, done: 0, failed: 0 };
77+
// setStatus stores a finished string, so this line cannot animate: a spinner
78+
// frame would freeze on whatever event happened to write it last.
7979
const visible = formatActivityStatus(
8080
identityTheme,
8181
"subagents",
8282
counts,
83-
SPINNER_INTERVAL_MS, // frame 1
8483
true,
8584
);
86-
assert.equal(visible, "subagents: 1 running");
85+
assert.equal(visible, "subagents: 1 running");
8786
assert.doesNotMatch(visible, /to view/);
8887

8988
const hidden = formatActivityStatus(
9089
identityTheme,
9190
"subagents",
9291
counts,
93-
SPINNER_INTERVAL_MS,
9492
false,
9593
);
96-
assert.equal(hidden, "subagents: 1 running · /subagents to view");
94+
assert.equal(hidden, "subagents: 1 running · /subagents to view");
9795
});

extensions/shared/activity-status.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2-
import { spinnerFrame } from "../subagents/src/ui/transcript.ts";
2+
3+
/**
4+
* Still marker for the push-based footer status. It deliberately does not
5+
* import the subagent spinner: `shared/` must not depend on a single
6+
* extension's internals, and a pushed string cannot animate anyway.
7+
*/
8+
const RUNNING_MARK = "●";
39

410
type Theme = ExtensionContext["ui"]["theme"];
511

@@ -43,17 +49,23 @@ export function hasActivity(counts: ActivityCounts) {
4349
return counts.running + counts.done + counts.failed > 0;
4450
}
4551

52+
/**
53+
* `ui.setStatus` stores a finished string and is only called when the watched
54+
* work changes, so this line is NOT re-evaluated per frame. A spinner here
55+
* would freeze on whatever frame the last event happened to land on, which
56+
* reads as a hung UI. The animated glyph belongs to the strip, which owns a
57+
* render loop; the footer states the count with a still marker.
58+
*/
4659
export function formatActivityStatus(
4760
theme: Theme,
4861
label: "subagents" | "workflows",
4962
counts: ActivityCounts,
50-
now: number = Date.now(),
5163
stripVisible = false,
5264
) {
5365
const parts: string[] = [];
5466
if (counts.running > 0) {
5567
parts.push(
56-
theme.fg("warning", `${spinnerFrame(now)} ${counts.running} running`),
68+
theme.fg("warning", `${RUNNING_MARK} ${counts.running} running`),
5769
);
5870
}
5971
if (counts.done > 0) {

extensions/subagents/index.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -393,13 +393,7 @@ export default function (pi: ExtensionAPI) {
393393
ui.setStatus(
394394
"subagents",
395395
hasActivity(counts)
396-
? formatActivityStatus(
397-
ui.theme,
398-
"subagents",
399-
counts,
400-
Date.now(),
401-
widgetVisible,
402-
)
396+
? formatActivityStatus(ui.theme, "subagents", counts, widgetVisible)
403397
: undefined,
404398
);
405399
};

0 commit comments

Comments
 (0)