From 5798c3bcb9deb820c41191d7826b43333a48abe5 Mon Sep 17 00:00:00 2001 From: Harshith Mullapudi <17528887+harshithmullapudi@users.noreply.github.com> Date: Sat, 7 Nov 2026 22:53:59 +0000 Subject: [PATCH 1/3] fix(desktop): stop every chat row re-rendering on each streamed token TranscriptRowItem compared the session snapshot by identity. The snapshot is replaced on every streamed frame, so each mounted row re-rendered once per frame even when nothing it shows changed. A reader who had scrolled through a long chat kept every row mounted, so a stream re-rendered hundreds of rows per frame and starved typing and scrolling. Rows now compare only what TurnGroupSection reads from the snapshot: the harness, the live text of their own items, and the items map for approval rows. Synthetic benchmark (real Transcript, happy-dom, 60 text deltas): - 150 turns, scrolled through: 750 -> 1 row renders per frame, React commit time 18 ms -> 8-15 ms per frame - 300 turns, scrolled through: 1500 -> 1 row renders per frame, React commit time 35 ms -> 11-13 ms per frame Refs SUPER-2637 --- .../TranscriptRowItem/TranscriptRowItem.tsx | 7 +- .../utils/sameRowSnapshot/index.ts | 1 + .../sameRowSnapshot/sameRowSnapshot.test.ts | 73 +++++++++++++++++++ .../utils/sameRowSnapshot/sameRowSnapshot.ts | 35 +++++++++ 4 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatSession/components/Transcript/components/TranscriptRowItem/utils/sameRowSnapshot/index.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatSession/components/Transcript/components/TranscriptRowItem/utils/sameRowSnapshot/sameRowSnapshot.test.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatSession/components/Transcript/components/TranscriptRowItem/utils/sameRowSnapshot/sameRowSnapshot.ts diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatSession/components/Transcript/components/TranscriptRowItem/TranscriptRowItem.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatSession/components/Transcript/components/TranscriptRowItem/TranscriptRowItem.tsx index e730409b9ea..d69549b3d51 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatSession/components/Transcript/components/TranscriptRowItem/TranscriptRowItem.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatSession/components/Transcript/components/TranscriptRowItem/TranscriptRowItem.tsx @@ -4,6 +4,7 @@ import { TurnGroupSection, type TurnGroupSectionProps, } from "../TurnGroupSection"; +import { sameRowSnapshot } from "./utils/sameRowSnapshot"; type TranscriptRowItemProps = TurnGroupSectionProps & { className: string; @@ -34,7 +35,11 @@ function sameRow( const keys = Object.keys(next) as (keyof TranscriptRowItemProps)[]; return ( keys.length === Object.keys(previous).length && - keys.every((key) => Object.is(previous[key], next[key])) + keys.every((key) => + key === "snapshot" + ? sameRowSnapshot(next.row, previous.snapshot, next.snapshot) + : Object.is(previous[key], next[key]), + ) ); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatSession/components/Transcript/components/TranscriptRowItem/utils/sameRowSnapshot/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatSession/components/Transcript/components/TranscriptRowItem/utils/sameRowSnapshot/index.ts new file mode 100644 index 00000000000..46fe8e79409 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatSession/components/Transcript/components/TranscriptRowItem/utils/sameRowSnapshot/index.ts @@ -0,0 +1 @@ +export { sameRowSnapshot } from "./sameRowSnapshot"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatSession/components/Transcript/components/TranscriptRowItem/utils/sameRowSnapshot/sameRowSnapshot.test.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatSession/components/Transcript/components/TranscriptRowItem/utils/sameRowSnapshot/sameRowSnapshot.test.ts new file mode 100644 index 00000000000..f6b1a537b64 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatSession/components/Transcript/components/TranscriptRowItem/utils/sameRowSnapshot/sameRowSnapshot.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, test } from "bun:test"; +import { reduceMany, type SessionSnapshot } from "@superset/chat/core"; +import type { Envelope, Item } from "@superset/chat/protocol"; +import type { TranscriptRow } from "../../../../utils/transcriptRows"; +import { sameRowSnapshot } from "./sameRowSnapshot"; + +const reply: Item = { + id: "a-1", + kind: "agent_message", + startedAtMs: 1, + completedAtMs: 2, + text: "Done.", +}; +const streaming: Item = { + id: "a-2", + kind: "agent_message", + startedAtMs: 3, + text: "", +}; +const approval: Item = { + id: "approval-1", + kind: "approval_request", + startedAtMs: 4, + title: "Run tests", + status: "pending", + options: [], +} as unknown as Item; + +function snapshotOf(items: readonly Item[]): SessionSnapshot { + return { + session: { status: "running", harness: "acp", backgroundTasks: [] }, + turns: new Map(), + items: new Map(items.map((item) => [item.id, { item, turnId: "t1" }])), + liveStreams: new Map(), + cursor: { epoch: "e", seq: 1 }, + pendingReset: null, + } as unknown as SessionSnapshot; +} + +function streamInto(snapshot: SessionSnapshot, itemId: string, append: string) { + return reduceMany(snapshot, [ + { + cursor: { epoch: "e", seq: 2 }, + delta: { type: "text", itemId, append }, + } as unknown as Envelope, + ]); +} + +function itemRow(item: Item): TranscriptRow { + return { kind: "item", key: item.id, groupStart: false, item }; +} + +describe("sameRowSnapshot", () => { + const base = snapshotOf([reply, streaming, approval]); + + test("another message streaming leaves a settled row alone", () => { + expect( + sameRowSnapshot(itemRow(reply), base, streamInto(base, "a-2", "Hi")), + ).toBe(true); + }); + + test("the row's own text streaming changes it", () => { + expect( + sameRowSnapshot(itemRow(streaming), base, streamInto(base, "a-2", "Hi")), + ).toBe(false); + }); + + test("an approval row follows changes to the items it looks up", () => { + const next = snapshotOf([reply, streaming, approval]); + expect(sameRowSnapshot(itemRow(approval), base, next)).toBe(false); + expect(sameRowSnapshot(itemRow(reply), base, next)).toBe(true); + }); +}); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatSession/components/Transcript/components/TranscriptRowItem/utils/sameRowSnapshot/sameRowSnapshot.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatSession/components/Transcript/components/TranscriptRowItem/utils/sameRowSnapshot/sameRowSnapshot.ts new file mode 100644 index 00000000000..2a41cae075b --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatSession/components/Transcript/components/TranscriptRowItem/utils/sameRowSnapshot/sameRowSnapshot.ts @@ -0,0 +1,35 @@ +import { displayText, type SessionSnapshot } from "@superset/chat/core"; +import type { TranscriptRow } from "../../../../utils/transcriptRows"; + +function textItemIds(row: TranscriptRow): string[] { + switch (row.kind) { + case "item": + case "reply_footer": + return [row.item.id]; + case "tool_run": + return row.steps + .filter((step) => step.kind === "reasoning") + .map((step) => step.id); + default: + return []; + } +} + +/** Mirrors what `TurnGroupSection` reads from the snapshot for `row`. */ +export function sameRowSnapshot( + row: TranscriptRow, + previous: SessionSnapshot, + next: SessionSnapshot, +): boolean { + if (previous === next) return true; + if (previous.session?.harness !== next.session?.harness) return false; + if ( + row.kind === "item" && + row.item.kind === "approval_request" && + previous.items !== next.items + ) + return false; + return textItemIds(row).every( + (id) => displayText(previous, id) === displayText(next, id), + ); +} From db34379ccc3e1ad0859b3d7104f4685fcd175c23 Mon Sep 17 00:00:00 2001 From: Harshith Mullapudi <17528887+harshithmullapudi@users.noreply.github.com> Date: Sat, 7 Nov 2026 22:57:55 +0000 Subject: [PATCH 2/3] test(desktop): cover tool-run, harness and approval-target cases in sameRowSnapshot Refs SUPER-2637 --- .../sameRowSnapshot/sameRowSnapshot.test.ts | 60 ++++++++++++++++--- 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatSession/components/Transcript/components/TranscriptRowItem/utils/sameRowSnapshot/sameRowSnapshot.test.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatSession/components/Transcript/components/TranscriptRowItem/utils/sameRowSnapshot/sameRowSnapshot.test.ts index f6b1a537b64..6ddd8246fb3 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatSession/components/Transcript/components/TranscriptRowItem/utils/sameRowSnapshot/sameRowSnapshot.test.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatSession/components/Transcript/components/TranscriptRowItem/utils/sameRowSnapshot/sameRowSnapshot.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; import { reduceMany, type SessionSnapshot } from "@superset/chat/core"; -import type { Envelope, Item } from "@superset/chat/protocol"; +import type { Envelope, Item, ToolCall } from "@superset/chat/protocol"; import type { TranscriptRow } from "../../../../utils/transcriptRows"; import { sameRowSnapshot } from "./sameRowSnapshot"; @@ -17,18 +17,35 @@ const streaming: Item = { startedAtMs: 3, text: "", }; -const approval: Item = { +const command: ToolCall = { + id: "tool-1", + kind: "tool_call", + startedAtMs: 4, + title: "bun test", + toolKind: "execute", + toolName: "bash", + status: "running", + content: [], +}; +const approval = { id: "approval-1", kind: "approval_request", - startedAtMs: 4, - title: "Run tests", + startedAtMs: 5, + title: "bun test", + targetItemId: command.id, status: "pending", options: [], } as unknown as Item; +const thinking: Item = { + id: "r-1", + kind: "reasoning", + startedAtMs: 6, + text: "", +}; -function snapshotOf(items: readonly Item[]): SessionSnapshot { +function snapshotOf(items: readonly Item[], harness = "acp"): SessionSnapshot { return { - session: { status: "running", harness: "acp", backgroundTasks: [] }, + session: { status: "running", harness, backgroundTasks: [] }, turns: new Map(), items: new Map(items.map((item) => [item.id, { item, turnId: "t1" }])), liveStreams: new Map(), @@ -50,8 +67,18 @@ function itemRow(item: Item): TranscriptRow { return { kind: "item", key: item.id, groupStart: false, item }; } +const toolRun: TranscriptRow = { + kind: "tool_run", + key: "run-1", + groupStart: false, + items: [command], + steps: [command, thinking], + defaultCollapsed: false, +}; + describe("sameRowSnapshot", () => { - const base = snapshotOf([reply, streaming, approval]); + const items = [reply, streaming, command, approval, thinking]; + const base = snapshotOf(items); test("another message streaming leaves a settled row alone", () => { expect( @@ -65,9 +92,24 @@ describe("sameRowSnapshot", () => { ).toBe(false); }); - test("an approval row follows changes to the items it looks up", () => { - const next = snapshotOf([reply, streaming, approval]); + test("a tool run changes when its reasoning step streams", () => { + const next = streamInto(base, thinking.id, "Checking"); + expect(sameRowSnapshot(toolRun, base, next)).toBe(false); + expect(sameRowSnapshot(toolRun, base, streamInto(base, "a-2", "Hi"))).toBe( + true, + ); + }); + + test("an approval row follows changes to its target", () => { + const finished: ToolCall = { ...command, status: "completed" }; + const next = snapshotOf([reply, streaming, finished, approval, thinking]); expect(sameRowSnapshot(itemRow(approval), base, next)).toBe(false); expect(sameRowSnapshot(itemRow(reply), base, next)).toBe(true); }); + + test("every row changes when the harness does", () => { + expect( + sameRowSnapshot(itemRow(reply), base, snapshotOf(items, "codex")), + ).toBe(false); + }); }); From 42375fa7ae4df003520cd31e9c2353afa813c093 Mon Sep 17 00:00:00 2001 From: Harshith Mullapudi <17528887+harshithmullapudi@users.noreply.github.com> Date: Sat, 7 Nov 2026 23:08:13 +0000 Subject: [PATCH 3/3] refactor(desktop): read the snapshot for a chat row through one view TurnGroupSection now reads the snapshot only through rowSnapshotView, and the row memo compares two views. The comparator can no longer drift from what the row renders, and approval rows compare their resolved asker and target instead of the items map identity. Refs SUPER-2637 --- .../utils/sameRowSnapshot/sameRowSnapshot.ts | 35 +++-------- .../TurnGroupSection/TurnGroupSection.tsx | 28 +++------ .../Transcript/utils/rowSnapshotView/index.ts | 1 + .../utils/rowSnapshotView/rowSnapshotView.ts | 63 +++++++++++++++++++ 4 files changed, 83 insertions(+), 44 deletions(-) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatSession/components/Transcript/utils/rowSnapshotView/index.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatSession/components/Transcript/utils/rowSnapshotView/rowSnapshotView.ts diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatSession/components/Transcript/components/TranscriptRowItem/utils/sameRowSnapshot/sameRowSnapshot.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatSession/components/Transcript/components/TranscriptRowItem/utils/sameRowSnapshot/sameRowSnapshot.ts index 2a41cae075b..200853ad4fa 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatSession/components/Transcript/components/TranscriptRowItem/utils/sameRowSnapshot/sameRowSnapshot.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatSession/components/Transcript/components/TranscriptRowItem/utils/sameRowSnapshot/sameRowSnapshot.ts @@ -1,35 +1,20 @@ -import { displayText, type SessionSnapshot } from "@superset/chat/core"; +import type { SessionSnapshot } from "@superset/chat/core"; +import { rowSnapshotView } from "../../../../utils/rowSnapshotView"; import type { TranscriptRow } from "../../../../utils/transcriptRows"; -function textItemIds(row: TranscriptRow): string[] { - switch (row.kind) { - case "item": - case "reply_footer": - return [row.item.id]; - case "tool_run": - return row.steps - .filter((step) => step.kind === "reasoning") - .map((step) => step.id); - default: - return []; - } -} - -/** Mirrors what `TurnGroupSection` reads from the snapshot for `row`. */ export function sameRowSnapshot( row: TranscriptRow, previous: SessionSnapshot, next: SessionSnapshot, ): boolean { if (previous === next) return true; - if (previous.session?.harness !== next.session?.harness) return false; - if ( - row.kind === "item" && - row.item.kind === "approval_request" && - previous.items !== next.items - ) - return false; - return textItemIds(row).every( - (id) => displayText(previous, id) === displayText(next, id), + const before = rowSnapshotView(row, previous); + const after = rowSnapshotView(row, next); + return ( + before.harness === after.harness && + before.asker === after.asker && + before.approvalTarget === after.approvalTarget && + before.texts.length === after.texts.length && + before.texts.every((text, index) => text === after.texts[index]) ); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatSession/components/Transcript/components/TurnGroupSection/TurnGroupSection.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatSession/components/Transcript/components/TurnGroupSection/TurnGroupSection.tsx index c77848eb27a..b272e4aee6a 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatSession/components/Transcript/components/TurnGroupSection/TurnGroupSection.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatSession/components/Transcript/components/TurnGroupSection/TurnGroupSection.tsx @@ -1,8 +1,6 @@ import { useLingui } from "@lingui/react/macro"; import type { OutboxEntry, SessionSnapshot } from "@superset/chat/core"; -import { approvalAsker, displayText } from "@superset/chat/core"; import type { - ApprovalRequest, AvailableCommand, Decision, Item, @@ -14,6 +12,7 @@ import type { ReactNode } from "react"; import type { ChatForkTarget } from "../../../../types"; import type { PageLink } from "../../../../utils/pageLinks"; import { PageLinkCard } from "../../../PageLinkCard"; +import { rowSnapshotView } from "../../utils/rowSnapshotView"; import type { TranscriptRow } from "../../utils/transcriptRows"; import { AttachmentRail, type RailImage } from "../AttachmentRail"; import { ReplyFooter } from "../ReplyFooter"; @@ -59,15 +58,6 @@ function imageRail(tools: readonly ToolCall[]): ReactNode { ) : null; } -function approvalTarget( - snapshot: SessionSnapshot, - item: Item, -): Item | undefined { - if (item.kind !== "approval_request") return undefined; - const targetId = (item as ApprovalRequest).targetItemId; - return targetId ? snapshot.items.get(targetId)?.item : undefined; -} - export type TurnGroupSectionProps = { row: TranscriptRow; lastReply: boolean; @@ -104,7 +94,10 @@ export function TurnGroupSection({ snapshot, }: TurnGroupSectionProps) { const { t } = useLingui(); - const harness = snapshot.session?.harness; + const { harness, texts, asker, approvalTarget } = rowSnapshotView( + row, + snapshot, + ); switch (row.kind) { case "working": return ( @@ -115,12 +108,11 @@ export function TurnGroupSection({ /> ); case "item": { - const asker = approvalAsker(snapshot.items, row.item); return ( <> {row.item.kind === "tool_call" && imageRail([row.item as ToolCall])} {pageCards(row.pages)} @@ -166,9 +158,7 @@ export function TurnGroupSection({ onToggle={onToggleEntry} rowKey={row.key} steps={row.steps} - stepTexts={row.steps.map((step) => - step.kind === "reasoning" ? displayText(snapshot, step.id) : "", - )} + stepTexts={texts} /> {imageRail(row.items)} {pageCards(row.pages)} @@ -199,7 +189,7 @@ export function TurnGroupSection({ className="hover:opacity-100" item={row.item} onFork={onFork} - text={displayText(snapshot, row.item.id)} + text={texts[0] ?? ""} /> ); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatSession/components/Transcript/utils/rowSnapshotView/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatSession/components/Transcript/utils/rowSnapshotView/index.ts new file mode 100644 index 00000000000..34b68617ec1 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatSession/components/Transcript/utils/rowSnapshotView/index.ts @@ -0,0 +1 @@ +export { type RowSnapshotView, rowSnapshotView } from "./rowSnapshotView"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatSession/components/Transcript/utils/rowSnapshotView/rowSnapshotView.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatSession/components/Transcript/utils/rowSnapshotView/rowSnapshotView.ts new file mode 100644 index 00000000000..1f02c4f90c5 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatSession/components/Transcript/utils/rowSnapshotView/rowSnapshotView.ts @@ -0,0 +1,63 @@ +import { + approvalAsker, + displayText, + type SessionSnapshot, +} from "@superset/chat/core"; +import type { ApprovalRequest, Item, ToolCall } from "@superset/chat/protocol"; +import type { TranscriptRow } from "../transcriptRows"; + +export type RowSnapshotView = { + harness: string | undefined; + /** Per item for item and footer rows, per step for tool runs. */ + texts: readonly string[]; + asker: ToolCall | undefined; + approvalTarget: Item | undefined; +}; + +function approvalTarget( + snapshot: SessionSnapshot, + item: Item, +): Item | undefined { + if (item.kind !== "approval_request") return undefined; + const targetId = (item as ApprovalRequest).targetItemId; + return targetId ? snapshot.items.get(targetId)?.item : undefined; +} + +export function rowSnapshotView( + row: TranscriptRow, + snapshot: SessionSnapshot, +): RowSnapshotView { + const harness = snapshot.session?.harness; + switch (row.kind) { + case "item": + return { + harness, + texts: [displayText(snapshot, row.item.id)], + asker: approvalAsker(snapshot.items, row.item), + approvalTarget: approvalTarget(snapshot, row.item), + }; + case "reply_footer": + return { + harness, + texts: [displayText(snapshot, row.item.id)], + asker: undefined, + approvalTarget: undefined, + }; + case "tool_run": + return { + harness, + texts: row.steps.map((step) => + step.kind === "reasoning" ? displayText(snapshot, step.id) : "", + ), + asker: undefined, + approvalTarget: undefined, + }; + default: + return { + harness, + texts: [], + asker: undefined, + approvalTarget: undefined, + }; + } +}