1+ /* eslint-disable no-console */
12"use client"
23
34import {
@@ -32,7 +33,7 @@ import { AgentSources } from "./agent-sources"
3233import { AgentArtifact , type ArtifactData } from "./agent-artifact"
3334import { AgentPlan , extractPlanFromText } from "./agent-plan"
3435import { AgentCheckpoint } from "./agent-checkpoint"
35- import { AgentTask , type AgentTaskData } from "./agent-task"
36+ import { AgentTask , type AgentTaskData , type TaskStep } from "./agent-task"
3637import { AgentQueue } from "./agent-queue"
3738import { AgentConfirmation } from "./agent-confirmation"
3839import { parseInlineCitations } from "./agent-inline-citation"
@@ -54,6 +55,56 @@ import { mapDataToolPartToDynamicToolPart } from "../helpers/tool-part-transform
5455import type { BundledLanguage } from "shiki"
5556import { Button } from "@/ui/button"
5657
58+ // Extract extractTasksFromText to module level to fix scope issues
59+ function extractTasksFromText ( content : string ) : AgentTaskData [ ] {
60+ const taskSections : AgentTaskData [ ] = [ ]
61+ const sectionRegex = / (?: t a s k s ? | c h e c k l i s t | t o d o ) [: \s] * \n ( (?: [ - • \d [ \] x X \s ] .+ \n ? ) + ) / gi
62+ let match : RegExpExecArray | null
63+ let sectionIndex = 0
64+
65+ while ( ( match = sectionRegex . exec ( content ) ) !== null ) {
66+ const sectionBody = match [ 1 ]
67+ const lines = sectionBody
68+ . split ( "\n" )
69+ . map ( ( line ) => line . trim ( ) )
70+ . filter ( ( line ) => line . length > 0 )
71+
72+ if ( lines . length === 0 ) { continue }
73+
74+ const steps : TaskStep [ ] = lines . map ( ( line , idx ) => {
75+ const statusMatch = / \[ ( [ x X - ] ) \] / . exec ( line )
76+ let status : TaskStep [ "status" ] = "pending"
77+ if ( statusMatch ) {
78+ const symbol = statusMatch [ 1 ] . toLowerCase ( )
79+ if ( symbol === "x" ) {
80+ status = "completed"
81+ } else if ( symbol === "-" ) {
82+ status = "running"
83+ } else {
84+ status = "pending"
85+ }
86+ }
87+
88+ const sanitized = line . replace ( / \[ [ x X - ] \] \s * / , "" ) . replace ( / ^ [ - • \d . ] + \s * / , "" )
89+
90+ return {
91+ id : `task-${ sectionIndex } -${ idx } ` ,
92+ text : sanitized ,
93+ status,
94+ }
95+ } )
96+
97+ taskSections . push ( {
98+ title : `Task Group ${ taskSections . length + 1 } ` ,
99+ steps,
100+ } )
101+
102+ sectionIndex += 1
103+ }
104+
105+ return taskSections
106+ }
107+
57108function CopyButton ( { text } : { text : string } ) {
58109 const [ copied , setCopied ] = useState ( false )
59110
@@ -148,7 +199,13 @@ function MessageItem({
148199 const isAssistant = message . role === "assistant"
149200 const isUser = message . role === "user"
150201 const textPart = message . parts ?. find ( isTextUIPart )
151- const rawContent = textPart ?. text || ""
202+ const rawContent = textPart ?. text ?? ""
203+ const [ inlinePreview , setInlinePreview ] = useState < WebPreviewData | null > ( null )
204+ const [ sandboxPreview , setSandboxPreview ] = useState < {
205+ code : string
206+ language : string
207+ title : string
208+ } | null > ( null )
152209
153210 const { content, artifacts, codeBlocks } = useMemo ( ( ) => {
154211 if ( isAssistant && showArtifacts ) {
@@ -185,11 +242,15 @@ function MessageItem({
185242 const otherFileParts = fileParts ?. filter ( ( f ) => ! f . mediaType ?. startsWith ( "image/" ) )
186243
187244 const reasoningSteps = useMemo ( ( ) => {
188- if ( showChainOfThought && messageReasoning ?. text ) {
245+ if ( messageReasoning ?. text ) {
189246 return parseReasoningToSteps ( messageReasoning . text )
190247 }
191248 return [ ]
192- } , [ showChainOfThought , messageReasoning ] )
249+ } , [ messageReasoning ] )
250+
251+ const hasChainOfThoughtSteps = showChainOfThought && reasoningSteps . length > 0
252+ const shouldShowReasoningFallback =
253+ showReasoning && ( ! showChainOfThought || ! hasChainOfThoughtSteps ) && ! ! messageReasoning ?. text
193254
194255 const plan = useMemo ( ( ) => {
195256 if ( isAssistant ) {
@@ -206,6 +267,8 @@ function MessageItem({
206267 return null
207268 } , [ hasCitations , content , sources ] )
208269
270+ const extractedTasks = useMemo ( ( ) => extractTasksFromText ( rawContent ) , [ rawContent ] )
271+
209272 // Find checkpoint for this message
210273 const checkpointIndex = checkpointMessageIndices . indexOf ( messageIndex )
211274 const isCheckpoint = checkpointIndex !== - 1
@@ -252,11 +315,67 @@ function MessageItem({
252315 </ MessageAttachments >
253316 ) }
254317
318+ { /* Non-image files with inline preview controls */ }
319+ { otherFileParts && otherFileParts . length > 0 && (
320+ < div className = "my-2 space-y-2 rounded-lg border bg-muted/30 p-3" >
321+ < p className = "text-xs font-medium text-muted-foreground" > Attachments</ p >
322+ { otherFileParts . map ( ( file , idx ) => (
323+ < div key = { `other-file-${ idx } ` } className = "flex items-center justify-between text-sm" >
324+ < span className = "truncate" > { file . filename ?? file . mediaType ?? `File ${ idx + 1 } ` } </ span >
325+ < div className = "flex items-center gap-2" >
326+ { file . url && (
327+ < Button
328+ variant = "outline"
329+ size = "sm"
330+ className = "h-6 px-2 text-xs"
331+ onClick = { ( ) =>
332+ setInlinePreview ( {
333+ id : ( file as { id ?: string } ) . id ?? `file-${ idx } ` ,
334+ url : file . url ,
335+ title : file . filename ?? "Preview" ,
336+ } )
337+ }
338+ >
339+ Preview
340+ </ Button >
341+ ) }
342+ < Button
343+ variant = "ghost"
344+ size = "sm"
345+ className = "h-6 px-2 text-xs"
346+ onClick = { ( ) => {
347+ if ( file . url ) {
348+ window . open ( file . url , "_blank" )
349+ }
350+ } }
351+ >
352+ Download
353+ </ Button >
354+ </ div >
355+ </ div >
356+ ) ) }
357+ </ div >
358+ ) }
359+
360+ { inlinePreview && (
361+ < div className = "my-3" >
362+ < AgentWebPreview
363+ preview = { inlinePreview }
364+ onClose = { ( ) => setInlinePreview ( null ) }
365+ defaultTab = "preview"
366+ height = { 360 }
367+ editable = { false }
368+ />
369+ </ div >
370+ ) }
371+
255372 { /* Chain of Thought / Reasoning - mutually exclusive display */ }
256- { isAssistant && messageReasoning && ( showChainOfThought || showReasoning ) && (
257- showChainOfThought && reasoningSteps . length > 0
258- ? < AgentChainOfThought steps = { reasoningSteps } isStreaming = { false } />
259- : < AgentReasoning reasoning = { messageReasoning . text || "" } isStreaming = { false } />
373+ { isAssistant && messageReasoning && ( hasChainOfThoughtSteps || shouldShowReasoningFallback ) && (
374+ hasChainOfThoughtSteps ? (
375+ < AgentChainOfThought steps = { reasoningSteps } isStreaming = { false } />
376+ ) : (
377+ < AgentReasoning reasoning = { messageReasoning . text || "" } isStreaming = { false } />
378+ )
260379 ) }
261380
262381 { /* Plan */ }
@@ -289,11 +408,37 @@ function MessageItem({
289408 ) : codeBlocks . length > 0 ? (
290409 < div className = "prose prose-sm max-w-none dark:prose-invert" >
291410 { renderContentWithCodeBlocks ( content ) }
411+ < Button
412+ variant = "ghost"
413+ size = "sm"
414+ className = "mt-2 gap-1 text-sm"
415+ onClick = { ( ) =>
416+ setSandboxPreview ( {
417+ code : codeBlocks [ 0 ] . code ,
418+ language : codeBlocks [ 0 ] . language ,
419+ title : messageReasoning ?. text ? "Reasoning Snippet" : "Code Snippet" ,
420+ } )
421+ }
422+ >
423+ Open first snippet in sandbox
424+ </ Button >
292425 </ div >
293426 ) : (
294427 < MessageResponse > { content } </ MessageResponse >
295428 ) }
296429
430+ { sandboxPreview && (
431+ < div className = "my-3" >
432+ < AgentCodeSandbox
433+ code = { sandboxPreview . code }
434+ language = { sandboxPreview . language }
435+ title = { sandboxPreview . title }
436+ onClose = { ( ) => setSandboxPreview ( null ) }
437+ onCodeChange = { ( code ) => setSandboxPreview ( ( prev ) => ( prev ? { ...prev , code } : prev ) ) }
438+ />
439+ </ div >
440+ ) }
441+
297442 { /* Artifacts */ }
298443 { isAssistant && showArtifacts && artifacts . length > 0 && (
299444 < div className = "mt-3 space-y-3" >
@@ -303,6 +448,15 @@ function MessageItem({
303448 </ div >
304449 ) }
305450
451+ { /* Parsed Tasks */ }
452+ { isAssistant && extractedTasks && extractedTasks . length > 0 && (
453+ < div className = "mt-4 space-y-3" >
454+ { extractedTasks . map ( ( task , idx : number ) => (
455+ < AgentTask key = { `task-${ idx } ` } task = { task } defaultOpen = { false } />
456+ ) ) }
457+ </ div >
458+ ) }
459+
306460 { /* Tool Confirmations */ }
307461 { isAssistant && showConfirmation && messageTools && messageTools . length > 0 && (
308462 < >
@@ -358,35 +512,36 @@ function MessageItem({
358512 )
359513}
360514
361- function WebPreviewPanel ( ) {
362- const { webPreview , setWebPreview, agentConfig } = useChatContext ( )
515+ function WebPreviewPanel ( { preview } : { preview : WebPreviewData | null } ) {
516+ const { setWebPreview, agentConfig } = useChatContext ( )
363517
364- if ( ! webPreview || ! agentConfig ?. features . webPreview ) { return null }
518+ if ( ! preview || ! agentConfig ?. features . webPreview ) { return null }
365519
366520 const handleCodeChange = useCallback ( ( newCode : string ) => {
367- if ( webPreview ) {
521+ if ( preview ) {
368522 setWebPreview ( {
369- ...webPreview ,
523+ ...preview ,
370524 code : newCode ,
371525 } )
372526 }
373- } , [ webPreview , setWebPreview ] )
527+ } , [ preview , setWebPreview ] )
374528
375529 const handleClose = useCallback ( ( ) => {
376530 setWebPreview ( null )
377531 } , [ setWebPreview ] )
378532
379533 // If we have code, use the enhanced preview with live editing
380- if ( webPreview . code ) {
534+ if ( preview . code ) {
381535 return (
382536 < div className = "mx-auto mb-4 max-w-4xl" >
383537 < AgentWebPreview
384538 preview = { {
385- id : webPreview . id ,
386- url : webPreview . url ,
387- title : webPreview . title ,
388- code : webPreview . code ,
389- language : webPreview . language ,
539+ id : preview . id ,
540+ url : preview . url ,
541+ title : preview . title ,
542+ code : preview . code ,
543+ language : preview . language ,
544+ // html: preview.html
390545 } }
391546 onClose = { handleClose }
392547 onCodeChange = { handleCodeChange }
@@ -404,13 +559,15 @@ function WebPreviewPanel() {
404559 < div className = "mx-auto mb-4 max-w-4xl" >
405560 < AgentWebPreview
406561 preview = { {
407- id : webPreview . id ,
408- url : webPreview . url ,
409- title : webPreview . title ,
562+ id : preview . id ,
563+ url : preview . url ,
564+ title : preview . title ,
410565 } }
411566 onClose = { handleClose }
567+ // onCodeChange={handleCodeChange}
412568 defaultTab = "preview"
413- height = { 400 }
569+ showConsole = { false }
570+ height = { 450 }
414571 editable = { false }
415572 />
416573 </ div >
@@ -460,11 +617,17 @@ export function ChatMessages() {
460617 )
461618
462619 const streamingReasoningSteps = useMemo ( ( ) => {
463- if ( showChainOfThought && streamingReasoning ) {
620+ if ( streamingReasoning ) {
464621 return parseReasoningToSteps ( streamingReasoning )
465622 }
466623 return [ ]
467- } , [ showChainOfThought , streamingReasoning ] )
624+ } , [ streamingReasoning ] )
625+
626+ const hasStreamingChainOfThought = showChainOfThought && streamingReasoningSteps . length > 0
627+ const shouldShowStreamingReasoningFallback =
628+ showReasoning &&
629+ ( ! showChainOfThought || ! hasStreamingChainOfThought ) &&
630+ ! ! streamingReasoning
468631
469632 // Get checkpoint data for message items
470633 const checkpointIds = useMemo ( ( ) => checkpoints . map ( ( cp ) => cp . id ) , [ checkpoints ] )
@@ -495,7 +658,7 @@ export function ChatMessages() {
495658 ) }
496659
497660 { /* Web Preview Panel */ }
498- < WebPreviewPanel />
661+ < WebPreviewPanel preview = { webPreview } />
499662
500663 { messages . map ( ( message , index ) => (
501664 < MessageItem
@@ -521,11 +684,11 @@ export function ChatMessages() {
521684 { isLoading && (
522685 < Message from = "assistant" >
523686 < MessageContent >
524- { showChainOfThought && streamingReasoningSteps . length > 0 && (
687+ { hasStreamingChainOfThought && (
525688 < AgentChainOfThought steps = { streamingReasoningSteps } isStreaming = { true } />
526689 ) }
527690
528- { showReasoning && ! showChainOfThought && streamingReasoning && (
691+ { shouldShowStreamingReasoningFallback && (
529692 < AgentReasoning reasoning = { streamingReasoning } isStreaming = { true } />
530693 ) }
531694
0 commit comments