fix: Add error toasts and optimistic messages to chat session hook

Errors from sendChatMessage were silently swallowed. Now shows toast
on failure and immediately renders the user's message optimistically.
This commit is contained in:
Lukas May
2026-03-04 10:31:19 +01:00
parent f06ac953eb
commit 1b5fdfde55

View File

@@ -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 { trpc } from '@/lib/trpc';
import { useLiveUpdates } from './useLiveUpdates'; import { useLiveUpdates } from './useLiveUpdates';
@@ -46,10 +47,13 @@ export function useChatSession(
): UseChatSessionResult { ): UseChatSessionResult {
const utils = trpc.useUtils(); const utils = trpc.useUtils();
// Optimistic messages shown before server confirms
const [optimisticMessages, setOptimisticMessages] = useState<ChatMessage[]>([]);
// Live updates for chat + agent events // Live updates for chat + agent events
useLiveUpdates([ useLiveUpdates([
{ prefix: 'chat:', invalidate: ['getChatSession'] }, { prefix: 'chat:', invalidate: ['getChatSession'] },
{ prefix: 'agent:', invalidate: ['getChatSession'] }, { prefix: 'agent:', invalidate: ['getChatSession', 'getAgent'] },
{ prefix: 'changeset:', invalidate: ['getChatSession'] }, { prefix: 'changeset:', invalidate: ['getChatSession'] },
]); ]);
@@ -59,7 +63,14 @@ export function useChatSession(
{ enabled: !!targetId }, { enabled: !!targetId },
); );
const session = (sessionQuery.data as ChatSession | null) ?? null; 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 // Query agent status if session has an agent
const agentQuery = trpc.getAgent.useQuery( const agentQuery = trpc.getAgent.useQuery(
@@ -86,6 +97,13 @@ export function useChatSession(
const sendMutation = trpc.sendChatMessage.useMutation({ const sendMutation = trpc.sendChatMessage.useMutation({
onSuccess: () => { onSuccess: () => {
void utils.getChatSession.invalidate({ targetType, targetId }); 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: () => { onSuccess: () => {
void utils.getChatSession.invalidate({ targetType, targetId }); void utils.getChatSession.invalidate({ targetType, targetId });
}, },
onError: (err) => {
toast.error(`Failed to close chat: ${err.message}`);
},
}); });
const sendMutateRef = useRef(sendMutation.mutate); const sendMutateRef = useRef(sendMutation.mutate);
@@ -105,6 +126,17 @@ export function useChatSession(
const sendMessage = useCallback( const sendMessage = useCallback(
(message: string) => { (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({ sendMutateRef.current({
targetType, targetType,
targetId, targetId,