feat(store,ui): handle Hub error messages and display error banner

- Handle `action: "error"` messages in connection-store (e.g. UNAUTHORIZED)
- Widen lastError type to `{code, message}` to support all error codes
- Display dismissible error banner in Chat with role="alert" and aria-live
- Add accessible close button with focus-visible ring

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Naiyuan Qing 2026-02-04 18:39:32 +08:00
parent 1f7951df1b
commit e4f1d51453
2 changed files with 27 additions and 3 deletions

View file

@ -20,7 +20,6 @@ import {
GatewayClient,
StreamAction,
type ConnectionState,
type SendErrorResponse,
type StreamPayload,
type AgentEvent,
type GetAgentMessagesResult,
@ -35,7 +34,7 @@ interface ConnectionStoreState {
hubId: string | null
agentId: string | null
connectionState: ConnectionState
lastError: SendErrorResponse | null
lastError: { code: string; message: string } | null
}
interface ConnectionStoreActions {
@ -146,13 +145,20 @@ function createClient(
return
}
// Handle error messages from Hub (e.g. UNAUTHORIZED)
if (msg.action === "error") {
const payload = msg.payload as { code: string; message: string }
set({ lastError: { code: payload.code, message: payload.message } })
return
}
// Handle direct (non-streaming) messages
const payload = msg.payload as { agentId?: string; content?: string }
if (payload?.agentId && payload?.content) {
useMessagesStore.getState().addAssistantMessage(payload.content, payload.agentId)
}
})
.onSendError((error) => set({ lastError: error }))
.onSendError((error) => set({ lastError: { code: error.code, message: error.error } }))
}
/** Fetch message history from Hub via RPC after connection is established */

View file

@ -16,6 +16,7 @@ export function Chat() {
const agentId = useConnectionStore((s) => s.agentId)
const gwState = useConnectionStore((s) => s.connectionState)
const hubId = useConnectionStore((s) => s.hubId)
const lastError = useConnectionStore((s) => s.lastError)
const messages = useMessagesStore((s) => s.messages)
const streamingIds = useMessagesStore((s) => s.streamingIds)
@ -65,6 +66,23 @@ export function Chat() {
)}
</main>
{/* Error banner */}
{lastError && (
<div className="px-4 py-2 max-w-4xl mx-auto w-full" role="alert" aria-live="polite">
<div className="rounded-md bg-destructive/10 text-destructive text-sm px-3 py-2 flex items-center justify-between">
<span>{lastError.message} ({lastError.code})</span>
<button
type="button"
aria-label="Dismiss error"
onClick={() => useConnectionStore.setState({ lastError: null })}
className="text-destructive/60 hover:text-destructive ml-2 text-xs focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 rounded outline-none"
>
&times;
</button>
</div>
</div>
)}
{/* Footer */}
<footer className="w-full p-2 pt-1 max-w-4xl mx-auto">
<ChatInput