fix(app): subagents track display + archive flicker
- Bucket finished subagents as `done` instead of `attention`; drop "needs attention" segment from the section header — subagents are transient and don't carry an unread state worth surfacing. - Gate the subagents selector on the existing pending-archive query so optimistic archives survive authoritative agent snapshots that wipe the local `archivedAt`. Last subagent archived now cleanly hides the whole track via the existing empty-rows guard.
This commit is contained in:
@@ -1,10 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* @vitest-environment jsdom
|
||||||
|
*/
|
||||||
|
import React from "react";
|
||||||
import type { DaemonClient } from "@server/client/daemon-client";
|
import type { DaemonClient } from "@server/client/daemon-client";
|
||||||
import { QueryClient } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import { act, renderHook, waitFor } from "@testing-library/react";
|
||||||
import { beforeEach, describe, expect, it } from "vitest";
|
import { beforeEach, describe, expect, it } from "vitest";
|
||||||
import type { Agent } from "@/stores/session-store";
|
import type { Agent } from "@/stores/session-store";
|
||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
import { agentHistoryQueryKey } from "./agent-history-query-key";
|
import { agentHistoryQueryKey } from "./agent-history-query-key";
|
||||||
import { __private__, applyArchivedAgentCloseResults } from "./use-archive-agent";
|
import {
|
||||||
|
__private__,
|
||||||
|
applyArchivedAgentCloseResults,
|
||||||
|
usePendingArchiveAgentIds,
|
||||||
|
} from "./use-archive-agent";
|
||||||
|
|
||||||
function makeAgent(overrides: Partial<Agent> = {}): Agent {
|
function makeAgent(overrides: Partial<Agent> = {}): Agent {
|
||||||
return {
|
return {
|
||||||
@@ -92,6 +101,48 @@ describe("useArchiveAgent", () => {
|
|||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("selects pending archive ids for a single server", () => {
|
||||||
|
const pendingIds = __private__.selectPendingArchiveAgentIds(
|
||||||
|
{
|
||||||
|
"server-a:agent-1": true,
|
||||||
|
"server-a:agent-2": true,
|
||||||
|
"server-b:agent-3": true,
|
||||||
|
},
|
||||||
|
"server-a",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(Array.from(pendingIds)).toEqual(["agent-1", "agent-2"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("subscribes to pending archive ids through react-query", async () => {
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
const wrapper = ({ children }: { children: React.ReactNode }) =>
|
||||||
|
React.createElement(QueryClientProvider, { client: queryClient }, children);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => usePendingArchiveAgentIds("server-a"), { wrapper });
|
||||||
|
|
||||||
|
expect(Array.from(result.current)).toEqual([]);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
__private__.setAgentArchiving({
|
||||||
|
queryClient,
|
||||||
|
serverId: "server-a",
|
||||||
|
agentId: "agent-1",
|
||||||
|
isArchiving: true,
|
||||||
|
});
|
||||||
|
__private__.setAgentArchiving({
|
||||||
|
queryClient,
|
||||||
|
serverId: "server-b",
|
||||||
|
agentId: "agent-2",
|
||||||
|
isArchiving: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(Array.from(result.current)).toEqual(["agent-1"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("removes an archived agent from cached list payloads", () => {
|
it("removes an archived agent from cached list payloads", () => {
|
||||||
const payload = {
|
const payload = {
|
||||||
entries: [{ agent: { id: "agent-1" } }, { agent: { id: "agent-2" } }],
|
entries: [{ agent: { id: "agent-1" } }, { agent: { id: "agent-2" } }],
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { useMutation, useQuery, useQueryClient, type QueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient, type QueryClient } from "@tanstack/react-query";
|
||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
import { agentHistoryQueryKey } from "./agent-history-query-key";
|
import { agentHistoryQueryKey } from "./agent-history-query-key";
|
||||||
|
|
||||||
export const ARCHIVE_AGENT_PENDING_QUERY_KEY = ["archive-agent-pending"] as const;
|
export const ARCHIVE_AGENT_PENDING_QUERY_KEY = ["archive-agent-pending"] as const;
|
||||||
|
const EMPTY_PENDING_ARCHIVE_AGENT_IDS = new Set<string>();
|
||||||
|
|
||||||
export interface ArchiveAgentInput {
|
export interface ArchiveAgentInput {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
@@ -67,6 +68,35 @@ function readPendingState(queryClient: QueryClient): ArchiveAgentPendingState {
|
|||||||
return queryClient.getQueryData<ArchiveAgentPendingState>(ARCHIVE_AGENT_PENDING_QUERY_KEY) ?? {};
|
return queryClient.getQueryData<ArchiveAgentPendingState>(ARCHIVE_AGENT_PENDING_QUERY_KEY) ?? {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectPendingArchiveAgentIds(
|
||||||
|
pendingState: ArchiveAgentPendingState,
|
||||||
|
serverId: string,
|
||||||
|
): ReadonlySet<string> {
|
||||||
|
const normalizedServerId = serverId.trim();
|
||||||
|
if (!normalizedServerId) {
|
||||||
|
return EMPTY_PENDING_ARCHIVE_AGENT_IDS;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefix = `${normalizedServerId}:`;
|
||||||
|
let agentIds: string[] | null = null;
|
||||||
|
for (const key of Object.keys(pendingState)) {
|
||||||
|
if (!key.startsWith(prefix)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const agentId = key.slice(prefix.length);
|
||||||
|
if (!agentId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
agentIds ??= [];
|
||||||
|
agentIds.push(agentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!agentIds || agentIds.length === 0) {
|
||||||
|
return EMPTY_PENDING_ARCHIVE_AGENT_IDS;
|
||||||
|
}
|
||||||
|
return new Set(agentIds);
|
||||||
|
}
|
||||||
|
|
||||||
function setAgentArchiving(input: SetAgentArchivingInput): void {
|
function setAgentArchiving(input: SetAgentArchivingInput): void {
|
||||||
const key = toArchiveKey(input);
|
const key = toArchiveKey(input);
|
||||||
if (!key) {
|
if (!key) {
|
||||||
@@ -329,16 +359,28 @@ export function clearArchiveAgentPending(input: IsAgentArchivingInput): void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useArchiveAgent() {
|
function useArchiveAgentPendingQuery() {
|
||||||
const queryClient = useQueryClient();
|
return useQuery({
|
||||||
|
|
||||||
const pendingQuery = useQuery({
|
|
||||||
queryKey: ARCHIVE_AGENT_PENDING_QUERY_KEY,
|
queryKey: ARCHIVE_AGENT_PENDING_QUERY_KEY,
|
||||||
queryFn: async (): Promise<ArchiveAgentPendingState> => ({}),
|
queryFn: async (): Promise<ArchiveAgentPendingState> => ({}),
|
||||||
initialData: {} as ArchiveAgentPendingState,
|
initialData: {} as ArchiveAgentPendingState,
|
||||||
staleTime: Infinity,
|
staleTime: Infinity,
|
||||||
gcTime: Infinity,
|
gcTime: Infinity,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePendingArchiveAgentIds(serverId: string): ReadonlySet<string> {
|
||||||
|
const pendingQuery = useArchiveAgentPendingQuery();
|
||||||
|
return useMemo(
|
||||||
|
() => selectPendingArchiveAgentIds(pendingQuery.data ?? {}, serverId),
|
||||||
|
[pendingQuery.data, serverId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useArchiveAgent() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const pendingQuery = useArchiveAgentPendingQuery();
|
||||||
|
|
||||||
const archiveMutation = useMutation({
|
const archiveMutation = useMutation({
|
||||||
mutationFn: async (input: ArchiveAgentInput): Promise<{ archivedAt: string }> => {
|
mutationFn: async (input: ArchiveAgentInput): Promise<{ archivedAt: string }> => {
|
||||||
@@ -434,6 +476,7 @@ export function useArchiveAgent() {
|
|||||||
export const __private__ = {
|
export const __private__ = {
|
||||||
toArchiveKey,
|
toArchiveKey,
|
||||||
readPendingState,
|
readPendingState,
|
||||||
|
selectPendingArchiveAgentIds,
|
||||||
setAgentArchiving,
|
setAgentArchiving,
|
||||||
isAgentArchiving,
|
isAgentArchiving,
|
||||||
removeAgentFromListPayload,
|
removeAgentFromListPayload,
|
||||||
|
|||||||
@@ -209,10 +209,14 @@ describe("workspace subagents integration", () => {
|
|||||||
|
|
||||||
expect(getWorkspaceTabIds(workspaceKey!)).toEqual(["agent_parent-agent"]);
|
expect(getWorkspaceTabIds(workspaceKey!)).toEqual(["agent_parent-agent"]);
|
||||||
expect(
|
expect(
|
||||||
selectSubagentsForParent(useSessionStore.getState(), {
|
selectSubagentsForParent(
|
||||||
serverId: SERVER_ID,
|
useSessionStore.getState(),
|
||||||
parentAgentId: "parent-agent",
|
{
|
||||||
}).map((row) => row.id),
|
serverId: SERVER_ID,
|
||||||
|
parentAgentId: "parent-agent",
|
||||||
|
},
|
||||||
|
new Set(),
|
||||||
|
).map((row) => row.id),
|
||||||
).toEqual(["child-agent"]);
|
).toEqual(["child-agent"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ describe("SubagentsSection", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("header copy", () => {
|
describe("header copy", () => {
|
||||||
it("renders '2 subagents' when two rows have no running and no attention state", () => {
|
it("renders '2 subagents' when two rows are not running", () => {
|
||||||
render([row({ id: "child-a" }), row({ id: "child-b" })]);
|
render([row({ id: "child-a" }), row({ id: "child-b" })]);
|
||||||
expect(queryByTestId("subagents-section-header")?.textContent).toBe("2 subagents");
|
expect(queryByTestId("subagents-section-header")?.textContent).toBe("2 subagents");
|
||||||
});
|
});
|
||||||
@@ -228,14 +228,12 @@ describe("SubagentsSection", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders '1 subagent · 1 needs attention' for a single attention row", () => {
|
it("renders '1 subagent' for a finished row that still requires attention upstream", () => {
|
||||||
render([row({ id: "child-a", requiresAttention: true })]);
|
render([row({ id: "child-a", requiresAttention: true })]);
|
||||||
expect(queryByTestId("subagents-section-header")?.textContent).toBe(
|
expect(queryByTestId("subagents-section-header")?.textContent).toBe("1 subagent");
|
||||||
"1 subagent · 1 needs attention",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders '5 subagents · 2 running · 1 needs attention' when both suffixes apply", () => {
|
it("renders '5 subagents · 2 running' when finished rows require attention upstream", () => {
|
||||||
render([
|
render([
|
||||||
row({ id: "a", status: "running" }),
|
row({ id: "a", status: "running" }),
|
||||||
row({ id: "b", status: "running" }),
|
row({ id: "b", status: "running" }),
|
||||||
@@ -244,31 +242,27 @@ describe("SubagentsSection", () => {
|
|||||||
row({ id: "e" }),
|
row({ id: "e" }),
|
||||||
]);
|
]);
|
||||||
expect(queryByTestId("subagents-section-header")?.textContent).toBe(
|
expect(queryByTestId("subagents-section-header")?.textContent).toBe(
|
||||||
"5 subagents · 2 running · 1 needs attention",
|
"5 subagents · 2 running",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("counts attention only from amber (idle + requiresAttention) rows", () => {
|
it("ignores requiresAttention for non-running header copy", () => {
|
||||||
render([
|
render([
|
||||||
row({ id: "a", status: "error", requiresAttention: false }),
|
row({ id: "a", status: "error", requiresAttention: false }),
|
||||||
row({ id: "b", status: "idle", requiresAttention: false }),
|
row({ id: "b", status: "idle", requiresAttention: false }),
|
||||||
row({ id: "c", status: "idle", requiresAttention: true }),
|
row({ id: "c", status: "idle", requiresAttention: true }),
|
||||||
]);
|
]);
|
||||||
expect(queryByTestId("subagents-section-header")?.textContent).toBe(
|
expect(queryByTestId("subagents-section-header")?.textContent).toBe("3 subagents");
|
||||||
"3 subagents · 1 needs attention",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("excludes errored or running rows from the needs attention count", () => {
|
it("still counts running rows even when they require attention", () => {
|
||||||
render([
|
render([
|
||||||
row({ id: "a", status: "error", requiresAttention: true }),
|
row({ id: "a", status: "error", requiresAttention: true }),
|
||||||
row({ id: "b", status: "running", requiresAttention: true }),
|
row({ id: "b", status: "running", requiresAttention: true }),
|
||||||
row({ id: "c", status: "idle", requiresAttention: true }),
|
row({ id: "c", status: "idle", requiresAttention: true }),
|
||||||
]);
|
]);
|
||||||
expect(queryByTestId("subagents-section-header")?.textContent).toBe(
|
expect(queryByTestId("subagents-section-header")?.textContent).toBe("3 subagents · 1 running");
|
||||||
"3 subagents · 1 running · 1 needs attention",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders each row through the shared workspace tab icon primitives", () => {
|
it("renders each row through the shared workspace tab icon primitives", () => {
|
||||||
|
|||||||
@@ -33,27 +33,16 @@ const SUBAGENTS_LIST_MAX_HEIGHT = 200;
|
|||||||
|
|
||||||
function formatHeaderLabel(rows: SubagentRow[]): string {
|
function formatHeaderLabel(rows: SubagentRow[]): string {
|
||||||
let runningCount = 0;
|
let runningCount = 0;
|
||||||
let attentionCount = 0;
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
if (row.status === "running") {
|
if (row.status === "running") {
|
||||||
runningCount += 1;
|
runningCount += 1;
|
||||||
}
|
}
|
||||||
const bucket = deriveSidebarStateBucket({
|
|
||||||
status: row.status,
|
|
||||||
requiresAttention: row.requiresAttention,
|
|
||||||
});
|
|
||||||
if (bucket === "attention") {
|
|
||||||
attentionCount += 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const parts = [`${rows.length} ${rows.length === 1 ? "subagent" : "subagents"}`];
|
const parts = [`${rows.length} ${rows.length === 1 ? "subagent" : "subagents"}`];
|
||||||
if (runningCount > 0) {
|
if (runningCount > 0) {
|
||||||
parts.push(`${runningCount} running`);
|
parts.push(`${runningCount} running`);
|
||||||
}
|
}
|
||||||
if (attentionCount > 0) {
|
|
||||||
parts.push(`${attentionCount} needs attention`);
|
|
||||||
}
|
|
||||||
return parts.join(" · ");
|
return parts.join(" · ");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +71,7 @@ function buildRowPresentation(row: SubagentRow): WorkspaceTabPresentation {
|
|||||||
icon: getProviderIcon(row.provider),
|
icon: getProviderIcon(row.provider),
|
||||||
statusBucket: deriveSidebarStateBucket({
|
statusBucket: deriveSidebarStateBucket({
|
||||||
status: row.status,
|
status: row.status,
|
||||||
requiresAttention: row.requiresAttention,
|
requiresAttention: false,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useSessionStore, type Agent } from "@/stores/session-store";
|
|||||||
|
|
||||||
const SERVER_ID = "server-1";
|
const SERVER_ID = "server-1";
|
||||||
const AGENT_TIMESTAMP = new Date("2026-03-08T10:00:00.000Z");
|
const AGENT_TIMESTAMP = new Date("2026-03-08T10:00:00.000Z");
|
||||||
|
const EMPTY_PENDING_ARCHIVE_IDS = new Set<string>();
|
||||||
|
|
||||||
const AGENT_DEFAULTS: Agent = {
|
const AGENT_DEFAULTS: Agent = {
|
||||||
serverId: SERVER_ID,
|
serverId: SERVER_ID,
|
||||||
@@ -71,10 +72,14 @@ describe("selectSubagentsForParent", () => {
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const rows = selectSubagentsForParent(useSessionStore.getState(), {
|
const rows = selectSubagentsForParent(
|
||||||
serverId: SERVER_ID,
|
useSessionStore.getState(),
|
||||||
parentAgentId: "parent-a",
|
{
|
||||||
});
|
serverId: SERVER_ID,
|
||||||
|
parentAgentId: "parent-a",
|
||||||
|
},
|
||||||
|
EMPTY_PENDING_ARCHIVE_IDS,
|
||||||
|
);
|
||||||
|
|
||||||
expect(rows.map((row) => row.id)).toEqual(["child-a"]);
|
expect(rows.map((row) => row.id)).toEqual(["child-a"]);
|
||||||
});
|
});
|
||||||
@@ -89,10 +94,14 @@ describe("selectSubagentsForParent", () => {
|
|||||||
makeAgent({ id: "unrelated" }),
|
makeAgent({ id: "unrelated" }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const rows = selectSubagentsForParent(useSessionStore.getState(), {
|
const rows = selectSubagentsForParent(
|
||||||
serverId: SERVER_ID,
|
useSessionStore.getState(),
|
||||||
parentAgentId: "parent-a",
|
{
|
||||||
});
|
serverId: SERVER_ID,
|
||||||
|
parentAgentId: "parent-a",
|
||||||
|
},
|
||||||
|
EMPTY_PENDING_ARCHIVE_IDS,
|
||||||
|
);
|
||||||
|
|
||||||
expect(rows.map((row) => row.id)).toEqual(["child-a"]);
|
expect(rows.map((row) => row.id)).toEqual(["child-a"]);
|
||||||
});
|
});
|
||||||
@@ -104,14 +113,22 @@ describe("selectSubagentsForParent", () => {
|
|||||||
makeAgent({ id: "grandchild", parentAgentId: "child" }),
|
makeAgent({ id: "grandchild", parentAgentId: "child" }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const parentRows = selectSubagentsForParent(useSessionStore.getState(), {
|
const parentRows = selectSubagentsForParent(
|
||||||
serverId: SERVER_ID,
|
useSessionStore.getState(),
|
||||||
parentAgentId: "parent",
|
{
|
||||||
});
|
serverId: SERVER_ID,
|
||||||
const childRows = selectSubagentsForParent(useSessionStore.getState(), {
|
parentAgentId: "parent",
|
||||||
serverId: SERVER_ID,
|
},
|
||||||
parentAgentId: "child",
|
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(parentRows.map((row) => row.id)).toEqual(["child"]);
|
||||||
expect(childRows.map((row) => row.id)).toEqual(["grandchild"]);
|
expect(childRows.map((row) => row.id)).toEqual(["grandchild"]);
|
||||||
@@ -137,10 +154,14 @@ describe("selectSubagentsForParent", () => {
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const rows = selectSubagentsForParent(useSessionStore.getState(), {
|
const rows = selectSubagentsForParent(
|
||||||
serverId: SERVER_ID,
|
useSessionStore.getState(),
|
||||||
parentAgentId: "parent",
|
{
|
||||||
});
|
serverId: SERVER_ID,
|
||||||
|
parentAgentId: "parent",
|
||||||
|
},
|
||||||
|
EMPTY_PENDING_ARCHIVE_IDS,
|
||||||
|
);
|
||||||
|
|
||||||
expect(rows.map((row) => row.id)).toEqual(["first", "second", "third"]);
|
expect(rows.map((row) => row.id)).toEqual(["first", "second", "third"]);
|
||||||
});
|
});
|
||||||
@@ -162,10 +183,14 @@ describe("selectSubagentsForParent", () => {
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const rows = selectSubagentsForParent(useSessionStore.getState(), {
|
const rows = selectSubagentsForParent(
|
||||||
serverId: SERVER_ID,
|
useSessionStore.getState(),
|
||||||
parentAgentId: "parent",
|
{
|
||||||
});
|
serverId: SERVER_ID,
|
||||||
|
parentAgentId: "parent",
|
||||||
|
},
|
||||||
|
EMPTY_PENDING_ARCHIVE_IDS,
|
||||||
|
);
|
||||||
|
|
||||||
expect(rows).toEqual([
|
expect(rows).toEqual([
|
||||||
{
|
{
|
||||||
@@ -195,16 +220,24 @@ describe("selectSubagentsForParent", () => {
|
|||||||
setAgents([makeAgent({ id: "parent-a" }), makeAgent({ id: "parent-b" }), child]);
|
setAgents([makeAgent({ id: "parent-a" }), makeAgent({ id: "parent-b" }), child]);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
selectSubagentsForParent(useSessionStore.getState(), {
|
selectSubagentsForParent(
|
||||||
serverId: SERVER_ID,
|
useSessionStore.getState(),
|
||||||
parentAgentId: "parent-a",
|
{
|
||||||
}).map((row) => row.id),
|
serverId: SERVER_ID,
|
||||||
|
parentAgentId: "parent-a",
|
||||||
|
},
|
||||||
|
EMPTY_PENDING_ARCHIVE_IDS,
|
||||||
|
).map((row) => row.id),
|
||||||
).toEqual(["child"]);
|
).toEqual(["child"]);
|
||||||
expect(
|
expect(
|
||||||
selectSubagentsForParent(useSessionStore.getState(), {
|
selectSubagentsForParent(
|
||||||
serverId: SERVER_ID,
|
useSessionStore.getState(),
|
||||||
parentAgentId: "parent-b",
|
{
|
||||||
}).map((row) => row.id),
|
serverId: SERVER_ID,
|
||||||
|
parentAgentId: "parent-b",
|
||||||
|
},
|
||||||
|
EMPTY_PENDING_ARCHIVE_IDS,
|
||||||
|
).map((row) => row.id),
|
||||||
).toEqual([]);
|
).toEqual([]);
|
||||||
|
|
||||||
setAgents([
|
setAgents([
|
||||||
@@ -214,16 +247,68 @@ describe("selectSubagentsForParent", () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
selectSubagentsForParent(useSessionStore.getState(), {
|
selectSubagentsForParent(
|
||||||
serverId: SERVER_ID,
|
useSessionStore.getState(),
|
||||||
parentAgentId: "parent-a",
|
{
|
||||||
}).map((row) => row.id),
|
serverId: SERVER_ID,
|
||||||
|
parentAgentId: "parent-a",
|
||||||
|
},
|
||||||
|
EMPTY_PENDING_ARCHIVE_IDS,
|
||||||
|
).map((row) => row.id),
|
||||||
).toEqual([]);
|
).toEqual([]);
|
||||||
expect(
|
expect(
|
||||||
selectSubagentsForParent(useSessionStore.getState(), {
|
selectSubagentsForParent(
|
||||||
serverId: SERVER_ID,
|
useSessionStore.getState(),
|
||||||
parentAgentId: "parent-b",
|
{
|
||||||
}).map((row) => row.id),
|
serverId: SERVER_ID,
|
||||||
|
parentAgentId: "parent-b",
|
||||||
|
},
|
||||||
|
EMPTY_PENDING_ARCHIVE_IDS,
|
||||||
|
).map((row) => row.id),
|
||||||
).toEqual(["child"]);
|
).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(["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(["child"]),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(rows).toEqual([]);
|
||||||
|
expect(rows).toBe(
|
||||||
|
selectSubagentsForParent(
|
||||||
|
useSessionStore.getState(),
|
||||||
|
{
|
||||||
|
serverId: SERVER_ID,
|
||||||
|
parentAgentId: "missing-parent",
|
||||||
|
},
|
||||||
|
EMPTY_PENDING_ARCHIVE_IDS,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { usePendingArchiveAgentIds } from "@/hooks/use-archive-agent";
|
||||||
import equal from "fast-deep-equal";
|
import equal from "fast-deep-equal";
|
||||||
import { useStoreWithEqualityFn } from "zustand/traditional";
|
import { useStoreWithEqualityFn } from "zustand/traditional";
|
||||||
import { useSessionStore, type Agent } from "@/stores/session-store";
|
import { useSessionStore, type Agent } from "@/stores/session-store";
|
||||||
@@ -34,6 +35,7 @@ function toSubagentRow(agent: Agent): SubagentRow {
|
|||||||
export function selectSubagentsForParent(
|
export function selectSubagentsForParent(
|
||||||
state: SessionStoreSnapshot,
|
state: SessionStoreSnapshot,
|
||||||
params: SelectSubagentsParams,
|
params: SelectSubagentsParams,
|
||||||
|
pendingArchiveIds: ReadonlySet<string>,
|
||||||
): SubagentRow[] {
|
): SubagentRow[] {
|
||||||
const agents = state.sessions[params.serverId]?.agents;
|
const agents = state.sessions[params.serverId]?.agents;
|
||||||
if (!agents || agents.size === 0) {
|
if (!agents || agents.size === 0) {
|
||||||
@@ -42,7 +44,11 @@ export function selectSubagentsForParent(
|
|||||||
|
|
||||||
const rows: SubagentRow[] = [];
|
const rows: SubagentRow[] = [];
|
||||||
for (const agent of agents.values()) {
|
for (const agent of agents.values()) {
|
||||||
if (agent.archivedAt || agent.parentAgentId !== params.parentAgentId) {
|
if (
|
||||||
|
agent.archivedAt ||
|
||||||
|
pendingArchiveIds.has(agent.id) ||
|
||||||
|
agent.parentAgentId !== params.parentAgentId
|
||||||
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
rows.push(toSubagentRow(agent));
|
rows.push(toSubagentRow(agent));
|
||||||
@@ -57,9 +63,10 @@ export function selectSubagentsForParent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useSubagentsForParent(params: SelectSubagentsParams): SubagentRow[] {
|
export function useSubagentsForParent(params: SelectSubagentsParams): SubagentRow[] {
|
||||||
|
const pendingArchiveIds = usePendingArchiveAgentIds(params.serverId);
|
||||||
return useStoreWithEqualityFn(
|
return useStoreWithEqualityFn(
|
||||||
useSessionStore,
|
useSessionStore,
|
||||||
(state) => selectSubagentsForParent(state, params),
|
(state) => selectSubagentsForParent(state, params, pendingArchiveIds),
|
||||||
equal,
|
equal,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user