diff --git a/.changeset/fn-7180-pr-prompt-settings.md b/.changeset/fn-7180-pr-prompt-settings.md new file mode 100644 index 0000000000..539ae59377 --- /dev/null +++ b/.changeset/fn-7180-pr-prompt-settings.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Add project settings to customize the AI prompts for PR title and description generation. +category: feature +dev: New project settings `prTitlePromptInstructions` / `prDescriptionPromptInstructions` (default undefined) are appended to the Create PR dialog's metadata-generation system prompt in generatePrMetadata. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index c20a047f44..dac30f632a 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -1004,6 +1004,7 @@ Inspect task definition, logs, review feedback, comments, artifacts, workflow ou - The **Create Pull Request** modal is a floating pop-out like Plan Mission, New Task, and Automations: drag its header or resize from desktop edges/corners, while mobile keeps the full-screen dialog layout. Close it with **X**, **Cancel**, or **Escape**; stray clicks inside or outside the floating shell do not dismiss it. - The modal shell renders immediately: preflight checks and PR options load independently of AI-generated title/body metadata, so slow AI suggestions no longer block base-branch selection, diagnostics, or manual PR authoring. The **Diff & commit preview** section starts collapsed and can be expanded on demand. - AI title/body generation in the dialog is bounded to 15 seconds and is canceled if the request disconnects; on timeout/cancel, Fusion falls back to deterministic task-based PR title/body content instead of leaving the spinner stuck forever. +- Project Settings → Project Models includes optional **PR title prompt guidance** and **PR description prompt guidance** fields. Blank fields preserve the default Create PR metadata prompt; populated fields append guidance for the generated title or body sections. - The **Artifacts** tab combines task documents written by agents or users with task-scoped registered media artifacts. The gallery uses thumbnail-first image/video cards, image and video previews can expand into a dismissible full-size lightbox, video and audio use native controls, document artifacts show text previews, and generic artifacts open through their media URL. - The **Review** tab is separate from **Comments**: Review shows actionable PR/reviewer feedback and same-task revision controls, while Comments remains the general collaboration thread. - Review comments hide GitHub template HTML comments in both Markdown and Plain modes, show author avatars or User/Bot fallbacks, label Human vs Bot/agent authors, and include All/Human/Bot filtering. diff --git a/docs/settings-reference.md b/docs/settings-reference.md index 0a441bdb51..01c6658062 100644 --- a/docs/settings-reference.md +++ b/docs/settings-reference.md @@ -584,6 +584,8 @@ Default notes: | `titleSummarizerModelId` | `string` | `undefined` | Model ID for title summarization. | | `titleSummarizerFallbackProvider` | `string` | `undefined` | Fallback provider for title summarization. | | `titleSummarizerFallbackModelId` | `string` | `undefined` | Fallback model ID for title summarization. | +| `prTitlePromptInstructions` | `string` | `undefined` | Optional project guidance appended to the Create PR dialog's AI metadata system prompt for the generated PR title. Blank or whitespace-only values are treated as unset and keep the default prompt behavior. | +| `prDescriptionPromptInstructions` | `string` | `undefined` | Optional project guidance appended to the Create PR dialog's AI metadata system prompt for generated PR body fields (`summary`, `changes`, `testing`). Blank or whitespace-only values are treated as unset and keep the default prompt behavior. | | `scripts` | `Record` | `undefined` | Named script map used by script-mode workflow steps and setup hooks. | | `setupScript` | `string` | `undefined` | Script key from `scripts` to run before task execution. | | `insightExtractionEnabled` | `boolean` | `false` | Enable scheduled memory insight extraction. | diff --git a/packages/core/src/__tests__/settings-defaults.test.ts b/packages/core/src/__tests__/settings-defaults.test.ts index 82d42311b9..df69e7e262 100644 --- a/packages/core/src/__tests__/settings-defaults.test.ts +++ b/packages/core/src/__tests__/settings-defaults.test.ts @@ -1,7 +1,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { DEFAULT_MAX_AUTO_MERGE_RETRIES, resolveMaxAutoMergeRetries } from "../in-review-stall.js"; import { isExperimentalFeatureEnabled } from "../experimental-features.js"; -import { DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS } from "../settings-schema.js"; +import { DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS, GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS } from "../settings-schema.js"; import { isWorkflowColumnsEnabled } from "../workflow-columns-settings.js"; import { __resetLegacyCwdMainWarningForTests, @@ -99,6 +99,15 @@ describe("settings defaults invariants", () => { expect("githubCloseSourceIssueOnDone" in DEFAULT_GLOBAL_SETTINGS).toBe(false); }); + it("defaults PR metadata prompt guidance to project-scoped unset strings", () => { + expect(DEFAULT_PROJECT_SETTINGS.prTitlePromptInstructions).toBeUndefined(); + expect(DEFAULT_PROJECT_SETTINGS.prDescriptionPromptInstructions).toBeUndefined(); + expect(PROJECT_SETTINGS_KEYS).toContain("prTitlePromptInstructions"); + expect(PROJECT_SETTINGS_KEYS).toContain("prDescriptionPromptInstructions"); + expect(GLOBAL_SETTINGS_KEYS).not.toContain("prTitlePromptInstructions"); + expect(GLOBAL_SETTINGS_KEYS).not.toContain("prDescriptionPromptInstructions"); + }); + it("defaults AI merge commit summaries to enabled", () => { // FN-5642/FN-5644 intentionally default this on for subject + body summary coverage. expect(DEFAULT_PROJECT_SETTINGS.useAiMergeCommitSummary).toBe(true); diff --git a/packages/core/src/__tests__/store-settings.test.ts b/packages/core/src/__tests__/store-settings.test.ts index 36b36306a6..2b97b8696e 100644 --- a/packages/core/src/__tests__/store-settings.test.ts +++ b/packages/core/src/__tests__/store-settings.test.ts @@ -48,6 +48,25 @@ describe("TaskStore", () => { }); }); + describe("PR metadata prompt guidance settings", () => { + it("round-trips title and description prompt guidance via project settings", async () => { + await harness.store().updateSettings({ + prTitlePromptInstructions: "Use release-note titles.", + prDescriptionPromptInstructions: "Group body bullets by operator impact.", + }); + + const settings = await harness.store().getSettings(); + expect(settings.prTitlePromptInstructions).toBe("Use release-note titles."); + expect(settings.prDescriptionPromptInstructions).toBe("Group body bullets by operator impact."); + + const { project, global } = await harness.store().getSettingsByScope(); + expect(project.prTitlePromptInstructions).toBe("Use release-note titles."); + expect(project.prDescriptionPromptInstructions).toBe("Group body bullets by operator impact."); + expect("prTitlePromptInstructions" in global).toBe(false); + expect("prDescriptionPromptInstructions" in global).toBe(false); + }); + }); + describe("worktreeCopyFiles setting", () => { it("round-trips populated copy-file paths via getSettings and project serialization", async () => { await harness.store().updateSettings({ worktreeCopyFiles: [".env", "config/local.env", "packages/api/.env.test"] }); diff --git a/packages/core/src/settings-schema.ts b/packages/core/src/settings-schema.ts index 298e7b90e3..3391e3721e 100644 --- a/packages/core/src/settings-schema.ts +++ b/packages/core/src/settings-schema.ts @@ -483,6 +483,8 @@ export const DEFAULT_PROJECT_SETTINGS = { titleSummarizerModelId: undefined, titleSummarizerFallbackProvider: undefined, titleSummarizerFallbackModelId: undefined, + prTitlePromptInstructions: undefined, + prDescriptionPromptInstructions: undefined, scripts: undefined, setupScript: undefined, insightExtractionEnabled: false, diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 3da5312f9e..b1029cd8ef 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -4229,6 +4229,18 @@ export interface ProjectSettings { * planning fallback, then global fallback. Must be set together with * `titleSummarizerFallbackProvider`. */ titleSummarizerFallbackModelId?: string; + /** + * FNXC:PrMetadataGeneration 2026-06-27-00:00: + * Project operators can add title-specific guidance to the Create PR metadata prompt without replacing the strict JSON schema contract. Blank or whitespace-only values are treated as unset so the default prompt remains byte-for-byte unchanged. + * Optional project-scoped guidance appended to the PR metadata system prompt for the generated `title` field. Default: undefined. + */ + prTitlePromptInstructions?: string; + /** + * FNXC:PrMetadataGeneration 2026-06-27-00:00: + * Project operators can add body-specific guidance to the Create PR metadata prompt without replacing the strict JSON schema contract. Blank or whitespace-only values are treated as unset so the default prompt remains byte-for-byte unchanged. + * Optional project-scoped guidance appended to the PR metadata system prompt for the generated `summary`, `changes`, and `testing` fields. Default: undefined. + */ + prDescriptionPromptInstructions?: string; /** Named scripts that can be referenced by setupScript or other automation. * A map of script name to shell command. */ scripts?: Record; diff --git a/packages/dashboard/app/__tests__/settings-sections.test.tsx b/packages/dashboard/app/__tests__/settings-sections.test.tsx index 52aeebbba9..fd9cdd8df7 100644 --- a/packages/dashboard/app/__tests__/settings-sections.test.tsx +++ b/packages/dashboard/app/__tests__/settings-sections.test.tsx @@ -17,6 +17,7 @@ import { AppearanceSection } from "../components/settings/sections/AppearanceSec import { NotificationsSection } from "../components/settings/sections/NotificationsSection"; import { ExperimentalSection } from "../components/settings/sections/ExperimentalSection"; import { MovedSettingsStub } from "../components/settings/sections/MovedSettingsStub"; +import { ProjectModelsSection } from "../components/settings/sections/ProjectModelsSection"; import { PromptsSection } from "../components/settings/sections/PromptsSection"; import { SecretsSection } from "../components/settings/sections/SecretsSection"; import { WorktreesSection } from "../components/settings/sections/WorktreesSection"; @@ -218,6 +219,59 @@ describe("WorktreesSection", () => { }); }); +describe("ProjectModelsSection", () => { + const models = { + modelLanes: [], + getLaneStatus: () => "inherited" as const, + getLaneValue: () => "", + updateLaneValue: vi.fn(), + resetLaneValue: vi.fn(), + availableModels: [], + modelsLoading: false, + favoriteProviders: [], + favoriteModels: [], + onToggleFavorite: vi.fn(), + onToggleModelFavorite: vi.fn(), + editingPresetId: null, + setEditingPresetId: vi.fn(), + presetDraft: null, + setPresetDraft: vi.fn(), + onSavePresetDraft: vi.fn(), + confirmDelete: vi.fn(), + }; + + it("renders PR prompt guidance textareas and emits edits through setForm", () => { + function ProjectModelsHost() { + const [form, setFormState] = useState({ + prTitlePromptInstructions: "Keep it short.", + prDescriptionPromptInstructions: "Mention testing.", + } as SettingsFormState); + return ( + + ); + } + + render(); + + const titleField = screen.getByLabelText("PR title prompt guidance") as HTMLTextAreaElement; + const descriptionField = screen.getByLabelText("PR description prompt guidance") as HTMLTextAreaElement; + expect(titleField.value).toBe("Keep it short."); + expect(descriptionField.value).toBe("Mention testing."); + + fireEvent.change(titleField, { target: { value: "Use release style." } }); + fireEvent.change(descriptionField, { target: { value: "Group by impact." } }); + + expect(titleField.value).toBe("Use release style."); + expect(descriptionField.value).toBe("Group by impact."); + }); +}); + describe("PromptsSection", () => { it("renders the title and mounts AgentPromptsManager", () => { render( diff --git a/packages/dashboard/app/components/SettingsModal.tsx b/packages/dashboard/app/components/SettingsModal.tsx index 358a2831dd..1f6507681c 100644 --- a/packages/dashboard/app/components/SettingsModal.tsx +++ b/packages/dashboard/app/components/SettingsModal.tsx @@ -753,6 +753,8 @@ export function SettingsModal({ webhookUrl: undefined, webhookFormat: "generic", webhookEvents: undefined, + prTitlePromptInstructions: "", + prDescriptionPromptInstructions: "", }); const [loading, setLoading] = useState(true); // Guards the Save action against double-submit (rapid clicks / Enter) while the @@ -2444,6 +2446,8 @@ export function SettingsModal({ taskPrefix: form.taskPrefix?.trim() || undefined, githubTrackingDefaultRepo: form.githubTrackingDefaultRepo?.trim() || undefined, githubAuthToken: form.githubAuthToken?.trim() || undefined, + prTitlePromptInstructions: form.prTitlePromptInstructions?.trim() || undefined, + prDescriptionPromptInstructions: form.prDescriptionPromptInstructions?.trim() || undefined, overlapIgnorePaths: (form.overlapIgnorePaths ?? []).map((path) => path.trim()).filter((path) => path.length > 0), worktreeCopyFiles: normalizedWorktreeCopyFiles.length > 0 || initialScopedValues?.project?.worktreeCopyFiles !== undefined ? normalizedWorktreeCopyFiles diff --git a/packages/dashboard/app/components/settings/sections/ProjectModelsSection.tsx b/packages/dashboard/app/components/settings/sections/ProjectModelsSection.tsx index 4e9f504214..f95ebda76f 100644 --- a/packages/dashboard/app/components/settings/sections/ProjectModelsSection.tsx +++ b/packages/dashboard/app/components/settings/sections/ProjectModelsSection.tsx @@ -475,6 +475,18 @@ export function ProjectModelsSection({ scopeBanner, form, setForm, models, proje {(form.autoSummarizeTitles || form.useAiMergeCommitSummary || form.githubTrackingEnabledByDefault || false) && (

{t("settings.movedStub.summarizerModelInline", "The summarization model lane above controls title auto-summarization, merge commit summaries, GitHub tracking titles, and PR metadata generation.")}

)} + +
+ +