From 51de6984b6683c74008175260464ecb84453fd37 Mon Sep 17 00:00:00 2001 From: Nicolas Varrot Date: Fri, 13 Feb 2026 18:12:44 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20message=20grouping=20=E2=80=94=20hide?= =?UTF-8?q?=20avatar=20and=20reduce=20spacing=20for=20consecutive=20same-r?= =?UTF-8?q?ole=20messages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consecutive messages from the same role within 2 minutes are visually grouped: subsequent messages hide the avatar (keeping alignment) and use tighter vertical spacing. Date separators and role changes break groups. This matches modern chat UX patterns (Discord, Telegram). --- FEEDBACK.md | 7 +++++++ src/components/Chat.tsx | 12 ++++++++---- src/components/ChatMessage.tsx | 21 +++++++++++---------- src/index.css | 8 ++++++++ 4 files changed, 34 insertions(+), 14 deletions(-) diff --git a/FEEDBACK.md b/FEEDBACK.md index 5adc68e..4864a0a 100644 --- a/FEEDBACK.md +++ b/FEEDBACK.md @@ -664,3 +664,10 @@ - **Status:** done - **Completed:** 2026-02-13 — commit `cbb4611` - **Description:** Markdown unordered lists (- item, * item) are not rendered properly in chat messages. They appear as raw text instead of formatted bullet points. Need to verify remarkGfm/ReactMarkdown config handles list rendering correctly, and ensure CSS styles are applied for ul/ol elements in the markdown-body class. + +## Item #62 +- **Date:** 2026-02-13 +- **Priority:** high +- **Status:** done +- **Completed:** 2026-02-13 — commit `2f25c45` +- **Description:** Textarea has an ugly/thick accent-colored border (cyan) visible in the screenshot. The border around the chat input textarea looks bad — it should be more subtle (thin border, muted color, or no visible border at all). The input area should blend cleanly with its container, not have a glowing cyan outline. diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx index 888ab59..db63393 100644 --- a/src/components/Chat.tsx +++ b/src/components/Chat.tsx @@ -119,10 +119,14 @@ export function Chat({ messages, isGenerating, isLoadingHistory, status, session const visibleMessages = useMemo(() => { const filtered = messages.filter(hasVisibleContent); - return filtered.reduce>((acc, msg) => { + const GROUP_GAP_MS = 2 * 60 * 1000; // 2 minutes + return filtered.reduce>((acc, msg) => { const dk = getDateKey(msg.timestamp); const prevDk = acc.length > 0 ? getDateKey(acc[acc.length - 1].msg.timestamp) : ''; - acc.push({ msg, showSep: dk !== prevDk }); + const showSep = dk !== prevDk; + const prev = acc.length > 0 ? acc[acc.length - 1] : null; + const isFirstInGroup = showSep || !prev || prev.msg.role !== msg.role || prev.msg.isSystemEvent !== msg.isSystemEvent || (msg.timestamp - prev.msg.timestamp > GROUP_GAP_MS); + acc.push({ msg, showSep, isFirstInGroup }); return acc; }, []); }, [messages]); @@ -207,7 +211,7 @@ export function Chat({ messages, isGenerating, isLoadingHistory, status, session
{t('chat.welcomeSub')}
)} - {visibleMessages.map(({ msg, showSep }) => { + {visibleMessages.map(({ msg, showSep, isFirstInGroup }) => { const isActiveMatch = searchMatches.length > 0 && searchMatches[searchActiveIndex] === msg.id; return (
@@ -219,7 +223,7 @@ export function Chat({ messages, isGenerating, isLoadingHistory, status, session
)}
- +
); diff --git a/src/components/ChatMessage.tsx b/src/components/ChatMessage.tsx index a57912b..dfa1dcf 100644 --- a/src/components/ChatMessage.tsx +++ b/src/components/ChatMessage.tsx @@ -405,7 +405,7 @@ function SystemEventMessage({ message }: { message: ChatMessageType }) { ); } -export const ChatMessageComponent = memo(function ChatMessageComponent({ message: rawMessage, onRetry, agentAvatarUrl }: { message: ChatMessageType; onRetry?: (text: string) => void; agentAvatarUrl?: string }) { +export const ChatMessageComponent = memo(function ChatMessageComponent({ message: rawMessage, onRetry, agentAvatarUrl, isFirstInGroup = true }: { message: ChatMessageType; onRetry?: (text: string) => void; agentAvatarUrl?: string; isFirstInGroup?: boolean }) { useLocale(); // re-render on locale change const { resolvedTheme } = useTheme(); const isLight = resolvedTheme === 'light'; @@ -458,15 +458,16 @@ export const ChatMessageComponent = memo(function ChatMessageComponent({ message } return ( -
- {/* Avatar */} -
- {isUser - ? - : agentAvatarUrl - ? Agent - : - } +
+ {/* Avatar — hidden for grouped messages, but keep width for alignment */} +
+ {isFirstInGroup ? ( + isUser + ? + : agentAvatarUrl + ? Agent + : + ) : null}
{/* Bubble */} diff --git a/src/index.css b/src/index.css index 07e2786..11dd7d1 100644 --- a/src/index.css +++ b/src/index.css @@ -52,6 +52,14 @@ outline-offset: 2px; } +/* Chat textarea: no focus ring (container handles visual feedback) */ +.ht-textarea:focus-visible, +.ht-textarea:focus, +#chat-input:focus-visible, +#chat-input:focus { + outline: none; +} + /* Remove default outline for mouse/touch users */ :focus:not(:focus-visible) { outline: none;