mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-10 10:25:03 +00:00
* perf(app): skip workspace agent visibility rebuild on stream ticks The workspace screen derived agent visibility on every session-store update: it copied every agent and agent detail into a new Map, walked all agents, and allocated two Sets, then compared them. Stream ticks update the store but leave the agent maps untouched, so this was pure waste at stream rate. createWorkspaceAgentVisibilitySelector returns the previous result until session.agents or session.agentDetails changes, and keeps it when a recompute yields the same sets. Parent lookups read the two maps directly instead of merging them. Fixes #4817 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * perf(app): skip subagent row rebuild on stream ticks Every mounted agent panel selected its subagent rows by walking all session agents, allocating rows, sorting, and deep-comparing on every session-store update, including every stream tick. createSubagentsForParentSelector returns the previous rows until session.agents changes, and keeps them when a recompute yields equal rows. The hook rebuilds the selector when the parent or the pending archive ids change. Refs #4817 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * perf(app): check pending submissions without allocating selectAgentTurnPresentation runs from several session-store selectors and filtered the submission list only to read its length. Use some(). Refs #4817 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Ivan Malison <IvanMalison@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
197 lines
6.9 KiB
TypeScript
197 lines
6.9 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"];
|
|
/** Managed agents have a real title, so the union's task line is always absent for them. */
|
|
description: null;
|
|
subtitle: null;
|
|
status: Agent["status"];
|
|
turn: Agent["turn"];
|
|
requiresAttention: Agent["requiresAttention"];
|
|
createdAt: Agent["createdAt"];
|
|
}
|
|
|
|
export interface ProviderSubagentRow {
|
|
kind: "provider";
|
|
id: string;
|
|
parentAgentId: string;
|
|
provider: ProviderSubagentDescriptorPayload["provider"];
|
|
// `title` is the subagent type ("Explore", "general-purpose") and repeats across a fan-out;
|
|
// `description` is the task it was given. Both are carried so presentation can choose which
|
|
// one names the row — collapsing them here is what makes every row read alike.
|
|
title: string | null;
|
|
description: string | null;
|
|
/** Compact provider-owned context. The app displays it without interpreting its contents. */
|
|
subtitle: 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;
|
|
/** Select children of this provider subagent instead of children of the managed agent. */
|
|
providerParentSubagentId?: 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,
|
|
description: null,
|
|
subtitle: null,
|
|
status: agent.status,
|
|
turn: agent.turn,
|
|
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;
|
|
}
|
|
|
|
// Session-store selectors run on every store update, including every stream tick, which
|
|
// leaves the agent map untouched. Reuse the previous rows until the map changes, and keep
|
|
// them when a recompute yields equal rows so the hook's equality check stays O(1).
|
|
export function createSubagentsForParentSelector(
|
|
params: SelectSubagentsParams,
|
|
pendingArchiveIds: ReadonlySet<string>,
|
|
): (state: SessionStoreSnapshot) => SubagentRow[] {
|
|
let previousAgents: Map<string, Agent> | undefined;
|
|
let previousRows: SubagentRow[] | null = null;
|
|
|
|
return (state) => {
|
|
const agents = state.sessions[params.serverId]?.agents;
|
|
if (previousRows && agents === previousAgents) {
|
|
return previousRows;
|
|
}
|
|
|
|
const rows = selectSubagentsForParent(state, params, pendingArchiveIds);
|
|
previousAgents = agents;
|
|
if (!previousRows || !equal(previousRows, rows)) {
|
|
previousRows = rows;
|
|
}
|
|
return previousRows;
|
|
};
|
|
}
|
|
|
|
export function selectProviderSubagentsForParent(
|
|
state: ProviderSubagentStoreSnapshot,
|
|
params: SelectSubagentsParams,
|
|
supported: boolean,
|
|
nestingSupported = false,
|
|
): ProviderSubagentRow[] {
|
|
if (!supported) return EMPTY_PROVIDER_SUBAGENT_ROWS;
|
|
if (params.providerParentSubagentId && !nestingSupported) 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;
|
|
if (
|
|
nestingSupported &&
|
|
(subagent.parentSubagentId ?? null) !== (params.providerParentSubagentId ?? null)
|
|
) {
|
|
continue;
|
|
}
|
|
rows.push({
|
|
kind: "provider",
|
|
id: subagent.id,
|
|
parentAgentId: subagent.parentAgentId,
|
|
provider: subagent.provider,
|
|
title: subagent.title,
|
|
description: subagent.description,
|
|
subtitle: subagent.subtitle ?? null,
|
|
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 { serverId, parentAgentId } = params;
|
|
const pendingArchiveIds = usePendingArchiveAgentIds(serverId);
|
|
const selectPaseoRows = useMemo(
|
|
() => createSubagentsForParentSelector({ serverId, parentAgentId }, pendingArchiveIds),
|
|
[serverId, parentAgentId, pendingArchiveIds],
|
|
);
|
|
const paseoRows = useStoreWithEqualityFn(useSessionStore, selectPaseoRows, equal);
|
|
const supported = useSessionStore(
|
|
(state) => state.sessions[params.serverId]?.serverInfo?.features?.providerSubagents === true,
|
|
);
|
|
const nestingSupported = useSessionStore(
|
|
(state) =>
|
|
state.sessions[params.serverId]?.serverInfo?.features?.providerSubagentNesting === true,
|
|
);
|
|
const providerRows = useStoreWithEqualityFn(
|
|
useProviderSubagentStore,
|
|
(state) => selectProviderSubagentsForParent(state, params, supported, nestingSupported),
|
|
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 (params.providerParentSubagentId) return providerRows;
|
|
if (providerRows.length === 0) return paseoRows;
|
|
const rows = [...paseoRows, ...providerRows];
|
|
rows.sort((left, right) => left.createdAt.getTime() - right.createdAt.getTime());
|
|
return rows;
|
|
}, [params.providerParentSubagentId, paseoRows, providerRows]);
|
|
}
|