TypeScript · 2557 bytes Raw Blame History
1 import { Virtuoso } from "react-virtuoso";
2
3 import type { Message } from "@/lib/ipc/types";
4
5 import { AssistantCard } from "./messages/AssistantCard";
6 import { AttachmentCard } from "./messages/AttachmentCard";
7 import { SystemCard } from "./messages/SystemCard";
8 import { UnknownCard } from "./messages/UnknownCard";
9 import { UserCard } from "./messages/UserCard";
10
11 interface MessageTimelineProps {
12 messages: Message[];
13 /** When true, Virtuoso smoothly follows new output as it's
14 * appended — used during an in-flight chat turn so streamed
15 * tokens stay visible without the user having to scroll. */
16 autoFollow?: boolean;
17 }
18
19 /**
20 * The single source of truth for rendering a session timeline. v1's
21 * chat pane reuses this component — the store appends an in-flight
22 * assistant message with `status: "streaming"` and `autoFollow=true`
23 * scrolls to it as new tokens arrive.
24 */
25 export function MessageTimeline({ messages, autoFollow = false }: MessageTimelineProps) {
26 if (messages.length === 0) {
27 return (
28 <div className="flex h-full items-center justify-center p-4 text-center text-xs text-fg-3">
29 this session has no renderable messages
30 </div>
31 );
32 }
33 return (
34 <Virtuoso
35 data={messages}
36 style={{ height: "100%" }}
37 followOutput={autoFollow ? "smooth" : false}
38 itemContent={(_i, message) => (
39 <div className="mx-auto max-w-4xl px-4 py-2">
40 <MessageCard message={message} />
41 </div>
42 )}
43 />
44 );
45 }
46
47 function MessageCard({ message }: { message: Message }) {
48 switch (message.kind) {
49 case "user":
50 return (
51 <UserCard
52 at={message.at}
53 text={message.text}
54 isMeta={message.isMeta}
55 />
56 );
57 case "assistant":
58 return (
59 <AssistantCard
60 at={message.at}
61 model={message.model}
62 blocks={message.blocks}
63 stopReason={message.stopReason}
64 usage={message.usage}
65 status={message.status}
66 />
67 );
68 case "system":
69 return (
70 <SystemCard
71 at={message.at}
72 text={message.text}
73 subtype={message.subtype}
74 />
75 );
76 case "attachment":
77 return (
78 <AttachmentCard
79 at={message.at}
80 attachmentType={message.attachmentType}
81 hookName={message.hookName}
82 text={message.text}
83 />
84 );
85 case "unknown":
86 return (
87 <UnknownCard at={message.at} rawType={message.rawType} raw={message.raw} />
88 );
89 }
90 }