11"use client"
22
3- import { useMemo } from "react"
4- import { LoaderCircle } from "lucide-react"
3+ import { useMemo , useState , useCallback , useEffect } from "react"
4+ import { toast } from "sonner"
5+ import { LoaderCircle , FileText , Copy , Check } from "lucide-react"
56
6- import type { Run , TaskMetrics as _TaskMetrics } from "@roo-code/evals"
7+ import type { Run , TaskMetrics as _TaskMetrics , Task } from "@roo-code/evals"
78
89import { formatCurrency , formatDuration , formatTokens , formatToolUsageSuccessRate } from "@/lib/formatters"
910import { useRunStatus } from "@/hooks/use-run-status"
@@ -17,6 +18,12 @@ import {
1718 Tooltip ,
1819 TooltipContent ,
1920 TooltipTrigger ,
21+ Dialog ,
22+ DialogContent ,
23+ DialogHeader ,
24+ DialogTitle ,
25+ ScrollArea ,
26+ Button ,
2027} from "@/components/ui"
2128
2229import { TaskStatus } from "./task-status"
@@ -35,10 +42,114 @@ function getToolAbbreviation(toolName: string): string {
3542 . join ( "" )
3643}
3744
45+ // Format log content with basic highlighting
46+ function formatLogContent ( log : string ) : React . ReactNode [ ] {
47+ const lines = log . split ( "\n" )
48+ return lines . map ( ( line , index ) => {
49+ // Highlight timestamps [YYYY-MM-DDTHH:MM:SS.sssZ]
50+ let formattedLine = line . replace (
51+ / \[ ( \d { 4 } - \d { 2 } - \d { 2 } T \d { 2 } : \d { 2 } : \d { 2 } \. \d { 3 } Z ) \] / g,
52+ '<span class="text-blue-400">[$1]</span>' ,
53+ )
54+
55+ // Highlight log levels
56+ formattedLine = formattedLine . replace ( / \| \s * ( I N F O ) \s * \| / g, '| <span class="text-green-400">$1</span> |' )
57+ formattedLine = formattedLine . replace (
58+ / \| \s * ( W A R N | W A R N I N G ) \s * \| / g,
59+ '| <span class="text-yellow-400">$1</span> |' ,
60+ )
61+ formattedLine = formattedLine . replace ( / \| \s * ( E R R O R ) \s * \| / g, '| <span class="text-red-400">$1</span> |' )
62+ formattedLine = formattedLine . replace ( / \| \s * ( D E B U G ) \s * \| / g, '| <span class="text-gray-400">$1</span> |' )
63+
64+ // Highlight task identifiers like taskCreated, taskFocused, etc.
65+ formattedLine = formattedLine . replace (
66+ / ( t a s k C r e a t e d | t a s k F o c u s e d | t a s k S t a r t e d | t a s k C o m p l e t e d | E v a l P a s s | E v a l F a i l ) / g,
67+ '<span class="text-purple-400">$1</span>' ,
68+ )
69+
70+ // Highlight message arrows
71+ formattedLine = formattedLine . replace ( / → / g, '<span class="text-cyan-400">→</span>' )
72+
73+ return (
74+ < div
75+ key = { index }
76+ className = "hover:bg-white/5"
77+ dangerouslySetInnerHTML = { { __html : formattedLine || " " } }
78+ />
79+ )
80+ } )
81+ }
82+
3883export function Run ( { run } : { run : Run } ) {
3984 const runStatus = useRunStatus ( run )
4085 const { tasks, tokenUsage, usageUpdatedAt } = runStatus
4186
87+ const [ selectedTask , setSelectedTask ] = useState < Task | null > ( null )
88+ const [ taskLog , setTaskLog ] = useState < string | null > ( null )
89+ const [ isLoadingLog , setIsLoadingLog ] = useState ( false )
90+ const [ copied , setCopied ] = useState ( false )
91+
92+ const onCopyLog = useCallback ( async ( ) => {
93+ if ( ! taskLog ) return
94+
95+ try {
96+ await navigator . clipboard . writeText ( taskLog )
97+ setCopied ( true )
98+ toast . success ( "Log copied to clipboard" )
99+ setTimeout ( ( ) => setCopied ( false ) , 2000 )
100+ } catch ( error ) {
101+ console . error ( "Failed to copy log:" , error )
102+ toast . error ( "Failed to copy log" )
103+ }
104+ } , [ taskLog ] )
105+
106+ // Handle ESC key to close the dialog
107+ useEffect ( ( ) => {
108+ const handleKeyDown = ( e : KeyboardEvent ) => {
109+ if ( e . key === "Escape" && selectedTask ) {
110+ setSelectedTask ( null )
111+ }
112+ }
113+
114+ document . addEventListener ( "keydown" , handleKeyDown )
115+ return ( ) => document . removeEventListener ( "keydown" , handleKeyDown )
116+ } , [ selectedTask ] )
117+
118+ const onViewTaskLog = useCallback (
119+ async ( task : Task ) => {
120+ // Only allow viewing logs for completed tasks
121+ if ( task . passed === null || task . passed === undefined ) {
122+ toast . error ( "Task is still running" )
123+ return
124+ }
125+
126+ setSelectedTask ( task )
127+ setIsLoadingLog ( true )
128+ setTaskLog ( null )
129+
130+ try {
131+ const response = await fetch ( `/api/runs/${ run . id } /logs/${ task . id } ` )
132+
133+ if ( ! response . ok ) {
134+ const error = await response . json ( )
135+ toast . error ( error . error || "Failed to load log" )
136+ setSelectedTask ( null )
137+ return
138+ }
139+
140+ const data = await response . json ( )
141+ setTaskLog ( data . logContent )
142+ } catch ( error ) {
143+ console . error ( "Error loading task log:" , error )
144+ toast . error ( "Failed to load log" )
145+ setSelectedTask ( null )
146+ } finally {
147+ setIsLoadingLog ( false )
148+ }
149+ } ,
150+ [ run . id ] ,
151+ )
152+
42153 const taskMetrics : Record < number , TaskMetrics > = useMemo ( ( ) => {
43154 const metrics : Record < number , TaskMetrics > = { }
44155
@@ -241,15 +352,28 @@ export function Run({ run }: { run: Run }) {
241352 </ TableHeader >
242353 < TableBody >
243354 { tasks . map ( ( task ) => (
244- < TableRow key = { task . id } >
355+ < TableRow
356+ key = { task . id }
357+ className = { task . finishedAt ? "cursor-pointer hover:bg-muted/50" : "" }
358+ onClick = { ( ) => task . finishedAt && onViewTaskLog ( task ) } >
245359 < TableCell >
246360 < div className = "flex items-center gap-2" >
247361 < TaskStatus
248362 task = { task }
249363 running = { ! ! task . startedAt || ! ! tokenUsage . get ( task . id ) }
250364 />
251- < div >
252- { task . language } /{ task . exercise }
365+ < div className = "flex items-center gap-2" >
366+ < span >
367+ { task . language } /{ task . exercise }
368+ </ span >
369+ { task . finishedAt && (
370+ < Tooltip >
371+ < TooltipTrigger asChild >
372+ < FileText className = "size-3 text-muted-foreground" />
373+ </ TooltipTrigger >
374+ < TooltipContent > Click to view log</ TooltipContent >
375+ </ Tooltip >
376+ ) }
253377 </ div >
254378 </ div >
255379 </ TableCell >
@@ -282,6 +406,60 @@ export function Run({ run }: { run: Run }) {
282406 </ Table >
283407 ) }
284408 </ div >
409+
410+ { /* Task Log Dialog - Full Screen */ }
411+ < Dialog open = { ! ! selectedTask } onOpenChange = { ( ) => setSelectedTask ( null ) } >
412+ < DialogContent className = "w-[95vw] !max-w-[95vw] h-[90vh] flex flex-col" >
413+ < DialogHeader className = "flex-shrink-0" >
414+ < div className = "flex items-center justify-between pr-8" >
415+ < DialogTitle className = "flex items-center gap-2" >
416+ < FileText className = "size-4" />
417+ { selectedTask ?. language } /{ selectedTask ?. exercise }
418+ < span
419+ className = { `ml-2 text-sm ${ selectedTask ?. passed ? "text-green-600" : "text-red-600" } ` } >
420+ ({ selectedTask ?. passed ? "Passed" : "Failed" } )
421+ </ span >
422+ </ DialogTitle >
423+ { taskLog && (
424+ < Button
425+ variant = "outline"
426+ size = "sm"
427+ onClick = { onCopyLog }
428+ className = "flex items-center gap-1" >
429+ { copied ? (
430+ < >
431+ < Check className = "size-4" />
432+ Copied!
433+ </ >
434+ ) : (
435+ < >
436+ < Copy className = "size-4" />
437+ Copy Log
438+ </ >
439+ ) }
440+ </ Button >
441+ ) }
442+ </ div >
443+ </ DialogHeader >
444+ < div className = "flex-1 min-h-0 overflow-hidden" >
445+ { isLoadingLog ? (
446+ < div className = "flex items-center justify-center h-full" >
447+ < LoaderCircle className = "size-6 animate-spin" />
448+ </ div >
449+ ) : taskLog ? (
450+ < ScrollArea className = "h-full w-full" >
451+ < div className = "text-xs font-mono bg-muted p-4 rounded-md overflow-x-auto" >
452+ { formatLogContent ( taskLog ) }
453+ </ div >
454+ </ ScrollArea >
455+ ) : (
456+ < div className = "flex items-center justify-center h-full text-muted-foreground" >
457+ Log file not available (may have been cleared)
458+ </ div >
459+ ) }
460+ </ div >
461+ </ DialogContent >
462+ </ Dialog >
285463 </ >
286464 )
287465}
0 commit comments