2026-04-18 18:51:53 +00:00
|
|
|
import { MessageBubble } from "@/components/MessageBubble";
|
2026-05-16 01:14:11 +08:00
|
|
|
import {
|
|
|
|
|
AgentActivityCluster,
|
|
|
|
|
isAgentActivityMember,
|
|
|
|
|
} from "@/components/thread/AgentActivityCluster";
|
2026-04-18 18:51:53 +00:00
|
|
|
import type { UIMessage } from "@/lib/types";
|
|
|
|
|
|
|
|
|
|
interface ThreadMessagesProps {
|
|
|
|
|
messages: UIMessage[];
|
2026-05-16 01:14:11 +08:00
|
|
|
/** When true, agent turn still in flight — keeps activity cluster expanded. */
|
|
|
|
|
isStreaming?: boolean;
|
2026-04-18 18:51:53 +00:00
|
|
|
}
|
|
|
|
|
|
2026-05-16 01:14:11 +08:00
|
|
|
export type DisplayUnit =
|
|
|
|
|
| { type: "cluster"; messages: UIMessage[] }
|
|
|
|
|
| { type: "single"; message: UIMessage };
|
|
|
|
|
|
|
|
|
|
/** True when this unit index is the last assistant text slice before the next user message (or end of thread). */
|
|
|
|
|
export function isFinalAssistantSliceBeforeNextUser(
|
|
|
|
|
units: DisplayUnit[],
|
|
|
|
|
index: number,
|
|
|
|
|
): boolean {
|
|
|
|
|
const u = units[index];
|
|
|
|
|
if (u.type !== "single" || u.message.role !== "assistant") return true;
|
|
|
|
|
for (let j = index + 1; j < units.length; j++) {
|
|
|
|
|
const v = units[j];
|
|
|
|
|
if (v.type === "single" && v.message.role === "user") break;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildDisplayUnits(messages: UIMessage[]): DisplayUnit[] {
|
|
|
|
|
const out: DisplayUnit[] = [];
|
|
|
|
|
let i = 0;
|
|
|
|
|
while (i < messages.length) {
|
|
|
|
|
const m = messages[i];
|
|
|
|
|
if (isAgentActivityMember(m)) {
|
|
|
|
|
const cluster: UIMessage[] = [];
|
|
|
|
|
while (i < messages.length && isAgentActivityMember(messages[i])) {
|
|
|
|
|
cluster.push(messages[i]);
|
|
|
|
|
i += 1;
|
|
|
|
|
}
|
|
|
|
|
out.push({ type: "cluster", messages: cluster });
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
out.push({ type: "single", message: m });
|
|
|
|
|
i += 1;
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function ThreadMessages({ messages, isStreaming = false }: ThreadMessagesProps) {
|
|
|
|
|
const units = buildDisplayUnits(messages);
|
|
|
|
|
|
2026-04-18 18:51:53 +00:00
|
|
|
return (
|
2026-05-13 08:05:34 +00:00
|
|
|
<div className="flex w-full flex-col">
|
2026-05-16 01:14:11 +08:00
|
|
|
{units.map((unit, index) => {
|
|
|
|
|
const prev = units[index - 1];
|
|
|
|
|
const marginTop =
|
|
|
|
|
index > 0
|
|
|
|
|
? marginAfterPrevUnit(prev)
|
|
|
|
|
: "";
|
|
|
|
|
const next = units[index + 1];
|
|
|
|
|
const hasBodyBelow =
|
|
|
|
|
unit.type === "cluster"
|
|
|
|
|
&& next?.type === "single"
|
|
|
|
|
&& next.message.role === "assistant";
|
|
|
|
|
|
2026-05-13 08:05:34 +00:00
|
|
|
return (
|
2026-05-16 01:14:11 +08:00
|
|
|
<div key={unitKey(unit, index)} className={marginTop}>
|
|
|
|
|
{unit.type === "cluster" ? (
|
|
|
|
|
<AgentActivityCluster
|
|
|
|
|
messages={unit.messages}
|
|
|
|
|
isTurnStreaming={isStreaming}
|
|
|
|
|
hasBodyBelow={hasBodyBelow}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<MessageBubble
|
|
|
|
|
message={unit.message}
|
|
|
|
|
showAssistantCopyAction={
|
|
|
|
|
unit.message.role === "assistant"
|
|
|
|
|
? isFinalAssistantSliceBeforeNextUser(units, index)
|
|
|
|
|
: true
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2026-05-13 08:05:34 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
2026-04-18 18:51:53 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-05-13 08:05:34 +00:00
|
|
|
|
2026-05-16 01:14:11 +08:00
|
|
|
function unitKey(unit: DisplayUnit, index: number): string {
|
|
|
|
|
if (unit.type === "cluster") {
|
|
|
|
|
const anchor = unit.messages[0]?.id;
|
|
|
|
|
return anchor != null ? `cluster-${anchor}` : `cluster-idx-${index}`;
|
|
|
|
|
}
|
|
|
|
|
return unit.message.id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function marginAfterPrevUnit(prev: DisplayUnit): string {
|
|
|
|
|
if (prev.type === "cluster") {
|
|
|
|
|
return "mt-4";
|
|
|
|
|
}
|
|
|
|
|
const p = prev.message;
|
|
|
|
|
const denseP =
|
|
|
|
|
p.kind === "trace"
|
2026-05-13 08:05:34 +00:00
|
|
|
|| (
|
2026-05-16 01:14:11 +08:00
|
|
|
p.role === "assistant"
|
|
|
|
|
&& p.content.trim().length === 0
|
|
|
|
|
&& (!!p.reasoning || !!p.reasoningStreaming)
|
|
|
|
|
);
|
|
|
|
|
if (denseP) {
|
|
|
|
|
return "mt-2";
|
|
|
|
|
}
|
|
|
|
|
return "mt-5";
|
2026-05-13 08:05:34 +00:00
|
|
|
}
|