FN-7181: improve PR metadata generation feedback
Ground PR metadata generation in repository evidence and make Create PR loading states explicit. - Upgrade the PR metadata system/context prompt while preserving configurable title and description guidance.\n- Show disabled title/body skeleton states while AI metadata generates and block submission until content resolves.\n- Extend dashboard tests, docs, and changeset coverage for the improved PR prompt and loading behavior.\n\nFiles changed:\n .../fn-7181-pr-generation-prompt-and-loading.md | 7 +++\n docs/dashboard-guide.md | 2 +-\n .../dashboard/app/components/PrCreateModal.css | 62 ++++++++++++++++++++++\n .../dashboard/app/components/PrCreateModal.tsx | 47 +++++++++++++---\n .../components/__tests__/PrCreateModal.test.tsx | 60 ++++++++++++++++-----\n .../src/__tests__/pr-metadata-generator.test.ts | 44 ++++++++++++---\n packages/dashboard/src/pr-metadata-generator.ts | 33 +++++++++---\n 7 files changed, 221 insertions(+), 34 deletions(-) Fusion-Task-Id: FN-7181 Fusion-Task-Lineage: 6565ddce-c205-4050-a37f-ffbb2f2d46d4 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7181-pr-generation-prompt-and-loading.md
Normal file
7
.changeset/fn-7181-pr-generation-prompt-and-loading.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": minor
|
||||
---
|
||||
|
||||
summary: Improve AI-generated PR titles and descriptions, and show a clear loading state while the description generates.
|
||||
category: feature
|
||||
dev: Rewrote the pr-metadata-generator system/context prompt (exported as a named default constant) for grounded, conventional-commit-style output while preserving the strict {title,summary,changes,testing,linkedTask} JSON schema; PrCreateModal now renders a skeleton + aria-busy loading affordance with disabled inputs during generation that clears into content or the existing error/manual-fallback path.
|
||||
@@ -1004,7 +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.
|
||||
- The **Body** section includes a **Preview/Edit** toggle so authors can review the rendered markdown description before creating the PR without changing the submitted raw body text.
|
||||
- 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.
|
||||
- AI title/body generation is bounded to 60 seconds on the server and 15 seconds in the dialog, and is canceled if the request disconnects; while it runs, the title and body fields show a skeleton loading state and are temporarily disabled, then resolve into generated content or deterministic task-based fallback content on timeout/cancel.
|
||||
- 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.
|
||||
|
||||
@@ -179,6 +179,68 @@ FN-7170 hosts Create PR in FloatingWindow: desktop geometry belongs to the share
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.pr-create-modal__field-shell {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.pr-create-modal__field-shell > .input:disabled {
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.pr-create-modal__metadata-skeleton {
|
||||
position: absolute;
|
||||
inset: var(--btn-border-width);
|
||||
pointer-events: none;
|
||||
border-radius: var(--radius-md);
|
||||
background: color-mix(in srgb, var(--border) 70%, var(--surface));
|
||||
animation: pr-create-modal-skeleton-pulse 1.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.pr-create-modal__metadata-skeleton--title {
|
||||
inline-size: min(100%, calc(var(--space-2xl) * 7));
|
||||
}
|
||||
|
||||
.pr-create-modal__metadata-skeleton--body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-sm);
|
||||
padding: var(--space-md);
|
||||
background: color-mix(in srgb, var(--surface) 82%, var(--border));
|
||||
}
|
||||
|
||||
.pr-create-modal__metadata-skeleton--body span {
|
||||
display: block;
|
||||
block-size: var(--space-md);
|
||||
border-radius: var(--radius-sm);
|
||||
background: color-mix(in srgb, var(--border) 72%, var(--surface));
|
||||
}
|
||||
|
||||
.pr-create-modal__metadata-skeleton--body span:nth-child(1) {
|
||||
inline-size: 92%;
|
||||
}
|
||||
|
||||
.pr-create-modal__metadata-skeleton--body span:nth-child(2) {
|
||||
inline-size: 78%;
|
||||
}
|
||||
|
||||
.pr-create-modal__metadata-skeleton--body span:nth-child(3) {
|
||||
inline-size: 88%;
|
||||
}
|
||||
|
||||
.pr-create-modal__metadata-skeleton--body span:nth-child(4) {
|
||||
inline-size: 64%;
|
||||
}
|
||||
|
||||
@keyframes pr-create-modal-skeleton-pulse {
|
||||
0%, 100% {
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.pr-create-template-hint {
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
|
||||
@@ -640,7 +640,11 @@ export function PrCreateModal({
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<section className="pr-create-modal__section">
|
||||
{/**
|
||||
* FNXC:PrCreateModal 2026-06-28-00:09:
|
||||
* PR title/body generation must read as active description work, not as empty editable fields. Keep both fields disabled with aria-busy plus a skeleton while metadataLoading is true, then clear the affordance into AI content or the existing metadataError/manual-fallback state.
|
||||
*/}
|
||||
<section className="pr-create-modal__section" aria-busy={metadataLoading ? "true" : undefined}>
|
||||
<div className="pr-create-modal__title-row">
|
||||
<label className="pr-create-modal__label" htmlFor="pr-create-modal-title">{t("pr.titleLabel", "Title")}</label>
|
||||
<div className="pr-create-modal__inline-actions">
|
||||
@@ -650,10 +654,20 @@ export function PrCreateModal({
|
||||
</div>
|
||||
{metadataLoading ? <div className="pr-create-modal__loading pr-create-modal__section-loading"><span className="status-dot status-dot--pending" aria-hidden="true" />{t("pr.generatingTitle", "Generating AI title…")}</div> : null}
|
||||
{metadataError ? <div className="form-error pr-error" role="alert"><p>{metadataError}</p></div> : null}
|
||||
<input id="pr-create-modal-title" className="input" value={title} onChange={(event) => { setTitle(event.target.value); setUserEditedTitle(true); }} />
|
||||
<div className="pr-create-modal__field-shell">
|
||||
<input
|
||||
id="pr-create-modal-title"
|
||||
className="input"
|
||||
value={title}
|
||||
onChange={(event) => { setTitle(event.target.value); setUserEditedTitle(true); }}
|
||||
disabled={metadataLoading}
|
||||
aria-busy={metadataLoading ? "true" : undefined}
|
||||
/>
|
||||
{metadataLoading ? <div className="pr-create-modal__metadata-skeleton pr-create-modal__metadata-skeleton--title" data-testid="pr-title-loading-skeleton" aria-hidden="true" /> : null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="pr-create-modal__section">
|
||||
<section className="pr-create-modal__section" aria-busy={metadataLoading ? "true" : undefined}>
|
||||
<div className="pr-create-modal__title-row">
|
||||
<label className="pr-create-modal__label" htmlFor="pr-create-modal-body">{t("pr.bodyLabel", "Body")}</label>
|
||||
<div className="pr-create-modal__inline-actions">
|
||||
@@ -675,13 +689,34 @@ export function PrCreateModal({
|
||||
{/**
|
||||
* FNXC:PrCreateModal 2026-06-28-00:00:
|
||||
* PR authors need to preview description markdown before creating the PR. The preview is render-only, uses the shared sanitized markdown pipeline, and submission/regeneration/revert always read and write the raw `body` state.
|
||||
*
|
||||
* FNXC:PrCreateModal 2026-06-28-00:16:
|
||||
* While AI metadata is generating, disable raw editing and show skeleton affordances even when Preview is selected so users see that the submitted body is still pending generation.
|
||||
*/}
|
||||
{showBodyPreview ? (
|
||||
{showBodyPreview && !metadataLoading ? (
|
||||
<div className="pr-create-modal__body-preview markdown-body" role="region" aria-label={t("pr.bodyPreviewLabel", "Body markdown preview")}>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={sharedRehypePlugins}>{body}</ReactMarkdown>
|
||||
</div>
|
||||
) : (
|
||||
<textarea id="pr-create-modal-body" className="input pr-create-modal__body-input" value={body} onChange={(event) => { setBody(event.target.value); setUserEditedBody(true); }} rows={8} />
|
||||
<div className="pr-create-modal__field-shell">
|
||||
<textarea
|
||||
id="pr-create-modal-body"
|
||||
className="input pr-create-modal__body-input"
|
||||
value={body}
|
||||
onChange={(event) => { setBody(event.target.value); setUserEditedBody(true); }}
|
||||
rows={8}
|
||||
disabled={metadataLoading}
|
||||
aria-busy={metadataLoading ? "true" : undefined}
|
||||
/>
|
||||
{metadataLoading ? (
|
||||
<div className="pr-create-modal__metadata-skeleton pr-create-modal__metadata-skeleton--body" data-testid="pr-body-loading-skeleton" aria-hidden="true">
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
{templateUsed && <p className="pr-create-template-hint">{t("pr.usingTemplate", "Using <code>.github/pull_request_template.md</code>")}</p>}
|
||||
</section>
|
||||
@@ -790,7 +825,7 @@ export function PrCreateModal({
|
||||
|
||||
<div className="modal-actions">
|
||||
<button type="button" className="btn" onClick={onClose} disabled={submitting}>{t("actions.cancel", "Cancel")}</button>
|
||||
<button type="button" className="btn btn-primary" onClick={() => void submit()} disabled={!preflight || preflightLoading || !canSubmit || !hasRequiredPrContent || submitting}>
|
||||
<button type="button" className="btn btn-primary" onClick={() => void submit()} disabled={!preflight || preflightLoading || metadataLoading || !canSubmit || !hasRequiredPrContent || submitting}>
|
||||
{submitting ? <RefreshCw size={14} className="spin" /> : null}
|
||||
{draft ? t("pr.createDraftPr", "Create draft PR") : t("pr.createPr", "Create PR")}
|
||||
</button>
|
||||
|
||||
@@ -87,6 +87,32 @@ function stubPointerCapture(element: HTMLElement) {
|
||||
Object.defineProperty(element, "releasePointerCapture", { configurable: true, value: vi.fn() });
|
||||
}
|
||||
|
||||
function expectMetadataLoadingAffordance() {
|
||||
const titleInput = screen.getByLabelText(/title/i);
|
||||
const bodyInput = screen.getByLabelText(/body/i);
|
||||
expect(screen.getByText(/generating ai title/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/generating ai body/i)).toBeInTheDocument();
|
||||
expect(screen.getByTestId("pr-title-loading-skeleton")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("pr-body-loading-skeleton")).toBeInTheDocument();
|
||||
expect(titleInput).toBeDisabled();
|
||||
expect(bodyInput).toBeDisabled();
|
||||
expect(titleInput).toHaveAttribute("aria-busy", "true");
|
||||
expect(bodyInput).toHaveAttribute("aria-busy", "true");
|
||||
}
|
||||
|
||||
function expectMetadataLoadingCleared() {
|
||||
const titleInput = screen.getByLabelText(/title/i);
|
||||
const bodyInput = screen.getByLabelText(/body/i);
|
||||
expect(screen.queryByText(/generating ai title/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/generating ai body/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("pr-title-loading-skeleton")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("pr-body-loading-skeleton")).not.toBeInTheDocument();
|
||||
expect(titleInput).toBeEnabled();
|
||||
expect(bodyInput).toBeEnabled();
|
||||
expect(titleInput).not.toHaveAttribute("aria-busy");
|
||||
expect(bodyInput).not.toHaveAttribute("aria-busy");
|
||||
}
|
||||
|
||||
describe("PrCreateModal", () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
@@ -408,7 +434,7 @@ describe("PrCreateModal", () => {
|
||||
expect(document.querySelectorAll(".pr-create-modal__commit-row")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("renders preflight and options before metadata resolves", async () => {
|
||||
it("renders preflight and options while metadata shows a disabled loading affordance, then clears into AI content", async () => {
|
||||
const metadataDeferred = createDeferred<typeof metadata>();
|
||||
mocks.generatePrMetadata.mockReturnValueOnce(metadataDeferred.promise);
|
||||
|
||||
@@ -417,20 +443,19 @@ describe("PrCreateModal", () => {
|
||||
expect(await screen.findByText("Branch pushed to remote")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/base branch/i)).toBeEnabled();
|
||||
expect(screen.getByText("Reviewers")).toBeInTheDocument();
|
||||
expect(screen.getByText(/generating ai title/i)).toBeInTheDocument();
|
||||
expectMetadataLoadingAffordance();
|
||||
expect(screen.getByRole("button", { name: "Create PR" })).toBeDisabled();
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/title/i), { target: { value: "Manual PR title" } });
|
||||
expect(screen.getByRole("button", { name: "Create PR" })).toBeDisabled();
|
||||
fireEvent.change(screen.getByLabelText(/body/i), { target: { value: "Manual PR body" } });
|
||||
await waitFor(() => expect(screen.getByRole("button", { name: "Create PR" })).toBeEnabled());
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText("Filter reviewers"), { target: { value: "rev" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: /reviewer 1/i }));
|
||||
expect(screen.getByRole("button", { name: /remove reviewer 1/i })).toBeInTheDocument();
|
||||
|
||||
metadataDeferred.resolve(metadata);
|
||||
expect(await screen.findByDisplayValue("Manual PR title")).toBeInTheDocument();
|
||||
expect(await screen.findByDisplayValue("AI title")).toBeInTheDocument();
|
||||
const bodyInput = screen.getByLabelText(/body/i) as HTMLTextAreaElement;
|
||||
expect(bodyInput.value).toContain("## Summary");
|
||||
expectMetadataLoadingCleared();
|
||||
expect(screen.getByRole("button", { name: "Create PR" })).toBeEnabled();
|
||||
});
|
||||
|
||||
it("keeps submit disabled while preflight is pending or failed", async () => {
|
||||
@@ -448,13 +473,20 @@ describe("PrCreateModal", () => {
|
||||
expect(screen.getByRole("button", { name: "Create PR" })).toBeDisabled();
|
||||
});
|
||||
|
||||
it("keeps metadata failure scoped so the modal remains usable", async () => {
|
||||
mocks.generatePrMetadata.mockRejectedValueOnce(new Error("metadata blew up"));
|
||||
it("clears metadata loading affordance into the error and manual fallback state", async () => {
|
||||
const metadataDeferred = createDeferred<typeof metadata>();
|
||||
mocks.generatePrMetadata.mockReturnValueOnce(metadataDeferred.promise);
|
||||
renderModal();
|
||||
|
||||
expect(await screen.findByText("Branch pushed to remote")).toBeInTheDocument();
|
||||
expectMetadataLoadingAffordance();
|
||||
|
||||
metadataDeferred.reject(new Error("metadata blew up"));
|
||||
|
||||
expect(await screen.findByText("metadata blew up")).toBeInTheDocument();
|
||||
expect(screen.getByText("Branch pushed to remote")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/base branch/i)).toBeEnabled();
|
||||
expectMetadataLoadingCleared();
|
||||
|
||||
const bodyInput = screen.getByLabelText(/body/i) as HTMLTextAreaElement;
|
||||
expect(bodyInput.value).toContain("## Summary");
|
||||
@@ -550,7 +582,7 @@ describe("PrCreateModal", () => {
|
||||
expect(screen.getByLabelText(/base branch/i)).toBeDisabled();
|
||||
});
|
||||
|
||||
it("regenerates AI content without re-blocking preflight and options", async () => {
|
||||
it("regenerates AI content without re-blocking preflight and options while restoring the loading affordance", async () => {
|
||||
const regenerateDeferred = createDeferred<typeof metadata>();
|
||||
mocks.generatePrMetadata.mockResolvedValueOnce(metadata).mockReturnValueOnce(regenerateDeferred.promise);
|
||||
await renderModalLoaded();
|
||||
@@ -560,9 +592,13 @@ describe("PrCreateModal", () => {
|
||||
fireEvent.click(screen.getAllByRole("button", { name: /^regenerate$/i })[0]);
|
||||
expect(screen.getByText("Branch pushed to remote")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/base branch/i)).toBeEnabled();
|
||||
expect(screen.getByText(/generating ai title/i)).toBeInTheDocument();
|
||||
expectMetadataLoadingAffordance();
|
||||
expect(screen.getByRole("button", { name: "Create PR" })).toBeDisabled();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Create PR" }));
|
||||
expect(mocks.createPr).not.toHaveBeenCalled();
|
||||
regenerateDeferred.resolve({ title: "New title", body: "New body", templateUsed: false });
|
||||
await screen.findByDisplayValue("New title");
|
||||
expectMetadataLoadingCleared();
|
||||
fireEvent.change(screen.getByDisplayValue("New title"), { target: { value: "edited" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: /revert to ai version/i }));
|
||||
expect(screen.getByDisplayValue("New title")).toBeInTheDocument();
|
||||
|
||||
@@ -30,7 +30,7 @@ vi.mock("@fusion/engine", () => ({
|
||||
}));
|
||||
|
||||
import { createFnAgent } from "@fusion/engine";
|
||||
import { generatePrMetadata } from "../pr-metadata-generator.js";
|
||||
import { generatePrMetadata, PR_METADATA_SYSTEM_PROMPT } from "../pr-metadata-generator.js";
|
||||
|
||||
function createTask(): Task {
|
||||
return {
|
||||
@@ -71,11 +71,7 @@ function capturedSystemPrompt(): string {
|
||||
return call?.systemPrompt ?? "";
|
||||
}
|
||||
|
||||
const BASE_PR_METADATA_SYSTEM_PROMPT = [
|
||||
"Generate GitHub PR metadata.",
|
||||
"Respond with strict JSON only.",
|
||||
"Schema: {title, summary, changes, testing, linkedTask}",
|
||||
].join("\n");
|
||||
const BASE_PR_METADATA_SYSTEM_PROMPT = PR_METADATA_SYSTEM_PROMPT;
|
||||
|
||||
describe("generatePrMetadata", () => {
|
||||
let repoRoot: string;
|
||||
@@ -148,7 +144,8 @@ describe("generatePrMetadata", () => {
|
||||
});
|
||||
|
||||
expect(capturedSystemPrompt()).toBe(BASE_PR_METADATA_SYSTEM_PROMPT);
|
||||
expect(capturedSystemPrompt()).toContain("Schema: {title, summary, changes, testing, linkedTask}");
|
||||
expect(capturedSystemPrompt()).toContain("\"title\": string");
|
||||
expect(capturedSystemPrompt()).toContain("\"linkedTask\": string");
|
||||
expect(result.title).toBe("feat: add routes");
|
||||
expect(result.body).toContain("## Summary");
|
||||
});
|
||||
@@ -213,6 +210,39 @@ describe("generatePrMetadata", () => {
|
||||
expect(result.body).toContain("## Testing");
|
||||
});
|
||||
|
||||
it("sends the upgraded prompt contract and keeps all context inputs", async () => {
|
||||
await generatePrMetadata({
|
||||
task: createTask(),
|
||||
repoRoot,
|
||||
settings: {
|
||||
titleSummarizerProvider: "anthropic",
|
||||
titleSummarizerModelId: "claude-haiku",
|
||||
} as never,
|
||||
});
|
||||
|
||||
expect(PR_METADATA_SYSTEM_PROMPT).toContain("STRICT JSON ONLY");
|
||||
expect(PR_METADATA_SYSTEM_PROMPT).toContain("conventional-commit style");
|
||||
expect(PR_METADATA_SYSTEM_PROMPT).toContain("at most 72 characters");
|
||||
expect(PR_METADATA_SYSTEM_PROMPT).toContain("commit log and diff stat first");
|
||||
for (const field of ["title", "summary", "changes", "testing", "linkedTask"]) {
|
||||
expect(PR_METADATA_SYSTEM_PROMPT).toContain(field);
|
||||
}
|
||||
expect(vi.mocked(createFnAgent)).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
systemPrompt: PR_METADATA_SYSTEM_PROMPT,
|
||||
}),
|
||||
);
|
||||
|
||||
const contextPrompt = promptMock.mock.calls[0]?.[0] as string;
|
||||
expect(contextPrompt).toContain("Ground the PR title, summary, changes, and testing in the commit log and diff stat");
|
||||
expect(contextPrompt).toContain("Task ID: FN-4991");
|
||||
expect(contextPrompt).toContain("Task title: Route contracts");
|
||||
expect(contextPrompt).toContain("Task description: Implement route contracts");
|
||||
expect(contextPrompt).toContain("Commit log (source of truth):\n\ncommit");
|
||||
expect(contextPrompt).toContain("Diff stat (source of truth):\n\n1 file changed");
|
||||
expect(contextPrompt).toContain("Task prompt (supporting context):\n\n# Prompt");
|
||||
});
|
||||
|
||||
it("fills known sections when template exists and preserves unknown headings", async () => {
|
||||
mkdirSync(join(repoRoot, ".github"), { recursive: true });
|
||||
writeFileSync(
|
||||
|
||||
@@ -9,6 +9,22 @@ import { createFnAgent, resolveMcpServersForStore } from "@fusion/engine";
|
||||
const execAsync = promisify(execCb);
|
||||
export const PR_METADATA_TIMEOUT_MS = 60_000;
|
||||
|
||||
/*
|
||||
FNXC:PrMetadataGeneration 2026-06-28-00:09:
|
||||
The Create PR AI prompt must produce operator-ready metadata grounded in the commit log and diff stat while preserving the strict JSON schema consumed by parseAiResult, buildBody, and fillTemplate. Keep the schema stable so later configurable-prompt work can reuse this default without changing downstream body assembly.
|
||||
*/
|
||||
export const PR_METADATA_SYSTEM_PROMPT = [
|
||||
"You generate GitHub pull request metadata for Fusion tasks.",
|
||||
"Respond with STRICT JSON ONLY: no markdown fences, no commentary, no prose outside the JSON object.",
|
||||
"Use exactly this schema: {\"title\": string, \"summary\": string, \"changes\": string, \"testing\": string, \"linkedTask\": string}.",
|
||||
"Title: concise imperative conventional-commit style, at most 72 characters, no trailing period, with a scope when the change clearly suggests one.",
|
||||
"Summary: 1-3 short plain-sentence paragraphs explaining what changed and why; be specific and avoid marketing fluff.",
|
||||
"Changes: a tight markdown bullet list of concrete, verifiable changes supported by the commit log or diff stat; do not speculate.",
|
||||
"Testing: markdown bullets naming actual verification shown in the evidence; if no testing evidence exists, say '- Not provided.' or '- Not run (not provided).'.",
|
||||
"LinkedTask: set to the task ID exactly as provided.",
|
||||
"Ground every field in the commit log and diff stat first, then use the task title, task description, and task prompt only as supporting context.",
|
||||
].join("\n");
|
||||
|
||||
export interface GeneratedPrMetadata {
|
||||
title: string;
|
||||
body: string;
|
||||
@@ -270,11 +286,7 @@ export async function generatePrMetadata(input: {
|
||||
const template = templateExists ? await raceWithAbort(readFile(templatePath, "utf8"), combinedSignal) : "";
|
||||
|
||||
const model = resolveTitleSummarizerSettingsModel(settings as Partial<Settings>);
|
||||
const systemPrompt = [
|
||||
"Generate GitHub PR metadata.",
|
||||
"Respond with strict JSON only.",
|
||||
"Schema: {title, summary, changes, testing, linkedTask}",
|
||||
];
|
||||
const systemPrompt = [PR_METADATA_SYSTEM_PROMPT];
|
||||
const titleGuidance = settings.prTitlePromptInstructions?.trim();
|
||||
const descriptionGuidance = settings.prDescriptionPromptInstructions?.trim();
|
||||
/*
|
||||
@@ -307,15 +319,20 @@ export async function generatePrMetadata(input: {
|
||||
|
||||
try {
|
||||
const contextPrompt = [
|
||||
"Source-of-truth guidance:",
|
||||
"- Ground the PR title, summary, changes, and testing in the commit log and diff stat below.",
|
||||
"- Use task title, task description, and task prompt only to clarify intent when the git evidence supports it.",
|
||||
"- Omit speculation; when evidence for testing or a change is absent, say so instead of inventing details.",
|
||||
"- Return only strict JSON matching the required schema.",
|
||||
`Task ID: ${task.id}`,
|
||||
`Task title: ${task.title}`,
|
||||
`Task description: ${task.description ?? ""}`,
|
||||
`Base branch: ${baseBranch}`,
|
||||
"Commit log:",
|
||||
"Commit log (source of truth):",
|
||||
logOut || "(none)",
|
||||
"Diff stat:",
|
||||
"Diff stat (source of truth):",
|
||||
diffStatOut || "(none)",
|
||||
"Task prompt:",
|
||||
"Task prompt (supporting context):",
|
||||
promptContent || "(none)",
|
||||
].join("\n\n");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user