fix(webui): stabilize repeated model preset rows
This commit is contained in:
@@ -3171,13 +3171,20 @@ function ModelsSettings({
|
|||||||
const namedPresets = settings.model_presets.filter((preset) => !preset.is_default);
|
const namedPresets = settings.model_presets.filter((preset) => !preset.is_default);
|
||||||
const namedPresetsByName = new Map(namedPresets.map((preset) => [preset.name, preset]));
|
const namedPresetsByName = new Map(namedPresets.map((preset) => [preset.name, preset]));
|
||||||
const unorderedPresets = namedPresets.filter((preset) => !callOrder.includes(preset.name));
|
const unorderedPresets = namedPresets.filter((preset) => !callOrder.includes(preset.name));
|
||||||
|
const callOrderOccurrences = new Map<string, number>();
|
||||||
const presetRows = [
|
const presetRows = [
|
||||||
...callOrder.map((name, orderIndex) => ({
|
...callOrder.map((name, orderIndex) => {
|
||||||
|
const occurrence = callOrderOccurrences.get(name) ?? 0;
|
||||||
|
callOrderOccurrences.set(name, occurrence + 1);
|
||||||
|
return {
|
||||||
|
key: `ordered:${name}:${occurrence}`,
|
||||||
name,
|
name,
|
||||||
orderIndex,
|
orderIndex,
|
||||||
preset: namedPresetsByName.get(name),
|
preset: namedPresetsByName.get(name),
|
||||||
})),
|
};
|
||||||
|
}),
|
||||||
...unorderedPresets.map((preset) => ({
|
...unorderedPresets.map((preset) => ({
|
||||||
|
key: `disabled:${preset.name}`,
|
||||||
name: preset.name,
|
name: preset.name,
|
||||||
orderIndex: -1,
|
orderIndex: -1,
|
||||||
preset,
|
preset,
|
||||||
@@ -3321,7 +3328,7 @@ function ModelsSettings({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div role="list" className="divide-y divide-border/45">
|
<div role="list" className="divide-y divide-border/45">
|
||||||
{presetRows.map(({ name, orderIndex, preset }) => {
|
{presetRows.map(({ key, name, orderIndex, preset }) => {
|
||||||
const ordered = orderIndex >= 0;
|
const ordered = orderIndex >= 0;
|
||||||
const provider = preset
|
const provider = preset
|
||||||
? modelPresetProviderKey(preset, settings)
|
? modelPresetProviderKey(preset, settings)
|
||||||
@@ -3343,7 +3350,7 @@ function ModelsSettings({
|
|||||||
draggedCallOrderIndex < orderIndex;
|
draggedCallOrderIndex < orderIndex;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={name}
|
key={key}
|
||||||
role="listitem"
|
role="listitem"
|
||||||
tabIndex={ordered ? 0 : -1}
|
tabIndex={ordered ? 0 : -1}
|
||||||
draggable={ordered && !callOrderBusy}
|
draggable={ordered && !callOrderBusy}
|
||||||
|
|||||||
@@ -2130,6 +2130,77 @@ describe("SettingsView Apps catalog", () => {
|
|||||||
expect(screen.getByRole("button", { name: "Save preset" })).toBeEnabled();
|
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 () => {
|
it("restores the model call order when immediate persistence fails", async () => {
|
||||||
const { payload } = settingsPayloadWithBackup();
|
const { payload } = settingsPayloadWithBackup();
|
||||||
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
|
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user