From 458b1e19e22e38b3b6141bba06c5a535357b2932 Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Mon, 13 Apr 2026 18:27:38 +0800 Subject: [PATCH] feat(chat): improve session history UX and align chat window offset - Add optimistic update + rollback to archive mutation - Replace Trash2 with Archive icon (correct semantics) - Add Tooltip on archive button, replace native title - Show spinner during archive, toast on error - Use cn() for className composition - Align chat window offset to bottom-2 right-2 (match FAB) Co-Authored-By: Claude Opus 4.6 --- packages/core/chat/mutations.ts | 24 +++++++++ .../chat/components/chat-session-history.tsx | 51 +++++++++++++------ .../views/chat/components/chat-window.tsx | 2 +- 3 files changed, 61 insertions(+), 16 deletions(-) diff --git a/packages/core/chat/mutations.ts b/packages/core/chat/mutations.ts index 2a208919f..9b7adb78c 100644 --- a/packages/core/chat/mutations.ts +++ b/packages/core/chat/mutations.ts @@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { api } from "../api"; import { useWorkspaceId } from "../hooks"; import { chatKeys } from "./queries"; +import type { ChatSession } from "../types"; export function useCreateChatSession() { const qc = useQueryClient(); @@ -23,6 +24,29 @@ export function useArchiveChatSession() { return useMutation({ mutationFn: (sessionId: string) => api.archiveChatSession(sessionId), + onMutate: async (sessionId) => { + await qc.cancelQueries({ queryKey: chatKeys.sessions(wsId) }); + await qc.cancelQueries({ queryKey: chatKeys.allSessions(wsId) }); + + const prevSessions = qc.getQueryData(chatKeys.sessions(wsId)); + const prevAll = qc.getQueryData(chatKeys.allSessions(wsId)); + + // Optimistic: remove from active, mark as archived in allSessions + qc.setQueryData(chatKeys.sessions(wsId), (old) => + old ? old.filter((s) => s.id !== sessionId) : old, + ); + qc.setQueryData(chatKeys.allSessions(wsId), (old) => + old?.map((s) => + s.id === sessionId ? { ...s, status: "archived" as const } : s, + ), + ); + + return { prevSessions, prevAll }; + }, + onError: (_err, _id, ctx) => { + if (ctx?.prevSessions) qc.setQueryData(chatKeys.sessions(wsId), ctx.prevSessions); + if (ctx?.prevAll) qc.setQueryData(chatKeys.allSessions(wsId), ctx.prevAll); + }, onSettled: () => { qc.invalidateQueries({ queryKey: chatKeys.sessions(wsId) }); qc.invalidateQueries({ queryKey: chatKeys.allSessions(wsId) }); diff --git a/packages/views/chat/components/chat-session-history.tsx b/packages/views/chat/components/chat-session-history.tsx index edf0eeb88..ba3c72431 100644 --- a/packages/views/chat/components/chat-session-history.tsx +++ b/packages/views/chat/components/chat-session-history.tsx @@ -1,11 +1,12 @@ "use client"; import { useQuery } from "@tanstack/react-query"; -import { ArrowLeft, MessageSquare, Archive, Trash2 } from "lucide-react"; +import { ArrowLeft, MessageSquare, Archive, Bot, Loader2 } from "lucide-react"; +import { toast } from "sonner"; +import { cn } from "@multica/ui/lib/utils"; import { Button } from "@multica/ui/components/ui/button"; import { Tooltip, TooltipTrigger, TooltipContent } from "@multica/ui/components/ui/tooltip"; import { Avatar, AvatarFallback, AvatarImage } from "@multica/ui/components/ui/avatar"; -import { Bot } from "lucide-react"; import { useWorkspaceId } from "@multica/core/hooks"; import { agentListOptions } from "@multica/core/workspace/queries"; import { allChatSessionsOptions } from "@multica/core/chat/queries"; @@ -36,10 +37,12 @@ export function ChatSessionHistory() { const handleArchive = (e: React.MouseEvent, sessionId: string) => { e.stopPropagation(); - archiveSession.mutate(sessionId); if (activeSessionId === sessionId) { setActiveSession(null); } + archiveSession.mutate(sessionId, { + onError: () => toast.error("Failed to archive session"), + }); }; const activeSessions = sessions.filter((s) => s.status === "active"); @@ -82,6 +85,7 @@ export function ChatSessionHistory() { sessions={activeSessions} agentMap={agentMap} activeSessionId={activeSessionId} + archivingId={archiveSession.isPending ? (archiveSession.variables as string) : null} onSelect={handleSelectSession} onArchive={handleArchive} /> @@ -92,6 +96,7 @@ export function ChatSessionHistory() { sessions={archivedSessions} agentMap={agentMap} activeSessionId={activeSessionId} + archivingId={null} onSelect={handleSelectSession} /> )} @@ -107,6 +112,7 @@ function SessionGroup({ sessions, agentMap, activeSessionId, + archivingId, onSelect, onArchive, }: { @@ -114,6 +120,7 @@ function SessionGroup({ sessions: ChatSession[]; agentMap: Map; activeSessionId: string | null; + archivingId: string | null; onSelect: (session: ChatSession) => void; onArchive?: (e: React.MouseEvent, sessionId: string) => void; }) { @@ -130,6 +137,7 @@ function SessionGroup({ session={session} agent={agentMap.get(session.agent_id) ?? null} isActive={session.id === activeSessionId} + isArchiving={session.id === archivingId} onSelect={() => onSelect(session)} onArchive={onArchive ? (e) => onArchive(e, session.id) : undefined} /> @@ -142,12 +150,14 @@ function SessionItem({ session, agent, isActive, + isArchiving, onSelect, onArchive, }: { session: ChatSession; agent: Agent | null; isActive: boolean; + isArchiving: boolean; onSelect: () => void; onArchive?: (e: React.MouseEvent) => void; }) { @@ -156,9 +166,10 @@ function SessionItem({ return ( + + + } + > + {isArchiving ? : } + + Archive + )} ); diff --git a/packages/views/chat/components/chat-window.tsx b/packages/views/chat/components/chat-window.tsx index d39e0f25d..9450a3452 100644 --- a/packages/views/chat/components/chat-window.tsx +++ b/packages/views/chat/components/chat-window.tsx @@ -232,7 +232,7 @@ export function ChatWindow() { const isVisible = isOpen && boundsReady; - const containerClass = "absolute bottom-4 right-4 z-50 flex flex-col rounded-xl ring-1 ring-foreground/10 bg-sidebar shadow-2xl overflow-hidden"; + const containerClass = "absolute bottom-2 right-2 z-50 flex flex-col rounded-xl ring-1 ring-foreground/10 bg-sidebar shadow-2xl overflow-hidden"; const containerStyle: React.CSSProperties = { width: `${renderWidth}px`, height: `${renderHeight}px`,