Files
paseo/packages/app/src/subagents/select.ts
T
Mohamed Boudra abfe955867 Show friendly native subagent names and hide finished work (#2073)
* feat(subagents): archive finished children and show native names

Keep native provider sessions intact while letting users clear completed children from the track. Persist dismissed provider child IDs so history replay does not restore them.

* fix(subagents): cover persistence and naming edge cases

* fix(subagents): preserve active descendants and native names

* fix(subagents): retain dismissed timelines and trim paths

* fix(subagents): retain history across hydration

* test(subagents): expect normalized native descriptions

* fix(subagents): separate hidden rows from retained history

* fix(subagents): preserve restored hidden tabs safely

* fix(subagents): integrate archive with run state

* test(codex): expect normalized child name

* fix(subagents): retain dismissed tabs on reload

* fix(subagents): hide finished native agents locally

Keep the header action presentation-only so it does not mutate agent lifecycle or expand the runtime protocol.

* fix(subagents): humanize native agent names

* fix(subagents): preserve names in Codex history

* fix(subagents): retain hidden rows across reloads
2026-07-15 11:58:31 +02:00

141 lines
4.4 KiB
TypeScript

import { useEffect, useMemo } from "react";
import { usePendingArchiveAgentIds } from "@/hooks/use-archive-agent";
import equal from "fast-deep-equal";
import { useStoreWithEqualityFn } from "zustand/traditional";
import { useSessionStore, type Agent } from "@/stores/session-store";
import { refreshProviderSubagents, useProviderSubagentStore } from "./provider-store";
import type { ProviderSubagentDescriptorPayload } from "@getpaseo/protocol/messages";
export interface PaseoSubagentRow {
kind: "paseo";
id: Agent["id"];
provider: Agent["provider"];
title: Agent["title"];
status: Agent["status"];
requiresAttention: Agent["requiresAttention"];
createdAt: Agent["createdAt"];
}
export interface ProviderSubagentRow {
kind: "provider";
id: string;
parentAgentId: string;
provider: ProviderSubagentDescriptorPayload["provider"];
title: string | null;
status: ProviderSubagentDescriptorPayload["status"];
requiresAttention: boolean;
createdAt: Date;
}
export type SubagentRow = PaseoSubagentRow | ProviderSubagentRow;
type SessionStoreSnapshot = ReturnType<typeof useSessionStore.getState>;
type ProviderSubagentStoreSnapshot = ReturnType<typeof useProviderSubagentStore.getState>;
interface SelectSubagentsParams {
serverId: string;
parentAgentId: string;
}
const EMPTY_SUBAGENT_ROWS: SubagentRow[] = [];
const EMPTY_PROVIDER_SUBAGENT_ROWS: ProviderSubagentRow[] = [];
function toSubagentRow(agent: Agent): SubagentRow {
return {
kind: "paseo",
id: agent.id,
provider: agent.provider,
title: agent.title,
status: agent.status,
requiresAttention: agent.requiresAttention,
createdAt: agent.createdAt,
};
}
export function selectSubagentsForParent(
state: SessionStoreSnapshot,
params: SelectSubagentsParams,
pendingArchiveIds: ReadonlySet<string>,
): SubagentRow[] {
const agents = state.sessions[params.serverId]?.agents;
if (!agents || agents.size === 0) {
return EMPTY_SUBAGENT_ROWS;
}
const rows: SubagentRow[] = [];
for (const agent of agents.values()) {
if (
agent.archivedAt ||
pendingArchiveIds.has(agent.id) ||
agent.parentAgentId !== params.parentAgentId
) {
continue;
}
rows.push(toSubagentRow(agent));
}
if (rows.length === 0) {
return EMPTY_SUBAGENT_ROWS;
}
rows.sort((left, right) => left.createdAt.getTime() - right.createdAt.getTime());
return rows;
}
export function selectProviderSubagentsForParent(
state: ProviderSubagentStoreSnapshot,
params: SelectSubagentsParams,
supported: boolean,
): ProviderSubagentRow[] {
if (!supported) return EMPTY_PROVIDER_SUBAGENT_ROWS;
const rows: ProviderSubagentRow[] = [];
const prefix = `${params.serverId}\0${params.parentAgentId}\0`;
for (const [key, subagent] of state.descriptors) {
if (!key.startsWith(prefix) || state.hiddenFromTrack.has(key)) continue;
rows.push({
kind: "provider",
id: subagent.id,
parentAgentId: subagent.parentAgentId,
provider: subagent.provider,
title: subagent.title ?? subagent.description,
status: subagent.status,
requiresAttention: subagent.status === "failed",
createdAt: new Date(subagent.createdAt),
});
}
rows.sort((left, right) => left.createdAt.getTime() - right.createdAt.getTime());
return rows;
}
export function useSubagentsForParent(params: SelectSubagentsParams): SubagentRow[] {
const pendingArchiveIds = usePendingArchiveAgentIds(params.serverId);
const paseoRows = useStoreWithEqualityFn(
useSessionStore,
(state) => selectSubagentsForParent(state, params, pendingArchiveIds),
equal,
);
const supported = useSessionStore(
(state) => state.sessions[params.serverId]?.serverInfo?.features?.providerSubagents === true,
);
const providerRows = useStoreWithEqualityFn(
useProviderSubagentStore,
(state) => selectProviderSubagentsForParent(state, params, supported),
equal,
);
const client = useSessionStore((state) => state.sessions[params.serverId]?.client ?? null);
useEffect(() => {
if (!client || !supported) return;
void refreshProviderSubagents(client, params.serverId, params.parentAgentId).catch(
() => undefined,
);
}, [client, params.parentAgentId, params.serverId, supported]);
return useMemo(() => {
if (providerRows.length === 0) return paseoRows;
const rows = [...paseoRows, ...providerRows];
rows.sort((left, right) => left.createdAt.getTime() - right.createdAt.getTime());
return rows;
}, [paseoRows, providerRows]);
}