* 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>
133 lines
4.6 KiB
TypeScript
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");
|
|
});
|
|
});
|