Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions web/app/src/hooks/workspace/useAgentController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,7 @@ export function useAgentController({
models: agentPageDiscoveredModels,
modelBusy: agentPageModelProbeBusy,
modelError: agentPageModelError,
retryModels: retryAgentPageModels,
} = useProfileModelOptions({
draft: agentPageDraft,
enabled: Boolean(selectedAgentForPage),
Expand Down Expand Up @@ -2423,6 +2424,7 @@ export function useAgentController({
modelProviders: agentPageModelProviders,
modelBusy: agentPageModelBusy,
modelError: agentPageModelError,
onRetryModels: retryAgentPageModels,
saving: agentPageBusy,
publishBusy: agentPagePublishBusy,
publishDisabled: !openCSGAuthenticated,
Expand Down
3 changes: 3 additions & 0 deletions web/app/src/hooks/workspace/useProfileModelOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}

Expand All @@ -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%;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(<Harness modelBusy={false} modelError={new Error(technicalError)} onRetryModels={onRetryModels} />);

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);
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
AlertCircle,
Check,
CheckCircle2,
CircleDashed,
Expand Down Expand Up @@ -135,6 +136,7 @@ export type AgentDetailPaneProps = {
onDraftChange?: (draft: AgentDraft) => void;
onInvite: AgentActionHandler;
onOpenDM: AgentActionHandler;
onRetryModels?: () => void | Promise<unknown>;
onProviderLogin?: (provider: string) => VoidOrPromise;
onPublish?: (target: AgentTemplatePublishTarget, name: string, description: string) => boolean | Promise<boolean>;
onRecreate: AgentActionHandler;
Expand Down Expand Up @@ -204,6 +206,7 @@ export const AgentDetailPane = forwardRef<AgentDetailPaneHandle, AgentDetailPane
noticeTone = "warning",
modelBusy = false,
modelError = null,
onRetryModels,
saving = false,
publishBusy = false,
publishDisabled = false,
Expand Down Expand Up @@ -829,6 +832,7 @@ export const AgentDetailPane = forwardRef<AgentDetailPaneHandle, AgentDetailPane
draft={draft}
modelBusy={modelBusy}
modelError={modelError}
onRetryModels={onRetryModels}
providerOptions={providerOptions}
selectedModelValue={selectedModelValue}
selectedProviderID={selectedProviderID}
Expand Down Expand Up @@ -1433,6 +1437,7 @@ type AgentModelPanelProps = {
draft: AgentDraft;
modelBusy: boolean;
modelError: unknown;
onRetryModels?: () => void | Promise<unknown>;
providerOptions: readonly ModelProviderSelectOption[];
selectedModelValue: string;
selectedProviderID: string;
Expand All @@ -1445,6 +1450,7 @@ function AgentModelPanel({
draft,
modelBusy,
modelError,
onRetryModels,
providerOptions,
selectedModelValue,
selectedProviderID,
Expand Down Expand Up @@ -1541,9 +1547,6 @@ function AgentModelPanel({
: []),
]}
/>
{modelError ? (
<span className="field-hint error">{errorMessage(modelError, t("modelLoadFailed"))}</span>
) : null}
</label>
<ReasoningControls
value={draft.reasoning_effort}
Expand All @@ -1562,6 +1565,35 @@ function AgentModelPanel({
<small className="agent-fast-mode-help">{t("profileFastModeHelp")}</small>
</label>
</div>
{modelError ? (
<div className="agent-model-load-error" role="alert">
<AlertCircle className="agent-model-load-error-icon" aria-hidden="true" size={20} strokeWidth={2} />
<div className="agent-model-load-error-content">
<strong>{t("modelLoadFailed")}</strong>
<p>{t("profileModelLoadErrorHelp")}</p>
{selectedModelValue ? <small>{t("profileModelCurrentSelectionRetained")}</small> : null}
<details className="agent-model-load-error-details">
<summary>{t("profileModelErrorDetails")}</summary>
<div className="agent-model-load-error-technical">
<pre>{errorMessage(modelError, t("modelLoadFailed"))}</pre>
</div>
</details>
</div>
{onRetryModels ? (
<Button
className="agent-model-load-error-retry"
loading={modelBusy}
loadingLabel={t("profileLoadingModels")}
size="sm"
variant="secondaryGray"
onClick={() => void onRetryModels()}
>
<RefreshCw aria-hidden="true" size={15} strokeWidth={2} />
{t("retry")}
</Button>
) : null}
</div>
) : null}
</div>
</div>
</div>
Expand Down
7 changes: 7 additions & 0 deletions web/app/src/shared/i18n/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -882,6 +882,9 @@ export const messages = {
profileSandboxEnabledHelp: "开启后,Agent 会在独立运行环境中执行任务;关闭后,则使用本地运行环境。",
profileModelSection: "模型配置",
profileModelSectionDescription: "选择模型并调整推理方式。",
profileModelLoadErrorHelp: "暂时无法获取模型列表,请检查服务配置或网络后重试。",
profileModelCurrentSelectionRetained: "当前已保存的模型配置已保留。",
profileModelErrorDetails: "详情",
profileRuntimeOptions: "运行时选项",
profileMCPServers: "MCP Servers",
profileMCPServersHint: '请输入 MCP server map,例如:{"context7":{...}}。',
Expand Down Expand Up @@ -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": {...}}.',
Expand Down
Loading