Skip to content
Merged
4 changes: 2 additions & 2 deletions extensions/shared/activity-status.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,14 @@ test("status text names its own view command", () => {
done: 2,
failed: 0,
}),
"subagents: 1 running · 2 done · /subagents to view",
"subagents: 1 running · 2 done · /subagents to view",
);
assert.equal(
formatActivityStatus(identityTheme, "workflows", {
running: 0,
done: 0,
failed: 3,
}),
"workflows: 3 failed · /workflows to view",
"workflows: 3 failed · /workflows to view",
);
});
11 changes: 6 additions & 5 deletions extensions/shared/activity-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ export interface ActivityCounts {
failed: number;
}

const SQUARE = "■";

/**
* Settled work is an unread notice, not a session tally: `done`/`failed` stay
* visible until the user's next explicit request acknowledges them, while
Expand Down Expand Up @@ -49,15 +47,18 @@ export function formatActivityStatus(
label: "subagents" | "workflows",
counts: ActivityCounts,
) {
// No status glyphs here: the footer line is a static string refreshed on
// events, so a spinner would freeze between updates — the colored words
// carry the state on their own.
const parts: string[] = [];
if (counts.running > 0) {
parts.push(theme.fg("warning", `${SQUARE} ${counts.running} running`));
parts.push(theme.fg("warning", `${counts.running} running`));
}
if (counts.done > 0) {
parts.push(theme.fg("success", `${SQUARE} ${counts.done} done`));
parts.push(theme.fg("success", `${counts.done} done`));
}
if (counts.failed > 0) {
parts.push(theme.fg("error", `${SQUARE} ${counts.failed} failed`));
parts.push(theme.fg("error", `${counts.failed} failed`));
}
parts.push(theme.fg("accent", `/${label}`) + theme.fg("dim", " to view"));

Expand Down
4 changes: 2 additions & 2 deletions extensions/subagents/docs/design-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ the parent conversation.

### 1.4 UI (carried over into v2 essentially as-is)

1. **Footer status** (`ctx.ui.setStatus("subagents", ...)`): `subagents: 2 running ·
1 done · 1 failed · /subagents to view` (warning/success/error colored squares;
1. **Footer status** (`ctx.ui.setStatus("subagents", ...)`): `subagents: 2 running ·
1 done · 1 failed · /subagents to view` (warning/success/error colored words;
cleared when no subagents). Driven by manager change listener.
2. **`subagent-result` message renderer**: status icon (`■`/`x`) + bold accent header
`subagent sa-N · title · finished/failed`; collapsed = first 8 body lines +
Expand Down
60 changes: 58 additions & 2 deletions extensions/workflows/dashboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import test from "node:test";
import type { KeybindingsManager } from "@earendil-works/pi-coding-agent";
import type { TUI } from "@earendil-works/pi-tui";
import type { Theme, WorkflowDetails } from "./model.ts";
import { SPINNER_INTERVAL_MS } from "../shared/spinner.ts";

// runsDir() resolves against getAgentDir(), which reads this env var.
const agentDir = mkdtempSync(join(tmpdir(), "my-pi-setup-workflows-"));
Expand Down Expand Up @@ -305,7 +306,14 @@ test("direct workflow navigation drills right and returns left through every lev
cost: 0,
turns: 1,
},
transcript: [{ role: "user", text: "Write the draft" }],
transcript: [
{ role: "user", text: "Write the draft" },
{
role: "tool",
name: "bash",
text: '{"command":"git status\u001b]52;c;clipboard\u0007"}',
},
],
},
],
};
Expand Down Expand Up @@ -348,7 +356,10 @@ test("direct workflow navigation drills right and returns left through every lev
assert.match(dashboard.render(120).at(-1) ?? "", /select agent/);

dashboard.handleInput("right");
assert.match(dashboard.render(120).join("\n"), /Transcript/);
const transcript = dashboard.render(120).join("\n");
assert.match(transcript, /Transcript/);
assert.match(transcript, /git status/);
assert.doesNotMatch(transcript, /clipboard|\u001b/);

dashboard.handleInput("left");
assert.match(dashboard.render(120).at(-1) ?? "", /select agent/);
Expand All @@ -363,6 +374,51 @@ test("direct workflow navigation drills right and returns left through every lev
}
});

test("live workflow dashboard repaints on the shared spinner cadence", (t) => {
t.mock.timers.enable({ apis: ["setInterval"] });
writeRun("wf_123abc", Date.now());
const details: WorkflowDetails = {
runId: "wf_123abc",
sessionId: SESSION,
name: "spinner",
status: "running",
background: false,
startedAt: Date.now(),
phases: [],
agents: [],
};
let renders = 0;
const dashboard = new WorkflowDashboard(
{
terminal: { rows: 30 },
requestRender() {
renders += 1;
},
} as unknown as TUI,
{
fg: (_color: string, text: string) => text,
bold: (text: string) => text,
} as unknown as Theme,
{
matches: () => false,
getKeys: () => ["esc"],
} as unknown as KeybindingsManager,
() => new Map([[details.runId, details]]),
SESSION,
new Set(),
0,
() => {},
);
try {
t.mock.timers.tick(SPINNER_INTERVAL_MS - 1);
assert.equal(renders, 0);
t.mock.timers.tick(1);
assert.equal(renders, 1);
} finally {
dashboard.dispose();
}
});

function saveReport(runId: string) {
writeRun(runId, Date.now() - 1_000);
const tui = {
Expand Down
Loading
Loading