Files
paseo/packages/app/src/subagents/track-presentation.test.ts
T
66445adc07 Show provider subagents in the subagents track (#2013)
* Add provider-owned subagent stream contract

* OpenCode provider: detect child sessions, attach adopted children to the hosting server, synthesize external turns

- Emit internal provider_child_session_detected / provider_session_deleted stream events from the global event stream (before the no-active-turn gate), including BFS hydration of existing descendants on subscribe
- Track child sessions in a registry mapping child session id -> hosting server url; resumeSession attaches via acquireExisting() so adopted children share the parent's server generation and keep receiving live events
- Synthesize external turns for adopted (externally driven) sessions so timeline, tool calls, and permission requests stream without a Paseo-initiated prompt
- Honor launchContext.env in resumeSession (acquireDedicated parity with createSession)
- Stop folding child tool parts into the parent's sub_agent action log; the parent row stays as a pointer

* Render provider subagents in workspace timelines

* fix provider subagent timeline reliability

* fix provider subagent lifecycle edge cases

* fix provider subagent event fidelity

* Fix provider subagent history fidelity

* Fix restored provider subagent state

* Fix provider subagent visibility edges

* Restore provider subagent timelines

* Page provider subagent history

* Forward subagent questions reliably

* fix(subagents): preserve OpenCode child context

* fix(subagents): harden provider timeline routing

* fix(subagents): preserve provider page continuity

* fix(subagents): retain live child prompts

---------

Co-authored-by: Omer <639682+omercnet@users.noreply.github.com>
2026-07-13 03:13:00 +08:00

133 lines
4.6 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { PaseoSubagentRow, SubagentRow } from "./select";
import {
buildSubagentRowPresentationData,
formatHeaderLabel,
resolveRowLabel,
} from "./track-presentation";
function row(
overrides: Partial<PaseoSubagentRow> & Pick<PaseoSubagentRow, "id">,
): PaseoSubagentRow {
return {
kind: "paseo",
id: overrides.id,
provider: overrides.provider ?? "codex",
title: overrides.title ?? `Agent ${overrides.id}`,
status: overrides.status ?? "idle",
requiresAttention: overrides.requiresAttention ?? false,
createdAt: overrides.createdAt ?? new Date("2026-04-20T00:00:00.000Z"),
};
}
describe("formatHeaderLabel", () => {
it("uses singular 'subagent' for a single row", () => {
expect(formatHeaderLabel([row({ id: "a" })])).toBe("1 subagent");
});
it("uses plural 'subagents' for two rows with no running rows", () => {
expect(formatHeaderLabel([row({ id: "a" }), row({ id: "b" })])).toBe("2 subagents");
});
it("appends the running count when at least one row is running", () => {
expect(
formatHeaderLabel([row({ id: "a", status: "running" }), row({ id: "b" }), row({ id: "c" })]),
).toBe("3 subagents · 1 running");
});
it("counts every running row in the suffix", () => {
expect(
formatHeaderLabel([
row({ id: "a", status: "running" }),
row({ id: "b", status: "running" }),
row({ id: "c", requiresAttention: true }),
row({ id: "d" }),
row({ id: "e" }),
]),
).toBe("5 subagents · 2 running");
});
it("ignores requiresAttention on non-running rows in the header copy", () => {
expect(
formatHeaderLabel([
row({ id: "a", status: "error", requiresAttention: false }),
row({ id: "b", status: "idle", requiresAttention: false }),
row({ id: "c", status: "idle", requiresAttention: true }),
]),
).toBe("3 subagents");
});
it("still counts running rows even when they require attention", () => {
expect(
formatHeaderLabel([
row({ id: "a", status: "error", requiresAttention: true }),
row({ id: "b", status: "running", requiresAttention: true }),
row({ id: "c", status: "idle", requiresAttention: true }),
]),
).toBe("3 subagents · 1 running");
});
it("uses singular 'subagent' for a single row that requires attention upstream", () => {
expect(formatHeaderLabel([row({ id: "a", requiresAttention: true })])).toBe("1 subagent");
});
});
describe("resolveRowLabel", () => {
it("returns null when title is not a string", () => {
expect(resolveRowLabel(null as unknown as SubagentRow["title"])).toBe(null);
});
it("returns null for whitespace-only titles", () => {
expect(resolveRowLabel(" ")).toBe(null);
});
it("returns null for the placeholder 'new agent' regardless of case", () => {
expect(resolveRowLabel("new agent")).toBe(null);
expect(resolveRowLabel("New Agent")).toBe(null);
expect(resolveRowLabel(" NEW AGENT ")).toBe(null);
});
it("returns the trimmed title for real names", () => {
expect(resolveRowLabel(" Build the thing ")).toBe("Build the thing");
});
});
describe("buildSubagentRowPresentationData", () => {
it("namespaces the key with a subagent prefix", () => {
expect(buildSubagentRowPresentationData(row({ id: "child-a" })).key).toBe(
"paseo_subagent_child-a",
);
});
it("marks the row ready when the title resolves to a real label", () => {
const presentation = buildSubagentRowPresentationData(row({ id: "a", title: "Build it" }));
expect(presentation.titleState).toBe("ready");
expect(presentation.label).toBe("Build it");
});
it("marks the row loading and blanks the label for the placeholder title", () => {
const presentation = buildSubagentRowPresentationData(row({ id: "a", title: "new agent" }));
expect(presentation.titleState).toBe("loading");
expect(presentation.label).toBe("");
});
it("maps a running row to the running status bucket so callers render the synced loader", () => {
expect(buildSubagentRowPresentationData(row({ id: "a", status: "running" })).statusBucket).toBe(
"running",
);
});
it("maps an idle row to the done status bucket so callers render the static provider icon", () => {
expect(buildSubagentRowPresentationData(row({ id: "a", status: "idle" })).statusBucket).toBe(
"done",
);
});
it("ignores requiresAttention on the source row when computing the bucket", () => {
expect(
buildSubagentRowPresentationData(row({ id: "a", status: "idle", requiresAttention: true }))
.statusBucket,
).toBe("done");
});
});