From cc6f389e719add6c283e54fc225d61cdb54c88d5 Mon Sep 17 00:00:00 2001 From: Fusion Agent Date: Tue, 18 Aug 2026 07:08:48 +0000 Subject: [PATCH] FN-003: consolidate model overrides and Docker tooling Consolidate model override settings while adding supported operator tools to the Docker image. - Group project and workflow model overrides in the Settings interface with updated search metadata, translations, tests, and documentation. - Install gh, tailscale, and cloudflared from signed vendor repositories and cover the Docker configuration. - Add release changesets for the settings and container capabilities. Files changed: .changeset/docker-operator-tooling.md | 7 + .changeset/fn-003-model-override-organization.md | 7 + Dockerfile | 36 ++++- docs/dashboard-guide.md | 6 +- docs/settings-reference.md | 9 +- .../app/__tests__/settings-sections.test.tsx | 10 ++ .../__tests__/SettingsModal.models-auth.test.tsx | 2 +- .../settings/sections/GlobalModelsSection.tsx | 16 +- .../settings/sections/ProjectModelsSection.tsx | 171 +++++++++++---------- packages/dashboard/src/shared/settings-sections.ts | 6 +- packages/i18n/locales/en/app.json | 5 + .../dockerfile-workspace-manifests.test.mjs | 22 +++ 12 files changed, 200 insertions(+), 97 deletions(-) Fusion-Task-Id: FN-003 Fusion-Task-Lineage: 4df75b7f-f8dd-472c-b343-d06800d6c615 Co-authored-by: Fusion --- .../fn-003-model-override-organization.md | 7 + docs/dashboard-guide.md | 6 +- docs/settings-reference.md | 9 +- .../app/__tests__/settings-sections.test.tsx | 10 + .../SettingsModal.models-auth.test.tsx | 2 +- .../settings/sections/GlobalModelsSection.tsx | 16 +- .../sections/ProjectModelsSection.tsx | 171 +++++++++--------- .../dashboard/src/shared/settings-sections.ts | 6 +- packages/i18n/locales/en/app.json | 5 + 9 files changed, 136 insertions(+), 96 deletions(-) create mode 100644 .changeset/fn-003-model-override-organization.md diff --git a/.changeset/fn-003-model-override-organization.md b/.changeset/fn-003-model-override-organization.md new file mode 100644 index 0000000000..2ffc7830b6 --- /dev/null +++ b/.changeset/fn-003-model-override-organization.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Organize project and workflow model overrides in one Settings group. +category: feature +dev: Moves the Project Models JSX into Model Overrides subgroups and adds matching i18n/search metadata. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index d6f699d660..a25a046d5a 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -506,7 +506,7 @@ Behavior: FNXC:WorkflowEditor 2026-06-29-21:10: Optional-group template entry/exit ownership remains visual-only. Non-editable boundary connector lines must explain how template children attach to the container without persisting fake edges into workflow IR. --> - Optional-group, foreach, and loop containers show their template nodes inside the block. Canvas connections between the surrounding workflow and the block attach to the container boundary; connections between template nodes stay inside the block. Optional groups also draw non-editable entry/exit connector lines between the boundary and the template entry/exit nodes so single-step blocks such as Plan Review and Code Review do not look disconnected; those visual connectors are not saved into workflow IR. - The Settings panel is value-first for built-in workflows and groups workflow settings by Models, Review & Approval, Step Execution, and Advanced. Known workflow model values use the same model dropdown picker as **Settings → Project Models** so provider/model pairs and their inline Thinking Level companions are saved together; custom or non-model string values can still use typed inputs. Definitions remain available for custom workflow schema authoring. -- The main Settings modal exposes project-baseline Plan/Triage, Executor, Reviewer, and declared fallback model lanes from **Project Models**. Those dropdown values auto-save on the active default workflow and are inherited by every workflow. Resolution is task-specific selection → project baseline → global lane → selected-workflow value; Project Models therefore wins over values configured in a workflow's **Settings → Values**. Project Models also includes project-scoped **Merger** and **Title Summarization** lanes (not workflow-moved). Settings fallback model pickers (Global Fallback Model, workflow fallbacks, and project Title Summarizer Fallback) include the same inline Thinking Level selector when their companion key exists. +- The main Settings modal groups project-baseline Plan/Triage, Executor, Reviewer, and declared fallback model lanes in **Models · Project → Model Overrides → Workflow lanes**, directly after **Project lanes**. Those dropdown values auto-save on the active default workflow and are inherited by every workflow. Resolution is task-specific selection → project baseline → global lane → selected-workflow value; Project Models therefore wins over values configured in a workflow's **Settings → Values**. Project-scoped **Merger** remains in Project lanes, while **Title Summarization** and its fallback intentionally remain under **AI Title and Git Commit Message Summarization** with their enable toggles. Settings fallback model pickers (Global Fallback Model, workflow fallbacks, and project Title Summarizer Fallback) include the same inline Thinking Level selector when their companion key exists. - On desktop, the editor uses a multi-panel canvas layout for editing the graph and adjacent workflow metadata. The **Show simple editor** toggle switches that same workflow into the graph-outline editor with dedicated **Graph**, **Add**, **Settings**, **Fields**, **Columns**, and **Actions** tabs. - On viewports `<=768px`, the editor switches to a full-screen mobile sheet. Global workflow entry points open to the workflow list with no workflow preselected and prompt users to select a workflow to edit; the Board/List workflow dropdown row edit action opens directly to the selected workflow editor when that selected workflow is available. - Simple/mobile editing uses a graph outline instead of making the canvas the primary control. The outline shows nodes, branch/rework edges, column placement, and optional-group/foreach/loop template children as tappable rows and chips that open the same node and edge detail editors as desktop. The structural **start** node opens an inspector for the workflow entry column when the workflow defines columns; the **Name** field remains unavailable because the start label is structural. For custom workflows, editable outline rows also expose **Move up** and **Move down** controls that reorder steps within their current column or template parent; built-in workflows remain read-only and hide those controls. @@ -515,7 +515,7 @@ FNXC:WorkflowEditor 2026-06-29-21:10: Optional-group template entry/exit ownersh ## Custom Providers -Custom Providers live in **Settings → Authentication → Custom Providers**, inside the **Advanced: Custom Providers** disclosure. Use this section to add user-defined model providers that speak an OpenAI-compatible API, the OpenAI Responses API, an Anthropic-compatible API, or Google Generative AI. After a provider is saved with models, those models become selectable in model dropdowns, including **Settings → Project Models** lanes and workflow model lanes. +Custom Providers live in **Settings → Authentication → Custom Providers**, inside the **Advanced: Custom Providers** disclosure. Use this section to add user-defined model providers that speak an OpenAI-compatible API, the OpenAI Responses API, an Anthropic-compatible API, or Google Generative AI. After a provider is saved with models, those models become selectable in model dropdowns, including **Settings → Models · Project → Model Overrides** project and workflow lanes. Settings → Global Models also includes **Model pricing overrides** for Command Center estimates. The section shows a compact pricing snapshot/override-count summary; use **View pricing table** to add or edit rows with lowercased `provider:model` keys (or bare `:model` fallback keys), USD-per-1M token prices for input/output/cache read/cache write, and optional source text. **Fetch LiteLLM pricing** remains available from the collapsed summary and performs an explicit one-click refresh from LiteLLM's published model pricing JSON, replaces the override table only after a successful parse, and records the fetched timestamp/source; failed fetches keep the existing overrides. @@ -551,7 +551,7 @@ Use **Detect Models** to auto-fill **Available models** while adding or editing - selecting **Detect Models** to query the provider's `/models` endpoint and prepend detected model IDs to the field. 9. Select **Save Provider**. -Expected outcome: the provider appears in the Custom Providers list with its API type and base URL. Each saved model is then available in model dropdowns as a `{provider}/{modelId}` option, including **Settings → Project Models** default-workflow lanes and workflow model lanes in the workflow editor. +Expected outcome: the provider appears in the Custom Providers list with its API type and base URL. Each saved model is then available in model dropdowns as a `{provider}/{modelId}` option, including **Settings → Models · Project → Model Overrides → Workflow lanes** and workflow model lanes in the workflow editor. ### Edit a custom provider diff --git a/docs/settings-reference.md b/docs/settings-reference.md index 76a4c8c7c5..89938aaab4 100644 --- a/docs/settings-reference.md +++ b/docs/settings-reference.md @@ -298,9 +298,10 @@ Some knobs that used to live in this Settings reference as project settings are govern that execution belong to the workflow. **Where to set them.** The common model lanes for every workflow in a project are -available directly in **Settings → Project Models → Project workflow model lanes**: -Plan/Triage, Executor, Reviewer, and their fallback lanes declared -by the default workflow. Primary Plan/Triage, Executor, Reviewer, and declared fallback rows show an inline Thinking Level control when the workflow declares the companion `*ThinkingLevel` setting; unset means inherit. Those dropdown controls use the shared model picker and are auto-saved by the Settings modal after an edit, which writes +available directly in **Settings → Models · Project → Model Overrides → Workflow lanes**, +immediately after **Project lanes**: Plan/Triage, Executor, Reviewer, and their fallback lanes declared +by the default workflow. The project Summarization lane and its fallback remain under +**AI Title and Git Commit Message Summarization** with the enable toggles that govern them. Primary Plan/Triage, Executor, Reviewer, and declared fallback rows show an inline Thinking Level control when the workflow declares the companion `*ThinkingLevel` setting; unset means inherit. Those dropdown controls use the shared model picker and are auto-saved by the Settings modal after an edit, which writes workflow setting values on the active project's default workflow. Those stored values are the project model baseline inherited by every selected workflow. The baseline wins over global and per-workflow values; task-specific selections win @@ -1135,7 +1136,7 @@ Short-lived token bounds are enforced server-side: ## Model Selection Hierarchy -Fusion resolves task models as task-specific selection -> project workflow-model baseline -> global lane -> selected-workflow value -> project/global default model. The project baseline is stored as setting values on the project's active default workflow and can be edited from Settings -> Project Models -> Project workflow model lanes (auto-saved by the Settings modal). Lower-priority per-workflow values remain editable from Workflow editor -> Settings -> Values for declared workflow lanes and fallbacks. General-scope fallback selection remains the global Fallback Model picker in Settings -> General Models. +Fusion resolves task models as task-specific selection -> project workflow-model baseline -> global lane -> selected-workflow value -> project/global default model. The project baseline is stored as setting values on the project's active default workflow and can be edited from Settings -> Models · Project -> Model Overrides -> Workflow lanes (auto-saved by the Settings modal, directly after Project lanes). The project Summarization lane and its fallback intentionally remain under AI Title and Git Commit Message Summarization with their enable toggles. Lower-priority per-workflow values remain editable from Workflow editor -> Settings -> Values for declared workflow lanes and fallbacks. General-scope fallback selection remains the global Fallback Model picker in Settings -> General Models. Direct-chat defaults are project-scoped and independent of task workflow lanes. Configure them in **Settings -> Project Models -> Chat**. `chatDefaultKind: "agent"` resolves only when `chatDefaultAgentId` is set; `chatDefaultKind: "model"` resolves only when both `chatDefaultModelProvider` and `chatDefaultModelId` are set, with optional `chatDefaultThinkingLevel`. If `chatNewSessionMode` is `"always-default"` and that target resolves, every New Chat entry point creates the session directly. If the target is incomplete, or the mode is unset/`"prompt"`, Fusion opens the New Chat dialog instead and preselects the resolved default when one exists. Chat Rooms additionally support a per-room `thinkingLevel` default that applies to every room responder; clearing it inherits the resolved project/global default. diff --git a/packages/dashboard/app/__tests__/settings-sections.test.tsx b/packages/dashboard/app/__tests__/settings-sections.test.tsx index 58d192941f..5ba68ac33b 100644 --- a/packages/dashboard/app/__tests__/settings-sections.test.tsx +++ b/packages/dashboard/app/__tests__/settings-sections.test.tsx @@ -833,6 +833,16 @@ describe("ProjectModelsSection", () => { "validator", "validator-fallback", ]); + + const projectLanes = screen.getByTestId("project-models-project-lanes"); + const workflowLanes = screen.getByTestId("project-models-workflow-lanes"); + const chat = screen.getByTestId("project-models-chat-kind"); + const summarizationSection = screen.getByTestId("project-models-ai-summarization"); + expect(projectLanes.nextElementSibling).toBe(workflowLanes); + expect(projectLanes).toContainElement(screen.getByTestId("project-models-summarization-pointer")); + expect(workflowLanes).toContainElement(screen.getByTestId("workflow-model-lane-planning")); + expect(workflowLanes.compareDocumentPosition(chat) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); + expect(workflowLanes.compareDocumentPosition(summarizationSection) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); }); it("wires workflow fallback lane thinking render, persist, and reset", async () => { diff --git a/packages/dashboard/app/components/__tests__/SettingsModal.models-auth.test.tsx b/packages/dashboard/app/components/__tests__/SettingsModal.models-auth.test.tsx index c0fdb09742..d6cb4fa5b2 100644 --- a/packages/dashboard/app/components/__tests__/SettingsModal.models-auth.test.tsx +++ b/packages/dashboard/app/components/__tests__/SettingsModal.models-auth.test.tsx @@ -551,7 +551,7 @@ describe("SettingsModal", () => { const onOpenWorkflowSettings = vi.fn(); await setupWorkflowModelLaneTest({ renderProps: { onOpenWorkflowSettings } }); - const workflowHeading = screen.getByRole("heading", { name: "Project workflow model lanes" }); + const workflowHeading = screen.getByRole("heading", { name: "Workflow lanes" }); const advancedButton = screen.getByRole("button", { name: "Advanced workflow policy" }); const actionRow = advancedButton.closest(".settings-model-lane-actions"); const presetsHeading = screen.getByRole("heading", { name: "Model Presets" }); diff --git a/packages/dashboard/app/components/settings/sections/GlobalModelsSection.tsx b/packages/dashboard/app/components/settings/sections/GlobalModelsSection.tsx index ba9043bdd3..532b12d127 100644 --- a/packages/dashboard/app/components/settings/sections/GlobalModelsSection.tsx +++ b/packages/dashboard/app/components/settings/sections/GlobalModelsSection.tsx @@ -51,8 +51,14 @@ export function GlobalModelsSection({ form, setForm, availableModels, modelsLoad : ""; return (<> - {/* --- Default Model --- */} -

{t("settings.globalModels.defaultModel", "Default Model")}

+ {/* + FNXC:SettingsModels 2026-08-18-06:41: + Global and project model-override surfaces share one heading vocabulary so operators recognize the same configuration structure in both scopes. Keep the default, fallback, thinking, and role lanes contiguous; sync and pricing controls remain separate. + */} +
+

{t("settings.globalModels.modelOverrides", "Model Overrides")}

+ {t("settings.globalModels.globalBaselineModelsForEachAIRoleProject", " Global baseline models for each AI role. Project settings can override these per-project. ")} +
{modelsLoading ? (
) : availableModels.length === 0 ? (
{t("settings.models.noModels", "No models available. Configure authentication first.")}
) : (<> @@ -138,8 +144,6 @@ export function GlobalModelsSection({ form, setForm, availableModels, modelsLoad })()} {availableModels.length > 0 && (<> -

{t("settings.globalModels.modelLanes", "Model Lanes")}

-

{t("settings.globalModels.globalBaselineModelsForEachAIRoleProject", " Global baseline models for each AI role. Project settings can override these per-project. ")}

{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; @@ -177,8 +181,6 @@ export function GlobalModelsSection({ form, setForm, availableModels, modelsLoad })} )} - - {/* --- Startup Model Sync --- */}

{t("settings.globalModels.startupModelSync", "Startup Model Sync")}

{/* @@ -417,6 +419,8 @@ export function GlobalModelsSection({ form, setForm, availableModels, modelsLoad }))} /> + + ); } export default GlobalModelsSection; diff --git a/packages/dashboard/app/components/settings/sections/ProjectModelsSection.tsx b/packages/dashboard/app/components/settings/sections/ProjectModelsSection.tsx index 68085ac14b..7cd1a0585a 100644 --- a/packages/dashboard/app/components/settings/sections/ProjectModelsSection.tsx +++ b/packages/dashboard/app/components/settings/sections/ProjectModelsSection.tsx @@ -337,7 +337,7 @@ export function ProjectModelsSection({ form, setForm, models, projectId, onOpenW // workflow settings below. // FNXC:Settings-MergerModel 2026-07-13-07:52: Merger is project-scoped (like summarization), not workflow-moved. // FNXC:GitHubImportTranslate 2026-07-15-09:30: The import-translate lane is project-scoped (like merger/summarization), so its project override must be editable here — otherwise the lane's projectProviderKey/projectModelKey would be unreachable and only the global lane could ever be set. - // FNXC:SettingsModels 2026-07-15-12:00: Summarization is still project-scoped but is rendered with the AI summarization section below rather than the general Model Lanes list. + // FNXC:SettingsModels 2026-08-18-06:41: Summarization remains project-scoped but stays with its AI summarization enable toggles instead of the Model Overrides group. const projectModelLanes = modelLanes.filter((lane) => ["default", "merger", "import-translate"].includes(lane.laneId)); const credentialInstanceKeyForLane = (lane: ModelLane): keyof Settings | undefined => PROJECT_LANE_CREDENTIAL_INSTANCE_KEYS[lane.laneId as keyof typeof PROJECT_LANE_CREDENTIAL_INSTANCE_KEYS]; const credentialInstanceValueForLane = (lane: ModelLane): string => { @@ -383,7 +383,7 @@ export function ProjectModelsSection({ form, setForm, models, projectId, onOpenW /* * FNXC:SettingsModels 2026-07-16-00:00: * The merger fallback is project-scoped and must sit directly after Project Merger - * in Model Lanes so operators configure the primary and retry models together. + * in Project lanes so operators configure the primary and retry models together. */ const mergerFallbackValue = form.mergerFallbackProvider && form.mergerFallbackModelId ? `${form.mergerFallbackProvider}/${form.mergerFallbackModelId}` @@ -527,27 +527,17 @@ export function ProjectModelsSection({ form, setForm, models, projectId, onOpenW }; return (<> - {/* --- Token Cap --- */} -

{t("settings.projectModels.tokenCap", "Token Cap")}

{/* - FNXC:SettingsModels 2026-07-15-17:35: - The reset affordance stays conditional on an actual cap being set: "no cap" is the unset state, so offering to reset a lane that is already unset would advertise an action with nothing to undo. - `v ? Math.trunc(v) : null` reproduces the previous `val ? parseInt(val, 10) : null` contract exactly \u2014 a token cap is a whole number of tokens, and 0 means "no cap" (null), not a cap of zero. + FNXC:SettingsModels 2026-08-18-06:41: + Operators configure which model does what in one pass, so project lanes and workflow lanes must remain one contiguous Model Overrides group. Chat, presets, and token-cap controls must not split the groups; summarization stays with its enable toggles below, with this pointer instead of moving the gated controls. */} - setForm((f) => ({ ...f, tokenCap: v ? Math.trunc(v) : null } as SettingsFormState))} - clearable={form.tokenCap != null} - /> - - {/* --- Project Model Lanes --- */} +
+

{t("settings.projectModels.modelOverrides", "Model Overrides")}

+ {t("settings.projectModels.overrideGlobalModelSettingsAtTheProjectLevel", " Override global model settings at the project level. Each lane controls a specific AI usage context. Unset lanes inherit from the corresponding global lane. The Project Default Model is the fallback for this project when a more specific lane is unset. ")} +
+
+
{t("settings.projectModels.projectLanesSubheading", "Project lanes")}
+ {/* --- Project lanes --- */} {/* FNXC:ExecutorEscalation 2026-08-03-05:43: The alternate executor target is a project model choice, while Scheduling owns only the retry policy and optional node routing. Use the shared provider-aware dropdown so complete persisted pairs hydrate together, selecting a model updates both existing keys, and the default choice clears both without accepting arbitrary text. @@ -586,17 +576,69 @@ export function ProjectModelsSection({ form, setForm, models, projectId, onOpenW menuWidth="readable" /> - {/* FNXC:SettingsHelp 2026-07-16-12:45: Section description moved behind the shared "?" affordance beside the heading — operator requirement: no inline description paragraphs in Settings. */} -
-

{t("settings.projectModels.modelLanes", "Model Lanes")}

- {t("settings.projectModels.overrideGlobalModelSettingsAtTheProjectLevel", " Override global model settings at the project level. Each lane controls a specific AI usage context. Unset lanes inherit from the corresponding global lane. The Project Default Model is the fallback for this project when a more specific lane is unset. ")} -
{modelsLoading ? (
) : availableModels.length === 0 ? (
{t("settings.projectModels.noModelsAvailableConfigureAuthenticationFirst", " No models available. Configure authentication first. ")}
) : (<> {projectModelLanes.filter((lane) => lane.laneId === "default" || lane.laneId === "merger").map(renderProjectLane)} {renderMergerFallbackLane()} {projectModelLanes.filter((lane) => lane.laneId === "import-translate").map(renderProjectLane)} )} +

{t("settings.projectModels.summarizationPointer", "Summarization models are configured under AI Title and Git Commit Message Summarization below.")}

+
+
+ {/* --- Workflow lanes --- */} + {/* FNXC:ProjectWorkflowModelBaseline 2026-07-22-00:00: These controls persist on the active default workflow as the cross-workflow project baseline. Runtime precedence is task-specific selection > project baseline > global lane > selected-workflow lane; keep this explanation in the shared heading help tip rather than restoring per-control prose. */} +
+
{t("settings.projectModels.workflowLanesSubheading", "Workflow lanes")}
+ + {t("settings.movedStub.modelLanes", "Per-phase model lanes (execution, planning, reviewer, and their fallbacks) are stored on the active default workflow.")}{t("settings.projectModels.theseProjectOverridesApplyToTheActiveDefault", " They apply as the project baseline for every workflow and take precedence over global and per-workflow values; task-specific selections still win. ")} +
+ {!projectId ? (
{t("settings.projectModels.openAProjectToEditWorkflowModelLanes", "Open a project to edit workflow model lanes.")}
) : workflowLoading ? (
) : availableModels.length === 0 ? (
{t("settings.projectModels.noModelsAvailableConfigureAuthenticationBeforeSelectingWorkflow", " No models available. Configure authentication before selecting workflow model lanes. ")}
) : (<> + {workflowModelPairs.map((pair) => { + const value = modelPairValue(effectiveWorkflowValues, pair); + const rawThinkingValue = pair.thinkingId ? effectiveWorkflowValues[pair.thinkingId] : undefined; + const thinkingValue: string = typeof rawThinkingValue === "string" ? rawThinkingValue : ""; + const rawCredentialInstanceId = pair.credentialInstanceId ? effectiveWorkflowValues[pair.credentialInstanceId] : undefined; + const credentialInstanceId = typeof rawCredentialInstanceId === "string" ? rawCredentialInstanceId : ""; + const modelCustomized = Object.prototype.hasOwnProperty.call(workflowPending, pair.providerId) + ? workflowPending[pair.providerId] !== null + : Boolean(workflowPayload?.stored && (Object.prototype.hasOwnProperty.call(workflowPayload.stored, pair.providerId) + || Object.prototype.hasOwnProperty.call(workflowPayload.stored, pair.modelId))); + const thinkingCustomized = pair.thinkingId + ? (Object.prototype.hasOwnProperty.call(workflowPending, pair.thinkingId) + ? workflowPending[pair.thinkingId] !== null + : Boolean(workflowPayload?.stored && Object.prototype.hasOwnProperty.call(workflowPayload.stored, pair.thinkingId))) + : false; + const credentialInstanceCustomized = pair.credentialInstanceId + ? (Object.prototype.hasOwnProperty.call(workflowPending, pair.credentialInstanceId) + ? workflowPending[pair.credentialInstanceId] !== null + : Boolean(workflowPayload?.stored && Object.prototype.hasOwnProperty.call(workflowPayload.stored, pair.credentialInstanceId))) + : false; + const customized = modelCustomized || thinkingCustomized || credentialInstanceCustomized; + const error = workflowRejections[pair.providerId]?.message ?? workflowRejections[pair.modelId]?.message ?? (pair.thinkingId ? workflowRejections[pair.thinkingId]?.message : undefined) ?? (pair.credentialInstanceId ? workflowRejections[pair.credentialInstanceId]?.message : undefined); + return (
+
+ + + {customized ? "Project baseline" : "Inherited (Workflow)"} + + {/* FNXC:SettingsHelp 2026-07-15-23:10: Same affordance as the project lanes above — a workflow lane is the same shape, so its help hangs off the label row too rather than sitting under the dropdown as prose. */} + {pair.help ? {pair.help} : null} +
+
+
+ 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" credentialInstanceId={credentialInstanceId} onCredentialInstanceChange={pair.credentialInstanceId ? (instanceId) => setWorkflowCredentialInstanceValue(pair, instanceId) : undefined} showThinkingLevel={Boolean(pair.thinkingId)} thinkingLevel={thinkingValue} onThinkingLevelChange={pair.thinkingId ? (level) => setWorkflowThinkingValue(pair, level) : undefined} defaultThinkingLevel={typeof form.defaultThinkingLevel === "string" ? form.defaultThinkingLevel : "off"}/> +
+ {customized && ()} +
+ {error ? {error} : null} +
); + })} + {onOpenWorkflowSettings ? (
+ +
) : null} + )} + +
{/* FNXC:ChatModels 2026-07-12-20:45: Project Models owns the Direct-chat default because New Chat needs a project-scoped model-or-agent target plus prompt-vs-direct creation mode without changing workflow or in-chat switcher settings. */} {/* FNXC:SettingsHelp 2026-07-16-12:45: Section description moved behind the shared "?" affordance beside the heading — operator requirement: no inline description paragraphs in Settings. */}
@@ -666,60 +708,6 @@ export function ProjectModelsSection({ form, setForm, models, projectId, onOpenW {agents.length === 0 && !agentsLoading ? ({t("settings.projectModels.chatDefaultAgentEmpty", "No agents are available for this project yet.")}) : null}
)} - {/* --- Project workflow model lanes --- */} - {/* FNXC:SettingsHelp 2026-07-16-12:45: Section description moved behind the shared "?" affordance beside the heading — operator requirement: no inline description paragraphs in Settings. */} - {/* FNXC:ProjectWorkflowModelBaseline 2026-07-22-00:00: These controls persist on the active default workflow as the cross-workflow project baseline. Runtime precedence is task-specific selection > project baseline > global lane > selected-workflow lane; keep this explanation in the shared heading help tip rather than restoring per-control prose. */} -
-

{t("settings.projectModels.defaultWorkflowModelLanes", "Project workflow model lanes")}

- - {t("settings.movedStub.modelLanes", "Per-phase model lanes (execution, planning, reviewer, and their fallbacks) are stored on the active default workflow.")}{t("settings.projectModels.theseProjectOverridesApplyToTheActiveDefault", " They apply as the project baseline for every workflow and take precedence over global and per-workflow values; task-specific selections still win. ")} -
- {!projectId ? (
{t("settings.projectModels.openAProjectToEditWorkflowModelLanes", "Open a project to edit workflow model lanes.")}
) : workflowLoading ? (
) : availableModels.length === 0 ? (
{t("settings.projectModels.noModelsAvailableConfigureAuthenticationBeforeSelectingWorkflow", " No models available. Configure authentication before selecting workflow model lanes. ")}
) : (<> - {workflowModelPairs.map((pair) => { - const value = modelPairValue(effectiveWorkflowValues, pair); - const rawThinkingValue = pair.thinkingId ? effectiveWorkflowValues[pair.thinkingId] : undefined; - const thinkingValue: string = typeof rawThinkingValue === "string" ? rawThinkingValue : ""; - const rawCredentialInstanceId = pair.credentialInstanceId ? effectiveWorkflowValues[pair.credentialInstanceId] : undefined; - const credentialInstanceId = typeof rawCredentialInstanceId === "string" ? rawCredentialInstanceId : ""; - const modelCustomized = Object.prototype.hasOwnProperty.call(workflowPending, pair.providerId) - ? workflowPending[pair.providerId] !== null - : Boolean(workflowPayload?.stored && (Object.prototype.hasOwnProperty.call(workflowPayload.stored, pair.providerId) - || Object.prototype.hasOwnProperty.call(workflowPayload.stored, pair.modelId))); - const thinkingCustomized = pair.thinkingId - ? (Object.prototype.hasOwnProperty.call(workflowPending, pair.thinkingId) - ? workflowPending[pair.thinkingId] !== null - : Boolean(workflowPayload?.stored && Object.prototype.hasOwnProperty.call(workflowPayload.stored, pair.thinkingId))) - : false; - const credentialInstanceCustomized = pair.credentialInstanceId - ? (Object.prototype.hasOwnProperty.call(workflowPending, pair.credentialInstanceId) - ? workflowPending[pair.credentialInstanceId] !== null - : Boolean(workflowPayload?.stored && Object.prototype.hasOwnProperty.call(workflowPayload.stored, pair.credentialInstanceId))) - : false; - const customized = modelCustomized || thinkingCustomized || credentialInstanceCustomized; - const error = workflowRejections[pair.providerId]?.message ?? workflowRejections[pair.modelId]?.message ?? (pair.thinkingId ? workflowRejections[pair.thinkingId]?.message : undefined) ?? (pair.credentialInstanceId ? workflowRejections[pair.credentialInstanceId]?.message : undefined); - return (
-
- - - {customized ? "Project baseline" : "Inherited (Workflow)"} - - {/* FNXC:SettingsHelp 2026-07-15-23:10: Same affordance as the project lanes above — a workflow lane is the same shape, so its help hangs off the label row too rather than sitting under the dropdown as prose. */} - {pair.help ? {pair.help} : null} -
-
-
- 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" credentialInstanceId={credentialInstanceId} onCredentialInstanceChange={pair.credentialInstanceId ? (instanceId) => setWorkflowCredentialInstanceValue(pair, instanceId) : undefined} showThinkingLevel={Boolean(pair.thinkingId)} thinkingLevel={thinkingValue} onThinkingLevelChange={pair.thinkingId ? (level) => setWorkflowThinkingValue(pair, level) : undefined} defaultThinkingLevel={typeof form.defaultThinkingLevel === "string" ? form.defaultThinkingLevel : "off"}/> -
- {customized && ()} -
- {error ? {error} : null} -
); - })} - {onOpenWorkflowSettings ? (
- -
) : null} - )} - {/* --- Model Presets --- */}

{t("settings.projectModels.modelPresets", "Model Presets")}

@@ -959,6 +947,27 @@ export function ProjectModelsSection({ form, setForm, models, projectId, onOpenW onChange={(v) => setForm((f) => ({ ...f, prDescriptionPromptInstructions: v ?? "" }))} /> - ); + ) + {/* --- Token Cap --- */} +

{t("settings.projectModels.tokenCap", "Token Cap")}

+ {/* + FNXC:SettingsModels 2026-07-15-17:35: + The reset affordance stays conditional on an actual cap being set: "no cap" is the unset state, so offering to reset a lane that is already unset would advertise an action with nothing to undo. + `v ? Math.trunc(v) : null` reproduces the previous `val ? parseInt(val, 10) : null` contract exactly \u2014 a token cap is a whole number of tokens, and 0 means "no cap" (null), not a cap of zero. + */} + setForm((f) => ({ ...f, tokenCap: v ? Math.trunc(v) : null } as SettingsFormState))} + clearable={form.tokenCap != null} + /> + +; } export default ProjectModelsSection; diff --git a/packages/dashboard/src/shared/settings-sections.ts b/packages/dashboard/src/shared/settings-sections.ts index b4ccdec667..7473754e7a 100644 --- a/packages/dashboard/src/shared/settings-sections.ts +++ b/packages/dashboard/src/shared/settings-sections.ts @@ -83,7 +83,7 @@ const SETTINGS_SECTION_DEFINITIONS: readonly SettingsSectionDefinition[] = [ FN-8130 changes the Settings landing surface from Authentication to Appearance. Authentication remains first within its own AI & Models group, but the always-visible global Preferences section is the default instead. */ { id: "authentication", label: "Authentication", labelKey: "settings.nav.authentication", scope: undefined, searchableText: ["login", "OAuth", "API key", "custom providers", "Anthropic", "OpenAI", "provider credentials"] }, - { id: "global-models", label: "Models · Global", labelKey: "settings.nav.globalModels", scope: "global", searchableText: ["global models", "model presets", "favorite providers", "model pricing overrides", "LiteLLM pricing", "token pricing", "translate", "translation model", "import translation model", "import auto-translation model"] }, + { id: "global-models", label: "Models · Global", labelKey: "settings.nav.globalModels", scope: "global", searchableText: ["global models", "model overrides", "model lanes", "planning model", "reviewer model", "model presets", "favorite providers", "model pricing overrides", "LiteLLM pricing", "token pricing", "translate", "translation model", "import translation model", "import auto-translation model"] }, /** * FNXC:SettingsNavigation 2026-07-13-00:00: * Project Models owns the FN-7907 Direct-chat default settings. Its shared Settings search index must advertise chat-default terms and i18n labels so desktop nav, the mobile section picker, and filtered search all surface this section when operators search for Chat defaults. @@ -102,7 +102,11 @@ const SETTINGS_SECTION_DEFINITIONS: readonly SettingsSectionDefinition[] = [ searchableText: [ "default provider", "default model", + "model overrides", + "model lanes", "workflow model lanes", + "planning model", + "reviewer model", "executor escalation model", "escalation model", "alternate model", diff --git a/packages/i18n/locales/en/app.json b/packages/i18n/locales/en/app.json index 96a03bad00..b721699fba 100644 --- a/packages/i18n/locales/en/app.json +++ b/packages/i18n/locales/en/app.json @@ -6167,6 +6167,7 @@ "leaveEmptyToOmitThisHeaderDefaultFusion": "Leave empty to omit this header. No default — unset (Fusion falls back to the title \"Fusion\" when unset).", "leaveEmptyToOmitThisHeaderDefaultHttps": "Leave empty to omit this header. No default — unset (Fusion falls back to https://runfusion.ai when unset).", "modelLanes": "Model Lanes", + "modelOverrides": "Model Overrides", "noFallback": "No fallback", "openaiAnthropic": "openai, anthropic", "openRouterAdvanced": "OpenRouter advanced", @@ -6598,6 +6599,10 @@ "loadingAvailableModels": "Loading available models…", "loadingWorkflowModelLanes": "Loading workflow model lanes…", "modelLanes": "Model Lanes", + "modelOverrides": "Model Overrides", + "projectLanesSubheading": "Project lanes", + "workflowLanesSubheading": "Workflow lanes", + "summarizationPointer": "Summarization models are configured under AI Title and Git Commit Message Summarization below.", "modelPresets": "Model Presets", "name": "Name", "noCap": "No cap",