From 34cfd156d182e3288458c945ebcfce217719cfa9 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Tue, 1 Jul 2025 15:53:06 -0400 Subject: [PATCH] Group messages to reduce visual noise (#179) --- .../app/(authenticated)/usage/Messages.tsx | 252 ++++++++++++------ 1 file changed, 166 insertions(+), 86 deletions(-) diff --git a/apps/web/src/app/(authenticated)/usage/Messages.tsx b/apps/web/src/app/(authenticated)/usage/Messages.tsx index d9f2b07025..ab3c8be91a 100644 --- a/apps/web/src/app/(authenticated)/usage/Messages.tsx +++ b/apps/web/src/app/(authenticated)/usage/Messages.tsx @@ -44,6 +44,44 @@ const parseQuestionData = (text: string): QuestionData | null => { return null; }; +type DecoratedMessage = Omit & { + role: 'user' | 'assistant'; + name: string; + timestamp: string; + showHeader?: boolean; +}; + +// Determine if a message should show its header based on grouping rules +const shouldShowHeader = ( + message: DecoratedMessage, + index: number, + messages: DecoratedMessage[], + groupingWindowMinutes: number, +): boolean => { + // Always show header for first message or user messages + if (index === 0 || message.role === 'user') return true; + + const prevMessage = messages[index - 1]; + if (!prevMessage) return true; // Safety check + + // Show header if previous message was from user + if (prevMessage.role === 'user') return true; + + // Show header if mode changed + if (message.mode !== prevMessage.mode) return true; + + // Show header if time gap between consecutive messages exceeds threshold + // Use the original timestamp (number) for calculation + const currentTime = message.ts; + const prevTime = prevMessage.ts; + const gapMinutes = (currentTime - prevTime) / (1000 * 60); + + return gapMinutes > groupingWindowMinutes; +}; + +// Constant for message grouping window (in minutes) +const GROUPING_WINDOW_MINUTES = 5; + export const Messages = ({ messages, enableMessageLinks = false, @@ -74,9 +112,25 @@ export const Messages = ({ ); }); - return deduplicatedMessages.map((message, index) => + // Decorate messages with role, name, and timestamp + const decoratedMessages = deduplicatedMessages.map((message, index) => decorate({ message, index }), ); + + // Add grouping information to each message + return decoratedMessages.map((message, index) => { + const showHeader = shouldShowHeader( + message, + index, + decoratedMessages, + GROUPING_WINDOW_MINUTES, + ); + + return { + ...message, + showHeader, + }; + }); }, [messages]); // Handle anchor link clicks @@ -142,50 +196,75 @@ export const Messages = ({ {/* Scrollable messages container */}
- {conversation.map((message) => { - const isQuestion = - message.type === 'ask' && message.ask === 'followup'; - const isCommand = message.type === 'ask' && message.ask === 'command'; - const questionData = - isQuestion && message.text ? parseQuestionData(message.text) : null; +
+ {conversation.map((message, index) => { + const isQuestion = + message.type === 'ask' && message.ask === 'followup'; + const isCommand = + message.type === 'ask' && message.ask === 'command'; + const questionData = + isQuestion && message.text + ? parseQuestionData(message.text) + : null; - const messageId = `message-${message.id}`; + const messageId = `message-${message.id}`; - return ( -
- enableMessageLinks && setHoveredMessageId(messageId) - } - onMouseLeave={() => - enableMessageLinks && setHoveredMessageId(null) - } - > -
-
-
{message.name}
-
·
-
{message.timestamp}
-
-
- {/* Anchor Link Button */} - {enableMessageLinks && hoveredMessageId === messageId && ( + return ( +
0 && 'mt-4', + )} + onMouseEnter={() => + enableMessageLinks && setHoveredMessageId(messageId) + } + onMouseLeave={() => + enableMessageLinks && setHoveredMessageId(null) + } + > + {message.showHeader && ( +
+
+
{message.name}
+
·
+
{message.timestamp}
+ {message.mode && ( + <> +
·
+
{message.mode}
+ + )} +
+
+ )} + + {/* Anchor Link Button - shown on hover for all messages */} + {enableMessageLinks && hoveredMessageId === messageId && ( +
- )} - {message.mode && ( -
- {message.mode} -
- )} -
-
+
+ )} - {isQuestion && questionData ? ( -
- {questionData.question && ( -
- {questionData.question} -
- )} - {questionData.suggestions && - questionData.suggestions.length > 0 && ( -
- {questionData.suggestions.map((suggestion, index) => ( -
- {typeof suggestion === 'string' - ? suggestion - : suggestion.answer} -
- ))} + {isQuestion && questionData ? ( +
+ {questionData.question && ( +
+ {questionData.question}
)} -
- ) : isCommand ? ( -
-
- {message.text} + {questionData.suggestions && + questionData.suggestions.length > 0 && ( +
+ {questionData.suggestions.map((suggestion, index) => ( +
+ {typeof suggestion === 'string' + ? suggestion + : suggestion.answer} +
+ ))} +
+ )}
-
- ) : ( -
- - {message.text} - -
- )} -
- ); - })} + ) : isCommand ? ( +
+
+ {message.text} +
+
+ ) : ( +
+ + {message.text} + +
+ )} +
+ ); + })} +
{/* Scroll to bottom button - shown when user has scrolled up */} @@ -282,8 +356,14 @@ export const Messages = ({ ); }; -const decorate = ({ message, index }: { message: Message; index: number }) => { - const role = +const decorate = ({ + message, + index, +}: { + message: Message; + index: number; +}): DecoratedMessage => { + const role: 'user' | 'assistant' = index === 0 || message.say === 'user_feedback' ? 'user' : 'assistant'; const name = role === 'user' ? 'User' : 'Roo Code';