FN-7983: colocate project summarization model with summarization settings
Move the Project Summarization model lane and title-summarizer fallback next to the AI title/commit summarization controls in Project Models. - Extract shared project-lane renderer and keep default/merger/import-translate in the general Model Lanes list - Render summarization + title-summarizer fallback inside the AI summarization section with the same models-available guard - Add regression tests for colocation and empty-models guard - Update settings reference docs and add a patch changeset Files changed: .changeset/fn-7983-summarization-lane-colocation.md | 7 ++ docs/settings-reference.md | 6 +- .../app/__tests__/settings-sections.test.tsx | 61 ++++++++++ .../settings/sections/ProjectModelsSection.tsx | 126 ++++++++++++--------- 4 files changed, 142 insertions(+), 58 deletions(-) Fusion-Task-Id: FN-7983 Fusion-Task-Lineage: c473ba61-f003-401d-bc66-86f2078ba047 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7983-summarization-lane-colocation.md
Normal file
7
.changeset/fn-7983-summarization-lane-colocation.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Move project summarization model controls next to summarization settings.
|
||||||
|
category: fix
|
||||||
|
dev: Dashboard Project Models layout only; no settings key or persistence changes.
|
||||||
@@ -271,8 +271,10 @@ workflow setting values for the active project's default workflow; they do not
|
|||||||
restore the old project settings keys. The global **Fallback Model** remains in
|
restore the old project settings keys. The global **Fallback Model** remains in
|
||||||
Settings → General Models and includes its own inline Thinking Level selector for `fallbackThinkingLevel`; workflow-specific fallbacks are also editable from
|
Settings → General Models and includes its own inline Thinking Level selector for `fallbackThinkingLevel`; workflow-specific fallbacks are also editable from
|
||||||
the workflow editor Values tab. Title summarization is separate: set it in
|
the workflow editor Values tab. Title summarization is separate: set it in
|
||||||
**Settings → Project Models → Title and Git Commit Message Summarization Model**,
|
**Settings → Project Models → AI Title and Git Commit Message Summarization**,
|
||||||
with its project fallback selector and global baseline in Settings → General/Global Models.
|
where the title-summarization lane and its project fallback selector are colocated
|
||||||
|
with the title and merge-commit summarization controls; the global baseline remains in
|
||||||
|
Settings → General/Global Models.
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
FNXC:WorkflowSettings 2026-06-17-09:13:
|
FNXC:WorkflowSettings 2026-06-17-09:13:
|
||||||
|
|||||||
@@ -441,6 +441,66 @@ describe("ProjectModelsSection", () => {
|
|||||||
expect(screen.getByTestId("mock-model-dropdown-preset-validator-model")).toHaveAttribute("data-menu-width", "readable");
|
expect(screen.getByTestId("mock-model-dropdown-preset-validator-model")).toHaveAttribute("data-menu-width", "readable");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("colocates summarization model controls with AI summarization settings", () => {
|
||||||
|
render(
|
||||||
|
<ProjectModelsSection
|
||||||
|
scopeBanner={null}
|
||||||
|
form={{} as SettingsFormState}
|
||||||
|
setForm={vi.fn()}
|
||||||
|
models={{
|
||||||
|
...models,
|
||||||
|
modelLanes: [
|
||||||
|
{ laneId: "default", label: "Default", helperText: "Default", fallbackOrder: "global" },
|
||||||
|
{ laneId: "merger", label: "Merger", helperText: "Merger", 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" }],
|
||||||
|
}}
|
||||||
|
addToast={vi.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const summarizationSection = screen.getByTestId("project-models-ai-summarization");
|
||||||
|
const heading = screen.getByRole("heading", { name: "AI Title and Git Commit Message Summarization" });
|
||||||
|
const autoSummarizeTitles = screen.getByRole("checkbox", { name: "Auto-summarize long descriptions as titles" });
|
||||||
|
const summarizationDropdown = screen.getByTestId("mock-model-dropdown-summarizationModel");
|
||||||
|
const fallbackDropdown = screen.getByTestId("mock-model-dropdown-titleSummarizerFallbackModel");
|
||||||
|
const defaultDropdown = screen.getByTestId("mock-model-dropdown-defaultModel");
|
||||||
|
const mergerDropdown = screen.getByTestId("mock-model-dropdown-mergerModel");
|
||||||
|
|
||||||
|
expect(summarizationSection).toContainElement(heading);
|
||||||
|
expect(summarizationSection).toContainElement(autoSummarizeTitles);
|
||||||
|
expect(summarizationSection).toContainElement(summarizationDropdown);
|
||||||
|
expect(summarizationSection).toContainElement(fallbackDropdown);
|
||||||
|
expect(defaultDropdown.compareDocumentPosition(heading) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||||
|
expect(mergerDropdown.compareDocumentPosition(heading) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||||
|
expect(heading.compareDocumentPosition(summarizationDropdown) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||||
|
expect(heading.compareDocumentPosition(fallbackDropdown) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps summarization controls behind the available-models guard", () => {
|
||||||
|
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,
|
||||||
|
}}
|
||||||
|
addToast={vi.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const summarizationSection = screen.getByTestId("project-models-ai-summarization");
|
||||||
|
expect(within(summarizationSection).getByText("No models available. Configure authentication first.")).toBeInTheDocument();
|
||||||
|
expect(within(summarizationSection).queryByTestId("mock-model-dropdown-summarizationModel")).not.toBeInTheDocument();
|
||||||
|
expect(within(summarizationSection).queryByTestId("mock-model-dropdown-titleSummarizerFallbackModel")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it("wires project model lane thinking overrides and reset", () => {
|
it("wires project model lane thinking overrides and reset", () => {
|
||||||
const updateLaneThinkingValue = vi.fn();
|
const updateLaneThinkingValue = vi.fn();
|
||||||
const resetLaneThinkingValue = vi.fn();
|
const resetLaneThinkingValue = vi.fn();
|
||||||
@@ -502,6 +562,7 @@ describe("ProjectModelsSection", () => {
|
|||||||
render(<ProjectTitleFallbackHost />);
|
render(<ProjectTitleFallbackHost />);
|
||||||
|
|
||||||
const dropdown = screen.getByTestId("mock-model-dropdown-titleSummarizerFallbackModel");
|
const dropdown = screen.getByTestId("mock-model-dropdown-titleSummarizerFallbackModel");
|
||||||
|
expect(screen.getByTestId("project-models-ai-summarization")).toContainElement(dropdown);
|
||||||
expect(dropdown).toHaveAttribute("data-thinking-visible", "true");
|
expect(dropdown).toHaveAttribute("data-thinking-visible", "true");
|
||||||
expect(dropdown).toHaveAttribute("data-thinking-value", "high");
|
expect(dropdown).toHaveAttribute("data-thinking-value", "high");
|
||||||
expect(dropdown).toHaveAttribute("data-default-thinking", "medium");
|
expect(dropdown).toHaveAttribute("data-default-thinking", "medium");
|
||||||
|
|||||||
@@ -276,7 +276,9 @@ export function ProjectModelsSection({ scopeBanner, form, setForm, models, proje
|
|||||||
// workflow settings below.
|
// workflow settings below.
|
||||||
// FNXC:Settings-MergerModel 2026-07-13-07:52: Merger is project-scoped (like summarization), not workflow-moved.
|
// 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: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.
|
||||||
const projectModelLanes = modelLanes.filter((lane) => ["default", "merger", "summarization", "import-translate"].includes(lane.laneId));
|
// 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.
|
||||||
|
const projectModelLanes = modelLanes.filter((lane) => ["default", "merger", "import-translate"].includes(lane.laneId));
|
||||||
|
const summarizationLane = modelLanes.find((lane) => lane.laneId === "summarization");
|
||||||
const getProjectLaneLabel = (lane: ModelLane) => {
|
const getProjectLaneLabel = (lane: ModelLane) => {
|
||||||
if (lane.laneId === "default") {
|
if (lane.laneId === "default") {
|
||||||
return "Project Default Model";
|
return "Project Default Model";
|
||||||
@@ -332,6 +334,36 @@ export function ProjectModelsSection({ scopeBanner, form, setForm, models, proje
|
|||||||
const resetTitleSummarizerFallbackValue = () => {
|
const resetTitleSummarizerFallbackValue = () => {
|
||||||
setForm((f) => ({ ...f, titleSummarizerFallbackProvider: undefined, titleSummarizerFallbackModelId: undefined, titleSummarizerFallbackThinkingLevel: undefined } as SettingsFormState));
|
setForm((f) => ({ ...f, titleSummarizerFallbackProvider: undefined, titleSummarizerFallbackModelId: undefined, titleSummarizerFallbackThinkingLevel: undefined } as SettingsFormState));
|
||||||
};
|
};
|
||||||
|
/*
|
||||||
|
* FNXC:SettingsModels 2026-07-15-12:00:
|
||||||
|
* Project Summarization and its title-summarizer fallback must be colocated
|
||||||
|
* with AI title and commit summarization settings, while keeping their shared
|
||||||
|
* lane UI, override behavior, and responsive layout identical to other project lanes.
|
||||||
|
*/
|
||||||
|
const renderProjectLane = (lane: ModelLane) => {
|
||||||
|
const status = getLaneStatus(lane);
|
||||||
|
const value = getLaneValue(lane);
|
||||||
|
const thinkingValue = getLaneThinkingValue(lane);
|
||||||
|
const isOverridden = status === "overridden" || Boolean(thinkingValue);
|
||||||
|
const laneLabel = getProjectLaneLabel(lane);
|
||||||
|
return (<div className="form-group" key={lane.laneId}>
|
||||||
|
<div className="settings-model-lane-label-row">
|
||||||
|
<label htmlFor={`${lane.laneId}Model`}>{laneLabel}</label>
|
||||||
|
<span className={`settings-lane-badge ${isOverridden ? "settings-lane-badge--override" : "settings-lane-badge--inherited"}`} title={isOverridden ? "Explicitly set for this project" : "Inherited from global settings"}>
|
||||||
|
{isOverridden ? "Override (Project)" : "Inherited (Global)"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="settings-model-lane-control-row">
|
||||||
|
<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} menuWidth="readable" showThinkingLevel={Boolean(lane.projectThinkingKey)} thinkingLevel={thinkingValue} onThinkingLevelChange={(level) => updateLaneThinkingValue(lane, level)} defaultThinkingLevel={form.defaultThinkingLevel}/>
|
||||||
|
</div>
|
||||||
|
{isOverridden && (<button type="button" className="btn btn-ghost btn-sm" title={t("settings.projectModels.resetToInheritFromGlobal", "Reset to inherit from global")} onClick={() => { resetLaneValue(lane); resetLaneThinkingValue(lane); }} style={{ whiteSpace: "nowrap" }}>{t("settings.projectModels.reset", " Reset ")}</button>)}
|
||||||
|
</div>
|
||||||
|
<small>
|
||||||
|
{getProjectLaneHelperText(lane)}{t("settings.projectModels.fallsBackTo", " Falls back to: ")}{lane.fallbackOrder}.
|
||||||
|
</small>
|
||||||
|
</div>);
|
||||||
|
};
|
||||||
const chatDefaultKind = form.chatDefaultKind ?? "model";
|
const chatDefaultKind = form.chatDefaultKind ?? "model";
|
||||||
const chatDefaultModelValue = form.chatDefaultModelProvider && form.chatDefaultModelId
|
const chatDefaultModelValue = form.chatDefaultModelProvider && form.chatDefaultModelId
|
||||||
? `${form.chatDefaultModelProvider}/${form.chatDefaultModelId}`
|
? `${form.chatDefaultModelProvider}/${form.chatDefaultModelId}`
|
||||||
@@ -383,46 +415,7 @@ export function ProjectModelsSection({ scopeBanner, form, setForm, models, proje
|
|||||||
<h4 className="settings-section-heading settings-section-heading--spaced">{t("settings.projectModels.modelLanes", "Model Lanes")}</h4>
|
<h4 className="settings-section-heading settings-section-heading--spaced">{t("settings.projectModels.modelLanes", "Model Lanes")}</h4>
|
||||||
<p className="settings-description">{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. ")}</p>
|
<p className="settings-description">{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. ")}</p>
|
||||||
{modelsLoading ? (<div className="settings-empty-state"><LoadingSpinner label={t("settings.projectModels.loadingAvailableModels", "Loading available models\u2026")} /></div>) : availableModels.length === 0 ? (<div className="settings-empty-state settings-muted">{t("settings.projectModels.noModelsAvailableConfigureAuthenticationFirst", " No models available. Configure authentication first. ")}</div>) : (<>
|
{modelsLoading ? (<div className="settings-empty-state"><LoadingSpinner label={t("settings.projectModels.loadingAvailableModels", "Loading available models\u2026")} /></div>) : availableModels.length === 0 ? (<div className="settings-empty-state settings-muted">{t("settings.projectModels.noModelsAvailableConfigureAuthenticationFirst", " No models available. Configure authentication first. ")}</div>) : (<>
|
||||||
{projectModelLanes.map((lane) => {
|
{projectModelLanes.map(renderProjectLane)}
|
||||||
const status = getLaneStatus(lane);
|
|
||||||
const value = getLaneValue(lane);
|
|
||||||
const thinkingValue = getLaneThinkingValue(lane);
|
|
||||||
const isOverridden = status === "overridden" || Boolean(thinkingValue);
|
|
||||||
const laneLabel = getProjectLaneLabel(lane);
|
|
||||||
return (<div className="form-group" key={lane.laneId}>
|
|
||||||
<div className="settings-model-lane-label-row">
|
|
||||||
<label htmlFor={`${lane.laneId}Model`}>{laneLabel}</label>
|
|
||||||
<span className={`settings-lane-badge ${isOverridden ? "settings-lane-badge--override" : "settings-lane-badge--inherited"}`} title={isOverridden ? "Explicitly set for this project" : "Inherited from global settings"}>
|
|
||||||
{isOverridden ? "Override (Project)" : "Inherited (Global)"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="settings-model-lane-control-row">
|
|
||||||
<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} menuWidth="readable" showThinkingLevel={Boolean(lane.projectThinkingKey)} thinkingLevel={thinkingValue} onThinkingLevelChange={(level) => updateLaneThinkingValue(lane, level)} defaultThinkingLevel={form.defaultThinkingLevel}/>
|
|
||||||
</div>
|
|
||||||
{isOverridden && (<button type="button" className="btn btn-ghost btn-sm" title={t("settings.projectModels.resetToInheritFromGlobal", "Reset to inherit from global")} onClick={() => { resetLaneValue(lane); resetLaneThinkingValue(lane); }} style={{ whiteSpace: "nowrap" }}>{t("settings.projectModels.reset", " Reset ")}</button>)}
|
|
||||||
</div>
|
|
||||||
<small>
|
|
||||||
{getProjectLaneHelperText(lane)}{t("settings.projectModels.fallsBackTo", " Falls back to: ")}{lane.fallbackOrder}.
|
|
||||||
</small>
|
|
||||||
</div>);
|
|
||||||
})}
|
|
||||||
{/* FNXC:Settings-ThinkingLevel 2026-07-10-12:08: Title-summarizer fallback provider/model/thinking settings are project-scoped, not workflow-declared. Render this fallback beside the project summarization lane so saves use project null-as-delete semantics instead of the workflow-values API. */}
|
|
||||||
<div className="form-group" data-testid="project-model-lane-title-summarizer-fallback">
|
|
||||||
<div className="settings-model-lane-label-row">
|
|
||||||
<label htmlFor="titleSummarizerFallbackModel">{t("settings.projectModels.titleSummarizerFallbackModel", "Title Summarizer Fallback Model")}</label>
|
|
||||||
<span className={`settings-lane-badge ${titleSummarizerFallbackCustomized ? "settings-lane-badge--override" : "settings-lane-badge--inherited"}`} title={titleSummarizerFallbackCustomized ? "Explicitly set for this project" : "Inherited from global settings"}>
|
|
||||||
{titleSummarizerFallbackCustomized ? "Override (Project)" : "Inherited (Global)"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="settings-model-lane-control-row">
|
|
||||||
<div className="settings-model-lane-control-main">
|
|
||||||
<CustomModelDropdown id="titleSummarizerFallbackModel" label="Title Summarizer Fallback Model" models={availableModels} value={titleSummarizerFallbackValue} onChange={setTitleSummarizerFallbackValue} placeholder={t("settings.projectModels.useGlobal", "Use global")} favoriteProviders={favoriteProviders} onToggleFavorite={onToggleFavorite} favoriteModels={favoriteModels} onToggleModelFavorite={onToggleModelFavorite} menuWidth="readable" showThinkingLevel={true} thinkingLevel={titleSummarizerFallbackThinkingValue} onThinkingLevelChange={setTitleSummarizerFallbackThinkingValue} defaultThinkingLevel={form.defaultThinkingLevel}/>
|
|
||||||
</div>
|
|
||||||
{titleSummarizerFallbackCustomized && (<button type="button" className="btn btn-ghost btn-sm" title={t("settings.projectModels.resetToInheritFromGlobal", "Reset to inherit from global")} onClick={resetTitleSummarizerFallbackValue} style={{ whiteSpace: "nowrap" }}>{t("settings.projectModels.reset", " Reset ")}</button>)}
|
|
||||||
</div>
|
|
||||||
<small>{t("settings.projectModels.titleSummarizerFallbackHelp", "Fallback provider and model used when the primary Title Summarizer model cannot be used. Falls back to the global summarization lane and then the default model chain.")}</small>
|
|
||||||
</div>
|
|
||||||
</>)}
|
</>)}
|
||||||
|
|
||||||
{/* 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: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. */}
|
||||||
@@ -640,23 +633,43 @@ export function ProjectModelsSection({ scopeBanner, form, setForm, models, proje
|
|||||||
</div>) : null}
|
</div>) : null}
|
||||||
|
|
||||||
{/* --- AI Title and Git Commit Message Summarization --- */}
|
{/* --- AI Title and Git Commit Message Summarization --- */}
|
||||||
<h4 className="settings-section-heading settings-section-heading--spaced">{t("settings.projectModels.aITitleAndGitCommitMessageSummarization", " AI Title and Git Commit Message Summarization ")}</h4>
|
<section data-testid="project-models-ai-summarization">
|
||||||
<p className="settings-description">{t("settings.projectModels.configuresTheModelUsedForTwoShortSummary", " Configures the model used for two short-summary jobs: auto-generating task titles from long descriptions, and generating merge commit summaries from step commits and diff stats. ")}</p>
|
<h4 className="settings-section-heading settings-section-heading--spaced">{t("settings.projectModels.aITitleAndGitCommitMessageSummarization", " AI Title and Git Commit Message Summarization ")}</h4>
|
||||||
<div className="form-group">
|
<p className="settings-description">{t("settings.projectModels.configuresTheModelUsedForTwoShortSummary", " Configures the model used for two short-summary jobs: auto-generating task titles from long descriptions, and generating merge commit summaries from step commits and diff stats. ")}</p>
|
||||||
<label htmlFor="autoSummarizeTitles" className="checkbox-label">
|
{modelsLoading ? (<div className="settings-empty-state"><LoadingSpinner label={t("settings.projectModels.loadingAvailableModels", "Loading available models…")} /></div>) : availableModels.length === 0 ? (<div className="settings-empty-state settings-muted">{t("settings.projectModels.noModelsAvailableConfigureAuthenticationFirst", " No models available. Configure authentication first. ")}</div>) : (<>
|
||||||
<input id="autoSummarizeTitles" type="checkbox" checked={form.autoSummarizeTitles || false} onChange={(e) => setForm((f) => ({ ...f, autoSummarizeTitles: e.target.checked }))}/>{t("settings.projectModels.autoSummarizeLongDescriptionsAsTitles", " Auto-summarize long descriptions as titles ")}</label>
|
{summarizationLane ? renderProjectLane(summarizationLane) : null}
|
||||||
<small>{t("settings.projectModels.whenEnabledTasksCreatedWithoutATitleBut", " When enabled, tasks created without a title but with descriptions over 200 characters will automatically get an AI-generated title (max 60 characters). The same model is also used to generate fallback merge commit message bodies when the branch's commit log is empty (e.g. squash merges with no unique commits), and GitHub tracking issue titles when a tracked task has no title yet. Default: disabled. ")}</small>
|
{/* FNXC:Settings-ThinkingLevel 2026-07-10-12:08: Title-summarizer fallback provider/model/thinking settings are project-scoped, not workflow-declared. Render it with the summarization controls so saves use project null-as-delete semantics instead of the workflow-values API. */}
|
||||||
</div>
|
<div className="form-group" data-testid="project-model-lane-title-summarizer-fallback">
|
||||||
|
<div className="settings-model-lane-label-row">
|
||||||
|
<label htmlFor="titleSummarizerFallbackModel">{t("settings.projectModels.titleSummarizerFallbackModel", "Title Summarizer Fallback Model")}</label>
|
||||||
|
<span className={`settings-lane-badge ${titleSummarizerFallbackCustomized ? "settings-lane-badge--override" : "settings-lane-badge--inherited"}`} title={titleSummarizerFallbackCustomized ? "Explicitly set for this project" : "Inherited from global settings"}>
|
||||||
|
{titleSummarizerFallbackCustomized ? "Override (Project)" : "Inherited (Global)"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="settings-model-lane-control-row">
|
||||||
|
<div className="settings-model-lane-control-main">
|
||||||
|
<CustomModelDropdown id="titleSummarizerFallbackModel" label="Title Summarizer Fallback Model" models={availableModels} value={titleSummarizerFallbackValue} onChange={setTitleSummarizerFallbackValue} placeholder={t("settings.projectModels.useGlobal", "Use global")} favoriteProviders={favoriteProviders} onToggleFavorite={onToggleFavorite} favoriteModels={favoriteModels} onToggleModelFavorite={onToggleModelFavorite} menuWidth="readable" showThinkingLevel={true} thinkingLevel={titleSummarizerFallbackThinkingValue} onThinkingLevelChange={setTitleSummarizerFallbackThinkingValue} defaultThinkingLevel={form.defaultThinkingLevel}/>
|
||||||
|
</div>
|
||||||
|
{titleSummarizerFallbackCustomized && (<button type="button" className="btn btn-ghost btn-sm" title={t("settings.projectModels.resetToInheritFromGlobal", "Reset to inherit from global")} onClick={resetTitleSummarizerFallbackValue} style={{ whiteSpace: "nowrap" }}>{t("settings.projectModels.reset", " Reset ")}</button>)}
|
||||||
|
</div>
|
||||||
|
<small>{t("settings.projectModels.titleSummarizerFallbackHelp", "Fallback provider and model used when the primary Title Summarizer model cannot be used. Falls back to the global summarization lane and then the default model chain.")}</small>
|
||||||
|
</div>
|
||||||
|
</>)}
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="autoSummarizeTitles" className="checkbox-label">
|
||||||
|
<input id="autoSummarizeTitles" type="checkbox" checked={form.autoSummarizeTitles || false} onChange={(e) => setForm((f) => ({ ...f, autoSummarizeTitles: e.target.checked }))}/>{t("settings.projectModels.autoSummarizeLongDescriptionsAsTitles", " Auto-summarize long descriptions as titles ")}</label>
|
||||||
|
<small>{t("settings.projectModels.whenEnabledTasksCreatedWithoutATitleBut", " When enabled, tasks created without a title but with descriptions over 200 characters will automatically get an AI-generated title (max 60 characters). The same model is also used to generate fallback merge commit message bodies when the branch's commit log is empty (e.g. squash merges with no unique commits), and GitHub tracking issue titles when a tracked task has no title yet. Default: disabled. ")}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="useAiMergeCommitSummary" className="checkbox-label">
|
<label htmlFor="useAiMergeCommitSummary" className="checkbox-label">
|
||||||
<input id="useAiMergeCommitSummary" type="checkbox" checked={form.useAiMergeCommitSummary || false} onChange={(e) => setForm((f) => ({ ...f, useAiMergeCommitSummary: e.target.checked }))}/>{t("settings.projectModels.aIMergeCommitSummaries", " AI merge commit summaries ")}</label>
|
<input id="useAiMergeCommitSummary" type="checkbox" checked={form.useAiMergeCommitSummary || false} onChange={(e) => setForm((f) => ({ ...f, useAiMergeCommitSummary: e.target.checked }))}/>{t("settings.projectModels.aIMergeCommitSummaries", " AI merge commit summaries ")}</label>
|
||||||
<small>{t("settings.projectModels.whenEnabledMergeCommitMessagesIncludeAnAI", " When enabled, merge commit messages include an AI-generated subject plus body summary (narrative + bullets + diff-stat) instead of just listing step commit subjects. Uses the title summarization model. Default: enabled. ")}</small>
|
<small>{t("settings.projectModels.whenEnabledMergeCommitMessagesIncludeAnAI", " When enabled, merge commit messages include an AI-generated subject plus body summary (narrative + bullets + diff-stat) instead of just listing step commit subjects. Uses the title summarization model. Default: enabled. ")}</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(form.autoSummarizeTitles || form.useAiMergeCommitSummary || form.githubTrackingEnabledByDefault || false) && (<p className="settings-description">
|
{(form.autoSummarizeTitles || form.useAiMergeCommitSummary || form.githubTrackingEnabledByDefault || false) && (<p className="settings-description">
|
||||||
{t("settings.movedStub.summarizerModelInline", "The summarization model lane above controls title auto-summarization, merge commit summaries, GitHub tracking titles, and PR metadata generation.")}
|
{t("settings.movedStub.summarizerModelInline", "These summarization model controls govern title auto-summarization, merge commit summaries, GitHub tracking titles, and PR metadata generation.")}
|
||||||
</p>)}
|
</p>)}
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="prTitlePromptInstructions">{t("settings.projectModels.prTitlePromptInstructions", "PR title prompt guidance")}</label>
|
<label htmlFor="prTitlePromptInstructions">{t("settings.projectModels.prTitlePromptInstructions", "PR title prompt guidance")}</label>
|
||||||
@@ -669,6 +682,7 @@ export function ProjectModelsSection({ scopeBanner, form, setForm, models, proje
|
|||||||
<textarea id="prDescriptionPromptInstructions" value={form.prDescriptionPromptInstructions || ""} onChange={(e) => setForm((f) => ({ ...f, prDescriptionPromptInstructions: e.target.value }))} rows={4} placeholder={t("settings.projectModels.prDescriptionPromptInstructionsPlaceholder", "Example: Emphasize operator-facing behavior and list verification commands exactly.")}/>
|
<textarea id="prDescriptionPromptInstructions" value={form.prDescriptionPromptInstructions || ""} onChange={(e) => setForm((f) => ({ ...f, prDescriptionPromptInstructions: e.target.value }))} rows={4} placeholder={t("settings.projectModels.prDescriptionPromptInstructionsPlaceholder", "Example: Emphasize operator-facing behavior and list verification commands exactly.")}/>
|
||||||
<small>{t("settings.projectModels.prDescriptionPromptInstructionsHelp", "Guides the AI-generated Create PR summary, changes, and testing sections. Leave blank to use the default PR metadata prompt. No default \u2014 unset.")}</small>
|
<small>{t("settings.projectModels.prDescriptionPromptInstructionsHelp", "Guides the AI-generated Create PR summary, changes, and testing sections. Leave blank to use the default PR metadata prompt. No default \u2014 unset.")}</small>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
</>);
|
</>);
|
||||||
}
|
}
|
||||||
export default ProjectModelsSection;
|
export default ProjectModelsSection;
|
||||||
|
|||||||
Reference in New Issue
Block a user