@@ -44,6 +44,44 @@ const parseQuestionData = (text: string): QuestionData | null => {
4444 return null ;
4545} ;
4646
47+ type DecoratedMessage = Omit < Message , 'timestamp' > & {
48+ role : 'user' | 'assistant' ;
49+ name : string ;
50+ timestamp : string ;
51+ showHeader ?: boolean ;
52+ } ;
53+
54+ // Determine if a message should show its header based on grouping rules
55+ const shouldShowHeader = (
56+ message : DecoratedMessage ,
57+ index : number ,
58+ messages : DecoratedMessage [ ] ,
59+ groupingWindowMinutes : number ,
60+ ) : boolean => {
61+ // Always show header for first message or user messages
62+ if ( index === 0 || message . role === 'user' ) return true ;
63+
64+ const prevMessage = messages [ index - 1 ] ;
65+ if ( ! prevMessage ) return true ; // Safety check
66+
67+ // Show header if previous message was from user
68+ if ( prevMessage . role === 'user' ) return true ;
69+
70+ // Show header if mode changed
71+ if ( message . mode !== prevMessage . mode ) return true ;
72+
73+ // Show header if time gap between consecutive messages exceeds threshold
74+ // Use the original timestamp (number) for calculation
75+ const currentTime = message . ts ;
76+ const prevTime = prevMessage . ts ;
77+ const gapMinutes = ( currentTime - prevTime ) / ( 1000 * 60 ) ;
78+
79+ return gapMinutes > groupingWindowMinutes ;
80+ } ;
81+
82+ // Constant for message grouping window (in minutes)
83+ const GROUPING_WINDOW_MINUTES = 5 ;
84+
4785export const Messages = ( {
4886 messages,
4987 enableMessageLinks = false ,
@@ -74,9 +112,25 @@ export const Messages = ({
74112 ) ;
75113 } ) ;
76114
77- return deduplicatedMessages . map ( ( message , index ) =>
115+ // Decorate messages with role, name, and timestamp
116+ const decoratedMessages = deduplicatedMessages . map ( ( message , index ) =>
78117 decorate ( { message, index } ) ,
79118 ) ;
119+
120+ // Add grouping information to each message
121+ return decoratedMessages . map ( ( message , index ) => {
122+ const showHeader = shouldShowHeader (
123+ message ,
124+ index ,
125+ decoratedMessages ,
126+ GROUPING_WINDOW_MINUTES ,
127+ ) ;
128+
129+ return {
130+ ...message ,
131+ showHeader,
132+ } ;
133+ } ) ;
80134 } , [ messages ] ) ;
81135
82136 // Handle anchor link clicks
@@ -142,50 +196,75 @@ export const Messages = ({
142196 { /* Scrollable messages container */ }
143197 < div
144198 ref = { containerRef }
145- className = "space-y-6 pr-2 overflow-y-auto"
199+ className = "pr-2 overflow-y-auto"
146200 style = { {
147201 scrollbarWidth : 'thin' ,
148202 scrollbarColor : 'hsl(var(--border)) transparent' ,
149203 } }
150204 >
151- { conversation . map ( ( message ) => {
152- const isQuestion =
153- message . type === 'ask' && message . ask === 'followup' ;
154- const isCommand = message . type === 'ask' && message . ask === 'command' ;
155- const questionData =
156- isQuestion && message . text ? parseQuestionData ( message . text ) : null ;
157-
158- const messageId = `message-${ message . id } ` ;
159-
160- return (
161- < div
162- key = { message . id }
163- id = { messageId }
164- className = { cn (
165- 'flex flex-col gap-3 rounded-lg p-4 relative transition-all duration-200' ,
166- message . role === 'user' ? 'bg-primary/10' : 'bg-secondary/10' ,
167- enableMessageLinks && 'hover:shadow-sm hover:bg-opacity-80' ,
168- ) }
169- onMouseEnter = { ( ) =>
170- enableMessageLinks && setHoveredMessageId ( messageId )
171- }
172- onMouseLeave = { ( ) =>
173- enableMessageLinks && setHoveredMessageId ( null )
174- }
175- >
176- < div className = "flex flex-row items-center justify-between gap-2 text-xs font-medium text-muted-foreground" >
177- < div className = "flex items-center gap-2" >
178- < div > { message . name } </ div >
179- < div > ·</ div >
180- < div > { message . timestamp } </ div >
181- </ div >
182- < div className = "flex items-center gap-2" >
183- { /* Anchor Link Button */ }
184- { enableMessageLinks && hoveredMessageId === messageId && (
205+ < div className = "space-y-1" >
206+ { conversation . map ( ( message , index ) => {
207+ const isQuestion =
208+ message . type === 'ask' && message . ask === 'followup' ;
209+ const isCommand =
210+ message . type === 'ask' && message . ask === 'command' ;
211+ const questionData =
212+ isQuestion && message . text
213+ ? parseQuestionData ( message . text )
214+ : null ;
215+
216+ const messageId = `message-${ message . id } ` ;
217+
218+ return (
219+ < div
220+ key = { message . id }
221+ id = { messageId }
222+ className = { cn (
223+ 'flex flex-col relative transition-all duration-200 gap-3 rounded-lg p-4' ,
224+ message . role === 'user'
225+ ? 'bg-primary/15 border border-primary/20 shadow-sm'
226+ : 'bg-secondary/10' ,
227+ enableMessageLinks && 'hover:shadow-sm hover:bg-opacity-80' ,
228+ // Add extra top margin for messages that start a new group
229+ message . showHeader && index > 0 && 'mt-4' ,
230+ ) }
231+ onMouseEnter = { ( ) =>
232+ enableMessageLinks && setHoveredMessageId ( messageId )
233+ }
234+ onMouseLeave = { ( ) =>
235+ enableMessageLinks && setHoveredMessageId ( null )
236+ }
237+ >
238+ { message . showHeader && (
239+ < div
240+ className = { cn (
241+ 'flex flex-row items-center justify-between gap-2 text-xs font-medium' ,
242+ message . role === 'user'
243+ ? 'text-primary font-semibold'
244+ : 'text-muted-foreground' ,
245+ ) }
246+ >
247+ < div className = "flex items-center gap-2" >
248+ < div > { message . name } </ div >
249+ < div > ·</ div >
250+ < div > { message . timestamp } </ div >
251+ { message . mode && (
252+ < >
253+ < div > ·</ div >
254+ < div > { message . mode } </ div >
255+ </ >
256+ ) }
257+ </ div >
258+ </ div >
259+ ) }
260+
261+ { /* Anchor Link Button - shown on hover for all messages */ }
262+ { enableMessageLinks && hoveredMessageId === messageId && (
263+ < div className = "absolute top-2 right-2 z-10" >
185264 < button
186265 onClick = { ( ) => handleAnchorClick ( messageId ) }
187266 className = { cn (
188- 'p-1 rounded hover:bg-muted transition-colors duration-200 cursor-pointer' ,
267+ 'p-1 rounded bg-background/90 backdrop-blur-sm border border-border/50 shadow-sm hover:bg-muted transition-colors duration-200 cursor-pointer' ,
189268 clickedMessageId === messageId && 'bg-primary/10' ,
190269 ) }
191270 title = "Copy link to this message"
@@ -198,59 +277,54 @@ export const Messages = ({
198277 ) }
199278 />
200279 </ button >
201- ) }
202- { message . mode && (
203- < div className = "px-2 py-1 bg-muted rounded text-xs font-medium" >
204- { message . mode }
205- </ div >
206- ) }
207- </ div >
208- </ div >
280+ </ div >
281+ ) }
209282
210- { isQuestion && questionData ? (
211- < div className = "space-y-4" >
212- { questionData . question && (
213- < div className = "text-sm leading-relaxed" >
214- { questionData . question }
215- </ div >
216- ) }
217- { questionData . suggestions &&
218- questionData . suggestions . length > 0 && (
219- < div className = "space-y-2" >
220- { questionData . suggestions . map ( ( suggestion , index ) => (
221- < div
222- key = { index }
223- className = "px-4 py-3 bg-background border border-border rounded-md text-sm hover:bg-muted/50 cursor-pointer transition-colors"
224- >
225- { typeof suggestion === 'string'
226- ? suggestion
227- : suggestion . answer }
228- </ div >
229- ) ) }
283+ { isQuestion && questionData ? (
284+ < div className = "space-y-4" >
285+ { questionData . question && (
286+ < div className = "text-sm leading-relaxed" >
287+ { questionData . question }
230288 </ div >
231289 ) }
232- </ div >
233- ) : isCommand ? (
234- < div className = "space-y-3" >
235- < div className = "bg-black/90 text-foreground p-3 rounded-md font-mono text-sm" >
236- { message . text }
290+ { questionData . suggestions &&
291+ questionData . suggestions . length > 0 && (
292+ < div className = "space-y-2" >
293+ { questionData . suggestions . map ( ( suggestion , index ) => (
294+ < div
295+ key = { index }
296+ className = "px-4 py-3 bg-background border border-border rounded-md text-sm hover:bg-muted/50 cursor-pointer transition-colors"
297+ >
298+ { typeof suggestion === 'string'
299+ ? suggestion
300+ : suggestion . answer }
301+ </ div >
302+ ) ) }
303+ </ div >
304+ ) }
237305 </ div >
238- </ div >
239- ) : (
240- < div className = "text-sm leading-relaxed markdown-prose" >
241- < ReactMarkdown
242- components = { {
243- a : PlainTextLink ,
244- code : CodeBlock ,
245- } }
246- >
247- { message . text }
248- </ ReactMarkdown >
249- </ div >
250- ) }
251- </ div >
252- ) ;
253- } ) }
306+ ) : isCommand ? (
307+ < div className = "space-y-3" >
308+ < div className = "bg-black/90 text-foreground p-3 rounded-md font-mono text-sm" >
309+ { message . text }
310+ </ div >
311+ </ div >
312+ ) : (
313+ < div className = "text-sm leading-relaxed markdown-prose" >
314+ < ReactMarkdown
315+ components = { {
316+ a : PlainTextLink ,
317+ code : CodeBlock ,
318+ } }
319+ >
320+ { message . text }
321+ </ ReactMarkdown >
322+ </ div >
323+ ) }
324+ </ div >
325+ ) ;
326+ } ) }
327+ </ div >
254328 </ div >
255329
256330 { /* Scroll to bottom button - shown when user has scrolled up */ }
@@ -282,8 +356,14 @@ export const Messages = ({
282356 ) ;
283357} ;
284358
285- const decorate = ( { message, index } : { message : Message ; index : number } ) => {
286- const role =
359+ const decorate = ( {
360+ message,
361+ index,
362+ } : {
363+ message : Message ;
364+ index : number ;
365+ } ) : DecoratedMessage => {
366+ const role : 'user' | 'assistant' =
287367 index === 0 || message . say === 'user_feedback' ? 'user' : 'assistant' ;
288368
289369 const name = role === 'user' ? 'User' : 'Roo Code' ;
0 commit comments