import { AgentTranscript, type AgentTranscriptClassNames, type TranscriptEntry } from "@sandbox-agent/react"; import { memo, useEffect, useMemo, type MutableRefObject, type RefObject } from "react"; import { useStyletron } from "baseui"; import { LabelSmall, LabelXSmall } from "baseui/typography"; import { Copy } from "lucide-react"; import { useFoundryTokens } from "../../app/theme"; import { HistoryMinimap } from "./history-minimap"; import { SpinnerDot } from "./ui"; import { buildDisplayMessages, formatMessageDuration, formatMessageTimestamp, type AgentSession, type HistoryEvent, type Message } from "./view-model"; const TranscriptMessageBody = memo(function TranscriptMessageBody({ message, messageRefs, copiedMessageId, onCopyMessage, isTarget, onTargetRendered, }: { message: Message; messageRefs: MutableRefObject>; copiedMessageId: string | null; onCopyMessage: (message: Message) => void; isTarget?: boolean; onTargetRendered?: () => void; }) { const [css] = useStyletron(); const t = useFoundryTokens(); const isUser = message.sender === "client"; const isCopied = copiedMessageId === message.id; const messageTimestamp = formatMessageTimestamp(message.createdAtMs); const displayFooter = isUser ? messageTimestamp : message.durationMs ? `${messageTimestamp} • Took ${formatMessageDuration(message.durationMs)}` : null; useEffect(() => { if (!isTarget) { return; } const targetNode = messageRefs.current.get(message.id); if (!targetNode) { return; } targetNode.scrollIntoView({ behavior: "smooth", block: "center" }); onTargetRendered?.(); }, [isTarget, message.id, messageRefs, onTargetRendered]); return (
{ if (node) { messageRefs.current.set(message.id, node); } else { messageRefs.current.delete(message.id); } }} className={css({ display: "flex", flexDirection: "column", alignItems: isUser ? "flex-end" : "flex-start", gap: "6px", })} >
{message.text}
{displayFooter ? ( {displayFooter} ) : null}
); }); export const MessageList = memo(function MessageList({ session, scrollRef, messageRefs, historyEvents, onSelectHistoryEvent, targetMessageId, onTargetMessageResolved, copiedMessageId, onCopyMessage, thinkingTimerLabel, pendingMessage, }: { session: AgentSession | null | undefined; scrollRef: RefObject; messageRefs: MutableRefObject>; historyEvents: HistoryEvent[]; onSelectHistoryEvent: (event: HistoryEvent) => void; targetMessageId?: string | null; onTargetMessageResolved?: () => void; copiedMessageId: string | null; onCopyMessage: (message: Message) => void; thinkingTimerLabel: string | null; pendingMessage: { text: string; sentAt: number } | null; }) { const [css] = useStyletron(); const t = useFoundryTokens(); const PENDING_MESSAGE_ID = "__pending__"; const messages = useMemo(() => buildDisplayMessages(session), [session]); const messagesById = useMemo(() => new Map(messages.map((message) => [message.id, message])), [messages]); const messageIndexById = useMemo(() => new Map(messages.map((message, index) => [message.id, index])), [messages]); const transcriptEntries = useMemo(() => { const entries: TranscriptEntry[] = messages.map((message) => ({ id: message.id, eventId: message.id, kind: "message", time: new Date(message.createdAtMs).toISOString(), role: message.sender === "client" ? "user" : "assistant", text: message.text, })); if (pendingMessage) { entries.push({ id: PENDING_MESSAGE_ID, eventId: PENDING_MESSAGE_ID, kind: "message", time: new Date(pendingMessage.sentAt).toISOString(), role: "user", text: pendingMessage.text, }); } return entries; }, [messages, pendingMessage]); const messageContentClass = css({ maxWidth: "100%", display: "flex", flexDirection: "column", }); const transcriptClassNames: Partial = { root: css({ display: "flex", flexDirection: "column", gap: "12px", }), message: css({ display: "flex", }), messageContent: messageContentClass, messageText: css({ width: "100%", }), thinkingRow: css({ display: "flex", alignItems: "center", gap: "8px", padding: "4px 0", }), thinkingIndicator: css({ display: "flex", alignItems: "center", gap: "8px", color: t.accent, fontSize: "11px", fontFamily: '"IBM Plex Mono", monospace', letterSpacing: "0.01em", }), }; const scrollContainerClass = css({ padding: "16px 52px 16px 20px", display: "flex", flexDirection: "column", flex: 1, minHeight: 0, overflowY: "auto", }); useEffect(() => { if (!targetMessageId) { return; } const targetNode = messageRefs.current.get(targetMessageId); if (targetNode) { targetNode.scrollIntoView({ behavior: "smooth", block: "center" }); onTargetMessageResolved?.(); return; } const targetIndex = messageIndexById.get(targetMessageId); if (targetIndex == null) { return; } scrollRef.current?.scrollTo({ top: Math.max(0, targetIndex * 88), behavior: "smooth", }); }, [messageIndexById, messageRefs, onTargetMessageResolved, scrollRef, targetMessageId]); return ( <> {historyEvents.length > 0 ? : null}
{session && transcriptEntries.length === 0 ? (
{!session?.created ? "Choose an agent and model, then send your first message" : "No messages yet in this session"}
) : ( { if (entry.id === PENDING_MESSAGE_ID && pendingMessage) { const pendingMsg: Message = { id: PENDING_MESSAGE_ID, sender: "client", text: pendingMessage.text, createdAtMs: pendingMessage.sentAt, event: { id: PENDING_MESSAGE_ID, eventIndex: -1, sessionId: "", connectionId: "", sender: "client", createdAt: pendingMessage.sentAt, payload: {}, }, }; return (
); } const message = messagesById.get(entry.id); if (!message) { return null; } return ( ); }} isThinking={Boolean((session && session.status === "running" && transcriptEntries.length > 0) || pendingMessage)} renderThinkingState={() => (
Agent is thinking {thinkingTimerLabel ? ( {thinkingTimerLabel} ) : null}
)} /> )}
); });