From 28af600c167df23bb17df4feab61fd041f066bed Mon Sep 17 00:00:00 2001 From: Hiep Le <69354317+hieptl@users.noreply.github.com> Date: Wed, 19 Nov 2025 20:15:42 +0700 Subject: [PATCH] fix(frontend): display LLM configuration errors to the user (#11776) --- .../features/controls/server-status.tsx | 6 ++++-- .../conversation-websocket-context.tsx | 6 ++++++ .../v1/core/events/conversation-state-event.ts | 18 ++++++++++++++++++ frontend/src/types/v1/core/openhands-event.ts | 2 ++ frontend/src/types/v1/type-guards.ts | 9 +++++++++ 5 files changed, 39 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/features/controls/server-status.tsx b/frontend/src/components/features/controls/server-status.tsx index a3bbbe9732..e79d4215ea 100644 --- a/frontend/src/components/features/controls/server-status.tsx +++ b/frontend/src/components/features/controls/server-status.tsx @@ -6,6 +6,7 @@ import { AgentState } from "#/types/agent-state"; import { useAgentState } from "#/hooks/use-agent-state"; import { useTaskPolling } from "#/hooks/query/use-task-polling"; import { getStatusColor } from "#/utils/utils"; +import { useErrorMessageStore } from "#/stores/error-message-store"; export interface ServerStatusProps { className?: string; @@ -21,6 +22,7 @@ export function ServerStatus({ const { curAgentState } = useAgentState(); const { t } = useTranslation(); const { isTask, taskStatus, taskDetail } = useTaskPolling(); + const { errorMessage } = useErrorMessageStore(); const isStartingStatus = curAgentState === AgentState.LOADING || curAgentState === AgentState.INIT; @@ -69,7 +71,7 @@ export function ServerStatus({ return t(I18nKey.COMMON$SERVER_STOPPED); } if (curAgentState === AgentState.ERROR) { - return t(I18nKey.COMMON$ERROR); + return errorMessage || t(I18nKey.COMMON$ERROR); } return t(I18nKey.COMMON$RUNNING); }; @@ -79,7 +81,7 @@ export function ServerStatus({ return (
- + {statusText}
diff --git a/frontend/src/contexts/conversation-websocket-context.tsx b/frontend/src/contexts/conversation-websocket-context.tsx index a58dd92c87..7c492e5936 100644 --- a/frontend/src/contexts/conversation-websocket-context.tsx +++ b/frontend/src/contexts/conversation-websocket-context.tsx @@ -24,6 +24,7 @@ import { isAgentStatusConversationStateUpdateEvent, isExecuteBashActionEvent, isExecuteBashObservationEvent, + isConversationErrorEvent, } from "#/types/v1/type-guards"; import { handleActionEventCacheInvalidation } from "#/utils/cache-utils"; import { buildWebSocketUrl } from "#/utils/websocket-url"; @@ -132,6 +133,11 @@ export function ConversationWebSocketProvider({ if (isV1Event(event)) { addEvent(event); + // Handle ConversationErrorEvent specifically + if (isConversationErrorEvent(event)) { + setErrorMessage(event.detail); + } + // Handle AgentErrorEvent specifically if (isAgentErrorEvent(event)) { setErrorMessage(event.error); diff --git a/frontend/src/types/v1/core/events/conversation-state-event.ts b/frontend/src/types/v1/core/events/conversation-state-event.ts index 225dbfa083..81b3640dfa 100644 --- a/frontend/src/types/v1/core/events/conversation-state-event.ts +++ b/frontend/src/types/v1/core/events/conversation-state-event.ts @@ -45,3 +45,21 @@ export interface ConversationStateUpdateEventAgentStatus export type ConversationStateUpdateEvent = | ConversationStateUpdateEventFullState | ConversationStateUpdateEventAgentStatus; + +// Conversation error event - contains error information +export interface ConversationErrorEvent extends BaseEvent { + /** + * The source is always "environment" for conversation error events + */ + source: "environment"; + + /** + * Error code (e.g., "AuthenticationError") + */ + code: string; + + /** + * Detailed error message + */ + detail: string; +} diff --git a/frontend/src/types/v1/core/openhands-event.ts b/frontend/src/types/v1/core/openhands-event.ts index 909f5221c0..4793c5a0ae 100644 --- a/frontend/src/types/v1/core/openhands-event.ts +++ b/frontend/src/types/v1/core/openhands-event.ts @@ -10,6 +10,7 @@ import { CondensationRequestEvent, CondensationSummaryEvent, ConversationStateUpdateEvent, + ConversationErrorEvent, PauseEvent, } from "./events/index"; @@ -30,5 +31,6 @@ export type OpenHandsEvent = | CondensationRequestEvent | CondensationSummaryEvent | ConversationStateUpdateEvent + | ConversationErrorEvent // Control events | PauseEvent; diff --git a/frontend/src/types/v1/type-guards.ts b/frontend/src/types/v1/type-guards.ts index bf409360c0..b479e4697b 100644 --- a/frontend/src/types/v1/type-guards.ts +++ b/frontend/src/types/v1/type-guards.ts @@ -12,6 +12,7 @@ import { ConversationStateUpdateEvent, ConversationStateUpdateEventAgentStatus, ConversationStateUpdateEventFullState, + ConversationErrorEvent, } from "./core/events/conversation-state-event"; import { SystemPromptEvent } from "./core/events/system-event"; import type { OpenHandsParsedEvent } from "../core/index"; @@ -138,6 +139,14 @@ export const isAgentStatusConversationStateUpdateEvent = ( ): event is ConversationStateUpdateEventAgentStatus => event.key === "execution_status"; +/** + * Type guard function to check if an event is a conversation error event + */ +export const isConversationErrorEvent = ( + event: OpenHandsEvent, +): event is ConversationErrorEvent => + "kind" in event && event.kind === "ConversationErrorEvent"; + // ============================================================================= // TEMPORARY COMPATIBILITY TYPE GUARDS // These will be removed once we fully migrate to V1 events