diff --git a/ui/src/components/TaskChatThread.test.tsx b/ui/src/components/TaskChatThread.test.tsx index eff64477a91..fea9dc6e999 100644 --- a/ui/src/components/TaskChatThread.test.tsx +++ b/ui/src/components/TaskChatThread.test.tsx @@ -3471,9 +3471,9 @@ describe("TaskChatThread live transcript", () => { "Streaming through the shared renderer", ); const phaseSummary = tail!.querySelector( - '[data-testid="task-chat-phase-summary"]', + '[data-testid="task-chat-activity-phase-toggle"]', ); - expect(phaseSummary?.getAttribute("aria-expanded")).toBe("true"); + expect(phaseSummary?.getAttribute("aria-expanded")).toBe("false"); expect(tail!.textContent).toContain("src/app.ts"); // None of the debug plumbing reaches the thread. for (const noise of [ diff --git a/ui/src/components/task-chat/TaskChatLiveRunPill.tsx b/ui/src/components/task-chat/TaskChatLiveRunPill.tsx index 8c58ff6b7a1..310f47e8a12 100644 --- a/ui/src/components/task-chat/TaskChatLiveRunPill.tsx +++ b/ui/src/components/task-chat/TaskChatLiveRunPill.tsx @@ -73,14 +73,16 @@ export function TaskChatLiveRunPill({ return (
{active ? ( - + + + ) : ( - + )} diff --git a/ui/src/components/task-chat/TaskChatLiveTail.test.tsx b/ui/src/components/task-chat/TaskChatLiveTail.test.tsx index e2a43a4a3f4..f7b427a81f5 100644 --- a/ui/src/components/task-chat/TaskChatLiveTail.test.tsx +++ b/ui/src/components/task-chat/TaskChatLiveTail.test.tsx @@ -5,6 +5,7 @@ import { createRoot, type Root } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import type { TranscriptEntry } from "@/adapters"; import { ThemeProvider } from "@/context/ThemeContext"; +import { MemoryRouter } from "@/lib/router"; import { TaskChatLiveTail } from "./TaskChatLiveTail"; import { transcriptToTaskChatItems } from "./transcript-adapter"; import type { TaskChatItem } from "./task-chat-model"; @@ -30,9 +31,11 @@ describe("TaskChatLiveTail", () => { function render(items: TaskChatItem[], emptyMessage?: string) { flushSync(() => root!.render( - - - , + + + + + , ), ); } @@ -41,7 +44,18 @@ describe("TaskChatLiveTail", () => { return transcriptToTaskChatItems(entries, { runId: "run-1", running }); } - it("renders streamed reply markdown and tool cards from a live transcript", () => { + const toggle = () => container.querySelector( + '[data-testid="task-chat-activity-phase-toggle"]', + )!; + const viewport = () => container.querySelector( + '[data-testid="task-chat-activity-viewport"]', + )!; + const expandFirstDetail = () => { + flushSync(() => toggle().click()); + flushSync(() => container.querySelector("li button")!.click()); + }; + + it("renders streamed reply markdown with one compact activity, without old tool cards", () => { const items = parse([ { kind: "assistant", ts: TS, text: "Looking into the failing test." }, { kind: "tool_call", ts: TS, name: "Read", toolUseId: "t1", input: { file_path: "src/app.ts" } }, @@ -52,11 +66,11 @@ describe("TaskChatLiveTail", () => { expect(container.querySelector('[data-testid="task-chat-phase-interstitial"]')?.textContent).toContain( "Looking into the failing test.", ); - const phaseSummary = container.querySelector('[data-testid="task-chat-phase-summary"]'); - expect(phaseSummary?.getAttribute("aria-expanded")).toBe("true"); - // Tool row renders with its name + mono target. - expect(container.textContent).toContain("Read"); - expect(container.textContent).toContain("src/app.ts"); + expect(toggle().getAttribute("aria-expanded")).toBe("false"); + expect(viewport().textContent).toContain("Read a file"); + expect(viewport().textContent).toContain("src/app.ts"); + expect(container.querySelector('[data-testid="task-chat-tool-card"]')).toBeNull(); + expect(container.querySelector('[data-testid="task-chat-phase-child-rail"]')).toBeNull(); }); it("keeps tool diff bodies out of the activity feed", () => { @@ -67,18 +81,15 @@ describe("TaskChatLiveTail", () => { ]); render(items); - const phaseSummary = container.querySelector('[data-testid="task-chat-phase-summary"]'); - expect(phaseSummary?.getAttribute("aria-expanded")).toBe("true"); + expect(toggle().getAttribute("aria-expanded")).toBe("false"); expect(container.textContent).not.toContain("const x = 1;"); expect(container.textContent).not.toContain("+1 −1"); - const tool = container.querySelector('[data-testid="task-chat-tool-card"] button'); - expect(tool).not.toBeNull(); - flushSync(() => tool?.click()); + expandFirstDetail(); expect(container.textContent).toContain("+1 −1"); expect(container.textContent).not.toContain("const x = 1;"); - expect(container.querySelector('[data-testid="task-chat-tool-change-summary"]')).not.toBeNull(); + expect(container.querySelector('[data-testid="task-chat-runner-activity-detail"]')).not.toBeNull(); }); it("drops the debug plumbing kinds RunTranscriptView surfaced", () => { @@ -110,8 +121,8 @@ describe("TaskChatLiveTail", () => { expect(container.querySelector('[data-testid="task-chat-phase-interstitial"]')?.textContent).toContain( "Here is the real reply.", ); - const phaseSummary = container.querySelector('[data-testid="task-chat-phase-summary"]'); - expect(phaseSummary?.getAttribute("aria-expanded")).toBe("true"); + expect(toggle().getAttribute("aria-expanded")).toBe("false"); + flushSync(() => toggle().click()); const text = container.textContent ?? ""; expect(text).toContain("Here is the real reply."); expect(text).toContain("pnpm test"); @@ -130,49 +141,64 @@ describe("TaskChatLiveTail", () => { } }); - it("renders provider-supplied reasoning in the live activity stream", () => { - const items = parse([ - { kind: "thinking", ts: TS, text: "Provider reasoning summary" }, - { kind: "assistant", ts: TS, text: "Visible answer." }, - ]); - expect(items[0]).toMatchObject({ kind: "thinking", streaming: false }); - render(items); + it("updates only the latest reasoning line in place, with full text available on expansion", () => { + const entries: TranscriptEntry[] = [ + { kind: "thinking", ts: TS, itemId: "reasoning", text: "First line\nChecking" }, + ]; + render(parse(entries)); + const row = viewport().querySelector("[data-activity-row]"); + entries.push({ kind: "thinking", ts: TS, itemId: "reasoning", text: " the file", delta: true }); + render(parse(entries)); - expect(container.textContent).toContain("Visible answer."); - const reasoningPhase = container.querySelector( - '[data-testid="task-chat-phase-summary"]', - ); - expect(reasoningPhase?.textContent).toContain("Reasoning"); - expect(reasoningPhase?.getAttribute("aria-expanded")).toBe("true"); + expect(viewport().querySelector("[data-activity-row]")).toBe(row); + expect(viewport().textContent).toContain("Checking the file"); + expect(container.textContent).not.toContain("First line"); + expect(container.querySelector(".runner-activity-roll-in")).toBeNull(); + expect(toggle().getAttribute("aria-expanded")).toBe("false"); + expandFirstDetail(); + expect(container.textContent).toContain("First line"); + expect(container.textContent).toContain("Checking the file"); + expect(container.querySelector('[data-testid="task-chat-phase-child-rail"]')).toBeNull(); + }); - expect(container.textContent).toContain("Provider reasoning summary"); - const thinking = container.querySelector('[data-testid="task-chat-thinking"]'); - expect(thinking?.getAttribute("data-state")).toBe("settled"); - expect( - thinking?.querySelector('[data-testid="task-chat-thinking-text"]') - ?.textContent, - ).toContain("Provider reasoning summary"); - const phaseChildren = thinking?.closest( - '[data-testid="task-chat-phase-children"]', + it("rolls successive CLI commands and image calls through one row without accumulating history", () => { + const entries: TranscriptEntry[] = [ + { kind: "assistant", ts: TS, text: "Checking the fix.", channel: "progress" }, + { kind: "tool_call", ts: TS, name: "exec_command", toolUseId: "t1", input: { command: "git status" } }, + ]; + render(parse(entries)); + const first = viewport().querySelector("[data-activity-row]"); + entries.push({ kind: "tool_result", ts: TS, toolUseId: "t1", content: "clean", isError: false }); + render(parse(entries)); + expect(viewport().querySelector("[data-activity-row]")).toBe(first); + expect(viewport().textContent).toContain("Ran a command"); + expect(container.querySelector(".runner-activity-roll-in")).toBeNull(); + + entries.push({ kind: "tool_call", ts: TS, name: "Bash", toolUseId: "t2", input: { command: "pnpm test" } }); + render(parse(entries)); + expect(container.querySelector(".runner-activity-roll-in")?.textContent).toContain("Running a command"); + const outgoing = container.querySelector(".runner-activity-roll-out")!; + expect(outgoing.getAttribute("aria-hidden")).toBe("true"); + flushSync(() => outgoing.dispatchEvent(new Event("webkitAnimationEnd", { bubbles: true }))); + expect(viewport().querySelectorAll("[data-activity-row]")).toHaveLength(1); + expect(container.textContent).not.toContain("git status"); + expect(container.querySelector('[data-testid="task-chat-runner-activity-list"]')).toBeNull(); + + entries.push( + { kind: "tool_result", ts: TS, toolUseId: "t2", content: "passed", isError: false }, + { kind: "tool_call", ts: TS, name: "image_generation", toolUseId: "t3", input: { file_path: "preview.png" } }, ); - expect(phaseChildren?.classList.contains("ml-2.5")).toBe(true); - expect(phaseChildren?.classList.contains("pl-6")).toBe(true); - expect( - phaseChildren?.querySelector('[data-testid="task-chat-phase-child-rail"]'), - ).not.toBeNull(); - expect(thinking?.textContent).not.toContain("Reasoning"); - expect(thinking?.querySelector(".shimmer-text")).toBeNull(); - expect( - thinking - ?.querySelector('[data-testid="task-chat-thinking-icon"]') - ?.classList.contains("text-(--status-agent-running)"), - ).toBe(false); - expect( - thinking - ?.querySelector('[data-testid="task-chat-thinking-icon"]') - ?.classList.contains("text-muted-foreground/50"), - ).toBe(true); - expect(thinking?.querySelector(".task-chat-reasoning-markdown")).toBeNull(); + render(parse(entries)); + expect(container.querySelector(".runner-activity-roll-in")?.textContent).toContain("Generating an image"); + flushSync(() => toggle().click()); + expect(container.querySelectorAll("li")).toHaveLength(3); + expect(container.textContent).toContain("git status"); + expect(container.textContent).toContain("pnpm test"); + + entries.push({ kind: "tool_result", ts: TS, toolUseId: "t3", content: "created", isError: false }); + render(parse(entries)); + expect(toggle().getAttribute("aria-expanded")).toBe("true"); + expect(container.textContent).toContain("Generated an image"); }); it("bounds long tool targets and wraps the full value only when expanded", () => { @@ -194,26 +220,21 @@ describe("TaskChatLiveTail", () => { .find((item) => item.kind === "tool")?.target; render(items); - const tool = container.querySelector( - '[data-testid="task-chat-tool-card"] button', - ); - const collapsedTarget = tool?.querySelector( - ".task-chat-collapsed-line-fade", - ); - expect(tool?.classList.contains("overflow-hidden")).toBe(true); - expect(tool?.getAttribute("aria-expanded")).toBe("false"); + const collapsedTarget = viewport().querySelector("[title]"); + expect(collapsedTarget?.classList.contains("truncate")).toBe(true); + expect(toggle().getAttribute("aria-expanded")).toBe("false"); expect(renderedTarget).toBeTruthy(); expect(collapsedTarget?.textContent).toBe(renderedTarget); - flushSync(() => tool?.click()); + expandFirstDetail(); const expandedTarget = container.querySelector( - '[data-testid="task-chat-tool-target-detail"]', + '[data-testid="task-chat-runner-activity-detail"] p', ); - expect(tool?.getAttribute("aria-expanded")).toBe("true"); + expect(toggle().getAttribute("aria-expanded")).toBe("true"); expect(expandedTarget?.textContent).toBe(renderedTarget); expect( - expandedTarget?.classList.contains("task-chat-expanded-line-wrap"), + expandedTarget?.classList.contains("break-all"), ).toBe(true); }); diff --git a/ui/src/components/task-chat/TaskChatLiveTail.tsx b/ui/src/components/task-chat/TaskChatLiveTail.tsx index 2a84e119f6f..28b73383c30 100644 --- a/ui/src/components/task-chat/TaskChatLiveTail.tsx +++ b/ui/src/components/task-chat/TaskChatLiveTail.tsx @@ -3,26 +3,16 @@ import { MarkdownBody } from "@/components/MarkdownBody"; import type { TaskChatItem, TaskChatRuntimeRequestDecision, TaskChatRuntimeRequestItem } from "./task-chat-model"; import { TaskChatToolCard } from "./TaskChatToolCard"; import { TaskChatUsageReadout } from "./TaskChatUsageReadout"; -import { TaskChatActivityPhase } from "./TaskChatActivityPhase"; +import { TaskChatRunnerActivityGroup } from "./TaskChatRunnerActivityGroup"; import { TaskChatThinking } from "./TaskChatThinking"; -import { TaskChatMarker } from "./TaskChatMarker"; import { TaskChatProtocolCard } from "./TaskChatProtocolCard"; -import { TaskChatProtocolActivityRow } from "./TaskChatProtocolActivityRow"; import { buildTurnTimelineRows } from "./transcript-adapter"; /** - * Live-tail body for the experimental chat-style view (PAP-463, Workstream C1 - * of PAP-458). - * - * Renders the in-flight run's streaming transcript as the SAME clean rows the - * settled thread uses — tool cards (with diffs) and the streamed reply markdown - * — instead of the verbatim `RunTranscriptView` debug viewer that the live tail - * used since `e4f3d7733`. The items come from `transcriptToTaskChatItems`, which - * already drops the debug plumbing (init / stdout / stderr / system / user / - * result), so none of `RunTranscriptView`'s noise can reach the thread: no INIT - * row, no "N LOG LINES" / "N SYSTEM MESSAGES" banners, no raw stdout/JSON dumps, - * no "Streaming" chip, no uppercase "USED TERMINAL" cards. The status pill above - * this body (`TaskChatLiveRunPill`) owns the run-status affordance. + * Live body for legacy runner transcripts. The adapter drops debug plumbing; + * the same activity group as the native runner shows one rolling current row + * with friendly tool labels and explicitly expandable history. The status pill + * above this body (`TaskChatLiveRunPill`) owns the run-status affordance. * * Stable assistant and runtime-request boundaries compact the rows into an * ordered turn timeline. Commentary remains readable above the activity group @@ -102,27 +92,9 @@ function renderTailRow( return ; case "activity_phase": return ( - - child.kind === "thinking" && - child.lines.some((line) => line.trim().length > 0), - )} - childrenClassName="relative ml-2.5 pl-6" - showChildRail - renderChild={(child) => child.kind === "tool" - ? - : child.kind === "thinking" - ? - : child.kind === "marker" - ? - : child.kind === "protocol" - ? child.surface === "runtime_request" - ? - : - : } /> ); case "protocol": diff --git a/ui/src/components/task-chat/TaskChatRunnerActivityGroup.test.tsx b/ui/src/components/task-chat/TaskChatRunnerActivityGroup.test.tsx index 132451e0cd1..7e26c7730f4 100644 --- a/ui/src/components/task-chat/TaskChatRunnerActivityGroup.test.tsx +++ b/ui/src/components/task-chat/TaskChatRunnerActivityGroup.test.tsx @@ -209,6 +209,45 @@ describe("TaskChatRunnerActivityGroup", () => { expect(toggle().textContent).not.toMatch(/\d+ failed/); }); + it("uses the compact runner group for a legacy persisted turn", () => { + act(() => + root.render( + + + + + + + , + ), + ); + + act(() => + container + .querySelector('[data-testid="task-chat-turn-summary"]')! + .click(), + ); + expect(container.querySelector('[data-testid="task-chat-activity-phase-toggle"]')).not.toBeNull(); + expect(container.querySelector('[data-testid="task-chat-phase-summary"]')).toBeNull(); + expect(container.textContent).toContain("Ran command"); + expect(container.textContent).not.toContain("command-legacy"); + }); + it("does not offer empty disclosures for sparse activities", () => { render([ { id: "thinking-empty", kind: "thinking", lines: [], streaming: true }, diff --git a/ui/src/components/task-chat/TaskChatThreadView.tsx b/ui/src/components/task-chat/TaskChatThreadView.tsx index 34bf4a1a735..372ce7d0513 100644 --- a/ui/src/components/task-chat/TaskChatThreadView.tsx +++ b/ui/src/components/task-chat/TaskChatThreadView.tsx @@ -195,35 +195,11 @@ function renderItem( case "usage": return ; case "activity_phase": - if (activityAppearance === "runner") return ; - return ( - - child.kind === "protocol" && child.surface !== "runtime_request" ? ( - - ) : ( - renderItem( - child, - onApprovalDecision, - undefined, - undefined, - undefined, - undefined, - onRuntimeRequestDecision, - activityAppearance, - undefined, - false, - undefined, - undefined, - undefined, - attachments, - ) - ) - } - /> - ); + // Legacy adapter transcripts and native runner transcripts now share the + // same compact activity treatment. Keeping this decision at the common + // renderer boundary also gives old persisted runs the current taxonomy, + // alignment, one-line targets, and collapsed-by-default behavior. + return ; case "interaction": return renderInteraction ? renderInteraction(item) : null; case "plan_document": diff --git a/ui/storybook/prototypes/runner-activity/README.md b/ui/storybook/prototypes/runner-activity/README.md index 28b8b5bc587..8aeb53b56f8 100644 --- a/ui/storybook/prototypes/runner-activity/README.md +++ b/ui/storybook/prototypes/runner-activity/README.md @@ -1,6 +1,7 @@ # Runner activity review Open **Tasks / Runner activity preview / 01 · Desktop live · animated** in Storybook. +Compare **05 · Legacy live · animated**, which uses the actual legacy live renderer. Use Pause / Next to step through the fixture, or Replay to watch the transitions. From this worktree, start the preview with: @@ -24,7 +25,12 @@ pnpm --filter @paperclipai/ui exec storybook dev --port 6024 --host 127.0.0.1 -- not leave the desktop animation squeezed into a mobile preview. - Reduced motion uses immediate replacement instead of the rolling transition. -The fixture renders the production `TaskChatRunnerTurn` and activity group, with -simulated event timing. It does not invoke a runner. Production integration tests +Native stories render `TaskChatRunnerTurn`. Live legacy stories feed raw CLI +transcript events through `transcriptToTaskChatItems` into `TaskChatLiveTail` and +`TaskChatLiveRunPill`. Both use the same production activity group. Legacy stories +cover live playback, expanded history, narrow long labels, light mode, and completed +turns. Only the completed story uses the saved-thread renderer from the start. + +Event timing is simulated; the fixture does not invoke a runner. Production integration tests cover commentary boundaries, approvals, final replies, retained expansion, neutral failures, and reduced motion. diff --git a/ui/storybook/prototypes/runner-activity/RunnerActivityPreview.tsx b/ui/storybook/prototypes/runner-activity/RunnerActivityPreview.tsx index 4f397d91c73..72d5f8e2cab 100644 --- a/ui/storybook/prototypes/runner-activity/RunnerActivityPreview.tsx +++ b/ui/storybook/prototypes/runner-activity/RunnerActivityPreview.tsx @@ -3,11 +3,15 @@ import { Pause, Play, RotateCcw, StepForward } from "lucide-react"; import { useReducedMotion } from "motion/react"; import { TaskChatThreadView } from "@/components/task-chat/TaskChatThreadView"; import { TaskChatRunnerTurn } from "@/components/task-chat/TaskChatRunnerTurn"; +import { TaskChatLiveTail } from "@/components/task-chat/TaskChatLiveTail"; +import { TaskChatLiveRunPill, toolCountSummaryFromEntries } from "@/components/task-chat/TaskChatLiveRunPill"; import { TaskChatExpansionState } from "@/components/task-chat/expansion-state"; -import { buildTurnTimelineRows } from "@/components/task-chat/transcript-adapter"; +import { buildTurnTimelineRows, transcriptToTaskChatItems } from "@/components/task-chat/transcript-adapter"; +import type { TranscriptEntry } from "@/adapters"; import type { TaskChatItem, TaskChatMessageItem, + TaskChatTurnItem, } from "@/components/task-chat/task-chat-model"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; @@ -48,7 +52,7 @@ const entries: Entry[] = [ kind: "activity", id: "read", tool: "read", - target: "TaskChatActivityPhase.tsx", + target: "TaskChatRunnerActivityGroup.tsx", detail: "The activity phase owns expansion. Individual tool rows have separate icon widths and padding.", }, @@ -104,10 +108,10 @@ const entries: Entry[] = [ { kind: "activity", id: "image", - tool: "view_image", + tool: "image_generation", target: "runner-activity-mobile.png", detail: - "Reviewed the narrow layout: tool paths truncate in both modes. Click a row to inspect its full target and detail.", + "Generated an image to check the activity feed’s image-tool label and icon.", }, { kind: "commentary", @@ -116,6 +120,35 @@ const entries: Entry[] = [ }, ]; +// Exercise the CLI transcript adapter, including provider names, stable call IDs, +// multi-line reasoning, and tool results, before entering the legacy live path. +function legacyTranscript(visible: Entry[], finished: boolean): TranscriptEntry[] { + const ts = "2026-09-15T12:00:00.000Z"; + return visible.flatMap((entry, index): TranscriptEntry[] => { + if (entry.kind === "commentary") return [{ + kind: "assistant", ts, itemId: entry.id, text: entry.text, + channel: entry.id === "final" ? "final" : "progress", + }]; + const active = !finished && index === visible.length - 1; + if (!entry.tool) return [{ + kind: "thinking", ts, itemId: entry.id, + text: `${entry.target}\n${entry.detail}`, + lifecycle: active ? "started" : "completed", + }]; + const name = entry.tool === "read" ? "Read" : entry.tool; + const input = entry.tool === "exec_command" + ? { command: entry.target } + : { file_path: entry.target }; + const call: TranscriptEntry = { + kind: "tool_call", ts, name, toolUseId: entry.id, input, + }; + return active && !entry.failed ? [call] : [call, { + kind: "tool_result", ts, toolUseId: entry.id, + content: entry.detail, isError: Boolean(entry.failed), + }]; + }); +} + export interface RunnerActivityPreviewProps { initialStep?: number; autoPlay?: boolean; @@ -123,6 +156,7 @@ export interface RunnerActivityPreviewProps { narrow?: boolean; longLabels?: boolean; failed?: boolean; + legacy?: boolean; } export function RunnerActivityPreview({ @@ -132,6 +166,7 @@ export function RunnerActivityPreview({ narrow = false, longLabels = false, failed = false, + legacy = false, }: RunnerActivityPreviewProps) { const [step, setStep] = useState(initialStep); const [playing, setPlaying] = useState(autoPlay); @@ -167,7 +202,10 @@ export function RunnerActivityPreview({ }; }); const memory = useMemo(() => new Map(), [replay]); - const items = visible.map((entry, index): TaskChatItem => { + const transcript = legacyTranscript(visible, finished); + const items = legacy + ? transcriptToTaskChatItems(transcript, { runId: `legacy-${replay}`, running: !finished }) + : visible.map((entry, index): TaskChatItem => { if (entry.kind === "commentary") return { kind: "message", @@ -199,13 +237,32 @@ export function RunnerActivityPreview({ if (row.kind === "activity_phase" && !memory.has(row.id)) memory.set(row.id, true); } + const finalResponse = items.find( + (item): item is TaskChatMessageItem => item.kind === "message" && item.channel === "final", + ); + const savedTurn: TaskChatTurnItem = { + id: "preview-saved-turn", + kind: "turn", + settled: true, + standaloneHeader: !legacy, + agentName: "Engineer", + agentIcon: "code", + items: buildTurnTimelineRows(items, false), + summary: { + durationLabel: "28s", + toolCount: items.filter((item) => item.kind === "tool").length, + added: 0, + removed: 0, + }, + finalResponse: legacy ? undefined : finalResponse, + }; return (

Runner activity

- Production component ·{" "} + {legacy ? "Legacy CLI transcript" : "Native runner"} ·{" "} {reducedMotion ? "Reduced motion" : "One activity at a time"}

@@ -262,28 +319,19 @@ export function RunnerActivityPreview({ {finished ? ( - item.kind === "message" && item.channel === "final", - ), - }, - ]} + items={legacy && finalResponse + ? [{ ...finalResponse, attachedTurn: savedTurn }] + : [savedTurn]} /> + ) : legacy ? ( +
+ + +
) : (