diff --git a/webui/src/components/thread/ThreadMessages.tsx b/webui/src/components/thread/ThreadMessages.tsx index 1ef5c864b..3d3d068f3 100644 --- a/webui/src/components/thread/ThreadMessages.tsx +++ b/webui/src/components/thread/ThreadMessages.tsx @@ -1,4 +1,5 @@ import { MessageBubble } from "@/components/MessageBubble"; +import { cn } from "@/lib/utils"; import type { UIMessage } from "@/lib/types"; interface ThreadMessagesProps { @@ -7,10 +8,30 @@ interface ThreadMessagesProps { export function ThreadMessages({ messages }: ThreadMessagesProps) { return ( -
- {messages.map((message) => ( - - ))} +
+ {messages.map((message, index) => { + const prev = messages[index - 1]; + const compact = isAuxiliaryRow(message) && prev && isAuxiliaryRow(prev); + return ( +
0 && (compact ? "mt-2" : "mt-5"))} + > + +
+ ); + })}
); } + +function isAuxiliaryRow(message: UIMessage): boolean { + return ( + message.kind === "trace" + || ( + message.role === "assistant" + && message.content.trim().length === 0 + && (!!message.reasoning || !!message.reasoningStreaming) + ) + ); +} diff --git a/webui/src/tests/thread-messages.test.tsx b/webui/src/tests/thread-messages.test.tsx new file mode 100644 index 000000000..710b86298 --- /dev/null +++ b/webui/src/tests/thread-messages.test.tsx @@ -0,0 +1,52 @@ +import { render } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import { ThreadMessages } from "@/components/thread/ThreadMessages"; +import type { UIMessage } from "@/lib/types"; + +describe("ThreadMessages", () => { + it("uses compact spacing between consecutive auxiliary rows", () => { + const messages: UIMessage[] = [ + { + id: "r1", + role: "assistant", + content: "", + reasoning: "thinking", + reasoningStreaming: false, + isStreaming: true, + createdAt: Date.now(), + }, + { + id: "t1", + role: "tool", + kind: "trace", + content: "search()", + traces: ["search()"], + createdAt: Date.now(), + }, + { + id: "r2", + role: "assistant", + content: "", + reasoning: "more thinking", + reasoningStreaming: false, + isStreaming: true, + createdAt: Date.now(), + }, + { + id: "a1", + role: "assistant", + content: "final answer", + createdAt: Date.now(), + }, + ]; + + const { container } = render(); + const rows = Array.from(container.firstElementChild?.children ?? []); + + expect(rows[0]).not.toHaveClass("mt-2", "mt-5"); + expect(rows[1]).toHaveClass("mt-2"); + expect(rows[2]).toHaveClass("mt-2"); + expect(rows[3]).toHaveClass("mt-5"); + }); +});