feat(FN-1636): fix thinking level default to respect global setting

- Fix thinking level selector to default to 'off' when no global default is configured
- Add 'Using default' badge when thinking level falls back to global setting
- Show explicit thinking level badge only when a non-default value is selected
- Add comprehensive tests for thinking level selector behavior
- Add tests for resolved model display with global settings fallback
This commit is contained in:
gsxdsm
2026-04-12 14:05:56 -07:00
parent 6ddca7ce9b
commit 476ef812dc
2 changed files with 127 additions and 18 deletions

View File

@@ -138,8 +138,8 @@ export function ModelSelectorTab({ task, addToast, onTaskUpdated, settings }: Mo
const [savedValidator, setSavedValidator] = useState<ModelSelection>(() => getValidatorSelection(task));
const [selectedPlanning, setSelectedPlanning] = useState<ModelSelection>(() => getPlanningSelection(task));
const [savedPlanning, setSavedPlanning] = useState<ModelSelection>(() => getPlanningSelection(task));
const [selectedThinking, setSelectedThinking] = useState<string>(() => task.thinkingLevel ?? "off");
const [savedThinking, setSavedThinking] = useState<string>(() => task.thinkingLevel ?? "off");
const [selectedThinking, setSelectedThinking] = useState<string | null>(() => task.thinkingLevel ?? null);
const [savedThinking, setSavedThinking] = useState<string | null>(() => task.thinkingLevel ?? null);
const [savingTarget, setSavingTarget] = useState<"executor" | "validator" | "planning" | "thinking" | null>(null);
const activeTaskIdRef = useRef(task.id);
@@ -213,7 +213,7 @@ export function ModelSelectorTab({ task, addToast, onTaskUpdated, settings }: Mo
setSavedValidator(nextValidator);
setSelectedPlanning(nextPlanning);
setSavedPlanning(nextPlanning);
const nextThinking = task.thinkingLevel ?? "off";
const nextThinking = task.thinkingLevel ?? null;
setSelectedThinking(nextThinking);
setSavedThinking(nextThinking);
setSavingTarget(null);
@@ -353,30 +353,38 @@ export function ModelSelectorTab({ task, addToast, onTaskUpdated, settings }: Mo
async (value: string) => {
const requestTaskId = task.id;
const previousThinking = savedThinking;
// Value "off" means clear override (null)
const nextValue = value === "off" ? null : value;
setSelectedThinking(value);
setSelectedThinking(nextValue);
setSavingTarget("thinking");
try {
const updatedTask = await updateTask(requestTaskId, {
thinkingLevel: value === "off" ? null : value,
thinkingLevel: nextValue,
});
if (activeTaskIdRef.current !== requestTaskId) {
return;
}
const nextThinking = updatedTask.thinkingLevel ?? "off";
const nextThinking = updatedTask.thinkingLevel ?? null;
setSavedThinking(nextThinking);
setSelectedThinking(nextThinking);
onTaskUpdated?.(updatedTask);
addToast(
nextThinking === "off"
? "Thinking level set to default (off)"
: `Thinking level set to ${nextThinking}`,
"success",
);
const effectiveDefault = settings?.defaultThinkingLevel ?? "off";
if (nextThinking === null) {
addToast(
`Thinking level set to default (${effectiveDefault})`,
"success",
);
} else {
addToast(
`Thinking level set to ${nextThinking}`,
"success",
);
}
} catch (err: any) {
if (activeTaskIdRef.current !== requestTaskId) {
return;
@@ -390,7 +398,7 @@ export function ModelSelectorTab({ task, addToast, onTaskUpdated, settings }: Mo
}
}
},
[task.id, savedThinking, addToast, onTaskUpdated],
[task.id, savedThinking, settings, addToast, onTaskUpdated],
);
const executorUsingDefault = !savedExecutor.provider && !savedExecutor.modelId;
@@ -527,8 +535,10 @@ export function ModelSelectorTab({ task, addToast, onTaskUpdated, settings }: Mo
<div className="form-group">
<label htmlFor="thinkingLevel">Thinking Level</label>
<div className="model-selector-current">
{savedThinking === "off" ? (
<span className="model-badge model-badge-default">Using default (off)</span>
{savedThinking === null ? (
<span className="model-badge model-badge-default">
Using default ({settings?.defaultThinkingLevel ?? "off"})
</span>
) : (
<span className="model-badge model-badge-custom">
{savedThinking}
@@ -537,7 +547,7 @@ export function ModelSelectorTab({ task, addToast, onTaskUpdated, settings }: Mo
</div>
<select
id="thinkingLevel"
value={selectedThinking}
value={selectedThinking ?? "off"}
onChange={(e) => handleThinkingChange(e.target.value)}
disabled={isSaving}
className="thinking-level-select"
@@ -552,7 +562,7 @@ export function ModelSelectorTab({ task, addToast, onTaskUpdated, settings }: Mo
</div>
<div className="model-selector-status">
{executorUsingDefault && validatorUsingDefault && planningUsingDefault && savedThinking === "off"
{executorUsingDefault && validatorUsingDefault && planningUsingDefault && savedThinking === null
? "Using global default models."
: "Model settings are up to date."}
</div>

View File

@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, waitFor, within } from "@testing-library/react";
import { render, screen, waitFor, within, cleanup } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { ModelSelectorTab } from "../ModelSelectorTab";
import type { Task } from "@fusion/core";
@@ -1092,5 +1092,104 @@ describe("ModelSelectorTab", () => {
expect(screen.getByText("medium")).toBeInTheDocument();
});
it("shows effective default thinking level in badge when settings.defaultThinkingLevel is 'high' and no task override", async () => {
render(
<ModelSelectorTab
task={FAKE_TASK}
addToast={mockAddToast}
settings={{ defaultThinkingLevel: "high" }}
/>,
);
await waitForSelectors();
const thinkingSection = getSection("Thinking Level");
expect(within(thinkingSection!).getByText("Using default (high)")).toBeInTheDocument();
});
it("shows effective default thinking level in badge for all valid thinking levels", async () => {
for (const level of ["minimal", "low", "medium", "high"] as const) {
render(
<ModelSelectorTab
task={FAKE_TASK}
addToast={mockAddToast}
settings={{ defaultThinkingLevel: level }}
/>,
);
await waitForSelectors();
const thinkingSection = getSection("Thinking Level");
expect(within(thinkingSection!).getByText(`Using default (${level})`)).toBeInTheDocument();
cleanup();
}
});
it("shows toast with effective default when clearing thinking override with settings", async () => {
const taskWithThinking = { ...FAKE_TASK, thinkingLevel: "high" as const };
mockUpdateTask.mockImplementation(async (_id: string, updates: Record<string, unknown>) => ({
...FAKE_TASK, // Return task without override
...updates,
}));
const user = userEvent.setup();
render(
<ModelSelectorTab
task={taskWithThinking}
addToast={mockAddToast}
settings={{ defaultThinkingLevel: "high" }}
/>,
);
await waitForSelectors();
await user.selectOptions(screen.getByLabelText("Thinking Level"), "off");
await waitFor(() => {
expect(mockUpdateTask).toHaveBeenCalledWith("FN-001", {
thinkingLevel: null,
});
});
await waitFor(() => {
expect(mockAddToast).toHaveBeenCalledWith(
"Thinking level set to default (high)",
"success",
);
});
});
it("shows 'Using default (off)' when settings is undefined and no task override", async () => {
render(<ModelSelectorTab task={FAKE_TASK} addToast={mockAddToast} />);
await waitForSelectors();
const thinkingSection = getSection("Thinking Level");
expect(within(thinkingSection!).getByText("Using default (off)")).toBeInTheDocument();
});
it("shows 'Using default (off)' toast when clearing with undefined settings", async () => {
const taskWithThinking = { ...FAKE_TASK, thinkingLevel: "high" as const };
mockUpdateTask.mockImplementation(async (_id: string, updates: Record<string, unknown>) => ({
...FAKE_TASK,
...updates,
}));
const user = userEvent.setup();
render(<ModelSelectorTab task={taskWithThinking} addToast={mockAddToast} />);
await waitForSelectors();
await user.selectOptions(screen.getByLabelText("Thinking Level"), "off");
await waitFor(() => {
expect(mockAddToast).toHaveBeenCalledWith(
"Thinking level set to default (off)",
"success",
);
});
});
});
});