Skip to content
Merged
4 changes: 2 additions & 2 deletions frontend/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"elliptic": "^6.6.1"
},
"dependencies": {
"@a2a-js/sdk": "^0.3.5",
"@a2a-js/sdk": "^0.3.10",
"@autoform/react": "^4.0.0",
"@autoform/zod": "^5.0.0",
"@buf/redpandadata_cloud.connectrpc_query-es": "^2.2.0-20251128173054-b9f9fc6e5a70.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const AIAgentChat = ({ agent, headerActions }: AIAgentChatProps) => {
const containerRef = useRef<HTMLDivElement>(null);

// Manage chat messages and context
const { messages, setMessages, contextId, setContextSeed, isLoadingHistory } = useChatMessages(agent.id);
const { messages, setMessages, contextId, setContextSeed, isLoadingHistory } = useChatMessages(agent.id, agent.url);

// Manage chat actions (submit, clear, cancel)
// Pass agent.url directly so the A2A client can try multiple agent card URLs
Expand Down Expand Up @@ -88,9 +88,9 @@ export const AIAgentChat = ({ agent, headerActions }: AIAgentChatProps) => {
return (
<div className="flex h-[calc(100vh-210px)] flex-col" ref={containerRef}>
{/* Context ID header */}
{Boolean(contextId) && (
<div className="shrink-0 border-b bg-gradient-to-r from-muted/50 to-muted/30 px-4 py-1.5">
<div className="flex items-center justify-between gap-4">
<div className="shrink-0 border-b bg-gradient-to-r from-muted/50 to-muted/30 px-4 py-1.5">
<div className="flex items-center justify-between gap-4">
{messages.length > 0 ? (
<div className="flex items-center gap-2">
<div className="flex h-5 w-5 items-center justify-center rounded bg-primary/10">
<FingerprintIcon className="h-3 w-3 text-primary" />
Expand All @@ -109,10 +109,12 @@ export const AIAgentChat = ({ agent, headerActions }: AIAgentChatProps) => {
/>
</div>
</div>
{headerActions}
</div>
) : (
<div />
)}
{headerActions}
</div>
)}
</div>

<Conversation className="min-h-0 flex-1" initial="instant" resize="instant">
<ConversationContent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Message, MessageBody, MessageContent, MessageMetadata } from 'component
import { ChatMessageActions } from './chat-message-actions';
import { A2AErrorBlock } from './message-blocks/a2a-error-block';
import { ArtifactBlock } from './message-blocks/artifact-block';
import { ConnectionStatusBlock } from './message-blocks/connection-status-block';
import { TaskStatusUpdateBlock } from './message-blocks/task-status-update-block';
import { ToolBlock } from './message-blocks/tool-block';
import { UserMessageContent } from './message-content/user-message-content';
Expand Down Expand Up @@ -105,6 +106,16 @@ export const ChatMessage = ({ message, isLoading: _isLoading }: ChatMessageProps
);
case 'a2a-error':
return <A2AErrorBlock error={block.error} key={`${message.id}-error-${index}`} timestamp={block.timestamp} />;
case 'connection-status':
return (
<ConnectionStatusBlock
attempt={block.attempt}
key={`${message.id}-conn-${index}`}
maxAttempts={block.maxAttempts}
status={block.status}
timestamp={block.timestamp}
/>
);
default:
return null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* Copyright 2025 Redpanda Data, Inc.
*
* Use of this software is governed by the Business Source License
* included in the file https://github.com/redpanda-data/redpanda/blob/dev/licenses/bsl.md
*
* As of the Change Date specified in that file, in accordance with
* the Business Source License, use of this software will be governed
* by the Apache License, Version 2.0
*/

import { Alert, AlertDescription, AlertTitle } from 'components/redpanda-ui/components/alert';
import { Button } from 'components/redpanda-ui/components/button';
import { Text } from 'components/redpanda-ui/components/typography';
import { AlertCircleIcon, LoaderCircleIcon, RefreshCwIcon, WifiIcon, WifiOffIcon } from 'lucide-react';

type ConnectionStatusBlockProps = {
status: 'disconnected' | 'reconnecting' | 'reconnected' | 'gave-up';
attempt?: number;
maxAttempts?: number;
timestamp: Date;
};

export const ConnectionStatusBlock = ({ status, attempt, maxAttempts, timestamp }: ConnectionStatusBlockProps) => {
const time = timestamp.toLocaleTimeString(undefined, {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});

if (status === 'reconnected') {
return (
<div className="mb-2 flex items-center gap-1.5 px-1 text-muted-foreground text-xs">
<WifiIcon className="size-3" />
<span>Reconnected at {time}</span>
</div>
);
}

if (status === 'disconnected' || status === 'reconnecting') {
const label =
status === 'reconnecting' && attempt
? `Reconnecting... (attempt ${attempt} of ${maxAttempts ?? '?'})`
: 'Connection lost, attempting to reconnect...';

return (
<Alert className="mb-4" icon={<WifiOffIcon />} variant="warning">
<AlertTitle className="flex items-center gap-2">
{label}
<LoaderCircleIcon className="size-3.5 animate-spin" />
</AlertTitle>
<AlertDescription>
<Text className="text-blue-600 text-xs" variant="body">
The agent task is still running. Trying to re-establish the event stream.
</Text>
</AlertDescription>
</Alert>
);
}

// gave-up
return (
<Alert className="mb-4" icon={<AlertCircleIcon />} variant="destructive">
<AlertTitle>Connection lost</AlertTitle>
<AlertDescription className="flex flex-col items-start gap-2">
<Text className="text-destructive/90" variant="body">
Unable to reconnect after {maxAttempts ?? '?'} attempts. The agent task may still be running server-side.
</Text>
<Button onClick={() => window.location.reload()} size="sm" variant="outline">
<RefreshCwIcon />
Reload to check status
</Button>
</AlertDescription>
</Alert>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ type UseChatMessagesResult = {
/**
* Hook to manage chat messages and context
*/
export const useChatMessages = (agentId: string): UseChatMessagesResult => {
export const useChatMessages = (agentId: string, agentCardUrl: string): UseChatMessagesResult => {
// Use a stable contextSeed - only generate once per agent, persists across reloads
// Only changes when user explicitly clears chat via setContextSeed
const [contextSeed, setContextSeed] = useState<string>(() => {
Expand Down Expand Up @@ -58,7 +58,7 @@ export const useChatMessages = (agentId: string): UseChatMessagesResult => {
async function loadChatMessages() {
setIsLoadingHistory(true);
try {
const loadedMessages = await loadMessages(agentId, contextId);
const loadedMessages = await loadMessages(agentId, contextId, agentCardUrl);
setMessages(loadedMessages);
} catch {
// Error loading messages - silently fail and show empty state
Expand All @@ -68,7 +68,7 @@ export const useChatMessages = (agentId: string): UseChatMessagesResult => {
}

loadChatMessages();
}, [agentId, contextId]);
}, [agentId, contextId, agentCardUrl]);

return {
messages,
Expand Down
Loading
Loading