1+ import { CheckIcon , ClipboardDocumentIcon , CodeBracketSquareIcon } from "@heroicons/react/20/solid" ;
12import { lazy , Suspense , useState } from "react" ;
23import { CodeBlock } from "~/components/code/CodeBlock" ;
4+ import { Button } from "~/components/primitives/Buttons" ;
35import { Header3 } from "~/components/primitives/Headers" ;
6+ import { Paragraph } from "~/components/primitives/Paragraph" ;
47import type { DisplayItem , ToolUse } from "./types" ;
58
69// Lazy load streamdown to avoid SSR issues
@@ -16,7 +19,7 @@ const StreamdownRenderer = lazy(() =>
1619
1720export function AIChatMessages ( { items } : { items : DisplayItem [ ] } ) {
1821 return (
19- < div className = "flex flex-col divide-y divide-grid-bright " >
22+ < div className = "flex flex-col gap-1 " >
2023 { items . map ( ( item , i ) => {
2124 switch ( item . type ) {
2225 case "system" :
@@ -37,13 +40,7 @@ export function AIChatMessages({ items }: { items: DisplayItem[] }) {
3740// Section header (shared across all sections)
3841// ---------------------------------------------------------------------------
3942
40- function SectionHeader ( {
41- label,
42- right,
43- } : {
44- label : string ;
45- right ?: React . ReactNode ;
46- } ) {
43+ function SectionHeader ( { label, right } : { label : string ; right ?: React . ReactNode } ) {
4744 return (
4845 < div className = "flex items-center justify-between" >
4946 < Header3 > { label } </ Header3 >
@@ -52,6 +49,14 @@ function SectionHeader({
5249 ) ;
5350}
5451
52+ export function ChatBubble ( { children } : { children : React . ReactNode } ) {
53+ return (
54+ < div className = "rounded-md border border-grid-bright bg-charcoal-750/50 px-3.5 py-2" >
55+ { children }
56+ </ div >
57+ ) ;
58+ }
59+
5560// ---------------------------------------------------------------------------
5661// System
5762// ---------------------------------------------------------------------------
@@ -62,7 +67,7 @@ function SystemSection({ text }: { text: string }) {
6267 const preview = isLong ? text . slice ( 0 , 150 ) + "..." : text ;
6368
6469 return (
65- < div className = "flex flex-col gap-1 py-2.5" >
70+ < div className = "flex flex-col gap-1.5 py-2.5" >
6671 < SectionHeader
6772 label = "System"
6873 right = {
@@ -76,9 +81,11 @@ function SystemSection({ text }: { text: string }) {
7681 ) : undefined
7782 }
7883 />
79- < pre className = "whitespace-pre-wrap text-xs leading-relaxed text-text-dimmed" >
80- { expanded || ! isLong ? text : preview }
81- </ pre >
84+ < ChatBubble >
85+ < Paragraph variant = "small/dimmed" className = "whitespace-pre-wrap" >
86+ { expanded || ! isLong ? text : preview }
87+ </ Paragraph >
88+ </ ChatBubble >
8289 </ div >
8390 ) ;
8491}
@@ -89,9 +96,11 @@ function SystemSection({ text }: { text: string }) {
8996
9097function UserSection ( { text } : { text : string } ) {
9198 return (
92- < div className = "flex flex-col gap-1 py-2.5" >
99+ < div className = "flex flex-col gap-1.5 py-2.5" >
93100 < SectionHeader label = "User" />
94- < p className = "text-sm text-text-bright" > { text } </ p >
101+ < ChatBubble >
102+ < Paragraph variant = "small/dimmed" > { text } </ Paragraph >
103+ </ ChatBubble >
95104 </ div >
96105 ) ;
97106}
@@ -108,36 +117,54 @@ export function AssistantResponse({
108117 headerLabel ?: string ;
109118} ) {
110119 const [ mode , setMode ] = useState < "rendered" | "raw" > ( "rendered" ) ;
120+ const [ copied , setCopied ] = useState ( false ) ;
121+
122+ function handleCopy ( ) {
123+ navigator . clipboard . writeText ( text ) ;
124+ setCopied ( true ) ;
125+ setTimeout ( ( ) => setCopied ( false ) , 2000 ) ;
126+ }
111127
112128 return (
113- < div className = "flex flex-col gap-1 py-2.5" >
129+ < div className = "flex flex-col gap-1.5 py-2.5" >
114130 < SectionHeader
115131 label = { headerLabel }
116132 right = {
117- < div className = "flex items-center gap-2" >
118- < button
133+ < div className = "flex items-center" >
134+ < Button
135+ variant = "minimal/small"
119136 onClick = { ( ) => setMode ( mode === "rendered" ? "raw" : "rendered" ) }
120- className = "text-[10px] text-text-link hover:underline"
137+ LeadingIcon = { CodeBracketSquareIcon }
121138 >
122139 { mode === "rendered" ? "Raw" : "Rendered" }
123- </ button >
124- < button
125- onClick = { ( ) => navigator . clipboard . writeText ( text ) }
126- className = "text-[10px] text-text-link hover:underline"
140+ </ Button >
141+ < Button
142+ variant = "minimal/small"
143+ onClick = { handleCopy }
144+ LeadingIcon = { copied ? CheckIcon : ClipboardDocumentIcon }
145+ leadingIconClassName = { copied ? "text-green-500" : undefined }
127146 >
128147 Copy
129- </ button >
148+ </ Button >
130149 </ div >
131150 }
132151 />
133152 { mode === "rendered" ? (
134- < div className = "streamdown-container text-sm text-text-bright" >
135- < Suspense fallback = { < pre className = "whitespace-pre-wrap" > { text } </ pre > } >
136- < StreamdownRenderer > { text } </ StreamdownRenderer >
137- </ Suspense >
138- </ div >
153+ < ChatBubble >
154+ < Paragraph variant = "small/dimmed" className = "streamdown-container" >
155+ < Suspense fallback = { < span className = "whitespace-pre-wrap" > { text } </ span > } >
156+ < StreamdownRenderer > { text } </ StreamdownRenderer >
157+ </ Suspense >
158+ </ Paragraph >
159+ </ ChatBubble >
139160 ) : (
140- < CodeBlock code = { text } maxLines = { 20 } showLineNumbers = { false } showCopyButton />
161+ < CodeBlock
162+ code = { text }
163+ maxLines = { 20 }
164+ showLineNumbers = { false }
165+ showCopyButton = { false }
166+ className = "pl-2"
167+ />
141168 ) }
142169 </ div >
143170 ) ;
@@ -151,9 +178,13 @@ function ToolUseSection({ tools }: { tools: ToolUse[] }) {
151178 return (
152179 < div className = "flex flex-col gap-1.5 py-2.5" >
153180 < SectionHeader label = { tools . length === 1 ? "Tool call" : `Tool calls (${ tools . length } )` } />
154- { tools . map ( ( tool ) => (
155- < ToolUseRow key = { tool . toolCallId } tool = { tool } />
156- ) ) }
181+ < ChatBubble >
182+ < div className = "flex flex-col gap-2" >
183+ { tools . map ( ( tool ) => (
184+ < ToolUseRow key = { tool . toolCallId } tool = { tool } />
185+ ) ) }
186+ </ div >
187+ </ ChatBubble >
157188 </ div >
158189 ) ;
159190}
@@ -228,9 +259,9 @@ function ToolUseRow({ tool }: { tool: ToolUse }) {
228259 ) }
229260
230261 { activeTab === "details" && hasDetails && (
231- < div className = "border-t border-grid-dimmed px-2.5 py-2 flex flex-col gap -2" >
262+ < div className = "flex flex-col gap-2 border-t border-grid-dimmed px-2.5 py-2" >
232263 { tool . description && (
233- < p className = "text-xs text-text-dimmed leading-relaxed " > { tool . description } </ p >
264+ < p className = "text-xs leading-relaxed text-text-dimmed" > { tool . description } </ p >
234265 ) }
235266 { tool . parametersJson && (
236267 < div >
0 commit comments