FN-7399: widen Project Models model menus
Make Project Models dropdown menus easier to scan without changing shared selector defaults. - Add an opt-in readable menu width mode to the shared custom model dropdown. - Apply the wider menu only to Project Models lane, workflow, and preset model selectors. - Cover readable sizing, viewport clamping, and default trigger-width behavior with dashboard tests. - Add a patch changeset for the published Fusion package. Files changed: .changeset/wide-project-model-dropdowns.md | 7 + .../app/__tests__/settings-sections.test.tsx | 66 ++++++- .../app/components/CustomModelDropdown.tsx | 17 +- .../__tests__/CustomModelDropdown.test.tsx | 210 +++++++++++++++++++++ .../settings/sections/ProjectModelsSection.tsx | 8 +- 5 files changed, 301 insertions(+), 7 deletions(-) Fusion-Task-Id: FN-7399 Fusion-Task-Lineage: 40aae990-37bc-4ed8-8e2a-7e78a543053b Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/wide-project-model-dropdowns.md
Normal file
7
.changeset/wide-project-model-dropdowns.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Widen Project Models dropdown menus so long provider and model names are easier to read.
|
||||||
|
category: fix
|
||||||
|
dev: Adds an opt-in readable menu width to the shared dashboard model dropdown and applies it only in Project Models.
|
||||||
@@ -23,6 +23,7 @@ import { PromptsSection } from "../components/settings/sections/PromptsSection";
|
|||||||
import { SecretsSection } from "../components/settings/sections/SecretsSection";
|
import { SecretsSection } from "../components/settings/sections/SecretsSection";
|
||||||
import { WorktreesSection } from "../components/settings/sections/WorktreesSection";
|
import { WorktreesSection } from "../components/settings/sections/WorktreesSection";
|
||||||
import type { SettingsFormState } from "../components/settings/sections/context";
|
import type { SettingsFormState } from "../components/settings/sections/context";
|
||||||
|
import { fetchWorkflow, fetchWorkflowSettingValues } from "../api";
|
||||||
|
|
||||||
vi.mock("../components/AgentPromptsManager", () => ({
|
vi.mock("../components/AgentPromptsManager", () => ({
|
||||||
AgentPromptsManager: () => <div data-testid="agent-prompts-manager" />,
|
AgentPromptsManager: () => <div data-testid="agent-prompts-manager" />,
|
||||||
@@ -35,12 +36,20 @@ vi.mock("../api", async (importOriginal) => {
|
|||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
fetchWorkflows: vi.fn(async () => []),
|
fetchWorkflows: vi.fn(async () => []),
|
||||||
fetchWorkflow: vi.fn(async () => ({ id: "builtin:coding", name: "Coding" })),
|
fetchWorkflow: vi.fn(async () => ({ id: "builtin:coding", name: "Coding", ir: {} })),
|
||||||
|
fetchWorkflowSettingValues: vi.fn(async () => ({ stored: {}, effective: {}, orphaned: [] })),
|
||||||
fetchProjectDefaultWorkflow: vi.fn(async () => ({ workflowId: null })),
|
fetchProjectDefaultWorkflow: vi.fn(async () => ({ workflowId: null })),
|
||||||
setProjectDefaultWorkflow: vi.fn(async () => ({ workflowId: null })),
|
setProjectDefaultWorkflow: vi.fn(async () => ({ workflowId: null })),
|
||||||
fetchGlobalSettings: vi.fn(async () => ({})),
|
fetchGlobalSettings: vi.fn(async () => ({})),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
vi.mock("../components/CustomModelDropdown", () => ({
|
||||||
|
CustomModelDropdown: ({ id, label, menuWidth = "trigger" }: { id?: string; label: string; menuWidth?: "trigger" | "readable" }) => (
|
||||||
|
<button type="button" data-testid={`mock-model-dropdown-${id ?? label}`} data-menu-width={menuWidth}>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
expect.extend(jestDomMatchers);
|
expect.extend(jestDomMatchers);
|
||||||
afterEach(() => cleanup());
|
afterEach(() => cleanup());
|
||||||
@@ -281,6 +290,61 @@ describe("ProjectModelsSection", () => {
|
|||||||
confirmDelete: vi.fn(),
|
confirmDelete: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
it("opts Project Models lane and preset dropdowns into readable menu width", () => {
|
||||||
|
render(
|
||||||
|
<ProjectModelsSection
|
||||||
|
scopeBanner={null}
|
||||||
|
form={{} as SettingsFormState}
|
||||||
|
setForm={vi.fn()}
|
||||||
|
models={{
|
||||||
|
...models,
|
||||||
|
modelLanes: [
|
||||||
|
{ laneId: "default", label: "Default", helperText: "Default", fallbackOrder: "global" },
|
||||||
|
{ laneId: "summarization", label: "Summarization", helperText: "Summarization", fallbackOrder: "global" },
|
||||||
|
] as never,
|
||||||
|
availableModels: [{ provider: "anthropic", id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5" }],
|
||||||
|
presetDraft: { id: "preset", name: "Preset", executorProvider: undefined, executorModelId: undefined, validatorProvider: undefined, validatorModelId: undefined },
|
||||||
|
}}
|
||||||
|
addToast={vi.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByTestId("mock-model-dropdown-defaultModel")).toHaveAttribute("data-menu-width", "readable");
|
||||||
|
expect(screen.getByTestId("mock-model-dropdown-summarizationModel")).toHaveAttribute("data-menu-width", "readable");
|
||||||
|
expect(screen.getByTestId("mock-model-dropdown-preset-executor-model")).toHaveAttribute("data-menu-width", "readable");
|
||||||
|
expect(screen.getByTestId("mock-model-dropdown-preset-validator-model")).toHaveAttribute("data-menu-width", "readable");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("opts default workflow model lane dropdowns into readable menu width", async () => {
|
||||||
|
vi.mocked(fetchWorkflow).mockResolvedValueOnce({
|
||||||
|
id: "builtin:coding",
|
||||||
|
name: "Coding",
|
||||||
|
ir: {
|
||||||
|
settings: [
|
||||||
|
{ id: "planningProvider", name: "Planning Provider", type: "string" },
|
||||||
|
{ id: "planningModelId", name: "Planning Model", type: "string" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
} as never);
|
||||||
|
vi.mocked(fetchWorkflowSettingValues).mockResolvedValueOnce({ stored: {}, effective: {}, orphaned: [] });
|
||||||
|
|
||||||
|
render(
|
||||||
|
<ProjectModelsSection
|
||||||
|
scopeBanner={null}
|
||||||
|
form={{ defaultWorkflowId: "builtin:coding" } as SettingsFormState}
|
||||||
|
setForm={vi.fn()}
|
||||||
|
models={{
|
||||||
|
...models,
|
||||||
|
availableModels: [{ provider: "anthropic", id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5" }],
|
||||||
|
}}
|
||||||
|
projectId="project-1"
|
||||||
|
addToast={vi.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await screen.findByTestId("mock-model-dropdown-workflow-planning-model")).toHaveAttribute("data-menu-width", "readable");
|
||||||
|
});
|
||||||
|
|
||||||
it("renders PR prompt guidance textareas and emits edits through setForm", () => {
|
it("renders PR prompt guidance textareas and emits edits through setForm", () => {
|
||||||
function ProjectModelsHost() {
|
function ProjectModelsHost() {
|
||||||
const [form, setFormState] = useState<SettingsFormState>({
|
const [form, setFormState] = useState<SettingsFormState>({
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export interface CustomModelDropdownProps {
|
|||||||
favoriteModels?: string[];
|
favoriteModels?: string[];
|
||||||
/** Called when user toggles a model's favorite status */
|
/** Called when user toggles a model's favorite status */
|
||||||
onToggleModelFavorite?: (modelId: string) => void;
|
onToggleModelFavorite?: (modelId: string) => void;
|
||||||
|
/** Request a wider menu for dense settings surfaces while default callers keep trigger-width sizing. */
|
||||||
|
menuWidth?: "trigger" | "readable";
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DropdownPosition {
|
interface DropdownPosition {
|
||||||
@@ -65,6 +67,7 @@ export function CustomModelDropdown({
|
|||||||
noChangeValue,
|
noChangeValue,
|
||||||
noChangeLabel: noChangeLabelProp,
|
noChangeLabel: noChangeLabelProp,
|
||||||
defaultOptionLabel: defaultOptionLabelProp,
|
defaultOptionLabel: defaultOptionLabelProp,
|
||||||
|
menuWidth = "trigger",
|
||||||
}: CustomModelDropdownProps) {
|
}: CustomModelDropdownProps) {
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
const placeholder = placeholderProp ?? t("model.selectPlaceholder", "Select a model…");
|
const placeholder = placeholderProp ?? t("model.selectPlaceholder", "Select a model…");
|
||||||
@@ -270,7 +273,16 @@ export function CustomModelDropdown({
|
|||||||
160,
|
160,
|
||||||
);
|
);
|
||||||
|
|
||||||
const dropdownWidth = Math.min(rect.width, viewportWidth - horizontalPadding * 2);
|
const maxDropdownWidth = viewportWidth - horizontalPadding * 2;
|
||||||
|
/*
|
||||||
|
FNXC:ModelDropdown 2026-07-01-00:00:
|
||||||
|
Project Models lanes need a readable portaled menu for long provider/model names, but shared model selectors elsewhere must retain trigger-width behavior unless they opt in.
|
||||||
|
Clamp the widened target to the effective viewport, including visualViewport offsets, so desktop and mobile keyboards never create offscreen click targets.
|
||||||
|
*/
|
||||||
|
const preferredDropdownWidth = menuWidth === "readable"
|
||||||
|
? Math.max(rect.width, Math.min(rect.width * 1.6, viewportWidth * 0.72))
|
||||||
|
: rect.width;
|
||||||
|
const dropdownWidth = Math.min(preferredDropdownWidth, maxDropdownWidth);
|
||||||
const left = Math.min(
|
const left = Math.min(
|
||||||
Math.max(triggerLeft, horizontalPadding),
|
Math.max(triggerLeft, horizontalPadding),
|
||||||
viewportWidth - horizontalPadding - dropdownWidth,
|
viewportWidth - horizontalPadding - dropdownWidth,
|
||||||
@@ -285,7 +297,7 @@ export function CustomModelDropdown({
|
|||||||
width: dropdownWidth,
|
width: dropdownWidth,
|
||||||
maxHeight,
|
maxHeight,
|
||||||
});
|
});
|
||||||
}, [getEffectiveViewport, getPreferredDropdownHeight]);
|
}, [getEffectiveViewport, getPreferredDropdownHeight, menuWidth]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPortalRoot(document.body);
|
setPortalRoot(document.body);
|
||||||
@@ -483,6 +495,7 @@ export function CustomModelDropdown({
|
|||||||
className="model-combobox-dropdown model-combobox-dropdown--portal"
|
className="model-combobox-dropdown model-combobox-dropdown--portal"
|
||||||
role="listbox"
|
role="listbox"
|
||||||
data-testid="model-combobox-portal"
|
data-testid="model-combobox-portal"
|
||||||
|
data-menu-width={menuWidth}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
style={{
|
style={{
|
||||||
top: `${dropdownPosition.top}px`,
|
top: `${dropdownPosition.top}px`,
|
||||||
|
|||||||
@@ -239,6 +239,216 @@ describe("CustomModelDropdown", () => {
|
|||||||
expect(portal.style.maxHeight).toBe("360px");
|
expect(portal.style.maxHeight).toBe("360px");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Readable menu sizing", () => {
|
||||||
|
const setupBoundingRectMock = (rectValues: DOMRect) => {
|
||||||
|
const originalGetBCR = Element.prototype.getBoundingClientRect;
|
||||||
|
Element.prototype.getBoundingClientRect = vi.fn(() => rectValues as DOMRect);
|
||||||
|
return () => {
|
||||||
|
Element.prototype.getBoundingClientRect = originalGetBCR;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupVisualViewportMock = (vv: { width: number; height: number; offsetTop: number; offsetLeft: number }) => {
|
||||||
|
const originalVV = window.visualViewport;
|
||||||
|
const mockVV = {
|
||||||
|
width: vv.width,
|
||||||
|
height: vv.height,
|
||||||
|
offsetTop: vv.offsetTop,
|
||||||
|
offsetLeft: vv.offsetLeft,
|
||||||
|
addEventListener: vi.fn(),
|
||||||
|
removeEventListener: vi.fn(),
|
||||||
|
dispatchEvent: vi.fn(),
|
||||||
|
};
|
||||||
|
Object.defineProperty(window, "visualViewport", {
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
value: mockVV,
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
Object.defineProperty(window, "visualViewport", {
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
value: originalVV,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
it("keeps trigger-width sizing unless readable width is requested", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const onChange = vi.fn();
|
||||||
|
|
||||||
|
vi.spyOn(window, "innerWidth", "get").mockReturnValue(1000);
|
||||||
|
const restore = setupBoundingRectMock({
|
||||||
|
top: 100,
|
||||||
|
left: 50,
|
||||||
|
bottom: 136,
|
||||||
|
width: 300,
|
||||||
|
height: 36,
|
||||||
|
right: 350,
|
||||||
|
x: 50,
|
||||||
|
y: 100,
|
||||||
|
} as DOMRect);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { unmount } = render(
|
||||||
|
<CustomModelDropdown
|
||||||
|
label="Executor Model"
|
||||||
|
value=""
|
||||||
|
onChange={onChange}
|
||||||
|
models={MOCK_MODELS}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: "Executor Model" }));
|
||||||
|
expect((await screen.findByTestId("model-combobox-portal")).style.width).toBe("300px");
|
||||||
|
unmount();
|
||||||
|
document.body.innerHTML = "";
|
||||||
|
|
||||||
|
render(
|
||||||
|
<CustomModelDropdown
|
||||||
|
label="Executor Model"
|
||||||
|
value=""
|
||||||
|
onChange={onChange}
|
||||||
|
models={MOCK_MODELS}
|
||||||
|
menuWidth="readable"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: "Executor Model" }));
|
||||||
|
const portal = await screen.findByTestId("model-combobox-portal");
|
||||||
|
expect(portal.style.width).toBe("480px");
|
||||||
|
expect(portal).toHaveAttribute("data-menu-width", "readable");
|
||||||
|
} finally {
|
||||||
|
restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clamps readable sizing inside the desktop viewport", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const onChange = vi.fn();
|
||||||
|
|
||||||
|
vi.spyOn(window, "innerWidth", "get").mockReturnValue(320);
|
||||||
|
const restore = setupBoundingRectMock({
|
||||||
|
top: 100,
|
||||||
|
left: 40,
|
||||||
|
bottom: 136,
|
||||||
|
width: 300,
|
||||||
|
height: 36,
|
||||||
|
right: 340,
|
||||||
|
x: 40,
|
||||||
|
y: 100,
|
||||||
|
} as DOMRect);
|
||||||
|
|
||||||
|
try {
|
||||||
|
render(
|
||||||
|
<CustomModelDropdown
|
||||||
|
label="Executor Model"
|
||||||
|
value=""
|
||||||
|
onChange={onChange}
|
||||||
|
models={MOCK_MODELS}
|
||||||
|
menuWidth="readable"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: "Executor Model" }));
|
||||||
|
const portal = await screen.findByTestId("model-combobox-portal");
|
||||||
|
const left = parseFloat(portal.style.left);
|
||||||
|
const width = parseFloat(portal.style.width);
|
||||||
|
|
||||||
|
expect(width).toBe(288);
|
||||||
|
expect(left).toBe(16);
|
||||||
|
expect(left + width).toBeLessThanOrEqual(320 - 16);
|
||||||
|
} finally {
|
||||||
|
restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clamps readable sizing with visualViewport horizontal offsets", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const onChange = vi.fn();
|
||||||
|
vi.spyOn(window, "innerWidth", "get").mockReturnValue(800);
|
||||||
|
const vvCleanup = setupVisualViewportMock({
|
||||||
|
width: 500,
|
||||||
|
height: 600,
|
||||||
|
offsetTop: 0,
|
||||||
|
offsetLeft: 100,
|
||||||
|
});
|
||||||
|
const restore = setupBoundingRectMock({
|
||||||
|
top: 100,
|
||||||
|
left: 550,
|
||||||
|
bottom: 136,
|
||||||
|
width: 200,
|
||||||
|
height: 36,
|
||||||
|
right: 750,
|
||||||
|
x: 550,
|
||||||
|
y: 100,
|
||||||
|
} as DOMRect);
|
||||||
|
|
||||||
|
try {
|
||||||
|
render(
|
||||||
|
<CustomModelDropdown
|
||||||
|
label="Executor Model"
|
||||||
|
value=""
|
||||||
|
onChange={onChange}
|
||||||
|
models={MOCK_MODELS}
|
||||||
|
menuWidth="readable"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: "Executor Model" }));
|
||||||
|
const portal = await screen.findByTestId("model-combobox-portal");
|
||||||
|
const left = parseFloat(portal.style.left);
|
||||||
|
const width = parseFloat(portal.style.width);
|
||||||
|
|
||||||
|
expect(width).toBe(320);
|
||||||
|
expect(left).toBe(264);
|
||||||
|
expect(left - 100).toBeGreaterThanOrEqual(16);
|
||||||
|
expect(left - 100 + width).toBeLessThanOrEqual(500 - 16);
|
||||||
|
} finally {
|
||||||
|
restore();
|
||||||
|
vvCleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps long readable rows searchable, selectable, and favorite-aware", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const onChange = vi.fn();
|
||||||
|
const longModels = [
|
||||||
|
...MOCK_MODELS,
|
||||||
|
{
|
||||||
|
provider: "anthropic-enterprise-cloud",
|
||||||
|
id: "claude-sonnet-4-5-20260701-extremely-long-production-model-id",
|
||||||
|
name: "Claude Sonnet 4.5 Enterprise Production with a Very Long Readable Name",
|
||||||
|
reasoning: true,
|
||||||
|
contextWindow: 200000,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
render(
|
||||||
|
<CustomModelDropdown
|
||||||
|
label="Executor Model"
|
||||||
|
value=""
|
||||||
|
onChange={onChange}
|
||||||
|
models={longModels}
|
||||||
|
favoriteModels={["anthropic-enterprise-cloud/claude-sonnet-4-5-20260701-extremely-long-production-model-id"]}
|
||||||
|
onToggleModelFavorite={vi.fn()}
|
||||||
|
menuWidth="readable"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: "Executor Model" }));
|
||||||
|
const portal = await screen.findByTestId("model-combobox-portal");
|
||||||
|
await user.type(within(portal).getByPlaceholderText("Filter models…"), "enterprise production");
|
||||||
|
|
||||||
|
expect(within(portal).getByText("Claude Sonnet 4.5 Enterprise Production with a Very Long Readable Name")).toBeTruthy();
|
||||||
|
expect(within(portal).queryByText("No models found")).toBeNull();
|
||||||
|
expect(within(portal).getByLabelText("Remove Claude Sonnet 4.5 Enterprise Production with a Very Long Readable Name from favorites")).toBeTruthy();
|
||||||
|
|
||||||
|
await user.click(within(portal).getByText("Claude Sonnet 4.5 Enterprise Production with a Very Long Readable Name"));
|
||||||
|
expect(onChange).toHaveBeenCalledWith("anthropic-enterprise-cloud/claude-sonnet-4-5-20260701-extremely-long-production-model-id");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
describe("Model Favorites", () => {
|
describe("Model Favorites", () => {
|
||||||
it("shows favorited models as pinned rows at the top before provider groups", async () => {
|
it("shows favorited models as pinned rows at the top before provider groups", async () => {
|
||||||
|
|||||||
@@ -286,7 +286,7 @@ export function ProjectModelsSection({ scopeBanner, form, setForm, models, proje
|
|||||||
</div>
|
</div>
|
||||||
<div className="settings-model-lane-control-row">
|
<div className="settings-model-lane-control-row">
|
||||||
<div className="settings-model-lane-control-main">
|
<div className="settings-model-lane-control-main">
|
||||||
<CustomModelDropdown id={`${lane.laneId}Model`} label={laneLabel} models={availableModels} value={value} onChange={(val) => updateLaneValue(lane, val)} placeholder={lane.laneId === "default" ? "Use global default" : "Use global"} favoriteProviders={favoriteProviders} onToggleFavorite={onToggleFavorite} favoriteModels={favoriteModels} onToggleModelFavorite={onToggleModelFavorite}/>
|
<CustomModelDropdown id={`${lane.laneId}Model`} label={laneLabel} models={availableModels} value={value} onChange={(val) => updateLaneValue(lane, val)} placeholder={lane.laneId === "default" ? "Use global default" : "Use global"} favoriteProviders={favoriteProviders} onToggleFavorite={onToggleFavorite} favoriteModels={favoriteModels} onToggleModelFavorite={onToggleModelFavorite} menuWidth="readable"/>
|
||||||
</div>
|
</div>
|
||||||
{isOverridden && (<button type="button" className="btn btn-ghost btn-sm" title={t("settings.projectModels.resetToInheritFromGlobal", "Reset to inherit from global")} onClick={() => resetLaneValue(lane)} style={{ whiteSpace: "nowrap" }}>{t("settings.projectModels.reset", " Reset ")}</button>)}
|
{isOverridden && (<button type="button" className="btn btn-ghost btn-sm" title={t("settings.projectModels.resetToInheritFromGlobal", "Reset to inherit from global")} onClick={() => resetLaneValue(lane)} style={{ whiteSpace: "nowrap" }}>{t("settings.projectModels.reset", " Reset ")}</button>)}
|
||||||
</div>
|
</div>
|
||||||
@@ -318,7 +318,7 @@ export function ProjectModelsSection({ scopeBanner, form, setForm, models, proje
|
|||||||
</div>
|
</div>
|
||||||
<div className="settings-model-lane-control-row">
|
<div className="settings-model-lane-control-row">
|
||||||
<div className="settings-model-lane-control-main">
|
<div className="settings-model-lane-control-main">
|
||||||
<CustomModelDropdown id={`workflow-${pair.id}-model`} label={pair.label} models={availableModels} value={value} onChange={(next) => setWorkflowPairValue(pair, next)} placeholder={t("settings.projectModels.useWorkflowDefault", "Use workflow default")} defaultOptionLabel="Use workflow default" favoriteProviders={favoriteProviders} onToggleFavorite={onToggleFavorite} favoriteModels={favoriteModels} onToggleModelFavorite={onToggleModelFavorite}/>
|
<CustomModelDropdown id={`workflow-${pair.id}-model`} label={pair.label} models={availableModels} value={value} onChange={(next) => setWorkflowPairValue(pair, next)} placeholder={t("settings.projectModels.useWorkflowDefault", "Use workflow default")} defaultOptionLabel="Use workflow default" favoriteProviders={favoriteProviders} onToggleFavorite={onToggleFavorite} favoriteModels={favoriteModels} onToggleModelFavorite={onToggleModelFavorite} menuWidth="readable"/>
|
||||||
</div>
|
</div>
|
||||||
{customized && (<button type="button" className="btn btn-ghost btn-sm" title={t("settings.projectModels.resetToInheritFromWorkflow", "Reset to inherit from workflow")} onClick={() => resetWorkflowPairValue(pair)} style={{ whiteSpace: "nowrap" }}>{t("settings.projectModels.reset", " Reset ")}</button>)}
|
{customized && (<button type="button" className="btn btn-ghost btn-sm" title={t("settings.projectModels.resetToInheritFromWorkflow", "Reset to inherit from workflow")} onClick={() => resetWorkflowPairValue(pair)} style={{ whiteSpace: "nowrap" }}>{t("settings.projectModels.reset", " Reset ")}</button>)}
|
||||||
</div>
|
</div>
|
||||||
@@ -406,7 +406,7 @@ export function ProjectModelsSection({ scopeBanner, form, setForm, models, proje
|
|||||||
executorProvider: val.slice(0, slashIdx),
|
executorProvider: val.slice(0, slashIdx),
|
||||||
executorModelId: val.slice(slashIdx + 1),
|
executorModelId: val.slice(slashIdx + 1),
|
||||||
} : current);
|
} : current);
|
||||||
}} placeholder={t("settings.projectModels.useDefault", "Use default")} favoriteProviders={favoriteProviders} onToggleFavorite={onToggleFavorite} favoriteModels={favoriteModels} onToggleModelFavorite={onToggleModelFavorite}/>
|
}} placeholder={t("settings.projectModels.useDefault", "Use default")} favoriteProviders={favoriteProviders} onToggleFavorite={onToggleFavorite} favoriteModels={favoriteModels} onToggleModelFavorite={onToggleModelFavorite} menuWidth="readable"/>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="preset-validator-model">{t("settings.projectModels.reviewerModel", "Reviewer model")}</label>
|
<label htmlFor="preset-validator-model">{t("settings.projectModels.reviewerModel", "Reviewer model")}</label>
|
||||||
@@ -421,7 +421,7 @@ export function ProjectModelsSection({ scopeBanner, form, setForm, models, proje
|
|||||||
validatorProvider: val.slice(0, slashIdx),
|
validatorProvider: val.slice(0, slashIdx),
|
||||||
validatorModelId: val.slice(slashIdx + 1),
|
validatorModelId: val.slice(slashIdx + 1),
|
||||||
} : current);
|
} : current);
|
||||||
}} placeholder={t("settings.projectModels.useDefault", "Use default")} favoriteProviders={favoriteProviders} onToggleFavorite={onToggleFavorite} favoriteModels={favoriteModels} onToggleModelFavorite={onToggleModelFavorite}/>
|
}} placeholder={t("settings.projectModels.useDefault", "Use default")} favoriteProviders={favoriteProviders} onToggleFavorite={onToggleFavorite} favoriteModels={favoriteModels} onToggleModelFavorite={onToggleModelFavorite} menuWidth="readable"/>
|
||||||
</div>
|
</div>
|
||||||
</>)}
|
</>)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user