From e819b7eea40c58701b3fc6bdfeedfd5f968c8426 Mon Sep 17 00:00:00 2001 From: chengyongru Date: Mon, 27 Jul 2026 18:04:06 +0800 Subject: [PATCH] fix(webui): stabilize repeated model preset rows --- .../src/components/settings/SettingsView.tsx | 21 ++++-- webui/src/tests/settings-view.test.tsx | 71 +++++++++++++++++++ 2 files changed, 85 insertions(+), 7 deletions(-) diff --git a/webui/src/components/settings/SettingsView.tsx b/webui/src/components/settings/SettingsView.tsx index f30cce44..1dcf16d2 100644 --- a/webui/src/components/settings/SettingsView.tsx +++ b/webui/src/components/settings/SettingsView.tsx @@ -3171,13 +3171,20 @@ function ModelsSettings({ const namedPresets = settings.model_presets.filter((preset) => !preset.is_default); const namedPresetsByName = new Map(namedPresets.map((preset) => [preset.name, preset])); const unorderedPresets = namedPresets.filter((preset) => !callOrder.includes(preset.name)); + const callOrderOccurrences = new Map(); const presetRows = [ - ...callOrder.map((name, orderIndex) => ({ - name, - orderIndex, - preset: namedPresetsByName.get(name), - })), + ...callOrder.map((name, orderIndex) => { + const occurrence = callOrderOccurrences.get(name) ?? 0; + callOrderOccurrences.set(name, occurrence + 1); + return { + key: `ordered:${name}:${occurrence}`, + name, + orderIndex, + preset: namedPresetsByName.get(name), + }; + }), ...unorderedPresets.map((preset) => ({ + key: `disabled:${preset.name}`, name: preset.name, orderIndex: -1, preset, @@ -3321,7 +3328,7 @@ function ModelsSettings({ ) : ( <>
- {presetRows.map(({ name, orderIndex, preset }) => { + {presetRows.map(({ key, name, orderIndex, preset }) => { const ordered = orderIndex >= 0; const provider = preset ? modelPresetProviderKey(preset, settings) @@ -3343,7 +3350,7 @@ function ModelsSettings({ draggedCallOrderIndex < orderIndex; return (
{ expect(screen.getByRole("button", { name: "Save preset" })).toBeEnabled(); }); + it("keeps repeated fallback preset rows stable when changing the primary preset", async () => { + const { payload, backupPreset } = settingsPayloadWithBackup(); + const initialPayload: SettingsPayload = { + ...payload, + agent: { + ...payload.agent, + model: backupPreset.model, + provider: backupPreset.provider, + resolved_provider: backupPreset.resolved_provider, + model_preset: backupPreset.name, + }, + model_presets: payload.model_presets.map((preset) => ({ + ...preset, + active: preset.name === backupPreset.name, + })), + model_call_order: ["backup", "primary", "backup"], + }; + const updatedPayload: SettingsPayload = { + ...initialPayload, + agent: { + ...payload.agent, + model_preset: "primary", + }, + model_presets: payload.model_presets.map((preset) => ({ + ...preset, + active: preset.name === "primary", + })), + model_call_order: ["primary", "backup", "backup"], + }; + const fetchMock = vi.fn(async (input: RequestInfo | URL) => { + const url = String(input); + if (url === "/api/settings") return jsonResponse(initialPayload); + if (url === "/api/settings/cli-apps") { + return jsonResponse({ apps: [], installed_count: 0 }); + } + if (url === "/api/settings/mcp-presets") { + return jsonResponse({ presets: [], installed_count: 0 }); + } + if (url.startsWith("/api/settings/model-call-order/update?")) { + return jsonResponse(updatedPayload); + } + return { ok: false, status: 404, json: async () => ({}) } as Response; + }); + vi.stubGlobal("fetch", fetchMock); + + renderSettingsView({ initialSection: "models", initialSettings: initialPayload }); + + const primaryRow = screen.getByTestId("model-call-order-row-primary"); + const firstBackupRow = screen.getAllByTestId("model-call-order-row-backup")[0]; + const dataTransfer = { + dropEffect: "move", + effectAllowed: "move", + setData: vi.fn(), + }; + fireEvent.dragStart(primaryRow, { dataTransfer }); + fireEvent.dragEnter(firstBackupRow, { dataTransfer }); + fireEvent.drop(firstBackupRow, { dataTransfer }); + + await waitFor(() => + expect( + screen + .getAllByTestId(/^model-call-order-row-/) + .map((row) => row.getAttribute("data-testid")), + ).toEqual([ + "model-call-order-row-primary", + "model-call-order-row-backup", + "model-call-order-row-backup", + ]), + ); + }); + it("restores the model call order when immediate persistence fails", async () => { const { payload } = settingsPayloadWithBackup(); const fetchMock = vi.fn(async (input: RequestInfo | URL) => {