Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -648,7 +648,7 @@ function agentTooltip(t: Translate, agent: Subagent): string | undefined {
/// What each running item is called, newest first: a subagent by its task, a command by its
/// description or its short command (processes.ts `commandLabel`), a chat by its title. An item with
/// no start time sorts after those with one, in its list order.
/// Only the newest two are named (the header never shows more), so a long list costs a sort of its
/// At most the newest two are named (the default arm names one), so a long list costs a sort of its
/// start times, never a label per item.
function newestNames(t: Translate, items: readonly TrayItem[]): readonly string[] {
return items
Expand Down
21 changes: 18 additions & 3 deletions webviews/src/agent-session/acpmux/subagents/runningTray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ export type TrayItem =

/// What the tray lists and its header counts: what runs or waits on the person, never what ended
/// (Leo 2026-10-21 on "2 subagents running · 2 commands · 32 sessions": "THIS IS LYING"). The
/// running subagents, each nested one right under the one that spawned it, then the background
/// commands still running, then the started chats whose turn runs or that wait on the person.
/// running subagents (each nested one right under the one that spawned it), the background commands
/// still running and the started chats whose turn runs or that wait on the person, newest first.
/// What ended is shown as bounded status cards after the live rows (``finishedTrayItems``).
///
/// Only the chat's own work: its subagents, its commands and the sub-sessions its agent started with
Expand Down Expand Up @@ -87,7 +87,22 @@ export function trayItems(
items.push({ kind: "command", id: command.id, command });
for (const session of sessions)
if (session.subsession && liveSession(session)) items.push({ kind: "session", id: session.sessionId, session });
return items;
return newestFirst(items);
}

/// The open tray's lines newest first, as its folded header names them, so the top line is what just
/// started (Leo, 2026-11-03: "this previews one of the latest but it doesn't show in latest it shows
/// in oldest order"). A nested subagent keeps its place under its parent: a parent and its subtree
/// move as one, by the parent's start. Those with no start time go last, in their list order.
function newestFirst(items: readonly TrayItem[]): TrayItem[] {
const groups: { start: number; index: number; items: TrayItem[] }[] = [];
for (const item of items) {
const last = groups.at(-1);
if (last && item.kind === "agent" && item.depth > 0) last.items.push(item);
else groups.push({ start: itemStart(item) ?? Number.NEGATIVE_INFINITY, index: groups.length, items: [item] });
}
groups.sort((a, b) => b.start - a.start || a.index - b.index);
return groups.flatMap((group) => group.items);
}

/// The retry acpmux waits to run for `agent`, if any: a sub-session's is on its own session row
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,7 @@ export const trayFoldHeadLook = defineExperiment({
description: "Only counts (1 subagent running · 1 command), as before, for comparison.",
},
},
defaultArm: "two",
// One newest name that fits beats two cut ones (Leo, 2026-11-03, on a bar that read "Context
// around the ask and memory search · glaeda-gh wait pid 88061 --timeout 800; tail -6 /t… +6 more").
defaultArm: "one",
});
9 changes: 5 additions & 4 deletions webviews/test/agent-pane-subagents-fixture.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ test("each step puts in the tray what the capture records, beside the message fr
const steps = Array.from({ length: SUBAGENT_TRAY_STEPS }, (_, step) => tray(step));
expect(steps.map(({ running }) => running)).toEqual([
[],
["fixture-reviewer", "fixture-auditor"],
["fixture-reviewer", "fixture-auditor"],
["fixture-auditor", "fixture-reviewer"],
["fixture-auditor", "fixture-reviewer"],
["fixture-auditor"],
[],
[],
Expand Down Expand Up @@ -60,9 +60,10 @@ test("the steps keep their rows' ids and times; the group's version moves with i
// `agents` lets a capture open 3 or more subagents' panes (design/pane-placement.md): past the
// reviewer and the auditor, helpers that run through every step. Unset, the fixture is unchanged.
test("agents starts that many running subagents; unset it starts the reviewer and the auditor", () => {
expect(tray(1, 4).running).toEqual(["fixture-reviewer", "fixture-auditor", "fixture-helper-1", "fixture-helper-2"]);
// The tray lists them newest first.
expect(tray(1, 4).running).toEqual(["fixture-helper-2", "fixture-helper-1", "fixture-auditor", "fixture-reviewer"]);
expect(tray(1).running).toEqual(tray(1, 2).running);
expect(tray(4, 4).running).toEqual(["fixture-helper-1", "fixture-helper-2"]);
expect(tray(4, 4).running).toEqual(["fixture-helper-2", "fixture-helper-1"]);
expect(tray(1, 1).running).toHaveLength(2);
expect(tray(1, 99).running).toHaveLength(SUBAGENT_FIXTURE_MAX);
expect(tray(5, 4).running).toEqual([]);
Expand Down
36 changes: 27 additions & 9 deletions webviews/test/subagent-tray-names.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
// composer is currently no longer usable bc it doesn't display much", on a bar that read only
// "1 subagent running · 1 command 4s"). Folded, it reads as Claude Code's running-tasks list does:
// the subagent's task and the command's description (its short command when it has none), the
// newest first; with more running it names the newest two and counts the rest. Open, the lines name
// them and the header keeps its counts.
// newest one, then a count of the rest: one name that fits beats two cut ones (Leo, 2026-11-03, on a
// bar whose two names were both cut). Open, the lines list them newest first, as the folded bar
// names them ("this previews one of the latest but it doesn't show in latest it shows in oldest
// order"), and the header keeps its counts.
import { afterAll, describe, expect, test } from "bun:test";
import { JSDOM, VirtualConsole } from "jsdom";

Expand Down Expand Up @@ -98,15 +100,15 @@ async function head(agents: Subagent[], commands: BackgroundCommand[]) {
}

describe("the folded running bar names what runs", () => {
test("one subagent and one command: the subagent's task and the command's description, newest first", async () => {
test("one subagent and one command: the newest one's name, then a count of the other", async () => {
const bar = await head(
[agent("a1", "Explore how the daemon updates itself", 4_000)],
[command("c1", "cd ~/app && bun test --watch", 2_000, "Watch the tests")],
);
try {
expect(bar.button.getAttribute("aria-expanded")).toBe("false");
expect(bar.title()).toBe("Watch the tests · Explore how the daemon updates itself");
expect(bar.more()).toBe("");
expect(bar.title()).toBe("Watch the tests");
expect(bar.more()).toBe("+1 more");
// Open, the lines name them and the header goes back to its counts.
await act(async () => bar.button.click());
expect(bar.title()).toBe("1 subagent running · 1 command");
Expand All @@ -121,13 +123,13 @@ describe("the folded running bar names what runs", () => {
[command("c1", "cd ~/Projects/app && bun run dev", 1_000)],
);
try {
expect(bar.title()).toBe("bun run dev · Review the tray");
expect(bar.title()).toBe("bun run dev");
} finally {
await bar.done();
}
});

test("five running: the newest two by name, then a count of the rest", async () => {
test("five running: the newest one by name, then a count of the rest", async () => {
const bar = await head(
[
agent("a1", "Review the tray", 50_000),
Expand All @@ -140,10 +142,26 @@ describe("the folded running bar names what runs", () => {
],
);
try {
expect(bar.title()).toBe("Lint the web · Audit the stack");
expect(bar.more()).toBe("+3 more");
expect(bar.title()).toBe("Lint the web");
expect(bar.more()).toBe("+4 more");
} finally {
await bar.done();
}
});
});

describe("the open running tray lists what runs newest first", () => {
test("subagents, commands and their order: the newest on top, a nested subagent under its parent", () => {
const items = trayItems(
[
agent("old", "Review the tray", 50_000),
{ ...agent("child", "Read the spec", 1_000), parent: "old" },
agent("mid", "Audit the stack", 20_000),
],
[command("c-old", "bun run dev", 40_000), command("c-new", "bun run lint", 500)],
[],
true,
);
expect(items.map((item) => item.id)).toEqual(["c-new", "mid", "c-old", "old", "child"]);
});
});
14 changes: 7 additions & 7 deletions webviews/test/subagent-tray.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ describe("a stopped subagent's line says why, and offers Restart", () => {
});

describe("the tray", () => {
test("lists running subagents, nested ones under their parent, then running commands", () => {
test("lists running subagents (nested ones under their parent) and running commands, newest first", () => {
const agents = [
agent("Tester", { startedAt: at(12) }),
agent("Reviewer", { startedAt: at(10) }),
Expand All @@ -295,9 +295,9 @@ describe("the tray", () => {
expect(
trayItems(agents, commands, chats, true).map((item) => [item.id, item.kind === "agent" ? item.depth : -1]),
).toEqual([
["Tester", 0],
["Reviewer", 0],
["Scout", 1],
["Tester", 0],
["bg1", -1],
["c1", -1],
]);
Expand Down Expand Up @@ -980,7 +980,7 @@ describe("the tray tells the truth", () => {
];
const items = trayItems(agents, commands, chats, true);
const finished = finishedTrayItems(agents, chats);
expect(items.map((item) => item.id)).toEqual(["Reviewer", "Tester", "bg1", "c1", "c2"]);
expect(items.map((item) => item.id)).toEqual(["Tester", "Reviewer", "bg1", "c1", "c2"]);
// What ended last comes first: the few the tray shows are the latest.
// (c3 and c4 have no creation time: they keep their order, after any chat that has one.)
expect(finished.map((item) => item.id)).toEqual(["Fuzzer", "Linter", "c3", "c4"]);
Expand All @@ -998,8 +998,8 @@ describe("the tray tells the truth", () => {
const lines = liveLines(root);
expect(lines.length).toBe(5);
expect(names(lines)).toEqual([
"Reviewer",
"Tester",
"Reviewer",
"Start the dev server",
"Port the history page",
"Draft the notes",
Expand Down Expand Up @@ -1229,9 +1229,9 @@ describe('the tray\'s header stays (Leo 2026-10-25: "sometimes it disappears")',
expect(tray.firstElementChild).toBe(head);
expect(head.isConnected).toBe(true);
expect(head.getAttribute("aria-expanded")).toBe("true");
// Running first, then what ended last.
// Running first, newest on top, then what ended last.
const names = [...list.querySelectorAll(".acpmux-summary-agent__name")].map((node) => node.textContent);
expect(names.slice(0, 3)).toEqual(["Run 0", "Run 1", "Done 115"]);
expect(names.slice(0, 3)).toEqual(["Run 1", "Run 0", "Done 115"]);
expect(names.at(-1)).toBe("Done 0");
} finally {
await done();
Expand Down Expand Up @@ -1399,7 +1399,7 @@ describe('the tray\'s header stays (Leo 2026-10-25: "sometimes it disappears")',
actionShell: true,
}),
// A progress summary is words, lowercase or not: never "Running find the bug".
agent("Planner", { startedAt: Date.now() + 1, action: "find the bug in the fold" }),
agent("Planner", { startedAt: Date.now() - 1, action: "find the bug in the fold" }),
],
[],
chats,
Expand Down
Loading