From 71bc44dff3959e2d8e8c362f18d08327658cfbe9 Mon Sep 17 00:00:00 2001 From: youngbeom Date: Fri, 7 Aug 2026 16:00:07 +0800 Subject: [PATCH] fix(web): improve model loading error UI --- .../src/hooks/workspace/useAgentController.ts | 2 + .../hooks/workspace/useProfileModelOptions.ts | 3 + .../AgentDetailPane/AgentDetailPane.css | 111 +++++++++++++++++- .../AgentDetailPane/AgentDetailPane.test.tsx | 21 ++++ .../AgentDetailPane/AgentDetailPane.tsx | 38 +++++- web/app/src/shared/i18n/messages.ts | 7 ++ 6 files changed, 178 insertions(+), 4 deletions(-) diff --git a/web/app/src/hooks/workspace/useAgentController.ts b/web/app/src/hooks/workspace/useAgentController.ts index 5afe23972..5568bc290 100644 --- a/web/app/src/hooks/workspace/useAgentController.ts +++ b/web/app/src/hooks/workspace/useAgentController.ts @@ -736,6 +736,7 @@ export function useAgentController({ models: agentPageDiscoveredModels, modelBusy: agentPageModelProbeBusy, modelError: agentPageModelError, + retryModels: retryAgentPageModels, } = useProfileModelOptions({ draft: agentPageDraft, enabled: Boolean(selectedAgentForPage), @@ -2423,6 +2424,7 @@ export function useAgentController({ modelProviders: agentPageModelProviders, modelBusy: agentPageModelBusy, modelError: agentPageModelError, + onRetryModels: retryAgentPageModels, saving: agentPageBusy, publishBusy: agentPagePublishBusy, publishDisabled: !openCSGAuthenticated, diff --git a/web/app/src/hooks/workspace/useProfileModelOptions.ts b/web/app/src/hooks/workspace/useProfileModelOptions.ts index 0ee57b005..c5d45bcf2 100644 --- a/web/app/src/hooks/workspace/useProfileModelOptions.ts +++ b/web/app/src/hooks/workspace/useProfileModelOptions.ts @@ -75,10 +75,13 @@ export function useProfileModelOptions({ draft, enabled = true, onDraftChange }: } }, [draftRequestKey, queryClient, requestKey]); + const retryModels = query.refetch; + return { models, modelBusy: Boolean(requestDraft) && query.isFetching, modelError: query.error, resetModels, + retryModels, }; } diff --git a/web/app/src/pages/AgentPage/components/AgentDetailPane/AgentDetailPane.css b/web/app/src/pages/AgentPage/components/AgentDetailPane/AgentDetailPane.css index b38ca10c9..3b1d1bc08 100644 --- a/web/app/src/pages/AgentPage/components/AgentDetailPane/AgentDetailPane.css +++ b/web/app/src/pages/AgentPage/components/AgentDetailPane/AgentDetailPane.css @@ -1834,6 +1834,10 @@ padding: 16px; } +.agent-detail-pane .agent-model-form-content { + container-type: inline-size; +} + .agent-detail-pane .field { margin-top: 0; min-width: 0; @@ -1852,7 +1856,7 @@ } .agent-detail-pane .agent-model-config-grid { - grid-template-columns: repeat(auto-fit, minmax(min(100%, 170px), 1fr)); + grid-template-columns: repeat(4, minmax(0, 1fr)); align-items: start; } @@ -1862,6 +1866,111 @@ min-width: 0; } +.agent-detail-pane .agent-model-load-error { + grid-column: 1 / -1; + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + gap: 10px 12px; + align-items: start; + min-width: 0; + padding: 12px; + border: 1px solid color-mix(in oklab, var(--danger) 34%, var(--line)); + border-radius: var(--radius-md); + background: color-mix(in oklab, var(--danger) 7%, var(--surface)); + color: var(--text); +} + +.agent-detail-pane .agent-model-load-error-icon { + flex: 0 0 auto; + color: var(--danger); +} + +.agent-detail-pane .agent-model-load-error-content { + display: grid; + gap: 4px; + min-width: 0; +} + +.agent-detail-pane .agent-model-load-error-content strong { + font: var(--text-sm-semibold); +} + +.agent-detail-pane .agent-model-load-error-content p, +.agent-detail-pane .agent-model-load-error-content small { + margin: 0; + color: var(--muted); + font: var(--text-xs-normal); + overflow-wrap: anywhere; +} + +.agent-detail-pane .agent-model-load-error-details { + min-width: 0; + margin-top: 4px; +} + +.agent-detail-pane .agent-model-load-error-details summary { + width: fit-content; + color: var(--text); + font: var(--text-xs-medium); + cursor: pointer; +} + +.agent-detail-pane .agent-model-load-error-details summary:focus-visible { + border-radius: var(--radius-sm); + outline: none; + box-shadow: var(--focus-ring); +} + +.agent-detail-pane .agent-model-load-error-technical { + display: grid; + gap: 8px; + min-width: 0; + margin-top: 8px; +} + +.agent-detail-pane .agent-model-load-error-technical pre { + max-width: 100%; + max-height: 144px; + margin: 0; + padding: 10px; + overflow: auto; + border: 1px solid var(--line); + border-radius: var(--radius-sm); + background: var(--surface); + color: var(--muted); + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 12px; + line-height: 1.45; + overflow-wrap: anywhere; + white-space: pre-wrap; + word-break: break-word; +} + +.agent-detail-pane .agent-model-load-error-retry { + min-width: 0; +} + +@container (max-width: 760px) { + .agent-detail-pane .agent-model-config-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@container (max-width: 460px) { + .agent-detail-pane .agent-model-config-grid { + grid-template-columns: minmax(0, 1fr); + } + + .agent-detail-pane .agent-model-load-error { + grid-template-columns: auto minmax(0, 1fr); + } + + .agent-detail-pane .agent-model-load-error-retry { + grid-column: 1 / -1; + width: 100%; + } +} + .agent-detail-pane .agent-model-config-grid .csg-select-trigger, .agent-detail-pane .agent-model-config-grid .agent-fast-mode-toggle { width: 100%; diff --git a/web/app/src/pages/AgentPage/components/AgentDetailPane/AgentDetailPane.test.tsx b/web/app/src/pages/AgentPage/components/AgentDetailPane/AgentDetailPane.test.tsx index 9f897c334..813ede5b8 100644 --- a/web/app/src/pages/AgentPage/components/AgentDetailPane/AgentDetailPane.test.tsx +++ b/web/app/src/pages/AgentPage/components/AgentDetailPane/AgentDetailPane.test.tsx @@ -168,3 +168,24 @@ describe("AgentDetailPane metadata editing", () => { await waitFor(() => expect(onMetadataSave).not.toHaveBeenCalled()); }); }); + +describe("AgentDetailPane model loading error", () => { + it("keeps the model controls aligned and exposes retryable technical details", async () => { + const user = userEvent.setup(); + const onRetryModels = vi.fn(); + const technicalError = "request models: proxyconnect tcp: connection refused"; + + render(); + + const alert = screen.getByRole("alert"); + expect(alert).toHaveTextContent("modelLoadFailed"); + expect(alert).toHaveTextContent("profileModelLoadErrorHelp"); + expect(alert).toHaveTextContent("profileModelCurrentSelectionRetained"); + + await user.click(screen.getByText("profileModelErrorDetails")); + expect(screen.getByText(technicalError)).toBeVisible(); + + await user.click(screen.getByRole("button", { name: "retry" })); + expect(onRetryModels).toHaveBeenCalledTimes(1); + }); +}); diff --git a/web/app/src/pages/AgentPage/components/AgentDetailPane/AgentDetailPane.tsx b/web/app/src/pages/AgentPage/components/AgentDetailPane/AgentDetailPane.tsx index dc253094e..e163a00d6 100644 --- a/web/app/src/pages/AgentPage/components/AgentDetailPane/AgentDetailPane.tsx +++ b/web/app/src/pages/AgentPage/components/AgentDetailPane/AgentDetailPane.tsx @@ -1,4 +1,5 @@ import { + AlertCircle, Check, CheckCircle2, CircleDashed, @@ -135,6 +136,7 @@ export type AgentDetailPaneProps = { onDraftChange?: (draft: AgentDraft) => void; onInvite: AgentActionHandler; onOpenDM: AgentActionHandler; + onRetryModels?: () => void | Promise; onProviderLogin?: (provider: string) => VoidOrPromise; onPublish?: (target: AgentTemplatePublishTarget, name: string, description: string) => boolean | Promise; onRecreate: AgentActionHandler; @@ -204,6 +206,7 @@ export const AgentDetailPane = forwardRef void | Promise; providerOptions: readonly ModelProviderSelectOption[]; selectedModelValue: string; selectedProviderID: string; @@ -1445,6 +1450,7 @@ function AgentModelPanel({ draft, modelBusy, modelError, + onRetryModels, providerOptions, selectedModelValue, selectedProviderID, @@ -1541,9 +1547,6 @@ function AgentModelPanel({ : []), ]} /> - {modelError ? ( - {errorMessage(modelError, t("modelLoadFailed"))} - ) : null} {t("profileFastModeHelp")} + {modelError ? ( +
+
+ ) : null} diff --git a/web/app/src/shared/i18n/messages.ts b/web/app/src/shared/i18n/messages.ts index 9ec547c98..4c79657df 100644 --- a/web/app/src/shared/i18n/messages.ts +++ b/web/app/src/shared/i18n/messages.ts @@ -882,6 +882,9 @@ export const messages = { profileSandboxEnabledHelp: "开启后,Agent 会在独立运行环境中执行任务;关闭后,则使用本地运行环境。", profileModelSection: "模型配置", profileModelSectionDescription: "选择模型并调整推理方式。", + profileModelLoadErrorHelp: "暂时无法获取模型列表,请检查服务配置或网络后重试。", + profileModelCurrentSelectionRetained: "当前已保存的模型配置已保留。", + profileModelErrorDetails: "详情", profileRuntimeOptions: "运行时选项", profileMCPServers: "MCP Servers", profileMCPServersHint: '请输入 MCP server map,例如:{"context7":{...}}。', @@ -2196,6 +2199,10 @@ export const messages = { "When enabled, the agent runs tasks in an isolated environment. When disabled, it uses the local runtime.", profileModelSection: "Model configuration", profileModelSectionDescription: "Choose a model and adjust its reasoning behavior.", + profileModelLoadErrorHelp: + "Could not load the model list. Check the service configuration or network connection and retry.", + profileModelCurrentSelectionRetained: "Your currently saved model configuration has been retained.", + profileModelErrorDetails: "Details", profileRuntimeOptions: "Runtime Options", profileMCPServers: "MCP Servers", profileMCPServersHint: 'Enter an MCP server map, for example: {"context7": {...}}.',