feat(FN-1996): merge fusion/fn-1996

This commit is contained in:
gsxdsm
2026-04-17 06:11:05 -07:00
parent ff2f29644b
commit 0f6dd7ed61
2 changed files with 125 additions and 0 deletions

View File

@@ -972,6 +972,9 @@ export function SettingsModal({
const selectedValue = form.defaultProvider && form.defaultModelId
? `${form.defaultProvider}/${form.defaultModelId}`
: "";
const globalModelLanes = MODEL_LANES.filter(
(lane) => lane.laneId !== "default",
);
return (
<>
@@ -1072,6 +1075,55 @@ export function SettingsModal({
);
})()}
{availableModels.length > 0 && (
<>
<h4 className="settings-section-heading" style={{ marginTop: "1.5rem" }}>Model Lanes</h4>
<p className="settings-description">
Global baseline models for each AI role. Project settings can override these per-project.
</p>
{globalModelLanes.map((lane) => {
const provider = form[lane.globalProviderKey as keyof Settings] as string | undefined;
const model = form[lane.globalModelKey as keyof Settings] as string | undefined;
const value = provider && model ? `${provider}/${model}` : "";
return (
<div className="form-group" key={`global-${lane.laneId}`}>
<label htmlFor={`global-${lane.laneId}-model`}>{lane.label}</label>
<CustomModelDropdown
id={`global-${lane.laneId}-model`}
label={lane.label}
models={availableModels}
value={value}
onChange={(selected) => {
if (!selected) {
setForm((f) => ({
...f,
[lane.globalProviderKey]: undefined,
[lane.globalModelKey]: undefined,
}));
return;
}
const slashIdx = selected.indexOf("/");
setForm((f) => ({
...f,
[lane.globalProviderKey]: selected.slice(0, slashIdx),
[lane.globalModelKey]: selected.slice(slashIdx + 1),
}));
}}
placeholder="Use default"
favoriteProviders={favoriteProviders}
onToggleFavorite={handleToggleFavorite}
favoriteModels={favoriteModels}
onToggleModelFavorite={handleToggleModelFavorite}
/>
<small>{lane.helperText}</small>
</div>
);
})}
</>
)}
{/* --- OpenRouter Model Sync --- */}
<h4 className="settings-section-heading" style={{ marginTop: "1.5rem" }}>OpenRouter Models</h4>
<div className="form-group">

View File

@@ -3650,6 +3650,79 @@ describe("Prompts section", () => {
expect(screen.getByLabelText("Fallback Model")).toBeTruthy();
});
it("renders global baseline lane dropdowns in Models section", async () => {
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
fireEvent.click(screen.getAllByText("Models")[0]);
await waitFor(() => expect(fetchModels).toHaveBeenCalled());
expect(screen.getByLabelText("Execution Model")).toBeTruthy();
expect(screen.getByLabelText("Planning Model")).toBeTruthy();
expect(screen.getByLabelText("Validator Model")).toBeTruthy();
expect(screen.getByLabelText("Title Summarization Model")).toBeTruthy();
});
it("saving a global lane selection persists execution global lane keys", async () => {
const user = userEvent.setup();
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
fireEvent.click(screen.getAllByText("Models")[0]);
await waitFor(() => expect(fetchModels).toHaveBeenCalled());
const executionTrigger = screen.getByLabelText("Execution Model");
await user.click(executionTrigger);
await user.click(screen.getByText("GPT-4o"));
fireEvent.click(screen.getByText("Save"));
await waitFor(() => expect(updateGlobalSettings).toHaveBeenCalledTimes(1));
const globalPayload = (updateGlobalSettings as ReturnType<typeof vi.fn>).mock.calls[0][0];
expect(globalPayload.executionGlobalProvider).toBe("openai");
expect(globalPayload.executionGlobalModelId).toBe("gpt-4o");
});
it("clearing a global lane selection sends null for execution global lane keys", async () => {
const user = userEvent.setup();
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
...defaultSettings,
executionGlobalProvider: "anthropic",
executionGlobalModelId: "claude-sonnet-4-5",
});
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
fireEvent.click(screen.getAllByText("Models")[0]);
await waitFor(() => expect(fetchModels).toHaveBeenCalled());
const executionTrigger = screen.getByLabelText("Execution Model");
await user.click(executionTrigger);
const useDefaultOption = await screen.findByRole("option", { name: /use default/i });
await user.click(useDefaultOption);
fireEvent.click(screen.getByText("Save"));
await waitFor(() => expect(updateGlobalSettings).toHaveBeenCalledTimes(1));
const globalPayload = (updateGlobalSettings as ReturnType<typeof vi.fn>).mock.calls[0][0];
expect(globalPayload.executionGlobalProvider).toBeNull();
expect(globalPayload.executionGlobalModelId).toBeNull();
});
it("shows Use default placeholder text for each global model lane when unset", async () => {
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
fireEvent.click(screen.getAllByText("Models")[0]);
await waitFor(() => expect(fetchModels).toHaveBeenCalled());
expect(screen.getByLabelText("Execution Model").textContent).toContain("Use default");
expect(screen.getByLabelText("Planning Model").textContent).toContain("Use default");
expect(screen.getByLabelText("Validator Model").textContent).toContain("Use default");
expect(screen.getByLabelText("Title Summarization Model").textContent).toContain("Use default");
});
it("renders Thinking Effort select in global Models section", async () => {
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());