diff --git a/.changeset/wide-project-model-dropdowns.md b/.changeset/wide-project-model-dropdowns.md
new file mode 100644
index 0000000000..6d3b345d5e
--- /dev/null
+++ b/.changeset/wide-project-model-dropdowns.md
@@ -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.
diff --git a/packages/dashboard/app/__tests__/settings-sections.test.tsx b/packages/dashboard/app/__tests__/settings-sections.test.tsx
index 9409029b26..6bc0f14891 100644
--- a/packages/dashboard/app/__tests__/settings-sections.test.tsx
+++ b/packages/dashboard/app/__tests__/settings-sections.test.tsx
@@ -23,6 +23,7 @@ import { PromptsSection } from "../components/settings/sections/PromptsSection";
import { SecretsSection } from "../components/settings/sections/SecretsSection";
import { WorktreesSection } from "../components/settings/sections/WorktreesSection";
import type { SettingsFormState } from "../components/settings/sections/context";
+import { fetchWorkflow, fetchWorkflowSettingValues } from "../api";
vi.mock("../components/AgentPromptsManager", () => ({
AgentPromptsManager: () =>
({
diff --git a/packages/dashboard/app/components/CustomModelDropdown.tsx b/packages/dashboard/app/components/CustomModelDropdown.tsx
index 6210529e34..3b9c317174 100644
--- a/packages/dashboard/app/components/CustomModelDropdown.tsx
+++ b/packages/dashboard/app/components/CustomModelDropdown.tsx
@@ -28,6 +28,8 @@ export interface CustomModelDropdownProps {
favoriteModels?: string[];
/** Called when user toggles a model's favorite status */
onToggleModelFavorite?: (modelId: string) => void;
+ /** Request a wider menu for dense settings surfaces while default callers keep trigger-width sizing. */
+ menuWidth?: "trigger" | "readable";
}
interface DropdownPosition {
@@ -65,6 +67,7 @@ export function CustomModelDropdown({
noChangeValue,
noChangeLabel: noChangeLabelProp,
defaultOptionLabel: defaultOptionLabelProp,
+ menuWidth = "trigger",
}: CustomModelDropdownProps) {
const { t } = useTranslation("app");
const placeholder = placeholderProp ?? t("model.selectPlaceholder", "Select a model…");
@@ -270,7 +273,16 @@ export function CustomModelDropdown({
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(
Math.max(triggerLeft, horizontalPadding),
viewportWidth - horizontalPadding - dropdownWidth,
@@ -285,7 +297,7 @@ export function CustomModelDropdown({
width: dropdownWidth,
maxHeight,
});
- }, [getEffectiveViewport, getPreferredDropdownHeight]);
+ }, [getEffectiveViewport, getPreferredDropdownHeight, menuWidth]);
useEffect(() => {
setPortalRoot(document.body);
@@ -483,6 +495,7 @@ export function CustomModelDropdown({
className="model-combobox-dropdown model-combobox-dropdown--portal"
role="listbox"
data-testid="model-combobox-portal"
+ data-menu-width={menuWidth}
onKeyDown={handleKeyDown}
style={{
top: `${dropdownPosition.top}px`,
diff --git a/packages/dashboard/app/components/__tests__/CustomModelDropdown.test.tsx b/packages/dashboard/app/components/__tests__/CustomModelDropdown.test.tsx
index 527ea89a48..6c76733c19 100644
--- a/packages/dashboard/app/components/__tests__/CustomModelDropdown.test.tsx
+++ b/packages/dashboard/app/components/__tests__/CustomModelDropdown.test.tsx
@@ -239,6 +239,216 @@ describe("CustomModelDropdown", () => {
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(
+ ,
+ );
+
+ 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(
+ ,
+ );
+
+ 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(
+ ,
+ );
+
+ 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(
+ ,
+ );
+
+ 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(
+ ,
+ );
+
+ 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", () => {
it("shows favorited models as pinned rows at the top before provider groups", async () => {
diff --git a/packages/dashboard/app/components/settings/sections/ProjectModelsSection.tsx b/packages/dashboard/app/components/settings/sections/ProjectModelsSection.tsx
index f95ebda76f..bec4af0ebc 100644
--- a/packages/dashboard/app/components/settings/sections/ProjectModelsSection.tsx
+++ b/packages/dashboard/app/components/settings/sections/ProjectModelsSection.tsx
@@ -286,7 +286,7 @@ export function ProjectModelsSection({ scopeBanner, form, setForm, models, proje
- updateLaneValue(lane, val)} placeholder={lane.laneId === "default" ? "Use global default" : "Use global"} favoriteProviders={favoriteProviders} onToggleFavorite={onToggleFavorite} favoriteModels={favoriteModels} onToggleModelFavorite={onToggleModelFavorite}/>
+ updateLaneValue(lane, val)} placeholder={lane.laneId === "default" ? "Use global default" : "Use global"} favoriteProviders={favoriteProviders} onToggleFavorite={onToggleFavorite} favoriteModels={favoriteModels} onToggleModelFavorite={onToggleModelFavorite} menuWidth="readable"/>
{isOverridden && (
resetLaneValue(lane)} style={{ whiteSpace: "nowrap" }}>{t("settings.projectModels.reset", " Reset ")} )}
@@ -318,7 +318,7 @@ export function ProjectModelsSection({ scopeBanner, form, setForm, models, proje
- setWorkflowPairValue(pair, next)} placeholder={t("settings.projectModels.useWorkflowDefault", "Use workflow default")} defaultOptionLabel="Use workflow default" favoriteProviders={favoriteProviders} onToggleFavorite={onToggleFavorite} favoriteModels={favoriteModels} onToggleModelFavorite={onToggleModelFavorite}/>
+ 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"/>
{customized && (
resetWorkflowPairValue(pair)} style={{ whiteSpace: "nowrap" }}>{t("settings.projectModels.reset", " Reset ")} )}
@@ -406,7 +406,7 @@ export function ProjectModelsSection({ scopeBanner, form, setForm, models, proje
executorProvider: val.slice(0, slashIdx),
executorModelId: val.slice(slashIdx + 1),
} : 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"/>
{t("settings.projectModels.reviewerModel", "Reviewer model")}
@@ -421,7 +421,7 @@ export function ProjectModelsSection({ scopeBanner, form, setForm, models, proje
validatorProvider: val.slice(0, slashIdx),
validatorModelId: val.slice(slashIdx + 1),
} : 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"/>
>)}