11import { User } from "lucide-react" ;
22import { useEffect , useRef , useState } from "react" ;
3+ import ReactMarkdown from "react-markdown" ;
4+ import rehypeHighlight from "rehype-highlight" ;
5+ import remarkGfm from "remark-gfm" ;
6+
37import { AgentIdenticon } from "./AgentIdenticon" ;
48import { formatRelative } from "./TaskDetailFields" ;
59import { Button } from "./ui/button" ;
@@ -35,56 +39,79 @@ const dotColors: Record<string, string> = {
3539 moved : "bg-zinc-500 border-zinc-500/30" ,
3640} ;
3741
38- function buildSentence ( log : any ) : { prefix : string ; actionText : string ; suffix : string } {
39- const name = log . actor_name || null ;
40- const isAgent = log . actor_type ?. startsWith ( "agent:" ) ;
41- const defaultPrefix = isAgent ? "Agent" : log . actor_type === "user" ? "User" : "System" ;
42+ const bodyActions = new Set ( [ "commented" , "rejected" , "completed" , "cancelled" ] ) ;
43+
44+ const markdownClass =
45+ "overflow-x-auto text-[13px] text-content-secondary [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 [&_h1]:text-base [&_h1]:font-semibold [&_h1]:text-content-primary [&_h1]:mt-3 [&_h1]:mb-1 [&_h2]:text-sm [&_h2]:font-semibold [&_h2]:text-content-primary [&_h2]:mt-3 [&_h2]:mb-1 [&_h3]:text-[13px] [&_h3]:font-semibold [&_h3]:text-content-primary [&_h3]:mt-2 [&_h3]:mb-1 [&_p]:mb-2 [&_ul]:mb-2 [&_ul]:pl-4 [&_ul]:list-disc [&_ol]:mb-2 [&_ol]:pl-4 [&_ol]:list-decimal [&_li]:mb-0.5 [&_a]:text-accent [&_a]:underline [&_a]:underline-offset-2 [&_pre]:bg-surface-primary [&_pre]:border [&_pre]:border-border [&_pre]:rounded-md [&_pre]:p-3 [&_pre]:overflow-x-auto [&_pre]:font-mono [&_pre]:text-[12px] [&_code]:font-mono [&_code]:text-accent [&_code]:bg-surface-primary [&_code]:px-1 [&_code]:rounded [&_code]:text-[12px] [&_pre_code]:bg-transparent [&_pre_code]:text-content-secondary [&_pre_code]:p-0 [&_table]:w-full [&_table]:border-collapse [&_th]:text-left [&_th]:text-[11px] [&_th]:font-medium [&_th]:text-content-tertiary [&_th]:uppercase [&_th]:tracking-wide [&_th]:border-b [&_th]:border-border [&_th]:pb-1 [&_td]:border-b [&_td]:border-border [&_td]:py-1 [&_td]:pr-3 [&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-3 [&_blockquote]:text-content-tertiary [&_hr]:border-border" ;
46+
47+ function actorLabel ( log : any ) : string {
48+ if ( log . actor_name ) return log . actor_name ;
49+ if ( log . actor_type ?. startsWith ( "agent:" ) ) return "Agent" ;
50+ if ( log . actor_type === "user" ) return "User" ;
51+ return "System" ;
52+ }
4253
54+ function buildSentence ( log : any ) : { actionText : string ; suffix : string } {
4355 switch ( log . action ) {
4456 case "claimed" :
45- return { prefix : name ?? defaultPrefix , actionText : "claimed this task" , suffix : "" } ;
57+ return { actionText : "claimed this task" , suffix : "" } ;
4658 case "assigned" :
47- return { prefix : name ?? "System" , actionText : "assigned to" , suffix : log . detail ?? "agent" } ;
59+ return { actionText : "assigned to" , suffix : log . detail ?? "agent" } ;
4860 case "completed" :
49- return { prefix : name ?? defaultPrefix , actionText : "completed this task" , suffix : log . detail ? `— ${ log . detail } ` : "" } ;
61+ return { actionText : "completed this task" , suffix : "" } ;
5062 case "released" :
51- return { prefix : name ?? defaultPrefix , actionText : "released this task" , suffix : "" } ;
63+ return { actionText : "released this task" , suffix : "" } ;
5264 case "timed_out" :
53- return { prefix : name ?? defaultPrefix , actionText : "timed out" , suffix : "" } ;
65+ return { actionText : "timed out" , suffix : "" } ;
5466 case "cancelled" :
55- return { prefix : name ?? "System" , actionText : "cancelled this task" , suffix : log . detail ? `— ${ log . detail } ` : "" } ;
67+ return { actionText : "cancelled this task" , suffix : "" } ;
5668 case "rejected" :
57- return { prefix : name ?? "Reviewer" , actionText : "rejected — sent back to agent " , suffix : log . detail ? `( ${ log . detail } )` : "" } ;
69+ return { actionText : "rejected this task " , suffix : "" } ;
5870 case "review_requested" :
59- return { prefix : name ?? defaultPrefix , actionText : "submitted for review" , suffix : "" } ;
71+ return { actionText : "submitted for review" , suffix : "" } ;
6072 case "created" :
61- return { prefix : "System" , actionText : "created this task" , suffix : "" } ;
73+ return { actionText : "created this task" , suffix : "" } ;
6274 case "moved" :
63- return { prefix : "System" , actionText : "moved" , suffix : log . detail ?? "" } ;
75+ return { actionText : "moved" , suffix : log . detail ?? "" } ;
6476 case "commented" :
65- return { prefix : name ?? defaultPrefix , actionText : "commented" , suffix : "" } ;
77+ return { actionText : "commented" , suffix : "" } ;
6678 default :
67- return { prefix : name ?? "System" , actionText : log . action , suffix : log . detail ?? "" } ;
79+ return { actionText : log . action , suffix : bodyActions . has ( log . action ) ? "" : ( log . detail ?? "" ) } ;
6880 }
6981}
7082
71- function NoteAvatar ( { actorType , actorPublicKey } : { actorType : string | null ; actorPublicKey : string | null } ) {
72- if ( actorType ?. startsWith ( "agent:" ) && actorPublicKey ) {
73- return < AgentIdenticon publicKey = { actorPublicKey } size = { 20 } /> ;
83+ function NoteAvatar ( { log } : { log : any } ) {
84+ if ( log . actor_type ?. startsWith ( "agent:" ) && log . actor_public_key ) {
85+ return < AgentIdenticon publicKey = { log . actor_public_key } size = { 28 } /> ;
7486 }
87+
7588 return (
76- < span className = "flex-shrink-0 w-5 h-5 rounded-full bg-zinc-500/10 border border-zinc-500/20 flex items-center justify-center" >
77- < User className = "w-3 h-3 text-content-tertiary" />
89+ < span className = "flex-shrink-0 w-7 h-7 rounded-full bg-zinc-500/10 border border-zinc-500/20 flex items-center justify-center" >
90+ < User className = "w-3.5 h-3.5 text-content-tertiary" />
7891 </ span >
7992 ) ;
8093}
8194
95+ function MarkdownBody ( { children } : { children : string } ) {
96+ return (
97+ < div className = { markdownClass } >
98+ < ReactMarkdown remarkPlugins = { [ remarkGfm ] } rehypePlugins = { [ rehypeHighlight ] } >
99+ { children }
100+ </ ReactMarkdown >
101+ </ div >
102+ ) ;
103+ }
104+
105+ function hasBody ( log : any ) : boolean {
106+ return bodyActions . has ( log . action ) && ! ! log . detail ;
107+ }
108+
82109export function ActivityLog ( { initialNotes, sseNotes, reconnecting } : ActivityLogProps ) {
83110 const containerRef = useRef < HTMLDivElement > ( null ) ;
84111 const [ autoScroll , setAutoScroll ] = useState ( true ) ;
85112 const [ newCount , setNewCount ] = useState ( 0 ) ;
86113
87- const allNotes = ( ( ) => {
114+ const displayed = ( ( ) => {
88115 const seen = new Set < string > ( ) ;
89116 const merged : any [ ] = [ ] ;
90117 for ( const note of [ ...initialNotes , ...sseNotes ] ) {
@@ -96,25 +123,24 @@ export function ActivityLog({ initialNotes, sseNotes, reconnecting }: ActivityLo
96123 return merged . sort ( ( a , b ) => a . created_at . localeCompare ( b . created_at ) ) ;
97124 } ) ( ) ;
98125
99- const displayed = allNotes . slice ( ) . reverse ( ) ;
100-
101126 useEffect ( ( ) => {
102127 if ( autoScroll && containerRef . current ) {
103- containerRef . current . scrollTop = 0 ;
128+ containerRef . current . scrollTop = containerRef . current . scrollHeight ;
104129 } else if ( ! autoScroll && sseNotes . length > 0 ) {
105130 setNewCount ( ( c ) => c + 1 ) ;
106131 }
107132 } , [ autoScroll , sseNotes . length ] ) ;
108133
109134 function handleScroll ( ) {
110135 if ( ! containerRef . current ) return ;
111- const atTop = containerRef . current . scrollTop < 20 ;
112- setAutoScroll ( atTop ) ;
113- if ( atTop ) setNewCount ( 0 ) ;
136+ const { scrollTop, scrollHeight, clientHeight } = containerRef . current ;
137+ const atBottom = scrollHeight - scrollTop - clientHeight < 20 ;
138+ setAutoScroll ( atBottom ) ;
139+ if ( atBottom ) setNewCount ( 0 ) ;
114140 }
115141
116- function scrollToTop ( ) {
117- containerRef . current ?. scrollTo ( { top : 0 , behavior : "smooth" } ) ;
142+ function scrollToLatest ( ) {
143+ containerRef . current ?. scrollTo ( { top : containerRef . current . scrollHeight , behavior : "smooth" } ) ;
118144 setNewCount ( 0 ) ;
119145 setAutoScroll ( true ) ;
120146 }
@@ -128,54 +154,52 @@ export function ActivityLog({ initialNotes, sseNotes, reconnecting }: ActivityLo
128154 { reconnecting && < div className = "text-[10px] text-warning mb-1" > Reconnecting...</ div > }
129155
130156 { newCount > 0 && ! autoScroll && (
131- < Button onClick = { scrollToTop } size = "xs" className = "absolute top-0 left-1/2 -translate-x-1/2 z-10 text-[11px] font-mono" >
132- ↑ { newCount } new
157+ < Button onClick = { scrollToLatest } size = "xs" className = "absolute bottom-2 left-1/2 -translate-x-1/2 z-10 text-[11px] font-mono" >
158+ ↓ { newCount } new
133159 </ Button >
134160 ) }
135161
136- < div ref = { containerRef } onScroll = { handleScroll } className = "mt-2 max-h-80 overflow-y-auto" aria-live = "polite" >
137- { /* Timeline container */ }
138- < div className = "relative ml-2.5" >
139- { /* Vertical line */ }
140- < div className = "absolute left-0 top-0 bottom-0 w-px bg-border" />
162+ < div ref = { containerRef } onScroll = { handleScroll } className = "mt-2 max-h-96 overflow-y-auto pr-1" aria-live = "polite" >
163+ < div className = "relative" >
164+ < div className = "absolute left-3.5 top-0 bottom-0 w-px bg-border" />
141165
142166 { displayed . map ( ( log : any ) => {
143- const { prefix, actionText, suffix } = buildSentence ( log ) ;
144- const isAgent = log . actor_type ?. startsWith ( "agent:" ) && ! ! log . actor_public_key ;
167+ const actor = actorLabel ( log ) ;
168+ const { actionText, suffix } = buildSentence ( log ) ;
169+ const isAgent = log . actor_type ?. startsWith ( "agent:" ) ;
145170 const dot = dotColors [ log . action ] || "bg-zinc-500 border-zinc-500/30" ;
146171 const actionColor = actionStyles [ log . action ] || "text-content-secondary" ;
147- const isComment = log . action === "commented" ;
172+ const body = hasBody ( log ) ;
148173
149174 return (
150- < div key = { log . id } className = "relative pl-5 pb-3" >
151- { /* Timeline dot */ }
152- < span className = { `absolute left-0 -translate-x-1/2 mt-[3px] w-2 h-2 rounded-full border ${ dot } ` } style = { { top : "4px" } } />
153-
154- < div className = "flex items-center gap-1.5 flex-wrap" >
155- < NoteAvatar actorType = { log . actor_type } actorPublicKey = { log . actor_public_key } />
156-
157- { /* Sentence: prefix (agent name) + action + suffix */ }
158- < span className = "text-[12px] leading-snug" >
159- < span className = { isAgent ? "font-mono text-accent" : "text-content-tertiary" } > { prefix } </ span > { " " }
160- < span className = { isComment ? "text-content-tertiary" : actionColor } > { actionText } </ span >
161- { suffix && (
162- < >
163- { " " }
164- < span className = "text-content-tertiary" > { suffix } </ span >
165- </ >
166- ) }
167- </ span >
168-
169- { /* Relative time */ }
170- < span className = "ml-auto font-mono text-[10px] text-content-tertiary whitespace-nowrap" > { formatRelative ( log . created_at ) } </ span >
175+ < div key = { log . id } className = "relative flex gap-3 pb-4" >
176+ < div className = "relative z-10 flex h-7 w-7 shrink-0 items-center justify-center" >
177+ { body ? < NoteAvatar log = { log } /> : < span className = { `w-2.5 h-2.5 rounded-full border ${ dot } ` } /> }
171178 </ div >
172179
173- { /* Comment body */ }
174- { isComment && log . detail && (
175- < div className = "mt-1.5 ml-6 bg-surface-primary border border-border rounded px-2.5 py-1.5 font-mono text-[11px] text-content-secondary leading-relaxed" >
176- { log . detail }
177- </ div >
178- ) }
180+ < div className = "min-w-0 flex-1" >
181+ { body ? (
182+ < div className = "overflow-hidden rounded-md border border-border bg-surface-secondary" >
183+ < div className = "flex items-center gap-1.5 border-b border-border bg-surface-tertiary px-3 py-2 text-[12px]" >
184+ < span className = { isAgent ? "font-mono text-accent" : "font-medium text-content-primary" } > { actor } </ span >
185+ < span className = { actionColor } > { actionText } </ span >
186+ < span className = "ml-auto font-mono text-[10px] text-content-tertiary whitespace-nowrap" >
187+ { formatRelative ( log . created_at ) }
188+ </ span >
189+ </ div >
190+ < div className = "px-3 py-2.5" >
191+ < MarkdownBody > { log . detail } </ MarkdownBody >
192+ </ div >
193+ </ div >
194+ ) : (
195+ < div className = "flex items-center gap-1.5 min-h-7 text-[12px] leading-snug" >
196+ < span className = { isAgent ? "font-mono text-accent" : "text-content-tertiary" } > { actor } </ span >
197+ < span className = { actionColor } > { actionText } </ span >
198+ { suffix && < span className = "text-content-tertiary" > { suffix } </ span > }
199+ < span className = "ml-auto font-mono text-[10px] text-content-tertiary whitespace-nowrap" > { formatRelative ( log . created_at ) } </ span >
200+ </ div >
201+ ) }
202+ </ div >
179203 </ div >
180204 ) ;
181205 } ) }
0 commit comments