diff --git a/apps/web/src/hooks/useChatSession.ts b/apps/web/src/hooks/useChatSession.ts index 571fe7c..ab07704 100644 --- a/apps/web/src/hooks/useChatSession.ts +++ b/apps/web/src/hooks/useChatSession.ts @@ -1,4 +1,5 @@ -import { useCallback, useMemo, useRef } from 'react'; +import { useCallback, useMemo, useRef, useState } from 'react'; +import { toast } from 'sonner'; import { trpc } from '@/lib/trpc'; import { useLiveUpdates } from './useLiveUpdates'; @@ -46,10 +47,13 @@ export function useChatSession( ): UseChatSessionResult { const utils = trpc.useUtils(); + // Optimistic messages shown before server confirms + const [optimisticMessages, setOptimisticMessages] = useState([]); + // Live updates for chat + agent events useLiveUpdates([ { prefix: 'chat:', invalidate: ['getChatSession'] }, - { prefix: 'agent:', invalidate: ['getChatSession'] }, + { prefix: 'agent:', invalidate: ['getChatSession', 'getAgent'] }, { prefix: 'changeset:', invalidate: ['getChatSession'] }, ]); @@ -59,7 +63,14 @@ export function useChatSession( { enabled: !!targetId }, ); const session = (sessionQuery.data as ChatSession | null) ?? null; - const messages = session?.messages ?? []; + const serverMessages = session?.messages ?? []; + + // Merge: show server messages, plus any optimistic ones not yet confirmed + const serverMsgIds = new Set(serverMessages.map(m => m.id)); + const messages = [ + ...serverMessages, + ...optimisticMessages.filter(m => !serverMsgIds.has(m.id)), + ]; // Query agent status if session has an agent const agentQuery = trpc.getAgent.useQuery( @@ -86,6 +97,13 @@ export function useChatSession( const sendMutation = trpc.sendChatMessage.useMutation({ onSuccess: () => { void utils.getChatSession.invalidate({ targetType, targetId }); + // Clear optimistic messages once server confirms + setOptimisticMessages([]); + }, + onError: (err) => { + toast.error(`Chat failed: ${err.message}`); + // Remove optimistic messages on error + setOptimisticMessages([]); }, }); @@ -94,6 +112,9 @@ export function useChatSession( onSuccess: () => { void utils.getChatSession.invalidate({ targetType, targetId }); }, + onError: (err) => { + toast.error(`Failed to close chat: ${err.message}`); + }, }); const sendMutateRef = useRef(sendMutation.mutate); @@ -105,6 +126,17 @@ export function useChatSession( const sendMessage = useCallback( (message: string) => { + // Add optimistic user message immediately + const optimistic: ChatMessage = { + id: `opt-${Date.now()}`, + chatSessionId: '', + role: 'user', + content: message, + changeSetId: null, + createdAt: new Date().toISOString(), + }; + setOptimisticMessages(prev => [...prev, optimistic]); + sendMutateRef.current({ targetType, targetId,