Configure a persisted output-language setting and apply it across AI-authored task text and workflow summaries. - Add project/global output-language settings with defaults, validation, UI controls, translations, and documentation. - Propagate the selected language through planning, execution, review, summaries, and task creation. - Add focused coverage for language resolution and update affected dashboard, engine, and localization tests. Files changed: .changeset/fn-037-task-output-language.md | 7 ++ docs/settings-reference.md | 4 + docs/task-management.md | 4 + .../core/src/__tests__/ai-output-language.test.ts | 21 +++++ packages/core/src/ai/ai-output-language.ts | 42 +++++++++ packages/core/src/ai/ai-summarize.ts | 16 +++- packages/core/src/config/settings-schema.ts | 2 + packages/core/src/index.gate.ts | 2 + packages/core/src/index.ts | 2 + packages/core/src/task-store/settings-ops.ts | 7 ++ packages/core/src/task-store/task-creation.ts | 4 + packages/core/src/types/settings/settings-scope.ts | 2 + .../app/__tests__/settings-sections.test.tsx | 21 +++-- .../__tests__/SettingsModal.models-auth.test.tsx | 10 +- .../app/components/settings/section-keys.ts | 2 +- .../sections/ProjectModelsSection.search.ts | 13 ++- .../settings/sections/ProjectModelsSection.tsx | 28 ++++-- .../settings-default-descriptions.test.tsx | 4 +- .../src/__tests__/github-tracking.test.ts | 4 +- .../__tests__/planning-prompt-resolution.test.ts | 29 ++++-- .../src/__tests__/routes-planning.test.ts | 32 ++++++- packages/dashboard/src/ai-session-store.ts | 15 ++- packages/dashboard/src/github-tracking.ts | 3 + packages/dashboard/src/planning.ts | 75 +++++++++++++-- .../routes/register-ai-text-assistant-routes.ts | 11 ++- .../src/routes/register-planning-subtask-routes.ts | 6 +- .../src/routes/register-task-workflow-routes.ts | 9 +- packages/dashboard/src/shared/settings-sections.ts | 2 +- .../__tests__/ce-workflow-step-executor.test.ts | 45 ++++++++- .../__tests__/executor-review-artifacts.test.ts | 65 +++++++++++++ packages/engine/src/__tests__/triage.test.ts | 7 +- .../src/__tests__/workflow-task-runtime.test.ts | 23 +++++ .../create-authoritative-workflow-primitives.ts | 14 ++- .../create-authoritative-workflow-seams.ts | 17 +++- .../engine/src/executor/execute-workflow-graph.ts | 19 +++- .../engine/src/executor/execute-workflow-step.ts | 13 ++- packages/engine/src/executor/execution-prompt.ts | 10 +- .../engine/src/executor/handoff-task-to-review.ts | 10 +- .../engine/src/executor/run-graph-custom-node.ts | 6 +- .../src/executor/task-executor-graph-facades.ts | 8 +- packages/engine/src/triage.ts | 35 ++----- .../src/workflows/workflow-completion-summary.ts | 101 ++++++++++++++++++--- .../engine/src/workflows/workflow-task-runtime.ts | 12 +++ packages/i18n/locales/en/app.json | 7 +- packages/i18n/locales/es/app.json | 7 +- packages/i18n/locales/fr/app.json | 7 +- packages/i18n/locales/ko/app.json | 7 +- packages/i18n/locales/pt-BR/app.json | 14 ++- packages/i18n/locales/zh-CN/app.json | 7 +- packages/i18n/locales/zh-TW/app.json | 7 +- packages/i18n/src/resources.d.ts | 13 ++- 51 files changed, 680 insertions(+), 151 deletions(-) Fusion-Task-Id: FN-037 Fusion-Task-Lineage: 968a707e-7ffe-43ba-b983-d679b031f7a5 Co-authored-by: Fusion <noreply@runfusion.ai>
145 lines
6.2 KiB
TypeScript
145 lines
6.2 KiB
TypeScript
import { resolveTaskOutputLanguage, type ResolvedTaskOutputLanguage, type Settings, type TaskDetail } from "@fusion/core";
|
|
|
|
export interface WorkflowCompletionSummaryStore {
|
|
updateTask?: (taskId: string, updates: { summary: string }) => Promise<unknown> | unknown;
|
|
logEntry?: (taskId: string, action: string, detail?: string) => Promise<unknown> | unknown;
|
|
}
|
|
|
|
export interface WorkflowCompletionSummaryInput {
|
|
reason: string;
|
|
workflowId?: string;
|
|
runId?: string;
|
|
/** Captured invocation settings; callers must not re-read mutable settings during completion. */
|
|
settings?: Partial<Settings>;
|
|
/** Original user input captured with the invocation; input mode must not infer from an AI title. */
|
|
originalInput?: string;
|
|
/** Resolved invocation target, retained by resumable workflow paths. */
|
|
outputLanguage?: ResolvedTaskOutputLanguage;
|
|
}
|
|
|
|
function truncateList(values: string[], limit: number, locale: string): string {
|
|
const head = values.slice(0, limit);
|
|
const remaining = values.length - head.length;
|
|
const more = locale === "fr" ? ` et ${remaining} autres`
|
|
: locale === "es" ? ` y ${remaining} más`
|
|
: locale === "pt-BR" ? ` e mais ${remaining}`
|
|
: locale === "ko" ? ` 외 ${remaining}개`
|
|
: locale === "zh-CN" || locale === "zh-TW" ? `,另有 ${remaining} 个`
|
|
: ` and ${remaining} more`;
|
|
return remaining > 0 ? `${head.join(", ")}${more}` : head.join(", ");
|
|
}
|
|
|
|
function completionCopy(locale: string, doneSteps: number, stepCount: number, passedChecks: number, checkCount: number, files: string, reason: string, workflowId?: string): string[] {
|
|
const source = `${reason}${workflowId ? ` (${workflowId})` : ""}`;
|
|
if (locale === "fr") return [
|
|
`Flux de travail terminé`,
|
|
`Étapes de tâche terminées : ${doneSteps}/${stepCount}.`,
|
|
`Contrôles du flux de travail réussis ou ignorés : ${passedChecks}/${checkCount}.`,
|
|
`Fichiers modifiés : ${files}.`,
|
|
`Source de fin : ${source}.`,
|
|
];
|
|
if (locale === "es") return [
|
|
"Flujo de trabajo completado",
|
|
`Pasos de tarea completados: ${doneSteps}/${stepCount}.`,
|
|
`Comprobaciones del flujo aprobadas u omitidas: ${passedChecks}/${checkCount}.`,
|
|
`Archivos modificados: ${files}.`,
|
|
`Origen de finalización: ${source}.`,
|
|
];
|
|
if (locale === "pt-BR") return [
|
|
"Fluxo de trabalho concluído",
|
|
`Etapas da tarefa concluídas: ${doneSteps}/${stepCount}.`,
|
|
`Verificações do fluxo aprovadas ou ignoradas: ${passedChecks}/${checkCount}.`,
|
|
`Arquivos alterados: ${files}.`,
|
|
`Origem da conclusão: ${source}.`,
|
|
];
|
|
if (locale === "ko") return [
|
|
"워크플로가 완료되었습니다",
|
|
`완료된 작업 단계: ${doneSteps}/${stepCount}.`,
|
|
`통과 또는 건너뛴 워크플로 검사: ${passedChecks}/${checkCount}.`,
|
|
`변경된 파일: ${files}.`,
|
|
`완료 출처: ${source}.`,
|
|
];
|
|
if (locale === "zh-CN") return [
|
|
"工作流已完成",
|
|
`已完成的任务步骤:${doneSteps}/${stepCount}。`,
|
|
`已通过或跳过的工作流检查:${passedChecks}/${checkCount}。`,
|
|
`已更改文件:${files}。`,
|
|
`完成来源:${source}。`,
|
|
];
|
|
if (locale === "zh-TW") return [
|
|
"工作流程已完成",
|
|
`已完成的工作步驟:${doneSteps}/${stepCount}。`,
|
|
`已通過或略過的工作流程檢查:${passedChecks}/${checkCount}。`,
|
|
`已變更檔案:${files}。`,
|
|
`完成來源:${source}。`,
|
|
];
|
|
return [
|
|
"Workflow completed",
|
|
`Completed ${doneSteps}/${stepCount} task step${stepCount === 1 ? "" : "s"}.`,
|
|
`Recorded ${passedChecks}/${checkCount} workflow check${checkCount === 1 ? "" : "s"} as passed or skipped.`,
|
|
`Changed files: ${files}.`,
|
|
`Completion source: ${source}.`,
|
|
];
|
|
}
|
|
|
|
export function buildWorkflowCompletionSummary(
|
|
task: Pick<TaskDetail, "id" | "title" | "description" | "steps" | "modifiedFiles" | "workflowStepResults">,
|
|
input: WorkflowCompletionSummaryInput,
|
|
): string {
|
|
const title = task.title?.trim() || task.id;
|
|
const steps = task.steps ?? [];
|
|
const doneSteps = steps.filter((step) => step.status === "done" || step.status === "skipped").length;
|
|
const workflowResults = task.workflowStepResults ?? [];
|
|
const passedWorkflowSteps = workflowResults.filter((step) => step.status === "passed" || step.status === "skipped").length;
|
|
const files = (task.modifiedFiles ?? []).filter((file) => file.trim().length > 0);
|
|
|
|
/* FNXC:TaskOutputLanguage 2026-08-19-15:47: Deterministic fallback localizes every human-readable clause from the captured target and original input, never an AI-authored title. */
|
|
const locale = (input.outputLanguage ?? resolveTaskOutputLanguage(input.settings, input.originalInput ?? task.description ?? "")).locale ?? "en";
|
|
const [completed, stepCopy, checkCopy, filesCopy, sourceCopy] = completionCopy(
|
|
locale,
|
|
doneSteps,
|
|
steps.length,
|
|
passedWorkflowSteps,
|
|
workflowResults.length,
|
|
truncateList(files, 6, locale),
|
|
input.reason,
|
|
input.workflowId,
|
|
);
|
|
const parts = [`${completed}: ${title}.`];
|
|
if (steps.length > 0) parts.push(stepCopy);
|
|
if (workflowResults.length > 0) parts.push(checkCopy);
|
|
if (files.length > 0) parts.push(filesCopy);
|
|
parts.push(sourceCopy);
|
|
return parts.join(" ");
|
|
}
|
|
|
|
export async function ensureWorkflowCompletionSummary(
|
|
store: WorkflowCompletionSummaryStore,
|
|
task: TaskDetail,
|
|
input: WorkflowCompletionSummaryInput,
|
|
): Promise<void> {
|
|
if (task.summary?.trim()) return;
|
|
if (!store.updateTask) return;
|
|
|
|
/*
|
|
* FNXC:WorkflowCompletion 2026-06-29-10:58:
|
|
* Workflow-owned tasks can finish through graph nodes and resumable merge work
|
|
* items without an agent calling `fn_task_done`. Persist a deterministic
|
|
* completion summary at the workflow lifecycle boundary so Done/Review cards,
|
|
* GitHub tracking, evals, and archival views see the same `task.summary`
|
|
* contract as legacy executor completions. Existing agent-authored summaries
|
|
* remain authoritative.
|
|
*/
|
|
const summary = buildWorkflowCompletionSummary(task, input);
|
|
await store.updateTask(task.id, { summary });
|
|
await store.logEntry?.(
|
|
task.id,
|
|
"Workflow completion summary recorded",
|
|
JSON.stringify({
|
|
reason: input.reason,
|
|
workflowId: input.workflowId,
|
|
runId: input.runId,
|
|
}),
|
|
);
|
|
}
|