diff --git a/frontend/src/utils/__tests__/status.test.ts b/frontend/__tests__/utils/status.test.ts similarity index 86% rename from frontend/src/utils/__tests__/status.test.ts rename to frontend/__tests__/utils/status.test.ts index cca6c0efaf..66dea4c799 100644 --- a/frontend/src/utils/__tests__/status.test.ts +++ b/frontend/__tests__/utils/status.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { getStatusCode, getIndicatorColor, IndicatorColor } from "../status"; +import { getStatusCode, getIndicatorColor, IndicatorColor } from "#/utils/status"; import { AgentState } from "#/types/agent-state"; import { I18nKey } from "#/i18n/declaration"; @@ -87,6 +87,36 @@ describe("getStatusCode", () => { // Should return runtime status since no agent state expect(result).toBe("STATUS$STARTING_RUNTIME"); }); + + it("should prioritize task ERROR status over websocket CONNECTING state", () => { + // Test case: Task has errored but websocket is still trying to connect + const result = getStatusCode( + { id: "", message: "", type: "info", status_update: true }, // statusMessage + "CONNECTING", // webSocketStatus (stuck connecting) + null, // conversationStatus + null, // runtimeStatus + AgentState.LOADING, // agentState + "ERROR", // taskStatus (ERROR) + ); + + // Should return error message, not "Connecting..." + expect(result).toBe(I18nKey.AGENT_STATUS$ERROR_OCCURRED); + }); + + it("should show Connecting when task is working and websocket is connecting", () => { + // Test case: Task is in progress and websocket is connecting normally + const result = getStatusCode( + { id: "", message: "", type: "info", status_update: true }, // statusMessage + "CONNECTING", // webSocketStatus + null, // conversationStatus + null, // runtimeStatus + AgentState.LOADING, // agentState + "WORKING", // taskStatus (in progress) + ); + + // Should show connecting message since task hasn't errored + expect(result).toBe(I18nKey.CHAT_INTERFACE$CONNECTING); + }); }); describe("getIndicatorColor", () => { diff --git a/frontend/src/components/features/controls/agent-status.tsx b/frontend/src/components/features/controls/agent-status.tsx index 5bfd18f486..68165ddf97 100644 --- a/frontend/src/components/features/controls/agent-status.tsx +++ b/frontend/src/components/features/controls/agent-status.tsx @@ -13,6 +13,7 @@ import { useConversationStore } from "#/state/conversation-store"; import CircleErrorIcon from "#/icons/circle-error.svg?react"; import { useAgentState } from "#/hooks/use-agent-state"; import { useUnifiedWebSocketStatus } from "#/hooks/use-unified-websocket-status"; +import { useTaskPolling } from "#/hooks/query/use-task-polling"; export interface AgentStatusProps { className?: string; @@ -35,6 +36,7 @@ export function AgentStatus({ const { curStatusMessage } = useStatusStore(); const webSocketStatus = useUnifiedWebSocketStatus(); const { data: conversation } = useActiveConversation(); + const { taskStatus } = useTaskPolling(); const statusCode = getStatusCode( curStatusMessage, @@ -42,17 +44,24 @@ export function AgentStatus({ conversation?.status || null, conversation?.runtime_status || null, curAgentState, + taskStatus, ); + const isTaskLoading = + taskStatus && taskStatus !== "ERROR" && taskStatus !== "READY"; + const shouldShownAgentLoading = isPausing || curAgentState === AgentState.INIT || curAgentState === AgentState.LOADING || - webSocketStatus === "CONNECTING"; + (webSocketStatus === "CONNECTING" && taskStatus !== "ERROR") || + isTaskLoading; const shouldShownAgentError = curAgentState === AgentState.ERROR || - curAgentState === AgentState.RATE_LIMITED; + curAgentState === AgentState.RATE_LIMITED || + webSocketStatus === "DISCONNECTED" || + taskStatus === "ERROR"; const shouldShownAgentStop = curAgentState === AgentState.RUNNING; @@ -61,7 +70,8 @@ export function AgentStatus({ // Update global state when agent loading condition changes useEffect(() => { - setShouldShownAgentLoading(shouldShownAgentLoading); + if (shouldShownAgentLoading) + setShouldShownAgentLoading(shouldShownAgentLoading); }, [shouldShownAgentLoading, setShouldShownAgentLoading]); return ( diff --git a/frontend/src/utils/status.ts b/frontend/src/utils/status.ts index b450892fa1..7b5ef5a126 100644 --- a/frontend/src/utils/status.ts +++ b/frontend/src/utils/status.ts @@ -4,6 +4,7 @@ import { AgentState } from "#/types/agent-state"; import { ConversationStatus } from "#/types/conversation-status"; import { StatusMessage } from "#/types/message"; import { RuntimeStatus } from "#/types/runtime-status"; +import { V1AppConversationStartTaskStatus } from "#/api/conversation-service/v1-conversation-service.types"; export enum IndicatorColor { BLUE = "bg-blue-500", @@ -103,8 +104,15 @@ export function getStatusCode( conversationStatus: ConversationStatus | null, runtimeStatus: RuntimeStatus | null, agentState: AgentState | null, + taskStatus?: V1AppConversationStartTaskStatus | null, ) { - // Handle conversation and runtime stopped states + // PRIORITY 1: Handle task error state (when start-tasks API returns ERROR) + // This must come first to prevent "Connecting..." from showing when task has errored + if (taskStatus === "ERROR") { + return I18nKey.AGENT_STATUS$ERROR_OCCURRED; + } + + // PRIORITY 2: Handle conversation and runtime stopped states if (conversationStatus === "STOPPED" || runtimeStatus === "STATUS$STOPPED") { return I18nKey.CHAT_INTERFACE$STOPPED; } @@ -134,7 +142,8 @@ export function getStatusCode( return runtimeStatus; } - // Handle WebSocket connection states + // PRIORITY 3: Handle WebSocket connection states + // Note: WebSocket may be stuck in CONNECTING when task errors, so we check taskStatus first if (webSocketStatus === "DISCONNECTED") { return I18nKey.CHAT_INTERFACE$DISCONNECTED; }