summaryrefslogtreecommitdiffhomepage
path: root/src/features/chat/ui.test.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-07 17:14:40 +0900
committerAdam Malczewski <[email protected]>2026-06-07 17:14:40 +0900
commit2e79dd122e5664353e02e0d33715ae8c1041a379 (patch)
tree737822344118e5c1c840b8399a554a1898f07093 /src/features/chat/ui.test.ts
parentc8c86dbc3fd23001cca7904791ab539300ec60f4 (diff)
downloaddispatch-web-2e79dd122e5664353e02e0d33715ae8c1041a379.tar.gz
dispatch-web-2e79dd122e5664353e02e0d33715ae8c1041a379.zip
feat(chat): restyle thinking — visible bubble, collapse, title swap, persisted open
Thinking renders inside a visible rounded-card bubble (like tool calls), capped to the same max-w-5xl column as assistant text. Uses a DaisyUI checkbox collapse (no arrow/plus icon) with smooth animation. Title reads "Thinking" + loading-dots while the model is actively generating, then flips to "Thoughts" with no dots once done. Open/closed state persists across the generating→completed→sealed transition via stable ordinal keys (per-conversation isolation via {#key} in App). Added optional streaming flag to RenderedChunk (pure selector, only on the accumulating chunk).
Diffstat (limited to 'src/features/chat/ui.test.ts')
-rw-r--r--src/features/chat/ui.test.ts62
1 files changed, 47 insertions, 15 deletions
diff --git a/src/features/chat/ui.test.ts b/src/features/chat/ui.test.ts
index c118115..b31cbf1 100644
--- a/src/features/chat/ui.test.ts
+++ b/src/features/chat/ui.test.ts
@@ -213,38 +213,70 @@ describe("ChatView", () => {
expect(screen.getByText("contents-of-a")).toBeInTheDocument();
});
- it("thinking <details> stays open across a streaming update", async () => {
- const initial: RenderedChunk[] = [
+ it("thinking is a checkbox collapse (no arrow) inside a visible bubble", () => {
+ const chunks: RenderedChunk[] = [
{
seq: null,
role: "assistant",
chunk: { type: "thinking", text: "Let me think..." },
provisional: true,
+ streaming: true,
},
];
- const { rerender } = render(ChatView, { props: { chunks: initial } });
+ const { container } = render(ChatView, { props: { chunks } });
- const details = screen.getByText("Thinking").closest("details");
- expect(details).not.toBeNull();
- expect(details).not.toHaveAttribute("open");
- if (details) details.open = true;
- expect(details).toHaveAttribute("open");
+ const collapse = container.querySelector(".collapse");
+ expect(collapse).not.toBeNull();
+ expect(collapse).not.toHaveClass("collapse-arrow"); // no indicator icon
+ expect(collapse).not.toHaveClass("collapse-plus");
+ // Visible bubble, like tool cards.
+ expect(collapse).toHaveClass("bg-base-200");
+ expect(collapse).toHaveClass("rounded-box");
+ expect(screen.getByRole("checkbox", { name: "Toggle thoughts" })).toBeInTheDocument();
+ });
- const updated: RenderedChunk[] = [
+ it("title is 'Thinking' + dots while streaming, then 'Thoughts' with no dots once complete; open state persists", async () => {
+ const streaming: RenderedChunk[] = [
{
seq: null,
role: "assistant",
- chunk: { type: "thinking", text: "Let me think... step by step" },
+ chunk: { type: "thinking", text: "hmm" },
provisional: true,
+ streaming: true,
},
];
- await rerender({ chunks: updated });
- const detailsAfter = screen.getByText("Thinking").closest("details");
- expect(detailsAfter).not.toBeNull();
- expect(detailsAfter).toHaveAttribute("open");
- expect(detailsAfter).toHaveTextContent("Let me think... step by step");
+ const { container, rerender } = render(ChatView, { props: { chunks: streaming } });
+
+ // Streaming: "Thinking" + loading dots.
+ expect(screen.getByText("Thinking")).toBeInTheDocument();
+ expect(screen.queryByText("Thoughts")).toBeNull();
+ expect(container.querySelector(".loading")).not.toBeNull();
+
+ // Open it.
+ const checkbox = screen.getByRole("checkbox", { name: "Toggle thoughts" });
+ await userEvent.click(checkbox);
+ expect(checkbox).toBeChecked();
+
+ // Transition generating → completed/committed (seq assigned, no longer streaming).
+ await rerender({
+ chunks: [
+ {
+ seq: 1,
+ role: "assistant",
+ chunk: { type: "thinking", text: "hmm, all done" },
+ provisional: false,
+ },
+ ],
+ });
+
+ // Completed: "Thoughts", no dots — and the open state survived the transition.
+ expect(screen.getByText("Thoughts")).toBeInTheDocument();
+ expect(screen.queryByText("Thinking")).toBeNull();
+ expect(container.querySelector(".loading")).toBeNull();
+ expect(screen.getByRole("checkbox", { name: "Toggle thoughts" })).toBeChecked();
+ expect(container).toHaveTextContent("hmm, all done");
});
});