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:
@@ -212,7 +212,7 @@ describe("SubagentsSection", () => {
|
||||
});
|
||||
|
||||
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" })]);
|
||||
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 })]);
|
||||
expect(queryByTestId("subagents-section-header")?.textContent).toBe(
|
||||
"1 subagent · 1 needs attention",
|
||||
);
|
||||
expect(queryByTestId("subagents-section-header")?.textContent).toBe("1 subagent");
|
||||
});
|
||||
|
||||
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([
|
||||
row({ id: "a", status: "running" }),
|
||||
row({ id: "b", status: "running" }),
|
||||
@@ -244,31 +242,27 @@ describe("SubagentsSection", () => {
|
||||
row({ id: "e" }),
|
||||
]);
|
||||
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([
|
||||
row({ id: "a", status: "error", requiresAttention: false }),
|
||||
row({ id: "b", status: "idle", requiresAttention: false }),
|
||||
row({ id: "c", status: "idle", requiresAttention: true }),
|
||||
]);
|
||||
expect(queryByTestId("subagents-section-header")?.textContent).toBe(
|
||||
"3 subagents · 1 needs attention",
|
||||
);
|
||||
expect(queryByTestId("subagents-section-header")?.textContent).toBe("3 subagents");
|
||||
});
|
||||
|
||||
it("excludes errored or running rows from the needs attention count", () => {
|
||||
it("still counts running rows even when they require attention", () => {
|
||||
render([
|
||||
row({ id: "a", status: "error", requiresAttention: true }),
|
||||
row({ id: "b", status: "running", requiresAttention: true }),
|
||||
row({ id: "c", status: "idle", requiresAttention: true }),
|
||||
]);
|
||||
expect(queryByTestId("subagents-section-header")?.textContent).toBe(
|
||||
"3 subagents · 1 running · 1 needs attention",
|
||||
);
|
||||
expect(queryByTestId("subagents-section-header")?.textContent).toBe("3 subagents · 1 running");
|
||||
});
|
||||
|
||||
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 {
|
||||
let runningCount = 0;
|
||||
let attentionCount = 0;
|
||||
for (const row of rows) {
|
||||
if (row.status === "running") {
|
||||
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"}`];
|
||||
if (runningCount > 0) {
|
||||
parts.push(`${runningCount} running`);
|
||||
}
|
||||
if (attentionCount > 0) {
|
||||
parts.push(`${attentionCount} needs attention`);
|
||||
}
|
||||
return parts.join(" · ");
|
||||
}
|
||||
|
||||
@@ -82,7 +71,7 @@ function buildRowPresentation(row: SubagentRow): WorkspaceTabPresentation {
|
||||
icon: getProviderIcon(row.provider),
|
||||
statusBucket: deriveSidebarStateBucket({
|
||||
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 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,
|
||||
@@ -71,10 +72,14 @@ describe("selectSubagentsForParent", () => {
|
||||
}),
|
||||
]);
|
||||
|
||||
const rows = selectSubagentsForParent(useSessionStore.getState(), {
|
||||
serverId: SERVER_ID,
|
||||
parentAgentId: "parent-a",
|
||||
});
|
||||
const rows = selectSubagentsForParent(
|
||||
useSessionStore.getState(),
|
||||
{
|
||||
serverId: SERVER_ID,
|
||||
parentAgentId: "parent-a",
|
||||
},
|
||||
EMPTY_PENDING_ARCHIVE_IDS,
|
||||
);
|
||||
|
||||
expect(rows.map((row) => row.id)).toEqual(["child-a"]);
|
||||
});
|
||||
@@ -89,10 +94,14 @@ describe("selectSubagentsForParent", () => {
|
||||
makeAgent({ id: "unrelated" }),
|
||||
]);
|
||||
|
||||
const rows = selectSubagentsForParent(useSessionStore.getState(), {
|
||||
serverId: SERVER_ID,
|
||||
parentAgentId: "parent-a",
|
||||
});
|
||||
const rows = selectSubagentsForParent(
|
||||
useSessionStore.getState(),
|
||||
{
|
||||
serverId: SERVER_ID,
|
||||
parentAgentId: "parent-a",
|
||||
},
|
||||
EMPTY_PENDING_ARCHIVE_IDS,
|
||||
);
|
||||
|
||||
expect(rows.map((row) => row.id)).toEqual(["child-a"]);
|
||||
});
|
||||
@@ -104,14 +113,22 @@ describe("selectSubagentsForParent", () => {
|
||||
makeAgent({ id: "grandchild", parentAgentId: "child" }),
|
||||
]);
|
||||
|
||||
const parentRows = selectSubagentsForParent(useSessionStore.getState(), {
|
||||
serverId: SERVER_ID,
|
||||
parentAgentId: "parent",
|
||||
});
|
||||
const childRows = selectSubagentsForParent(useSessionStore.getState(), {
|
||||
serverId: SERVER_ID,
|
||||
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"]);
|
||||
@@ -137,10 +154,14 @@ describe("selectSubagentsForParent", () => {
|
||||
}),
|
||||
]);
|
||||
|
||||
const rows = selectSubagentsForParent(useSessionStore.getState(), {
|
||||
serverId: SERVER_ID,
|
||||
parentAgentId: "parent",
|
||||
});
|
||||
const rows = selectSubagentsForParent(
|
||||
useSessionStore.getState(),
|
||||
{
|
||||
serverId: SERVER_ID,
|
||||
parentAgentId: "parent",
|
||||
},
|
||||
EMPTY_PENDING_ARCHIVE_IDS,
|
||||
);
|
||||
|
||||
expect(rows.map((row) => row.id)).toEqual(["first", "second", "third"]);
|
||||
});
|
||||
@@ -162,10 +183,14 @@ describe("selectSubagentsForParent", () => {
|
||||
}),
|
||||
]);
|
||||
|
||||
const rows = selectSubagentsForParent(useSessionStore.getState(), {
|
||||
serverId: SERVER_ID,
|
||||
parentAgentId: "parent",
|
||||
});
|
||||
const rows = selectSubagentsForParent(
|
||||
useSessionStore.getState(),
|
||||
{
|
||||
serverId: SERVER_ID,
|
||||
parentAgentId: "parent",
|
||||
},
|
||||
EMPTY_PENDING_ARCHIVE_IDS,
|
||||
);
|
||||
|
||||
expect(rows).toEqual([
|
||||
{
|
||||
@@ -195,16 +220,24 @@ describe("selectSubagentsForParent", () => {
|
||||
setAgents([makeAgent({ id: "parent-a" }), makeAgent({ id: "parent-b" }), child]);
|
||||
|
||||
expect(
|
||||
selectSubagentsForParent(useSessionStore.getState(), {
|
||||
serverId: SERVER_ID,
|
||||
parentAgentId: "parent-a",
|
||||
}).map((row) => row.id),
|
||||
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",
|
||||
}).map((row) => row.id),
|
||||
selectSubagentsForParent(
|
||||
useSessionStore.getState(),
|
||||
{
|
||||
serverId: SERVER_ID,
|
||||
parentAgentId: "parent-b",
|
||||
},
|
||||
EMPTY_PENDING_ARCHIVE_IDS,
|
||||
).map((row) => row.id),
|
||||
).toEqual([]);
|
||||
|
||||
setAgents([
|
||||
@@ -214,16 +247,68 @@ describe("selectSubagentsForParent", () => {
|
||||
]);
|
||||
|
||||
expect(
|
||||
selectSubagentsForParent(useSessionStore.getState(), {
|
||||
serverId: SERVER_ID,
|
||||
parentAgentId: "parent-a",
|
||||
}).map((row) => row.id),
|
||||
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",
|
||||
}).map((row) => row.id),
|
||||
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(["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 { useStoreWithEqualityFn } from "zustand/traditional";
|
||||
import { useSessionStore, type Agent } from "@/stores/session-store";
|
||||
@@ -34,6 +35,7 @@ function toSubagentRow(agent: Agent): SubagentRow {
|
||||
export function selectSubagentsForParent(
|
||||
state: SessionStoreSnapshot,
|
||||
params: SelectSubagentsParams,
|
||||
pendingArchiveIds: ReadonlySet<string>,
|
||||
): SubagentRow[] {
|
||||
const agents = state.sessions[params.serverId]?.agents;
|
||||
if (!agents || agents.size === 0) {
|
||||
@@ -42,7 +44,11 @@ export function selectSubagentsForParent(
|
||||
|
||||
const rows: SubagentRow[] = [];
|
||||
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;
|
||||
}
|
||||
rows.push(toSubagentRow(agent));
|
||||
@@ -57,9 +63,10 @@ export function selectSubagentsForParent(
|
||||
}
|
||||
|
||||
export function useSubagentsForParent(params: SelectSubagentsParams): SubagentRow[] {
|
||||
const pendingArchiveIds = usePendingArchiveAgentIds(params.serverId);
|
||||
return useStoreWithEqualityFn(
|
||||
useSessionStore,
|
||||
(state) => selectSubagentsForParent(state, params),
|
||||
(state) => selectSubagentsForParent(state, params, pendingArchiveIds),
|
||||
equal,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user