Files
paseo/packages/app/src/subagents/select.test.ts
T
Mohamed Boudra ddae0a4c36 feat(app): the client and app work on options and controls
The agent snapshot's `options` are the state and `controls` only draw them.
Running and closed agents draw from the snapshot; drafts and profile forms ask
`provider.controls.list.request` with their `providerOptions`, so aliases such
as a legacy Fast profile's `service_tier: "fast"` are resolved once, by the
daemon, and the draft keeps Fast highlighted. A draft adds only the user's
edits and each provider's last-used control values; applying a profile seeds
its non-control options (e.g. `sandbox_mode`) into the draft, and a profile
form saves them back instead of pruning to the controls it can draw.

A daemon without `server_info.features.providerOptionControls` still sends
`features` and `featureValues`. `LegacyProviderOptions` in the client package
is the one place that translates snapshots, draft listings, create and apply
requests, and daemon-config profiles, so the app never branches on it.
Persisted preferences and tab layouts rename `featureValues` through tagged
COMPAT preprocessors.

The features/controls bridge moves from the server into the protocol package
so the client can reuse it.
2026-11-01 22:56:11 +01:00

643 lines
19 KiB
TypeScript

import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
import { afterEach, describe, expect, it } from "vitest";
import {
createSubagentsForParentSelector,
selectProviderSubagentsForParent,
selectSubagentsForParent,
} from "./select";
import { createWorkspaceAgentVisibilitySelector } from "@/workspace-tabs/agent-visibility";
import { useProviderSubagentStore } from "./provider-store";
import { useSessionStore, type Agent } from "@/stores/session-store";
const SERVER_ID = "server-1";
const AGENT_TIMESTAMP = new Date("2026-03-08T10:00:00.000Z");
const EMPTY_PENDING_ARCHIVE_IDS = new Set<string>();
const AGENT_DEFAULTS: Agent = {
serverId: SERVER_ID,
id: "agent",
provider: "codex",
status: "idle",
turn: { phase: "idle", cancellationRequestId: null },
createdAt: AGENT_TIMESTAMP,
updatedAt: AGENT_TIMESTAMP,
lastUserMessageAt: null,
lastActivityAt: AGENT_TIMESTAMP,
capabilities: {
supportsStreaming: true,
supportsSessionPersistence: true,
supportsDynamicModes: true,
supportsMcpServers: true,
supportsReasoningStream: true,
supportsToolInvocations: true,
},
currentModeId: null,
availableModes: [],
pendingPermissions: [],
persistence: null,
runtimeInfo: undefined,
lastUsage: undefined,
lastError: null,
title: "Agent",
cwd: "/tmp/project",
model: null,
thinkingOptionId: undefined,
requiresAttention: false,
attentionReason: null,
attentionTimestamp: null,
archivedAt: null,
parentAgentId: null,
labels: {},
projectPlacement: null,
};
function makeAgent(input: Partial<Agent> & Pick<Agent, "id">): Agent {
return {
...AGENT_DEFAULTS,
...input,
labels: {
...(input.parentAgentId ? { "paseo.parent-agent-id": input.parentAgentId } : {}),
...input.labels,
},
};
}
function setAgents(agents: Agent[]): void {
useSessionStore.getState().initializeSession(SERVER_ID, null as unknown as DaemonClient);
useSessionStore
.getState()
.setAgents(SERVER_ID, new Map(agents.map((agent) => [agent.id, agent])));
}
afterEach(() => {
useSessionStore.getState().clearSession(SERVER_ID);
useProviderSubagentStore.setState({
descriptors: new Map(),
timelines: new Map(),
hiddenFromTrack: new Set(),
});
});
describe("selectSubagentsForParent", () => {
it("keeps equal child IDs on different hosts qualified to the complete parent address", () => {
const parentId = "shared-parent";
const childId = "shared-child";
const local = makeAgent({
id: childId,
parentAgentId: parentId,
labels: { "paseo.parent-agent-id": parentId, "paseo.parent-server-id": SERVER_ID },
});
const remote = makeAgent({
id: childId,
serverId: "server-2",
parentAgentId: null,
labels: { "paseo.parent-agent-id": parentId, "paseo.parent-server-id": SERVER_ID },
});
setAgents([local]);
useSessionStore.getState().initializeSession("server-2", null as unknown as DaemonClient);
useSessionStore.getState().setAgents("server-2", new Map([[childId, remote]]));
try {
const rows = selectSubagentsForParent(
useSessionStore.getState(),
{ serverId: SERVER_ID, parentAgentId: parentId },
EMPTY_PENDING_ARCHIVE_IDS,
);
expect(rows).toHaveLength(2);
expect(rows).toEqual(
expect.arrayContaining([
expect.objectContaining({ address: { serverId: SERVER_ID, agentId: childId } }),
expect.objectContaining({ address: { serverId: "server-2", agentId: childId } }),
]),
);
} finally {
useSessionStore.getState().clearSession("server-2");
}
});
it("hides cached provider children when the host does not support them", () => {
useProviderSubagentStore.getState().applyUpdate(SERVER_ID, {
kind: "upsert",
subagent: {
id: "provider-child",
parentAgentId: "parent-a",
provider: "codex",
title: "Provider child",
description: null,
subtitle: "Codex worker · 4.2k tokens",
status: "completed",
createdAt: "2026-03-08T10:01:00.000Z",
updatedAt: "2026-03-08T10:02:00.000Z",
toolCallId: "call-1",
},
});
const params = { serverId: SERVER_ID, parentAgentId: "parent-a" };
expect(
selectProviderSubagentsForParent(useProviderSubagentStore.getState(), params, false),
).toEqual([]);
expect(
selectProviderSubagentsForParent(useProviderSubagentStore.getState(), params, true).map(
(row) => row.id,
),
).toEqual(["provider-child"]);
expect(
selectProviderSubagentsForParent(useProviderSubagentStore.getState(), params, true)[0]
?.subtitle,
).toBe("Codex worker · 4.2k tokens");
});
it("hides locally dismissed provider children while retaining their descriptor", () => {
const store = useProviderSubagentStore.getState();
store.applyUpdate(SERVER_ID, {
kind: "upsert",
subagent: {
id: "provider-child",
parentAgentId: "parent-a",
provider: "codex",
title: "Provider child",
description: null,
status: "completed",
createdAt: "2026-03-08T10:01:00.000Z",
updatedAt: "2026-03-08T10:02:00.000Z",
toolCallId: "call-1",
},
});
store.hideFromTrack(SERVER_ID, "parent-a", ["provider-child"]);
expect(
selectProviderSubagentsForParent(
useProviderSubagentStore.getState(),
{ serverId: SERVER_ID, parentAgentId: "parent-a" },
true,
),
).toEqual([]);
expect(useProviderSubagentStore.getState().descriptors.size).toBe(1);
});
it("places nested provider children only beneath their direct provider parent", () => {
const store = useProviderSubagentStore.getState();
const base = {
parentAgentId: "parent-a",
provider: "claude" as const,
title: "general-purpose",
subtitle: null,
status: "running" as const,
createdAt: "2026-09-04T10:00:00.000Z",
updatedAt: "2026-09-04T10:00:00.000Z",
toolCallId: null,
};
store.applyUpdate(SERVER_ID, {
kind: "upsert",
subagent: { ...base, id: "direct", description: "Direct", parentSubagentId: null },
});
store.applyUpdate(SERVER_ID, {
kind: "upsert",
subagent: {
...base,
id: "nested",
description: "Nested",
parentSubagentId: "direct",
},
});
expect(
selectProviderSubagentsForParent(
useProviderSubagentStore.getState(),
{ serverId: SERVER_ID, parentAgentId: "parent-a" },
true,
true,
).map((row) => row.id),
).toEqual(["direct"]);
expect(
selectProviderSubagentsForParent(
useProviderSubagentStore.getState(),
{
serverId: SERVER_ID,
parentAgentId: "parent-a",
providerParentSubagentId: "direct",
},
true,
true,
).map((row) => row.id),
).toEqual(["nested"]);
});
it("returns only non-archived children for the requested parent", () => {
setAgents([
makeAgent({ id: "parent-a" }),
makeAgent({ id: "child-a", parentAgentId: "parent-a" }),
makeAgent({
id: "archived-child",
parentAgentId: "parent-a",
archivedAt: new Date("2026-03-08T12:00:00.000Z"),
}),
]);
const rows = selectSubagentsForParent(
useSessionStore.getState(),
{
serverId: SERVER_ID,
parentAgentId: "parent-a",
},
EMPTY_PENDING_ARCHIVE_IDS,
);
expect(rows.map((row) => row.id)).toEqual(["child-a"]);
});
it("excludes siblings, unrelated agents, and grandchildren", () => {
setAgents([
makeAgent({ id: "parent-a" }),
makeAgent({ id: "parent-b" }),
makeAgent({ id: "child-a", parentAgentId: "parent-a" }),
makeAgent({ id: "sibling-b", parentAgentId: "parent-b" }),
makeAgent({ id: "grandchild-a", parentAgentId: "child-a" }),
makeAgent({ id: "unrelated" }),
]);
const rows = selectSubagentsForParent(
useSessionStore.getState(),
{
serverId: SERVER_ID,
parentAgentId: "parent-a",
},
EMPTY_PENDING_ARCHIVE_IDS,
);
expect(rows.map((row) => row.id)).toEqual(["child-a"]);
});
it("shows only direct children for each parent", () => {
setAgents([
makeAgent({ id: "parent" }),
makeAgent({ id: "child", parentAgentId: "parent" }),
makeAgent({ id: "grandchild", parentAgentId: "child" }),
]);
const parentRows = selectSubagentsForParent(
useSessionStore.getState(),
{
serverId: SERVER_ID,
parentAgentId: "parent",
},
EMPTY_PENDING_ARCHIVE_IDS,
);
const childRows = selectSubagentsForParent(
useSessionStore.getState(),
{
serverId: SERVER_ID,
parentAgentId: "child",
},
EMPTY_PENDING_ARCHIVE_IDS,
);
expect(parentRows.map((row) => row.id)).toEqual(["child"]);
expect(childRows.map((row) => row.id)).toEqual(["grandchild"]);
});
it("sorts by createdAt ascending", () => {
setAgents([
makeAgent({ id: "parent" }),
makeAgent({
id: "third",
parentAgentId: "parent",
createdAt: new Date("2026-03-08T10:03:00.000Z"),
}),
makeAgent({
id: "first",
parentAgentId: "parent",
createdAt: new Date("2026-03-08T10:01:00.000Z"),
}),
makeAgent({
id: "second",
parentAgentId: "parent",
createdAt: new Date("2026-03-08T10:02:00.000Z"),
}),
]);
const rows = selectSubagentsForParent(
useSessionStore.getState(),
{
serverId: SERVER_ID,
parentAgentId: "parent",
},
EMPTY_PENDING_ARCHIVE_IDS,
);
expect(rows.map((row) => row.id)).toEqual(["first", "second", "third"]);
});
it("maps only row-rendered fields and does not expose onOpen", () => {
const createdAt = new Date("2026-03-08T10:01:00.000Z");
setAgents([
makeAgent({ id: "parent" }),
makeAgent({
id: "child",
parentAgentId: "parent",
provider: "claude",
title: "Review child",
status: "running",
requiresAttention: true,
createdAt,
model: "should-not-leak",
cwd: "/private/project",
}),
]);
const rows = selectSubagentsForParent(
useSessionStore.getState(),
{
serverId: SERVER_ID,
parentAgentId: "parent",
},
EMPTY_PENDING_ARCHIVE_IDS,
);
expect(rows).toEqual([
{
kind: "paseo",
address: { serverId: SERVER_ID, agentId: "child" },
id: "child",
provider: "claude",
title: "Review child",
description: null,
subtitle: null,
status: "running",
turn: { phase: "idle", cancellationRequestId: null },
requiresAttention: true,
createdAt,
},
]);
expect(Object.keys(rows[0] ?? {}).sort()).toEqual([
"address",
"createdAt",
"description",
"id",
"kind",
"provider",
"requiresAttention",
"status",
"subtitle",
"title",
"turn",
]);
expect(rows[0]).not.toHaveProperty("onOpen");
expect(rows[0]).not.toHaveProperty("model");
expect(rows[0]).not.toHaveProperty("cwd");
});
it("moves a child when parentAgentId changes", () => {
const child = makeAgent({ id: "child", parentAgentId: "parent-a" });
setAgents([makeAgent({ id: "parent-a" }), makeAgent({ id: "parent-b" }), child]);
expect(
selectSubagentsForParent(
useSessionStore.getState(),
{
serverId: SERVER_ID,
parentAgentId: "parent-a",
},
EMPTY_PENDING_ARCHIVE_IDS,
).map((row) => row.id),
).toEqual(["child"]);
expect(
selectSubagentsForParent(
useSessionStore.getState(),
{
serverId: SERVER_ID,
parentAgentId: "parent-b",
},
EMPTY_PENDING_ARCHIVE_IDS,
).map((row) => row.id),
).toEqual([]);
setAgents([
makeAgent({ id: "parent-a" }),
makeAgent({ id: "parent-b" }),
{ ...child, parentAgentId: "parent-b", labels: { "paseo.parent-agent-id": "parent-b" } },
]);
expect(
selectSubagentsForParent(
useSessionStore.getState(),
{
serverId: SERVER_ID,
parentAgentId: "parent-a",
},
EMPTY_PENDING_ARCHIVE_IDS,
).map((row) => row.id),
).toEqual([]);
expect(
selectSubagentsForParent(
useSessionStore.getState(),
{
serverId: SERVER_ID,
parentAgentId: "parent-b",
},
EMPTY_PENDING_ARCHIVE_IDS,
).map((row) => row.id),
).toEqual(["child"]);
});
it("excludes children whose archive is pending", () => {
setAgents([
makeAgent({ id: "parent" }),
makeAgent({ id: "child-a", parentAgentId: "parent" }),
makeAgent({ id: "child-b", parentAgentId: "parent" }),
]);
const rows = selectSubagentsForParent(
useSessionStore.getState(),
{
serverId: SERVER_ID,
parentAgentId: "parent",
},
new Set([`${SERVER_ID}:child-b`]),
);
expect(rows.map((row) => row.id)).toEqual(["child-a"]);
});
it("returns the shared empty array when pending archive hides the last child", () => {
setAgents([makeAgent({ id: "parent" }), makeAgent({ id: "child", parentAgentId: "parent" })]);
const rows = selectSubagentsForParent(
useSessionStore.getState(),
{
serverId: SERVER_ID,
parentAgentId: "parent",
},
new Set([`${SERVER_ID}:child`]),
);
expect(rows).toEqual([]);
expect(rows).toBe(
selectSubagentsForParent(
useSessionStore.getState(),
{
serverId: SERVER_ID,
parentAgentId: "missing-parent",
},
EMPTY_PENDING_ARCHIVE_IDS,
),
);
});
});
describe("createSubagentsForParentSelector", () => {
class IterationCountingMap<K, V> extends Map<K, V> {
iterations = 0;
override values() {
this.iterations += 1;
return super.values();
}
override entries() {
this.iterations += 1;
return super.entries();
}
override [Symbol.iterator]() {
this.iterations += 1;
return super[Symbol.iterator]();
}
}
const params = { serverId: SERVER_ID, parentAgentId: "parent" };
function streamTick(text: string): void {
useSessionStore.getState().setAgentStreamState(SERVER_ID, "parent", {
head: [
{
kind: "assistant_message",
id: "message-1",
text,
timestamp: new Date("2026-03-08T10:05:00.000Z"),
},
],
});
}
it("returns the cached rows without walking agents when a stream tick leaves them unchanged", () => {
const agents = new IterationCountingMap<string, Agent>(
[
makeAgent({ id: "parent" }),
makeAgent({ id: "child", parentAgentId: "parent" }),
makeAgent({ id: "unrelated" }),
].map((agent) => [agent.id, agent]),
);
useSessionStore.getState().initializeSession(SERVER_ID, null as unknown as DaemonClient);
useSessionStore.getState().setAgents(SERVER_ID, agents);
const select = createSubagentsForParentSelector(params, EMPTY_PENDING_ARCHIVE_IDS);
const before = select(useSessionStore.getState());
const iterationsAfterFirstSelect = agents.iterations;
for (let tick = 0; tick < 100; tick += 1) {
streamTick(`token ${tick}`);
expect(select(useSessionStore.getState())).toBe(before);
}
expect(useSessionStore.getState().sessions[SERVER_ID]?.agents).toBe(agents);
expect(agents.iterations).toBe(iterationsAfterFirstSelect);
expect(before.map((row) => row.id)).toEqual(["child"]);
});
it("recomputes when the agents map changes", () => {
setAgents([makeAgent({ id: "parent" }), makeAgent({ id: "child-a", parentAgentId: "parent" })]);
const select = createSubagentsForParentSelector(params, EMPTY_PENDING_ARCHIVE_IDS);
const before = select(useSessionStore.getState());
useSessionStore.getState().setAgents(SERVER_ID, (current) => {
const child = makeAgent({
id: "child-b",
parentAgentId: "parent",
createdAt: new Date("2026-03-08T10:01:00.000Z"),
});
return new Map(current).set(child.id, child);
});
const after = select(useSessionStore.getState());
expect(after).not.toBe(before);
expect(after.map((row) => row.id)).toEqual(["child-a", "child-b"]);
});
it("keeps the previous rows when a changed agents map yields the same rows", () => {
setAgents([
makeAgent({ id: "parent" }),
makeAgent({ id: "child", parentAgentId: "parent" }),
makeAgent({ id: "unrelated" }),
]);
const select = createSubagentsForParentSelector(params, EMPTY_PENDING_ARCHIVE_IDS);
const before = select(useSessionStore.getState());
useSessionStore.getState().setAgents(SERVER_ID, (current) => {
return new Map(current).set("unrelated", makeAgent({ id: "unrelated", status: "running" }));
});
expect(select(useSessionStore.getState())).toBe(before);
});
it("applies the pending archive ids it was created with", () => {
setAgents([
makeAgent({ id: "parent" }),
makeAgent({ id: "child-a", parentAgentId: "parent" }),
makeAgent({ id: "child-b", parentAgentId: "parent" }),
]);
const rows = createSubagentsForParentSelector(
params,
new Set([`${SERVER_ID}:child-b`]),
)(useSessionStore.getState());
expect(rows.map((row) => row.id)).toEqual(["child-a"]);
});
it("does no agent map walks for 100 stream commits with unchanged source maps", () => {
const agents = new IterationCountingMap<string, Agent>(
Array.from({ length: 200 }, (_, i) => {
const agent = makeAgent({
id: `agent-${i}`,
workspaceId: "workspace",
parentAgentId: i < 3 ? null : `agent-${i % 3}`,
});
return [agent.id, agent] as const;
}),
);
useSessionStore.getState().initializeSession(SERVER_ID, null as unknown as DaemonClient);
useSessionStore.getState().setAgents(SERVER_ID, agents);
const selectVisibility = createWorkspaceAgentVisibilitySelector({
serverId: SERVER_ID,
workspaceId: "workspace",
});
const selectChildren = Array.from({ length: 3 }, (_, i) =>
createSubagentsForParentSelector(
{ serverId: SERVER_ID, parentAgentId: `agent-${i}` },
EMPTY_PENDING_ARCHIVE_IDS,
),
);
const select = (state: ReturnType<typeof useSessionStore.getState>) => {
selectVisibility(state);
selectChildren.forEach((selector) => selector(state));
};
select(useSessionStore.getState());
const before = agents.iterations;
const unsubscribe = useSessionStore.subscribe(select);
try {
for (let tick = 0; tick < 100; tick++) {
useSessionStore.getState().setAgentStreamState(SERVER_ID, "agent-0", {
head: [
{
kind: "assistant_message",
id: "message",
text: `token ${tick}`,
timestamp: AGENT_TIMESTAMP,
},
],
});
}
expect(useSessionStore.getState().sessions[SERVER_ID]?.agents).toBe(agents);
expect(agents.iterations - before).toBe(0);
} finally {
unsubscribe();
useSessionStore.getState().clearSession(SERVER_ID);
}
});
});