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 (
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