feat: Show agent errors in chat UI with retry button
When the chat agent crashes (e.g., expired OAuth token), display the error message inline with a Retry button that re-sends the last user message. Input stays enabled so users can also send a new message.
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { motion, AnimatePresence } from 'motion/react';
|
import { motion, AnimatePresence } from 'motion/react';
|
||||||
import { X, Loader2 } from 'lucide-react';
|
import { X, Loader2, AlertTriangle, RotateCcw } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
import { useChatSession } from '@/hooks/useChatSession';
|
import { useChatSession } from '@/hooks/useChatSession';
|
||||||
import { ChatBubble } from './ChatBubble';
|
import { ChatBubble } from './ChatBubble';
|
||||||
import { ChatInput } from './ChatInput';
|
import { ChatInput } from './ChatInput';
|
||||||
@@ -41,7 +42,7 @@ function ChatSlideOverInner({
|
|||||||
initiativeId: string;
|
initiativeId: string;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { messages, agentStatus, sendMessage, closeSession, isSending } =
|
const { messages, agentStatus, agentError, sendMessage, closeSession, isSending } =
|
||||||
useChatSession(target.type, target.id, initiativeId);
|
useChatSession(target.type, target.id, initiativeId);
|
||||||
|
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -130,6 +131,32 @@ function ChatSlideOverInner({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{agentStatus === 'crashed' && agentError && (
|
||||||
|
<div className="mx-4 my-2 rounded-lg border border-status-error-border bg-status-error-bg px-3 py-2.5">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<AlertTriangle className="mt-0.5 h-3.5 w-3.5 shrink-0 text-status-error-fg" />
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="text-xs font-medium text-status-error-fg">Agent error</p>
|
||||||
|
<p className="mt-0.5 text-xs text-status-error-fg/80 break-words">{agentError}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 flex justify-end">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 gap-1.5 text-xs"
|
||||||
|
onClick={() => {
|
||||||
|
// Re-send the last user message to retry
|
||||||
|
const lastUserMsg = [...messages].reverse().find(m => m.role === 'user');
|
||||||
|
if (lastUserMsg) sendMessage(lastUserMsg.content);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RotateCcw className="h-3 w-3" />
|
||||||
|
Retry
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Input */}
|
{/* Input */}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { toast } from 'sonner';
|
|||||||
import { trpc } from '@/lib/trpc';
|
import { trpc } from '@/lib/trpc';
|
||||||
import { useLiveUpdates } from './useLiveUpdates';
|
import { useLiveUpdates } from './useLiveUpdates';
|
||||||
|
|
||||||
export type ChatAgentStatus = 'idle' | 'running' | 'waiting' | 'none';
|
export type ChatAgentStatus = 'idle' | 'running' | 'waiting' | 'crashed' | 'none';
|
||||||
|
|
||||||
export interface ChatMessage {
|
export interface ChatMessage {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -28,6 +28,7 @@ export interface UseChatSessionResult {
|
|||||||
session: ChatSession | null;
|
session: ChatSession | null;
|
||||||
messages: ChatMessage[];
|
messages: ChatMessage[];
|
||||||
agentStatus: ChatAgentStatus;
|
agentStatus: ChatAgentStatus;
|
||||||
|
agentError: string | null;
|
||||||
sendMessage: (message: string) => void;
|
sendMessage: (message: string) => void;
|
||||||
closeSession: () => void;
|
closeSession: () => void;
|
||||||
isSending: boolean;
|
isSending: boolean;
|
||||||
@@ -88,11 +89,26 @@ export function useChatSession(
|
|||||||
return 'waiting';
|
return 'waiting';
|
||||||
case 'idle':
|
case 'idle':
|
||||||
return 'idle';
|
return 'idle';
|
||||||
|
case 'crashed':
|
||||||
|
return 'crashed';
|
||||||
default:
|
default:
|
||||||
return 'none';
|
return 'none';
|
||||||
}
|
}
|
||||||
}, [session?.agentId, agentQuery.data]);
|
}, [session?.agentId, agentQuery.data]);
|
||||||
|
|
||||||
|
// Extract error message when agent crashed
|
||||||
|
const agentError = useMemo(() => {
|
||||||
|
if (agentStatus !== 'crashed' || !agentQuery.data) return null;
|
||||||
|
const result = (agentQuery.data as { result?: string }).result;
|
||||||
|
if (!result) return 'Agent crashed unexpectedly';
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(result);
|
||||||
|
return parsed.message ?? result;
|
||||||
|
} catch {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}, [agentStatus, agentQuery.data]);
|
||||||
|
|
||||||
// Send message mutation
|
// Send message mutation
|
||||||
const sendMutation = trpc.sendChatMessage.useMutation({
|
const sendMutation = trpc.sendChatMessage.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -161,6 +177,7 @@ export function useChatSession(
|
|||||||
session,
|
session,
|
||||||
messages,
|
messages,
|
||||||
agentStatus,
|
agentStatus,
|
||||||
|
agentError,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
closeSession,
|
closeSession,
|
||||||
isSending,
|
isSending,
|
||||||
|
|||||||
Reference in New Issue
Block a user