From 5ed73f06ad13c34f6d8defee460af89eeee33c2b Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 5 Aug 2026 14:50:17 +0100 Subject: [PATCH] fix(site/src/pages/AgentsPage): show error state with retry when chat fetch fails (#27887) --- .../AgentsPage/AgentChatPage.stories.tsx | 121 ++++++++++++++++++ site/src/pages/AgentsPage/AgentChatPage.tsx | 34 ++++- .../AgentsPage/AgentChatPageErrorView.tsx | 60 +++++++++ 3 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 site/src/pages/AgentsPage/AgentChatPageErrorView.tsx diff --git a/site/src/pages/AgentsPage/AgentChatPage.stories.tsx b/site/src/pages/AgentsPage/AgentChatPage.stories.tsx index 7364b71aa4..eb9f1f631b 100644 --- a/site/src/pages/AgentsPage/AgentChatPage.stories.tsx +++ b/site/src/pages/AgentsPage/AgentChatPage.stories.tsx @@ -1,6 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import type { FC } from "react"; import { useRef } from "react"; +import { hashKey } from "react-query"; import { Outlet, useNavigate } from "react-router"; import { expect, spyOn, userEvent, waitFor, within } from "storybook/test"; import { @@ -22,6 +23,7 @@ import { import { workspaceByIdKey } from "#/api/queries/workspaces"; import type * as TypesGen from "#/api/typesGenerated"; import { + MockChat, MockChatMessage, MockChatQueuedMessage, } from "#/testHelpers/chatEntities"; @@ -32,6 +34,7 @@ import { MockOrganizationMember2, MockUserOwner, MockWorkspace, + mockApiError, } from "#/testHelpers/entities"; import { withAuthProvider, @@ -3059,6 +3062,124 @@ export const SendResponseAfterChatSwitch: Story = { }, }; +const mockErrorChat: TypesGen.Chat = { + ...MockChat, + id: CHAT_ID, + ...baseChatFields, + title: "Failing chat", +}; + +const mockServerError = { + ...mockApiError({ message: "Internal server error." }), + status: 500, +}; + +const withoutQuery = ( + queries: ReturnType, + queryKey: readonly unknown[], +) => queries.filter(({ key }) => hashKey(key) !== hashKey(queryKey)); + +export const DetailQueryError: Story = { + parameters: { + queries: withoutQuery( + buildQueries(mockErrorChat, { + messages: [], + queued_messages: [], + has_more: false, + }), + chatKey(CHAT_ID), + ), + }, + beforeEach: () => { + spyOn(API.experimental, "getChat").mockRejectedValue(mockServerError); + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(await canvas.findByText("Failed to load chat")).toBeVisible(); + expect(canvas.queryByText("Chat not found")).not.toBeInTheDocument(); + expect( + canvas.getByRole("button", { name: "Try again" }), + ).toBeInTheDocument(); + }, +}; + +export const InitialMessagesError: Story = { + parameters: { + queries: withoutQuery( + buildQueries(mockErrorChat, { + messages: [], + queued_messages: [], + has_more: false, + }), + chatMessagesKey(CHAT_ID), + ), + }, + beforeEach: () => { + spyOn(API.experimental, "getChatMessages").mockRejectedValue( + mockServerError, + ); + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(await canvas.findByText("Failed to load chat")).toBeVisible(); + expect(canvas.queryByText("Chat not found")).not.toBeInTheDocument(); + }, +}; + +export const ErrorRetryRecovers: Story = { + parameters: { + queries: withoutQuery( + buildQueries(mockErrorChat, { + messages: [], + queued_messages: [], + has_more: false, + }), + chatKey(CHAT_ID), + ), + }, + beforeEach: ({ parameters }) => { + const getChatSpy = spyOn(API.experimental, "getChat") + .mockRejectedValueOnce(mockServerError) + .mockResolvedValue(mockErrorChat); + parameters.getChatCallsForChat = () => + getChatSpy.mock.calls.filter(([chatId]) => chatId === CHAT_ID).length; + }, + play: async ({ canvasElement, parameters }) => { + const canvas = within(canvasElement); + expect(await canvas.findByText("Failed to load chat")).toBeVisible(); + await userEvent.click(canvas.getByRole("button", { name: "Try again" })); + await waitFor(() => { + expect(canvas.queryByText("Failed to load chat")).not.toBeInTheDocument(); + }); + expect(canvas.queryByText("Chat not found")).not.toBeInTheDocument(); + expect(parameters.getChatCallsForChat()).toBeGreaterThanOrEqual(2); + }, +}; + +export const ChatNotFound: Story = { + parameters: { + queries: withoutQuery( + buildQueries(mockErrorChat, { + messages: [], + queued_messages: [], + has_more: false, + }), + chatKey(CHAT_ID), + ), + }, + beforeEach: () => { + spyOn(API.experimental, "getChat").mockRejectedValue({ + ...mockApiError({ message: "Chat not found." }), + status: 404, + }); + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(await canvas.findByText("Chat not found")).toBeVisible(); + expect(canvas.queryByText("Failed to load chat")).not.toBeInTheDocument(); + }, +}; + export const SendRejectedByHookDispatchFailure: Story = { parameters: { queries: buildQueries( diff --git a/site/src/pages/AgentsPage/AgentChatPage.tsx b/site/src/pages/AgentsPage/AgentChatPage.tsx index 7a4ef233e7..8242f78d5a 100644 --- a/site/src/pages/AgentsPage/AgentChatPage.tsx +++ b/site/src/pages/AgentsPage/AgentChatPage.tsx @@ -22,7 +22,7 @@ import { type CreateChatMessageRequestWithClearablePlanMode, watchWorkspace, } from "#/api/api"; -import { getErrorMessage, isApiError } from "#/api/errors"; +import { getErrorMessage, getErrorStatus, isApiError } from "#/api/errors"; import { checkAuthorization } from "#/api/queries/authCheck"; import { buildOptimisticEditedMessage } from "#/api/queries/chatMessageEdits"; import { @@ -68,6 +68,7 @@ import { isMobileViewport } from "#/utils/mobile"; import { pageTitle } from "#/utils/page"; import { rewriteLocalhostURL } from "#/utils/portForward"; import { createReconnectingWebSocket } from "#/utils/reconnectingWebSocket"; +import { AgentChatPageErrorView } from "./AgentChatPageErrorView"; import { AgentChatPageLoadingView, AgentChatPageNotFoundView, @@ -1881,6 +1882,37 @@ const AgentChatPage: FC = () => { ); } + if (chatQuery.isLoadingError || chatMessagesQuery.isLoadingError) { + if (getErrorStatus(chatQuery.error) === 404) { + return ( + + ); + } + + return ( + { + if (chatQuery.isLoadingError) { + void chatQuery.refetch(); + } + if (chatMessagesQuery.isLoadingError) { + void chatMessagesQuery.refetch(); + } + }} + /> + ); + } + if (!chatQuery.data || !chatMessagesQuery.data?.pages?.length || !agentId) { return ( void; + error: unknown; + onRetry: () => void; +} + +export const AgentChatPageErrorView: FC = ({ + titleElement, + isSidebarCollapsed, + onToggleSidebarCollapsed, + error, + onRetry, +}) => { + const detail = getErrorDetail(error); + + return ( +
+ {titleElement} + {}, + }} + onArchiveAgent={() => {}} + onUnarchiveAgent={() => {}} + onArchiveAndDeleteWorkspace={() => {}} + hasWorkspace={false} + isSidebarCollapsed={isSidebarCollapsed} + onToggleSidebarCollapsed={onToggleSidebarCollapsed} + /> +
+
+

+ Failed to load chat +

+

+ {getErrorMessage(error, "The chat could not be loaded.")} +

+ {detail && ( +

+ {detail} +

+ )} + +
+
+
+ ); +};