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:
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user