diff --git a/docs/settings-reference.md b/docs/settings-reference.md
index 9a75581a37..57ac4c730c 100644
--- a/docs/settings-reference.md
+++ b/docs/settings-reference.md
@@ -779,7 +779,7 @@ Database backups work with both external PostgreSQL and Fusion's default embedde
| `memoryBackupRetention` | `number` | `14` | Number of memory backups to retain. |
| `memoryBackupDir` | `string` | `".fusion/backups/memory"` | Relative memory backup directory path. |
| `memoryBackupScope` | `"project" \| "agents" \| "all"` | `"all"` | Backup scope: project memory, agent memory, or both. |
-| `autoSummarizeTitles` | `boolean` | `false` | Project-scoped automatic policy for every non-empty untitled task description across dashboard/API, direct store, agent, scheduled, signal, and CLI-backed creates. Enabled creates attempt an AI title regardless of description length; disabled creates use the deterministic planner fallback. Explicit titles are preserved, `summarize:true` and Task Detail/manual requests remain explicit force paths, and the setting is snapshotted per create rather than applied retroactively. |
+| `autoSummarizeTitles` | `boolean` | `false` | Project-scoped automatic policy for every non-empty untitled task description across dashboard/API, direct store, agent, scheduled, signal, and CLI-backed creates. In Settings → Project Models it sits immediately after AI-authored task language. Enabled creates attempt an AI title regardless of description length and snapshot the selected output language before deferred generation; disabled or unavailable generation leaves the title blank for bounded display-only fallback. Explicit titles are preserved, `summarize:true` and Task Detail/manual requests remain explicit force paths. |
| `taskDefinitionInInputLanguage` | `boolean` | `false` | When enabled, generated task-definition (`PROMPT.md`) prose uses a confidently detected supported input language: Spanish (`es`), French (`fr`), Korean (`ko`), or Chinese (`zh-CN`). Only planner-authored prose is localized; headings, markers, the verbatim Original Description, code, paths, tool names, and commit conventions stay canonical English for deterministic parsing. Chinese always authors as `zh-CN`; Traditional Chinese is not variant-detected. English, short/uncertain, and unsupported input such as Japanese fall back to English. Configure in **Settings → Project Models**. |
| `useAiMergeCommitSummary` | `boolean` | `true` | Use AI-generated merge commit summaries (subject + bullet body + diff-stat) instead of raw step-commit subject lists. |
| `titleSummarizerProvider` | `string` | `undefined` | Provider for title summarization. |
@@ -2000,7 +2000,7 @@ When adding a picker provider, add a census entry with `autoDerive`, `guardNotAp
### AI-authored task output language
-`taskOutputLanguage` is a project setting with `"english"` (default), `"input"`, and `"interface"` modes. English explicitly targets English; input follows the original task text (with detection only as a hint); interface follows the global Fusion UI `language`, falling back to English when unset. Existing `taskDefinitionInInputLanguage: true` projects resolve as input until an explicit new mode is saved; explicit modes clear that legacy flag atomically. The setting affects future planning, triage, title, execution, workflow-summary, and recommendation generation sessions only, never existing task text. Human prose is targeted while headings, JSON keys, markers, code, paths, tool names, and schemas remain canonical.
+`taskOutputLanguage` is a project setting with `"english"` (default), `"input"`, and `"interface"` modes. English explicitly targets English; input follows the original task text (with detection only as a hint); interface follows the global Fusion UI `language`, falling back to English when unset. Existing `taskDefinitionInInputLanguage: true` projects resolve as input until an explicit new mode is saved; explicit modes clear that legacy flag atomically. The setting affects future planning, triage, title, execution, workflow-summary, and recommendation generation sessions only, never existing task text. The immediately following **Auto-summarize task titles** project toggle snapshots this selected language at task creation for every non-empty untitled description when enabled; no description-length threshold controls the policy. When disabled or generation is unavailable, titleless Board and List cards use bounded presentation-only fallback without persisting a description-derived title. Human prose is targeted while headings, JSON keys, markers, code, paths, tool names, and schemas remain canonical.
### JIRA branch naming
diff --git a/packages/core/src/__tests__/ai-summarize.test.ts b/packages/core/src/__tests__/ai-summarize.test.ts
index 761c21176b..65a820d164 100644
--- a/packages/core/src/__tests__/ai-summarize.test.ts
+++ b/packages/core/src/__tests__/ai-summarize.test.ts
@@ -173,6 +173,24 @@ describe("ai-summarize", () => {
expect(getFnAgentMock).toHaveBeenCalledTimes(1);
});
+ it.each([
+ ["English", { mode: "english", locale: "en", instruction: "English" }, "Write the title in English."],
+ ["interface", { mode: "interface", locale: "fr", instruction: "French" }, "Write the title in Français (fr)."],
+ ["input", { mode: "input", locale: "es", instruction: "input language" }, "SAME language as the task description"],
+ ] as const)("honors the resolved %s output target", async (_mode, target, expectedInstruction) => {
+ const prompt = vi.fn().mockResolvedValue(undefined);
+ getFnAgentMock.mockResolvedValue(() => Promise.resolve({
+ session: {
+ prompt,
+ dispose: vi.fn(),
+ state: { messages: [{ role: "assistant", content: "Generated task title" }] },
+ },
+ }));
+
+ await expect(summarizeTitle("Necesitamos mejorar la creación de tareas.", "/tmp", undefined, undefined, target)).resolves.toBe("Generated task title");
+ expect(prompt.mock.calls[0][0]).toContain(expectedInstruction);
+ });
+
it.each(["", " ", "\n\t"]) ("rejects invalid %j before creating an agent", async (description) => {
await expect(summarizeTitle(description, "/tmp")).rejects.toThrow(ValidationError);
expect(getFnAgentMock).not.toHaveBeenCalled();
diff --git a/packages/dashboard/app/__tests__/settings-sections.test.tsx b/packages/dashboard/app/__tests__/settings-sections.test.tsx
index 9ddb4e15f3..2b3d413a32 100644
--- a/packages/dashboard/app/__tests__/settings-sections.test.tsx
+++ b/packages/dashboard/app/__tests__/settings-sections.test.tsx
@@ -626,6 +626,26 @@ describe("ProjectModelsSection", () => {
});
});
+ it("places the sole title toggle directly after task language and updates project form state", () => {
+ const setForm = vi.fn();
+ render(
+ ,
+ );
+
+ const language = screen.getByRole("combobox", { name: "AI-authored task language" });
+ const toggle = screen.getByRole("checkbox", { name: "Auto-summarize task titles" });
+ expect(screen.getAllByRole("checkbox", { name: "Auto-summarize task titles" })).toHaveLength(1);
+ expect(language.compareDocumentPosition(toggle) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
+ fireEvent.click(toggle);
+ const update = setForm.mock.calls[0]?.[0] as (form: SettingsFormState) => SettingsFormState;
+ expect(update({ autoSummarizeTitles: false } as SettingsFormState)).toMatchObject({ autoSummarizeTitles: true });
+ });
+
it("colocates summarization model controls with AI summarization settings", () => {
render(
{
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 task 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).not.toContainElement(screen.getByRole("checkbox", { name: "Auto-summarize task titles" }));
expect(summarizationSection).toContainElement(summarizationDropdown);
expect(summarizationSection).toContainElement(fallbackDropdown);
expect(defaultDropdown.compareDocumentPosition(heading) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
diff --git a/packages/dashboard/app/components/settings/sections/ProjectModelsSection.tsx b/packages/dashboard/app/components/settings/sections/ProjectModelsSection.tsx
index eee75ed7b2..3c87bd4d58 100644
--- a/packages/dashboard/app/components/settings/sections/ProjectModelsSection.tsx
+++ b/packages/dashboard/app/components/settings/sections/ProjectModelsSection.tsx
@@ -867,6 +867,23 @@ export function ProjectModelsSection({ form, setForm, models, projectId, onOpenW
}))}
/>
+ {/*
+ FNXC:TitleSummarization 2026-08-20-20:19:
+ The automatic title policy belongs immediately beside its task-language selector so operators
+ can see that every enabled create snapshots this language for title generation. Keep this
+ project form row outside the model guard: availability of a model must not hide the policy.
+ */}
+ setForm((f) => ({ ...f, autoSummarizeTitles: v === true }))}
+ />
+
{/* --- AI Title and Git Commit Message Summarization --- */}
{/*
@@ -905,26 +922,6 @@ export function ProjectModelsSection({ form, setForm, models, projectId, onOpenW
>)}
- {/*
- FNXC:SettingsSearch 2026-07-15-17:35:
- This is the row operators searched "summarize" for and could not find (FN-7907, patched again 2026-07-14). It is now indexed by descriptor key from ProjectModelsSection.search.ts, so the word in its own label is what search matches — no hand-maintained keyword list to fall behind again.
- */}
- setForm((f) => ({ ...f, autoSummarizeTitles: v === true }))}
- />
-