From 6cc2b6f2c89e58b6b35e61f66347d72e45a9d016 Mon Sep 17 00:00:00 2001 From: Lakshmi Tanmay Date: Tue, 3 Nov 2026 02:45:27 +0530 Subject: [PATCH 1/4] fix(web): Lineage lists working threads above finished ones Lineage ordered rows by pin rank and creation time only, so a running fork or subagent could sit below several finished ones. Rows now sort by status (working, then idle, then finished) after the parent and merge-target pins. The web control applies workflow statuses before sorting so the order matches the status each row shows. Fixes #29485 Co-Authored-By: Claude Opus 5.5 --- .../chat/ThreadRelationshipsControl.tsx | 28 +++++----- .../src/state/threadRelationships.test.ts | 52 ++++++++++++++++++- .../src/state/threadRelationships.ts | 31 ++++++++++- 3 files changed, 93 insertions(+), 18 deletions(-) diff --git a/apps/web/src/components/chat/ThreadRelationshipsControl.tsx b/apps/web/src/components/chat/ThreadRelationshipsControl.tsx index 102d9734ded92..5910cc0e0b26c 100644 --- a/apps/web/src/components/chat/ThreadRelationshipsControl.tsx +++ b/apps/web/src/components/chat/ThreadRelationshipsControl.tsx @@ -498,22 +498,22 @@ export function ThreadRelationshipsPanel(props: { const mergeTargetThreadId = resolveMergeBackTargetThreadId(projection); const relationshipRows = orderWebThreadLineageRows({ graph, - rows: immediateThreadRelationships(graph, props.threadId), + rows: immediateThreadRelationships(graph, props.threadId) + // The graph rebuilds missing parents from lineage, so filter the rows it returns. + .filter((row) => !isHiddenRelation(row.threadId) && !isHiddenUnloadedRelation(row)) + .map((row) => { + const agent = + row.edge.kind === "subagent" && !isParentThreadRelationship(row.edge, props.threadId) + ? subagentsByThreadId.get(row.threadId) + : undefined; + const workflowGroup = agent === undefined ? undefined : workflowGroupsById.get(agent.id); + return workflowGroup === undefined + ? row + : { ...row, edge: { ...row.edge, status: workflowGroup.workflow.status } }; + }), currentThreadId: props.threadId, mergeTargetThreadId, - }) - // The graph rebuilds missing parents from lineage, so filter the rows it returns. - .filter((row) => !isHiddenRelation(row.threadId) && !isHiddenUnloadedRelation(row)) - .map((row) => { - const agent = - row.edge.kind === "subagent" && !isParentThreadRelationship(row.edge, props.threadId) - ? subagentsByThreadId.get(row.threadId) - : undefined; - const workflowGroup = agent === undefined ? undefined : workflowGroupsById.get(agent.id); - return workflowGroup === undefined - ? row - : { ...row, edge: { ...row.edge, status: workflowGroup.workflow.status } }; - }); + }); const canMerge = mergeTargetThreadId !== null && latestMergeBackRun !== null; const unlistedThreadIds = relationshipRows .filter( diff --git a/packages/client-runtime/src/state/threadRelationships.test.ts b/packages/client-runtime/src/state/threadRelationships.test.ts index 90dc68c21dd82..7ade5c7796bd8 100644 --- a/packages/client-runtime/src/state/threadRelationships.test.ts +++ b/packages/client-runtime/src/state/threadRelationships.test.ts @@ -368,7 +368,6 @@ describe("web thread lineage ordering", () => { parentThreadId: current, createdAt: "2026-06-03T00:00:00.000Z", updatedAt: "2026-07-30T00:00:00.000Z", - status: "running", }), forkShell({ id: third, parentThreadId: current, createdAt: "2026-06-04T00:00:00.000Z" }), forkShell({ id: current, parentThreadId: null, createdAt: "2026-06-01T00:00:00.000Z" }), @@ -377,7 +376,6 @@ describe("web thread lineage ordering", () => { parentThreadId: current, createdAt: "2026-06-02T00:00:00.000Z", updatedAt: "2026-07-31T00:00:00.000Z", - status: "failed", }), ], }); @@ -386,6 +384,56 @@ describe("web thread lineage ordering", () => { expect(after).toEqual(before); }); + it("puts working rows above idle rows, and idle rows above finished ones", () => { + const parent = ThreadId.make("thread-parent"); + const newestDone = ThreadId.make("thread-newest-done"); + const failed = ThreadId.make("thread-failed"); + const idle = ThreadId.make("thread-idle"); + const oldRunning = ThreadId.make("thread-old-running"); + const pendingSubagent = ThreadId.make("thread-pending-subagent"); + + expect( + orderedLineageIds({ + threads: [ + forkShell({ id: parent, parentThreadId: null, createdAt: "2026-06-01T00:00:00.000Z" }), + forkShell({ + id: current, + parentThreadId: parent, + createdAt: "2026-06-02T00:00:00.000Z", + }), + forkShell({ + id: newestDone, + parentThreadId: current, + createdAt: "2026-06-09T00:00:00.000Z", + }), + forkShell({ + id: failed, + parentThreadId: current, + createdAt: "2026-06-08T00:00:00.000Z", + status: "failed", + }), + forkShell({ + id: idle, + parentThreadId: current, + createdAt: "2026-06-07T00:00:00.000Z", + status: "idle", + }), + forkShell({ + id: oldRunning, + parentThreadId: current, + createdAt: "2026-06-03T00:00:00.000Z", + status: "running", + }), + ], + projection: { + thread: { id: current }, + subagents: [{ childThreadId: pendingSubagent, status: "pending" }], + contextTransfers: [], + }, + }), + ).toEqual([parent, oldRunning, pendingSubagent, idle, newestDone, failed]); + }); + it("pins the parent first and a distinct merge-back target second", () => { const parent = ThreadId.make("thread-parent"); const mergeTarget = ThreadId.make("thread-merge-target"); diff --git a/packages/client-runtime/src/state/threadRelationships.ts b/packages/client-runtime/src/state/threadRelationships.ts index 2aecc93470c2f..e659e2f5eb39d 100644 --- a/packages/client-runtime/src/state/threadRelationships.ts +++ b/packages/client-runtime/src/state/threadRelationships.ts @@ -176,9 +176,32 @@ function threadCreatedAtMillis(node: ThreadRelationshipNode | undefined): number return Number.isFinite(millis) ? millis : null; } +const WORKING_ROW_STATUSES = new Set([ + "pending", + "preparing", + "queued", + "starting", + "running", + "waiting", +]); +const FINISHED_ROW_STATUSES = new Set([ + "completed", + "failed", + "error", + "cancelled", + "interrupted", + "rolled_back", +]); + +function rowStatusRank(status: string | null): number { + if (status !== null && WORKING_ROW_STATUSES.has(status)) return 0; + if (status !== null && FINISHED_ROW_STATUSES.has(status)) return 2; + return 1; +} + /** - * Web pins the parent and distinct merge-back target before newest-created-first ordering. - * Mobile does not share this exception; do not reuse it as the canonical relationship order. + * Web pins the parent and distinct merge-back target, then orders working, idle and finished + * rows newest-created-first. Mobile does not share this order; do not reuse it as canonical. */ export function orderWebThreadLineageRows(input: { readonly graph: ThreadRelationshipGraph; @@ -195,6 +218,10 @@ export function orderWebThreadLineageRows(input: { return [...input.rows].sort((left, right) => { const rankDelta = pinRank(left) - pinRank(right); if (rankDelta !== 0) return rankDelta; + const statusDelta = + rowStatusRank(threadRelationshipRowStatus(input.graph, left)) - + rowStatusRank(threadRelationshipRowStatus(input.graph, right)); + if (statusDelta !== 0) return statusDelta; const leftCreatedAt = threadCreatedAtMillis(input.graph.nodes.get(left.threadId)); const rightCreatedAt = threadCreatedAtMillis(input.graph.nodes.get(right.threadId)); if (leftCreatedAt !== rightCreatedAt) { From 2d81605e7b53419347f74a1a4544587f25ce31b6 Mon Sep 17 00:00:00 2001 From: Lakshmi Tanmay Date: Tue, 3 Nov 2026 03:13:04 +0530 Subject: [PATCH 2/4] fix(web): preserve Lineage order when previous agents expand --- ...hreadRelationshipsControl.browser.test.tsx | 72 +++++++++ .../chat/ThreadRelationshipsControl.tsx | 151 +++++++++--------- .../src/state/threadRelationships.ts | 6 +- 3 files changed, 149 insertions(+), 80 deletions(-) diff --git a/apps/web/src/components/chat/ThreadRelationshipsControl.browser.test.tsx b/apps/web/src/components/chat/ThreadRelationshipsControl.browser.test.tsx index 9679f42569c6b..63b4bf5b44e65 100644 --- a/apps/web/src/components/chat/ThreadRelationshipsControl.browser.test.tsx +++ b/apps/web/src/components/chat/ThreadRelationshipsControl.browser.test.tsx @@ -210,6 +210,78 @@ it("opens an idle child from Lineage when its shell has not been loaded", async ); }); +it("keeps status order across forks and collapsed previous agents", async () => { + setup(); + const projection = makeThreadProjectionFixture(); + state.projection = { + ...projection, + subagents: (["running", "idle", "completed", "failed"] as const).map((status) => ({ + id: NodeId.make(`${status}-agent`), + threadId: projection.thread.id, + runId: null, + parentNodeId: NodeId.make("parent"), + origin: "app_owned", + createdBy: "agent", + driver: "codex", + providerInstanceId: projection.thread.providerInstanceId, + providerThreadId: null, + childThreadId: ThreadId.make(`${status}-child`), + nativeTaskRef: null, + prompt: "Inspect the change", + title: `${status} child`, + model: null, + status, + result: null, + startedAt: projection.thread.updatedAt, + completedAt: null, + updatedAt: projection.thread.updatedAt, + })), + }; + state.relatedShells = [ + { + ...projection.thread, + id: ThreadId.make("finished-fork"), + title: "Finished fork", + status: "completed", + lineage: { parentThreadId: projection.thread.id, relationshipToParent: "fork" }, + }, + ]; + await using screen = await render( + , + ); + + await expect.element(screen.getByText("Finished fork")).toBeVisible(); + const titles = () => + [...document.querySelectorAll("[data-thread-relationships-panel] li")].map( + (row) => row.textContent, + ); + expect(titles()).toEqual([ + expect.stringContaining("running child"), + expect.stringContaining("Finished fork"), + ]); + + const previous = screen.getByRole("button", { name: /Previous agents/ }); + await previous.click(); + expect(titles()).toEqual([ + expect.stringContaining("running child"), + expect.stringContaining("idle child"), + expect.stringContaining("Finished fork"), + expect.stringContaining("completed child"), + expect.stringContaining("failed child"), + ]); + + await previous.click(); + expect(titles()).toEqual([ + expect.stringContaining("running child"), + expect.stringContaining("Finished fork"), + ]); +}); + 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 5910cc0e0b26c..034edd5dc888a 100644 --- a/apps/web/src/components/chat/ThreadRelationshipsControl.tsx +++ b/apps/web/src/components/chat/ThreadRelationshipsControl.tsx @@ -21,6 +21,7 @@ import { threadRelationshipRowStatus, orderWebThreadLineageRows, resolveMergeBackTargetThreadId, + webThreadLineageStatusRank, type ThreadRelationshipEdge, type ThreadRelationshipWalkRow, } from "@t3tools/client-runtime/state/thread-relationships"; @@ -150,31 +151,41 @@ const isFinishedAgentStatus = (status: string | null | undefined) => ["completed", "failed", "error", "cancelled", "interrupted", "idle"].includes(status ?? ""); function ThreadLineageGroup(props: { - readonly label: string | null; readonly rows: ReadonlyArray; - readonly expanded: boolean; + readonly previousRows?: ReadonlyArray; readonly children: (rows: ReadonlyArray) => ReactNode; }) { - const [expanded, setExpanded] = useState(props.expanded); + const [expanded, setExpanded] = useState(false); const [visibleCount, setVisibleCount] = useState(THREAD_LINEAGE_INITIAL_COUNT); - const { visibleRows, hiddenCount } = resolveThreadLineageWindow(props.rows, visibleCount); - const failedCount = props.rows.filter((row) => { + const previousRows = props.previousRows ?? []; + const previousSet = new Set(previousRows); + const rows = expanded ? props.rows : props.rows.filter((row) => !previousSet.has(row)); + const { visibleRows, hiddenCount } = resolveThreadLineageWindow(rows, visibleCount); + const failedCount = previousRows.filter((row) => { const status = lineageRowStatus(row); return status === "failed" || status === "error"; }).length; if (props.rows.length === 0) return null; return (
- {props.label ? ( + {rows.length > 0 ? ( + setVisibleCount((count) => count + THREAD_LINEAGE_PAGE_COUNT)} + > + {props.children(visibleRows)} + + ) : null} + {previousRows.length > 0 ? ( setExpanded(!expanded)}> - {props.label} + Previous agents {failedCount > 0 ? {failedCount} failed : null} {expanded ? null : ( - {props.rows.length} + {previousRows.length} )} (props: { /> ) : null} - {expanded ? ( - setVisibleCount((count) => count + THREAD_LINEAGE_PAGE_COUNT)} - > - {props.children(visibleRows)} - - ) : null}
); } @@ -542,55 +545,54 @@ export function ThreadRelationshipsPanel(props: { ); const canDetach = projection ? canDetachThreadProviderSession(projection) : false; - const { - related = [], - active = [], - previous = [], - sideChats = [], - } = groupBy(visibleRelationships, ({ threadId, edge }) => { + const { lineage = [], sideChats = [] } = groupBy(visibleRelationships, ({ threadId, edge }) => { const isParent = isParentThreadRelationship(edge, props.threadId); 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 "lineage"; }); - const { remoteActive = [], remotePrevious = [] } = groupBy( - (projection?.subagents ?? []) + const isPinnedRow = ({ threadId, edge }: ThreadRelationshipWalkRow) => + isParentThreadRelationship(edge, props.threadId) || threadId === mergeTargetThreadId; + // Remote tasks have no thread here; stable-sort them into the status band they belong to. + const lineageRows: ReadonlyArray = [ + ...lineage, + ...(projection?.subagents ?? []) .filter((agent): agent is RemoteSubagent => agent.remoteChild !== undefined) .map((remote) => ({ remote })), - ({ remote }) => (isFinishedAgentStatus(remote.status) ? "remotePrevious" : "remoteActive"), + ].toSorted((left, right) => { + const band = (row: LineageRow) => + "remote" in row + ? webThreadLineageStatusRank(row.remote.status) + : isPinnedRow(row) + ? -1 + : webThreadLineageStatusRank(threadRelationshipRowStatus(graph, row)); + return band(left) - band(right); + }); + const previous = lineageRows.filter((row) => + "remote" in row + ? isFinishedAgentStatus(row.remote.status) + : row.edge.kind === "subagent" && + !isParentThreadRelationship(row.edge, props.threadId) && + isFinishedAgentStatus(row.edge.status), ); - const groups: ReadonlyArray<{ - id: string; - label: string | null; - rows: ReadonlyArray; - expanded: boolean; - }> = [ - { id: "related", label: null, rows: related, expanded: true }, - { id: "active", label: null, rows: [...active, ...remoteActive], expanded: true }, - { - id: "previous", - label: "Previous agents", - rows: [...previous, ...remotePrevious], - expanded: false, - }, - ]; const runningCount = (projection?.subagents.filter( (agent) => agent.childThreadId === null && agent.status === "running", - ).length ?? 0) + active.filter(({ edge }) => edge.status === "running").length; + ).length ?? 0) + + lineage.filter( + ({ edge }) => + edge.kind === "subagent" && + !isParentThreadRelationship(edge, props.threadId) && + edge.status === "running", + ).length; // A linked environment's agent may have delegated this thread from there. const delegatedFrom = currentThread?.delegatedFrom; const lineageVisible = props.showLineage && - (related.length > 0 || - active.length > 0 || - previous.length > 0 || + (lineageRows.length > 0 || runningCount > 0 || - remoteActive.length > 0 || - remotePrevious.length > 0 || delegatedFrom !== undefined || // Disconnect lives in this heading, so a thread with only side chats still shows it. (canDetach && sideChats.length > 0)); @@ -950,9 +952,7 @@ export function ThreadRelationshipsPanel(props: { showHeading={ runningCount > 0 || canDetach || - related.length > 0 || - active.length > 0 || - remoteActive.length > 0 || + lineageRows.length > previous.length || delegatedFrom !== undefined } data-thread-relationships-panel @@ -990,26 +990,28 @@ export function ThreadRelationshipsPanel(props: { /> )} - {groups.map((group) => ( - - {(visibleRows) => - visibleRows.map((row) => - "remote" in row ? ( - entry.instanceId === row.remote.providerInstanceId)} - /> - ) : ( - renderRelationshipRow(row) - ), - ) - } - - ))} + + {(visibleRows) => + visibleRows.map((row) => + "remote" in row ? ( + entry.instanceId === row.remote.providerInstanceId)} + /> + ) : ( + renderRelationshipRow(row) + ), + ) + } + ) : null} {sideChatsVisible ? ( @@ -1018,12 +1020,7 @@ export function ThreadRelationshipsPanel(props: { title={runningSideChats > 0 ? `Side chats ยท ${runningSideChats} running` : "Side chats"} data-thread-side-chats-panel > - + {(visibleRows) => visibleRows.map(renderRelationshipRow)} diff --git a/packages/client-runtime/src/state/threadRelationships.ts b/packages/client-runtime/src/state/threadRelationships.ts index e659e2f5eb39d..d0bd68cb6c5ec 100644 --- a/packages/client-runtime/src/state/threadRelationships.ts +++ b/packages/client-runtime/src/state/threadRelationships.ts @@ -193,7 +193,7 @@ const FINISHED_ROW_STATUSES = new Set([ "rolled_back", ]); -function rowStatusRank(status: string | null): number { +export function webThreadLineageStatusRank(status: string | null): number { if (status !== null && WORKING_ROW_STATUSES.has(status)) return 0; if (status !== null && FINISHED_ROW_STATUSES.has(status)) return 2; return 1; @@ -219,8 +219,8 @@ export function orderWebThreadLineageRows(input: { const rankDelta = pinRank(left) - pinRank(right); if (rankDelta !== 0) return rankDelta; const statusDelta = - rowStatusRank(threadRelationshipRowStatus(input.graph, left)) - - rowStatusRank(threadRelationshipRowStatus(input.graph, right)); + webThreadLineageStatusRank(threadRelationshipRowStatus(input.graph, left)) - + webThreadLineageStatusRank(threadRelationshipRowStatus(input.graph, right)); if (statusDelta !== 0) return statusDelta; const leftCreatedAt = threadCreatedAtMillis(input.graph.nodes.get(left.threadId)); const rightCreatedAt = threadCreatedAtMillis(input.graph.nodes.get(right.threadId)); From 01a729cdadf35cdaa6e8aa9a6e1d4106a87b3ec6 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Thu, 5 Nov 2026 04:55:21 +0000 Subject: [PATCH 3/4] test(web): wait for Lineage row order after toggling previous agents --- ...hreadRelationshipsControl.browser.test.tsx | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/apps/web/src/components/chat/ThreadRelationshipsControl.browser.test.tsx b/apps/web/src/components/chat/ThreadRelationshipsControl.browser.test.tsx index 63b4bf5b44e65..c6290eba76549 100644 --- a/apps/web/src/components/chat/ThreadRelationshipsControl.browser.test.tsx +++ b/apps/web/src/components/chat/ThreadRelationshipsControl.browser.test.tsx @@ -260,26 +260,32 @@ it("keeps status order across forks and collapsed previous agents", async () => [...document.querySelectorAll("[data-thread-relationships-panel] li")].map( (row) => row.textContent, ); - expect(titles()).toEqual([ - expect.stringContaining("running child"), - expect.stringContaining("Finished fork"), - ]); + await vi.waitFor(() => + expect(titles()).toEqual([ + expect.stringContaining("running child"), + expect.stringContaining("Finished fork"), + ]), + ); const previous = screen.getByRole("button", { name: /Previous agents/ }); await previous.click(); - expect(titles()).toEqual([ - expect.stringContaining("running child"), - expect.stringContaining("idle child"), - expect.stringContaining("Finished fork"), - expect.stringContaining("completed child"), - expect.stringContaining("failed child"), - ]); + await vi.waitFor(() => + expect(titles()).toEqual([ + expect.stringContaining("running child"), + expect.stringContaining("idle child"), + expect.stringContaining("Finished fork"), + expect.stringContaining("completed child"), + expect.stringContaining("failed child"), + ]), + ); await previous.click(); - expect(titles()).toEqual([ - expect.stringContaining("running child"), - expect.stringContaining("Finished fork"), - ]); + await vi.waitFor(() => + expect(titles()).toEqual([ + expect.stringContaining("running child"), + expect.stringContaining("Finished fork"), + ]), + ); }); it("shows a lineage thread's full title in its tooltip", async () => { From 97294ab9196f071d4c7db4b5ab48b3f3206719c3 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 12 Nov 2026 22:53:26 +0000 Subject: [PATCH 4/4] fix(web): Previous agents keep their own row window in Lineage Expanding Previous agents shared the six-row window with working rows, so it could reveal nothing. Give previous agents their own window again and render the selected rows in the unified status order. AI-assisted; verified with the tests and tools listed above. --- ...hreadRelationshipsControl.browser.test.tsx | 42 +++++++++++++++++++ .../chat/ThreadRelationshipsControl.tsx | 21 +++++++--- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/chat/ThreadRelationshipsControl.browser.test.tsx b/apps/web/src/components/chat/ThreadRelationshipsControl.browser.test.tsx index c6290eba76549..a21db09c8408e 100644 --- a/apps/web/src/components/chat/ThreadRelationshipsControl.browser.test.tsx +++ b/apps/web/src/components/chat/ThreadRelationshipsControl.browser.test.tsx @@ -288,6 +288,48 @@ it("keeps status order across forks and collapsed previous agents", async () => ); }); +it("reveals previous agents even when working rows fill the first page", async () => { + setup(); + const projection = makeThreadProjectionFixture(); + const statuses = [...Array.from({ length: 6 }, () => "running" as const), "completed" as const]; + state.projection = { + ...projection, + subagents: statuses.map((status, index) => ({ + id: NodeId.make(`agent-${index}`), + threadId: projection.thread.id, + runId: null, + parentNodeId: NodeId.make("parent"), + origin: "app_owned", + createdBy: "agent", + driver: "codex", + providerInstanceId: projection.thread.providerInstanceId, + providerThreadId: null, + childThreadId: ThreadId.make(`child-${index}`), + nativeTaskRef: null, + prompt: "Inspect the change", + title: `${status} child ${index}`, + model: null, + status, + result: null, + startedAt: projection.thread.updatedAt, + completedAt: null, + updatedAt: projection.thread.updatedAt, + })), + }; + await using screen = await render( + , + ); + + await screen.getByRole("button", { name: /Previous agents/ }).click(); + await expect.element(screen.getByText("completed child 6")).toBeVisible(); + expect(document.querySelectorAll("[data-thread-relationships-panel] li")).toHaveLength(7); +}); + 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 034edd5dc888a..7839a47309db3 100644 --- a/apps/web/src/components/chat/ThreadRelationshipsControl.tsx +++ b/apps/web/src/components/chat/ThreadRelationshipsControl.tsx @@ -157,10 +157,17 @@ function ThreadLineageGroup(props: { }) { const [expanded, setExpanded] = useState(false); const [visibleCount, setVisibleCount] = useState(THREAD_LINEAGE_INITIAL_COUNT); + const [previousVisibleCount, setPreviousVisibleCount] = useState(THREAD_LINEAGE_INITIAL_COUNT); const previousRows = props.previousRows ?? []; const previousSet = new Set(previousRows); - const rows = expanded ? props.rows : props.rows.filter((row) => !previousSet.has(row)); - const { visibleRows, hiddenCount } = resolveThreadLineageWindow(rows, visibleCount); + // Previous agents keep their own window so expanding them always reveals rows. + const current = resolveThreadLineageWindow( + props.rows.filter((row) => !previousSet.has(row)), + visibleCount, + ); + const previous = resolveThreadLineageWindow(expanded ? previousRows : [], previousVisibleCount); + const shownSet = new Set([...current.visibleRows, ...previous.visibleRows]); + const visibleRows = props.rows.filter((row) => shownSet.has(row)); const failedCount = previousRows.filter((row) => { const status = lineageRowStatus(row); return status === "failed" || status === "error"; @@ -168,10 +175,14 @@ function ThreadLineageGroup(props: { if (props.rows.length === 0) return null; return (
- {rows.length > 0 ? ( + {visibleRows.length > 0 ? ( setVisibleCount((count) => count + THREAD_LINEAGE_PAGE_COUNT)} + hiddenCount={current.hiddenCount > 0 ? current.hiddenCount : previous.hiddenCount} + onShowMore={() => + (current.hiddenCount > 0 ? setVisibleCount : setPreviousVisibleCount)( + (count) => count + THREAD_LINEAGE_PAGE_COUNT, + ) + } > {props.children(visibleRows)}