diff --git a/apps/web/src/components/chat/ThreadRelationshipsControl.browser.test.tsx b/apps/web/src/components/chat/ThreadRelationshipsControl.browser.test.tsx
index 9679f42569c6b..2af2a1a9ce49a 100644
--- a/apps/web/src/components/chat/ThreadRelationshipsControl.browser.test.tsx
+++ b/apps/web/src/components/chat/ThreadRelationshipsControl.browser.test.tsx
@@ -210,6 +210,60 @@ it("opens an idle child from Lineage when its shell has not been loaded", async
);
});
+it("keeps the latest batch of finished agents in view after a later turn launches none", async () => {
+ setup();
+ const projection = makeThreadProjectionFixture();
+ const agent = (id: string, runId: string, title: string) => ({
+ id: NodeId.make(id),
+ threadId: projection.thread.id,
+ runId,
+ parentNodeId: NodeId.make("parent"),
+ origin: "provider_native",
+ createdBy: "agent",
+ driver: "claudeAgent",
+ providerInstanceId: projection.thread.providerInstanceId,
+ providerThreadId: null,
+ childThreadId: ThreadId.make(`${id}-thread`),
+ nativeTaskRef: null,
+ prompt: title,
+ title,
+ model: null,
+ status: "completed",
+ result: null,
+ startedAt: projection.thread.updatedAt,
+ completedAt: projection.thread.updatedAt,
+ updatedAt: projection.thread.updatedAt,
+ });
+ state.projection = {
+ ...projection,
+ runs: [1, 2, 3].map((ordinal) => ({
+ id: `run-${ordinal}`,
+ ordinal,
+ status: "completed",
+ startedAt: projection.thread.updatedAt,
+ })),
+ subagents: [
+ agent("older", "run-1", "Older batch agent"),
+ agent("latest", "run-2", "Latest batch agent"),
+ ],
+ };
+ await using screen = await render(
+ ,
+ );
+
+ await expect
+ .element(screen.getByRole("button", { name: /Latest batch agent/ }))
+ .toBeInTheDocument();
+ await expect
+ .element(screen.getByRole("button", { name: /Older batch agent/ }))
+ .not.toBeInTheDocument();
+});
+
it("shows a lineage thread's full title in its tooltip", async () => {
setup();
const longTitle = "Compare the lineage tooltip against a title that the row truncates";
diff --git a/apps/web/src/components/chat/ThreadRelationshipsControl.tsx b/apps/web/src/components/chat/ThreadRelationshipsControl.tsx
index 102d9734ded92..23a5023494ce9 100644
--- a/apps/web/src/components/chat/ThreadRelationshipsControl.tsx
+++ b/apps/web/src/components/chat/ThreadRelationshipsControl.tsx
@@ -37,6 +37,7 @@ import {
type OrchestrationV2RemoteTaskChild,
type OrchestrationV2Subagent,
type OrchestrationV2ThreadShell,
+ type RunId,
type ServerProvider,
type ThreadId,
} from "@t3tools/contracts";
@@ -541,6 +542,23 @@ export function ThreadRelationshipsPanel(props: {
Object.hasOwn(resolvedThreadTitles, threadId),
);
const canDetach = projection ? canDetachThreadProviderSession(projection) : false;
+ // The latest batch stays in view after it finishes, until a later turn launches agents.
+ const runOrdinals = new Map(
+ (projection?.runs ?? [])
+ .filter((run) => run.status !== "rolled_back")
+ .map((run) => [run.id, run.ordinal]),
+ );
+ const latestBatchRunId = projected.reduce(
+ (latest, agent) =>
+ agent.runId !== null &&
+ runOrdinals.has(agent.runId) &&
+ (latest === null || runOrdinals.get(agent.runId)! > runOrdinals.get(latest)!)
+ ? agent.runId
+ : latest,
+ null,
+ );
+ const isPreviousAgent = (status: string | null | undefined, runId: RunId | null | undefined) =>
+ isFinishedAgentStatus(status) && (latestBatchRunId === null || runId !== latestBatchRunId);
const {
related = [],
@@ -552,13 +570,16 @@ export function ThreadRelationshipsPanel(props: {
if (!isParent && graph.nodes.get(threadId)?.thread?.lineage.sideChat === true)
return "sideChats";
if (edge.kind !== "subagent" || isParent) return "related";
- return isFinishedAgentStatus(edge.status) ? "previous" : "active";
+ return isPreviousAgent(edge.status, subagentsByThreadId.get(threadId)?.runId)
+ ? "previous"
+ : "active";
});
const { remoteActive = [], remotePrevious = [] } = groupBy(
(projection?.subagents ?? [])
.filter((agent): agent is RemoteSubagent => agent.remoteChild !== undefined)
.map((remote) => ({ remote })),
- ({ remote }) => (isFinishedAgentStatus(remote.status) ? "remotePrevious" : "remoteActive"),
+ ({ remote }) =>
+ isPreviousAgent(remote.status, remote.runId) ? "remotePrevious" : "remoteActive",
);
const groups: ReadonlyArray<{
id: string;