From aeec68d22444f4e86c854ed081eabaaabe618e0a Mon Sep 17 00:00:00 2001 From: Nelson Pires Date: Tue, 3 Feb 2026 08:05:19 -0300 Subject: [PATCH] Add optional context to PR description generation (#270) * types: add optional context parameter to generatePullRequestDescription signature * api: update gitApi to pass optional context to PR description generation * http: include trimmed context in PR description API request body * ui: add additional context input for PR generation with desktop disclosure and mobile sheet * desktop: pass optional context to Tauri generate_pr_description command * tauri: add optional context parameter and inject into PR description prompt * server: read optional context from request and inject into PR description prompt * vscode: update type signature for context parameter (compatibility only) * fix(vscode): drop context from pr-description payload Remove context field from PR description payload Send only base and head to the bridge API for PR descriptions Clarify payload compatibility across web/desktop environments --- .../desktop/src-tauri/src/commands/git.rs | 23 +++-- packages/desktop/src/api/git.ts | 10 +- .../views/git/PullRequestSection.tsx | 92 ++++++++++++++++++- packages/ui/src/lib/api/types.ts | 2 +- packages/ui/src/lib/gitApi.ts | 2 +- packages/ui/src/lib/gitApiHttp.ts | 11 ++- packages/web/server/index.js | 18 +++- 7 files changed, 139 insertions(+), 19 deletions(-) diff --git a/packages/desktop/src-tauri/src/commands/git.rs b/packages/desktop/src-tauri/src/commands/git.rs index d9d016a1..6e094a6e 100644 --- a/packages/desktop/src-tauri/src/commands/git.rs +++ b/packages/desktop/src-tauri/src/commands/git.rs @@ -2343,6 +2343,7 @@ pub async fn generate_pr_description( directory: String, base: String, head: String, + context: Option, state: State<'_, DesktopRuntime>, ) -> Result { let root = validate_git_path(&directory, state.settings()) @@ -2398,8 +2399,8 @@ pub async fn generate_pr_description( } // 2. Construct PR-specific prompt - let prompt = format!( - r#"You are drafting a GitHub Pull Request title + description. Respond in JSON of the shape {{\"title\": string, \"body\": string}} (ONLY JSON in response, no markdown fences) with these rules: + let mut prompt = format!( + r#"You are drafting a GitHub Pull Request title + description. Respond in JSON of the shape {{"title": string, "body": string}} (ONLY JSON in response, no markdown fences) with these rules: - title: concise, sentence case, <= 80 chars, no trailing punctuation, no commit-style prefixes (no \"feat:\", \"fix:\") - body: GitHub-flavored markdown with these sections in this order: Summary, Testing, Notes - Summary: 3-6 bullet points describing user-visible changes; avoid internal helper function names @@ -2407,15 +2408,21 @@ pub async fn generate_pr_description( - Notes: bullet list; include breaking/rollout notes only when relevant Context: - base branch: {base} -- head branch: {head} - -Diff summary: -{diffs}"#, +- head branch: {head}"#, base = base.trim(), - head = head.trim(), - diffs = diff_summaries + head = head.trim() ); + // Include additional context if provided + if let Some(ctx) = context { + let trimmed = ctx.trim(); + if !trimmed.is_empty() { + prompt.push_str(&format!("\n\nAdditional context provided by user:\n{}", trimmed)); + } + } + + prompt.push_str(&format!("\n\nDiff summary:\n{}", diff_summaries)); + let model = "gpt-5-nano"; // 3. Call API diff --git a/packages/desktop/src/api/git.ts b/packages/desktop/src/api/git.ts index 09083db3..9747240d 100644 --- a/packages/desktop/src/api/git.ts +++ b/packages/desktop/src/api/git.ts @@ -111,13 +111,17 @@ export const createDesktopGitAPI = (): GitAPI => ({ async generatePullRequestDescription( directory: string, - payload: { base: string; head: string } + payload: { base: string; head: string; context?: string } ): Promise { - return safeGitInvoke('generate_pr_description', { + const params: { directory: string; base: string; head: string; context?: string } = { directory, base: payload.base, head: payload.head, - }); + }; + if (payload.context?.trim()) { + params.context = payload.context.trim(); + } + return safeGitInvoke('generate_pr_description', params); }, async listGitWorktrees(directory: string): Promise { diff --git a/packages/ui/src/components/views/git/PullRequestSection.tsx b/packages/ui/src/components/views/git/PullRequestSection.tsx index c672983a..5d0dfbef 100644 --- a/packages/ui/src/components/views/git/PullRequestSection.tsx +++ b/packages/ui/src/components/views/git/PullRequestSection.tsx @@ -25,6 +25,8 @@ import { } from '@/components/ui/collapsible'; import { generatePullRequestDescription } from '@/lib/gitApi'; import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs'; +import { useDeviceInfo } from '@/lib/device'; +import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel'; import { useUIStore } from '@/stores/useUIStore'; import { useMessageStore } from '@/stores/messageStore'; import { useSessionStore } from '@/stores/useSessionStore'; @@ -66,6 +68,7 @@ type PullRequestDraftSnapshot = { body: string; draft: boolean; isOpen: boolean; + additionalContext: string; }; const pullRequestDraftSnapshots = new Map(); @@ -102,6 +105,7 @@ export const PullRequestSection: React.FC<{ const setSidebarSection = useUIStore((state) => state.setSidebarSection); const setActiveMainTab = useUIStore((state) => state.setActiveMainTab); const currentSessionId = useSessionStore((state) => state.currentSessionId); + const { isMobile } = useDeviceInfo(); const openGitHubSettings = React.useCallback(() => { setSidebarSection('settings'); @@ -122,6 +126,7 @@ export const PullRequestSection: React.FC<{ const [title, setTitle] = React.useState(() => initialSnapshot?.title ?? branchToTitle(branch)); const [body, setBody] = React.useState(() => initialSnapshot?.body ?? ''); const [draft, setDraft] = React.useState(() => initialSnapshot?.draft ?? false); + const [additionalContext, setAdditionalContext] = React.useState(() => initialSnapshot?.additionalContext ?? ''); const [mergeMethod, setMergeMethod] = React.useState('squash'); const [isGenerating, setIsGenerating] = React.useState(false); @@ -129,6 +134,9 @@ export const PullRequestSection: React.FC<{ const [isMerging, setIsMerging] = React.useState(false); const [isMarkingReady, setIsMarkingReady] = React.useState(false); + const [isContextOpen, setIsContextOpen] = React.useState(false); + const [isContextSheetOpen, setIsContextSheetOpen] = React.useState(false); + const [checksDialogOpen, setChecksDialogOpen] = React.useState(false); const [checkDetails, setCheckDetails] = React.useState(null); const [isLoadingCheckDetails, setIsLoadingCheckDetails] = React.useState(false); @@ -407,8 +415,9 @@ export const PullRequestSection: React.FC<{ body, draft, isOpen, + additionalContext, }); - }, [snapshotKey, title, body, draft, isOpen, directory, branch]); + }, [snapshotKey, title, body, draft, isOpen, additionalContext, directory, branch]); const generateDescription = React.useCallback(async () => { if (isGenerating) return; @@ -418,6 +427,7 @@ export const PullRequestSection: React.FC<{ const generated = await generatePullRequestDescription(directory, { base: baseBranch, head: branch, + context: additionalContext, }); if (generated.title?.trim()) { @@ -433,7 +443,7 @@ export const PullRequestSection: React.FC<{ } finally { setIsGenerating(false); } - }, [baseBranch, branch, directory, isGenerating]); + }, [baseBranch, branch, directory, isGenerating, additionalContext]); const createPr = React.useCallback(async () => { if (!github?.prCreate) { @@ -731,6 +741,84 @@ export const PullRequestSection: React.FC<{ Draft + {/* Additional Context Section */} + {isMobile ? ( +
+
+ + Additional context (optional) + + +
+ {additionalContext.trim() && ( +
+ + Context added + +
+ )} +
+ ) : ( + + + + Additional context (optional) + + + {isContextOpen ? 'Hide' : additionalContext.trim() ? 'Edit' : 'Add'} + + + +
+