diff --git a/webviews/src/agent-session/acpmux/subagents/SubagentTray.tsx b/webviews/src/agent-session/acpmux/subagents/SubagentTray.tsx index 1acadb491239..0cb9424839be 100644 --- a/webviews/src/agent-session/acpmux/subagents/SubagentTray.tsx +++ b/webviews/src/agent-session/acpmux/subagents/SubagentTray.tsx @@ -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 diff --git a/webviews/src/agent-session/acpmux/subagents/runningTray.ts b/webviews/src/agent-session/acpmux/subagents/runningTray.ts index 6eb4acf7bb5c..ca73caed26cc 100644 --- a/webviews/src/agent-session/acpmux/subagents/runningTray.ts +++ b/webviews/src/agent-session/acpmux/subagents/runningTray.ts @@ -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 @@ -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 diff --git a/webviews/src/agent-session/acpmux/subagents/trayFoldHead.experiment.ts b/webviews/src/agent-session/acpmux/subagents/trayFoldHead.experiment.ts index cf68281978f5..aafd250148b1 100644 --- a/webviews/src/agent-session/acpmux/subagents/trayFoldHead.experiment.ts +++ b/webviews/src/agent-session/acpmux/subagents/trayFoldHead.experiment.ts @@ -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", }); diff --git a/webviews/test/agent-pane-subagents-fixture.test.ts b/webviews/test/agent-pane-subagents-fixture.test.ts index e542ed21f5a3..1d6bebe73670 100644 --- a/webviews/test/agent-pane-subagents-fixture.test.ts +++ b/webviews/test/agent-pane-subagents-fixture.test.ts @@ -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"], [], [], @@ -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([]); diff --git a/webviews/test/subagent-tray-names.test.tsx b/webviews/test/subagent-tray-names.test.tsx index 49052bf01cde..aa43a4e5acb0 100644 --- a/webviews/test/subagent-tray-names.test.tsx +++ b/webviews/test/subagent-tray-names.test.tsx @@ -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"; @@ -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"); @@ -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), @@ -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"]); + }); +}); diff --git a/webviews/test/subagent-tray.test.tsx b/webviews/test/subagent-tray.test.tsx index 5faf99a0504a..1f4ed8b15f2b 100644 --- a/webviews/test/subagent-tray.test.tsx +++ b/webviews/test/subagent-tray.test.tsx @@ -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) }), @@ -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], ]); @@ -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"]); @@ -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", @@ -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(); @@ -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,