summaryrefslogtreecommitdiffhomepage
path: root/src/features/chat/ui/ChatView.test.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-10-10 01:36:39 +0900
committerAdam Malczewski <[email protected]>2026-10-10 01:36:39 +0900
commit551a7a050e8cd25e795026a1060c9d5b5ea028bf (patch)
treecba29eb5ada26778c09aea7cc767e2ddf665bce8 /src/features/chat/ui/ChatView.test.ts
parentd8381e201cb1407a939b496642a0803aec58a47d (diff)
downloaddispatch-web-551a7a050e8cd25e795026a1060c9d5b5ea028bf.tar.gz
dispatch-web-551a7a050e8cd25e795026a1060c9d5b5ea028bf.zip
perf(chat): fast open of long in-flight turns
- Batch live chat events: fold a burst in one pass per animation frame (rAF + timeout fallback for hidden tabs) instead of one reactive cycle per WS frame. Flushed before send/resync to keep ordering. - De-dup the late-join replay against committed history: provisional chunks of the in-flight turn already persisted (CR-6 per-step persistence) are hidden by aligned-prefix matching (fail-safe: mismatch shows, never hides). - Memoize chunks/turnMetrics getters by state identity. - LazyCollapse: tool/thinking card bodies mount only on first open. - content-visibility:auto on transcript rows. - scripts/probe-replay.ts + bench-pure.ts for measuring the replay path.
Diffstat (limited to 'src/features/chat/ui/ChatView.test.ts')
-rw-r--r--src/features/chat/ui/ChatView.test.ts42
1 files changed, 41 insertions, 1 deletions
diff --git a/src/features/chat/ui/ChatView.test.ts b/src/features/chat/ui/ChatView.test.ts
index eb807c1..fcac312 100644
--- a/src/features/chat/ui/ChatView.test.ts
+++ b/src/features/chat/ui/ChatView.test.ts
@@ -1,16 +1,25 @@
import type { StepId } from "@dispatch/wire";
import { render, screen } from "@testing-library/svelte";
+import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import type { RenderedChunk } from "../../../core/chunks";
import ChatView from "./ChatView.svelte";
+/** Collapsed card bodies render lazily — open every card so its body mounts. */
+async function openAllCollapses(container: HTMLElement): Promise<void> {
+ const boxes = container.querySelectorAll<HTMLInputElement>('.collapse > input[type="checkbox"]');
+ for (const box of boxes) {
+ if (!box.checked) await userEvent.click(box);
+ }
+}
+
// Defensive rendering: a conversation transcript can contain duplicate
// `toolCallId`s (provider double-emission, persisted — e.g. PII_REDACTED_CONVERSATION_SHORT_01). The
// grouped render path MUST dedupe them so the keyed {#each} never throws
// Svelte `each_key_duplicate` (which aborts the reactive flush and makes the
// tab appear dead). See fix-toolcall-duplicate-keys plan Part 1.
describe("ChatView — duplicate toolCallIds", () => {
- it("renders a tool batch with duplicated toolCallIds without throwing, showing one card per unique call", () => {
+ it("renders a tool batch with duplicated toolCallIds without throwing, showing one card per unique call", async () => {
const chunks: RenderedChunk[] = [
{
seq: 1,
@@ -117,6 +126,7 @@ describe("ChatView — duplicate toolCallIds", () => {
];
const { container } = render(ChatView, { props: { chunks } });
+ await openAllCollapses(container);
// No each_key_duplicate crash. Batched calls render as collapsible cards —
// exactly ONE per unique toolCallId (deduped), not 4.
@@ -130,3 +140,33 @@ describe("ChatView — duplicate toolCallIds", () => {
expect(screen.getByText("contents-of-b")).toBeInTheDocument();
});
});
+
+describe("ChatView — lazy collapse bodies", () => {
+ it("does not mount a collapsed card's body until it is first opened, then keeps it", async () => {
+ const chunks: RenderedChunk[] = [
+ {
+ seq: 1,
+ role: "tool",
+ chunk: {
+ type: "tool-result",
+ toolCallId: "x",
+ toolName: "run_shell",
+ content: "HUGE-OUTPUT",
+ isError: false,
+ },
+ provisional: false,
+ },
+ ];
+ const { container } = render(ChatView, { props: { chunks } });
+ expect(screen.getByText("run_shell")).toBeInTheDocument();
+ expect(screen.queryByText("HUGE-OUTPUT")).toBeNull();
+
+ const box = screen.getByRole("checkbox", { name: "Toggle tool result" });
+ await userEvent.click(box);
+ expect(screen.getByText("HUGE-OUTPUT")).toBeInTheDocument();
+
+ await userEvent.click(box); // close: body stays mounted (close animation)
+ expect(screen.getByText("HUGE-OUTPUT")).toBeInTheDocument();
+ expect(container.querySelectorAll(".chat-row")).toHaveLength(1);
+ });
+});