feat(FN-1996): merge fusion/fn-1996
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user