From 711395f81f56b81e96d799f04011bda01416a136 Mon Sep 17 00:00:00 2001 From: viettranx Date: Thu, 19 Mar 2026 21:47:46 +0700 Subject: [PATCH] =?UTF-8?q?fix(ui):=20chat=20polish=20=E2=80=94=20floating?= =?UTF-8?q?=20bar,=20dotted=20bg,=20tool=20icon,=20flash=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Chat input bar: floating with backdrop blur and rounded corners - Chat thread: dotted background pattern matching kanban board - Tool call cards: blue Wrench icon for completed (was green Check) - Memo wrap ChatSidebar, SessionSwitcher, ChatThread to prevent re-renders - Keep old messages visible during session switch to prevent flash --- ui/web/src/components/chat/chat-input.tsx | 2 +- ui/web/src/components/chat/session-switcher.tsx | 5 +++-- ui/web/src/components/chat/tool-call-card.tsx | 6 +++--- ui/web/src/pages/chat/chat-sidebar.tsx | 5 +++-- ui/web/src/pages/chat/chat-thread.tsx | 16 ++++++++++++---- ui/web/src/pages/chat/hooks/use-chat-messages.ts | 6 +++--- 6 files changed, 25 insertions(+), 15 deletions(-) diff --git a/ui/web/src/components/chat/chat-input.tsx b/ui/web/src/components/chat/chat-input.tsx index 730510f9..ff43fc14 100644 --- a/ui/web/src/components/chat/chat-input.tsx +++ b/ui/web/src/components/chat/chat-input.tsx @@ -69,7 +69,7 @@ export function ChatInput({ onSend, onAbort, isRunning, disabled }: ChatInputPro return (
{/* Attached files preview */} diff --git a/ui/web/src/components/chat/session-switcher.tsx b/ui/web/src/components/chat/session-switcher.tsx index d8bb476f..2ffaf89c 100644 --- a/ui/web/src/components/chat/session-switcher.tsx +++ b/ui/web/src/components/chat/session-switcher.tsx @@ -1,3 +1,4 @@ +import { memo } from "react"; import { useTranslation } from "react-i18next"; import { MessageSquare } from "lucide-react"; import { formatRelativeTime } from "@/lib/format"; @@ -40,7 +41,7 @@ function sessionLabel(session: SessionInfo): string { return scope.length > 24 ? scope.slice(0, 21) + "…" : scope; } -export function SessionSwitcher({ sessions, activeKey, onSelect, loading }: SessionSwitcherProps) { +export const SessionSwitcher = memo(function SessionSwitcher({ sessions, activeKey, onSelect, loading }: SessionSwitcherProps) { const { t } = useTranslation("common"); if (sessions.length === 0 && loading) { @@ -90,4 +91,4 @@ export function SessionSwitcher({ sessions, activeKey, onSelect, loading }: Sess })}
); -} +}); diff --git a/ui/web/src/components/chat/tool-call-card.tsx b/ui/web/src/components/chat/tool-call-card.tsx index 98096dd9..5650c048 100644 --- a/ui/web/src/components/chat/tool-call-card.tsx +++ b/ui/web/src/components/chat/tool-call-card.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { Wrench, Check, AlertTriangle, Loader2, ChevronDown, ChevronRight, Zap } from "lucide-react"; +import { Wrench, AlertTriangle, Loader2, ChevronDown, ChevronRight, Zap } from "lucide-react"; import type { ToolStreamEntry } from "@/types/chat"; const isSkillTool = (name: string) => name === "use_skill"; @@ -89,7 +89,7 @@ function ToolIcon({ phase, isSkill }: { phase: ToolStreamEntry["phase"]; isSkill } switch (phase) { case "calling": return ; - case "completed": return ; + case "completed": return ; case "error": return ; default: return ; } @@ -102,7 +102,7 @@ function PhaseLabel({ phase, isSkill }: { phase: ToolStreamEntry["phase"]; isSki : { calling: t("toolRunning"), completed: t("toolDone"), error: t("toolFailed") }; const colors: Record = { calling: "text-blue-500", - completed: "text-green-500", + completed: "text-blue-500", error: "text-red-500", }; return {labels[phase] ?? phase}; diff --git a/ui/web/src/pages/chat/chat-sidebar.tsx b/ui/web/src/pages/chat/chat-sidebar.tsx index 3b9f0e36..43b24ff7 100644 --- a/ui/web/src/pages/chat/chat-sidebar.tsx +++ b/ui/web/src/pages/chat/chat-sidebar.tsx @@ -1,3 +1,4 @@ +import { memo } from "react"; import { useTranslation } from "react-i18next"; import { Plus } from "lucide-react"; import { Button } from "@/components/ui/button"; @@ -15,7 +16,7 @@ interface ChatSidebarProps { onNewChat: () => void; } -export function ChatSidebar({ +export const ChatSidebar = memo(function ChatSidebar({ agentId, onAgentChange, sessions, @@ -55,4 +56,4 @@ export function ChatSidebar({ ); -} +}); diff --git a/ui/web/src/pages/chat/chat-thread.tsx b/ui/web/src/pages/chat/chat-thread.tsx index ba34f075..6d1268ba 100644 --- a/ui/web/src/pages/chat/chat-thread.tsx +++ b/ui/web/src/pages/chat/chat-thread.tsx @@ -1,4 +1,4 @@ -import { useMemo } from "react"; +import { memo, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { Bot } from "lucide-react"; import { MessageBubble } from "@/components/chat/message-bubble"; @@ -70,7 +70,7 @@ function buildDisplayItems(messages: ChatMessage[]): DisplayItem[] { return items; } -export function ChatThread({ +export const ChatThread = memo(function ChatThread({ messages, streamText, thinkingText, toolStream, blockReplies, activity, teamTasks, isRunning, loading, scrollTrigger = 0, }: ChatThreadProps) { @@ -101,7 +101,15 @@ export function ChatThread({ } return ( -
+
{displayItems.map((item) => { switch (item.kind) { @@ -127,7 +135,7 @@ export function ChatThread({
); -} +}); /** Renders multiple consecutive tool-only messages as a single compact card */ function MergedToolGroup({ msgs }: { msgs: ChatMessage[] }) { diff --git a/ui/web/src/pages/chat/hooks/use-chat-messages.ts b/ui/web/src/pages/chat/hooks/use-chat-messages.ts index 56c19e79..df449b79 100644 --- a/ui/web/src/pages/chat/hooks/use-chat-messages.ts +++ b/ui/web/src/pages/chat/hooks/use-chat-messages.ts @@ -35,12 +35,12 @@ export function useChatMessages(sessionKey: string, agentId: string) { const activityRef = useRef(null); const blockRepliesRef = useRef([]); - // Synchronously clear state during render when session changes. - // This prevents a flash of old messages before the useEffect fires. + // Clear streaming/run state when session changes, but keep messages + // until new history loads to avoid a flash of empty content. const [prevKey, setPrevKey] = useState(sessionKey); if (sessionKey !== prevKey) { setPrevKey(sessionKey); - setMessages([]); + // Don't clear messages — they'll be replaced by loadHistory() setStreamText(null); setThinkingText(null); setToolStream([]);