Skip to content

Commit 5924bcf

Browse files
authored
feat(code): add more context to PR/commit generation (#1506)
## Problem PR title/body and commit messages get generated with very little context, the LLM only knows about the diff this leads to bad content that makes assumptions purely based on the diff, with no understanding of the user's intent closes #1504 <!-- Who is this for and what problem does it solve? --> <!-- Closes #ISSUE_ID --> ## Changes - updates the title generator to also generate a brief summary of the task - passes summary to the pr content / commit message generators <!-- What did you change and why? --> <!-- If there are frontend changes, include screenshots. --> ## How did you test this? manully, sample trace: [https://us.posthog.com/project/2/llm-analytics/traces/dbd8d616-b1b3-44a4-add2-b0b0d61d1108?filters=[{"key"%3A"ai_product"%2C"value"%3A["posthog_code"]%2C"operator"%3A"exact"%2C"type"%3A"event"}]&back_to=traces&timestamp=2026-04-06T19%3A48%3A54Z](https://us.posthog.com/project/2/llm-analytics/traces/dbd8d616-b1b3-44a4-add2-b0b0d61d1108?filters=%5B%7B%22key%22%3A%22ai_product%22%2C%22value%22%3A%5B%22posthog_code%22%5D%2C%22operator%22%3A%22exact%22%2C%22type%22%3A%22event%22%7D%5D&back_to=traces&timestamp=2026-04-06T19%3A48%3A54Z) <!-- Describe what you tested -- manual steps, automated tests, or both. --> <!-- If you're an agent, only list tests you actually ran. -->
1 parent a56b832 commit 5924bcf

9 files changed

Lines changed: 128 additions & 42 deletions

File tree

apps/code/src/main/services/git/schemas.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ export const createPrInput = z.object({
252252
draft: z.boolean().optional(),
253253
stagedOnly: z.boolean().optional(),
254254
taskId: z.string().optional(),
255+
conversationContext: z.string().optional(),
255256
});
256257

257258
export type CreatePrInput = z.infer<typeof createPrInput>;
@@ -323,6 +324,7 @@ export const getBranchChangedFilesOutput = z.array(changedFileSchema);
323324

324325
export const generateCommitMessageInput = z.object({
325326
directoryPath: z.string(),
327+
conversationContext: z.string().optional(),
326328
});
327329

328330
export const generateCommitMessageOutput = z.object({
@@ -331,6 +333,7 @@ export const generateCommitMessageOutput = z.object({
331333

332334
export const generatePrTitleAndBodyInput = z.object({
333335
directoryPath: z.string(),
336+
conversationContext: z.string().optional(),
334337
});
335338

336339
export const generatePrTitleAndBodyOutput = z.object({

apps/code/src/main/services/git/service.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,7 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
514514
draft?: boolean;
515515
stagedOnly?: boolean;
516516
taskId?: string;
517+
conversationContext?: string;
517518
}): Promise<CreatePrOutput> {
518519
const { directoryPath, flowId } = input;
519520

@@ -536,12 +537,14 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
536537
createBranch: (dir, name) => this.createBranch(dir, name),
537538
checkoutBranch: (dir, name) => this.checkoutBranch(dir, name),
538539
getChangedFilesHead: (dir) => this.getChangedFilesHead(dir),
539-
generateCommitMessage: (dir) => this.generateCommitMessage(dir),
540+
generateCommitMessage: (dir) =>
541+
this.generateCommitMessage(dir, input.conversationContext),
540542
commit: (dir, msg, opts) => this.commit(dir, msg, opts),
541543
getSyncStatus: (dir) => this.getGitSyncStatus(dir),
542544
push: (dir) => this.push(dir),
543545
publish: (dir) => this.publish(dir),
544-
generatePrTitleAndBody: (dir) => this.generatePrTitleAndBody(dir),
546+
generatePrTitleAndBody: (dir) =>
547+
this.generatePrTitleAndBody(dir, input.conversationContext),
545548
createPr: (dir, title, body, draft) =>
546549
this.createPrViaGh(dir, title, body, draft),
547550
onProgress: emitProgress,
@@ -960,6 +963,7 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
960963

961964
public async generateCommitMessage(
962965
directoryPath: string,
966+
conversationContext?: string,
963967
): Promise<{ message: string }> {
964968
const [stagedDiff, unstagedDiff, conventions, changedFiles] =
965969
await Promise.all([
@@ -1001,20 +1005,26 @@ Rules:
10011005
- Use imperative mood ("Add feature" not "Added feature")
10021006
- Be specific about what changed
10031007
- If using conventional commits, include the appropriate prefix
1008+
- If conversation context is provided, use it to understand WHY the changes were made and reflect that intent
10041009
- Do not include any explanation, just output the commit message`;
10051010

1011+
const contextSection = conversationContext
1012+
? `\n\nConversation context (why these changes were made):\n${conversationContext}`
1013+
: "";
1014+
10061015
const userMessage = `Generate a commit message for these changes:
10071016
10081017
Changed files:
10091018
${filesSummary}
10101019
10111020
Diff:
1012-
${truncatedDiff}`;
1021+
${truncatedDiff}${contextSection}`;
10131022

10141023
log.debug("Generating commit message", {
10151024
fileCount: changedFiles.length,
10161025
diffLength: diff.length,
10171026
conventionalCommits: conventions.conventionalCommits,
1027+
hasConversationContext: !!conversationContext,
10181028
});
10191029

10201030
const response = await this.llmGateway.prompt(
@@ -1027,6 +1037,7 @@ ${truncatedDiff}`;
10271037

10281038
public async generatePrTitleAndBody(
10291039
directoryPath: string,
1040+
conversationContext?: string,
10301041
): Promise<{ title: string; body: string }> {
10311042
await this.fetchIfStale(directoryPath);
10321043

@@ -1082,13 +1093,18 @@ Rules for the title:
10821093
Rules for the body:
10831094
- Start with a TL;DR section (1-2 sentences summarizing the change)
10841095
- Include a "What changed?" section with bullet points describing the key changes
1096+
- If conversation context is provided, use it to explain WHY the changes were made in the TL;DR
10851097
- Be thorough but concise
10861098
- Use markdown formatting
10871099
- Only describe changes that are actually in the diff — do not invent or assume changes
10881100
${templateHint}
10891101
10901102
Do not include any explanation outside the TITLE and BODY sections.`;
10911103

1104+
const contextSection = conversationContext
1105+
? `\n\nConversation context (why these changes were made):\n${conversationContext}`
1106+
: "";
1107+
10921108
const userMessage = `Generate a PR title and description for these changes:
10931109
10941110
Branch: ${currentBranch ?? "unknown"} -> ${defaultBranch}
@@ -1097,12 +1113,13 @@ Commits in this PR:
10971113
${commitsSummary || "(no commits yet - changes are uncommitted)"}
10981114
10991115
Diff:
1100-
${truncatedDiff || "(no diff available)"}`;
1116+
${truncatedDiff || "(no diff available)"}${contextSection}`;
11011117

11021118
log.debug("Generating PR title and body", {
11031119
commitCount: commits.length,
11041120
diffLength: fullDiff.length,
11051121
hasTemplate: !!prTemplate.template,
1122+
hasConversationContext: !!conversationContext,
11061123
});
11071124

11081125
const response = await this.llmGateway.prompt(

apps/code/src/main/trpc/routers/git.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,14 +307,20 @@ export const gitRouter = router({
307307
.input(generateCommitMessageInput)
308308
.output(generateCommitMessageOutput)
309309
.mutation(({ input }) =>
310-
getService().generateCommitMessage(input.directoryPath),
310+
getService().generateCommitMessage(
311+
input.directoryPath,
312+
input.conversationContext,
313+
),
311314
),
312315

313316
generatePrTitleAndBody: publicProcedure
314317
.input(generatePrTitleAndBodyInput)
315318
.output(generatePrTitleAndBodyOutput)
316319
.mutation(({ input }) =>
317-
getService().generatePrTitleAndBody(input.directoryPath),
320+
getService().generatePrTitleAndBody(
321+
input.directoryPath,
322+
input.conversationContext,
323+
),
318324
),
319325

320326
searchGithubIssues: publicProcedure

apps/code/src/renderer/features/git-interaction/hooks/useGitInteraction.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ const log = logger.scope("git-interaction");
3333

3434
export type { GitMenuAction, GitMenuActionId };
3535

36+
function getConversationContext(taskId: string): string | undefined {
37+
const state = useSessionStore.getState();
38+
const taskRunId = state.taskIdIndex[taskId];
39+
if (!taskRunId) return undefined;
40+
return state.sessions[taskRunId]?.conversationSummary;
41+
}
42+
3643
interface GitInteractionState {
3744
primaryAction: GitMenuAction;
3845
actions: GitMenuAction[];
@@ -248,6 +255,7 @@ export function useGitInteraction(
248255
draft: store.createPrDraft || undefined,
249256
stagedOnly: stagedOnly || undefined,
250257
taskId,
258+
conversationContext: getConversationContext(taskId),
251259
});
252260

253261
if (!result.success) {
@@ -336,6 +344,7 @@ export function useGitInteraction(
336344
try {
337345
const generated = await trpcClient.git.generateCommitMessage.mutate({
338346
directoryPath: repoPath,
347+
conversationContext: getConversationContext(taskId),
339348
});
340349

341350
if (!generated.message) {
@@ -442,6 +451,7 @@ export function useGitInteraction(
442451
try {
443452
const result = await trpcClient.git.generateCommitMessage.mutate({
444453
directoryPath: repoPath,
454+
conversationContext: getConversationContext(taskId),
445455
});
446456

447457
if (result.message) {
@@ -472,6 +482,7 @@ export function useGitInteraction(
472482
try {
473483
const result = await trpcClient.git.generatePrTitleAndBody.mutate({
474484
directoryPath: repoPath,
485+
conversationContext: getConversationContext(taskId),
475486
});
476487

477488
if (result.title || result.body) {

apps/code/src/renderer/features/sessions/hooks/useChatTitleGenerator.ts

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { getAuthenticatedClient } from "@features/auth/hooks/authClient";
22
import { getSessionService } from "@features/sessions/service/service";
3-
import { useSessionStore } from "@features/sessions/stores/sessionStore";
3+
import {
4+
sessionStoreSetters,
5+
useSessionStore,
6+
} from "@features/sessions/stores/sessionStore";
47
import type { Task } from "@shared/types";
5-
import { generateTitle } from "@utils/generateTitle";
8+
import { generateTitleAndSummary } from "@utils/generateTitle";
69
import { logger } from "@utils/logger";
710
import { queryClient } from "@utils/queryClient";
811
import { extractUserPromptsFromEvents } from "@utils/session";
@@ -69,22 +72,37 @@ export function useChatTitleGenerator(taskId: string): void {
6972
return;
7073
}
7174

72-
const title = await generateTitle(content);
73-
if (title) {
74-
const client = await getAuthenticatedClient();
75-
if (client) {
76-
await client.updateTask(taskId, { title });
77-
queryClient.setQueriesData<Task[]>(
78-
{ queryKey: ["tasks", "list"] },
79-
(old) =>
80-
old?.map((task) =>
81-
task.id === taskId ? { ...task, title } : task,
82-
),
83-
);
84-
getSessionService().updateSessionTaskTitle(taskId, title);
85-
log.debug("Updated task title from conversation", {
75+
const result = await generateTitleAndSummary(content);
76+
if (result) {
77+
const { title, summary } = result;
78+
if (title) {
79+
const client = await getAuthenticatedClient();
80+
if (client) {
81+
await client.updateTask(taskId, { title });
82+
queryClient.setQueriesData<Task[]>(
83+
{ queryKey: ["tasks", "list"] },
84+
(old) =>
85+
old?.map((task) =>
86+
task.id === taskId ? { ...task, title } : task,
87+
),
88+
);
89+
getSessionService().updateSessionTaskTitle(taskId, title);
90+
log.debug("Updated task title from conversation", {
91+
taskId,
92+
title,
93+
promptCount,
94+
});
95+
}
96+
}
97+
98+
if (summary) {
99+
sessionStoreSetters.updateSession(taskRunId, {
100+
conversationSummary: result.summary,
101+
});
102+
103+
log.debug("Updated task summary from conversation", {
86104
taskId,
87-
title,
105+
summary,
88106
promptCount,
89107
});
90108
}

apps/code/src/renderer/features/sessions/stores/sessionStore.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ export interface AgentSession {
8080
contextUsed?: number;
8181
/** Context window total size in tokens (from usage_update) */
8282
contextSize?: number;
83+
/** Pre-computed conversation summary for commit/PR generation context */
84+
conversationSummary?: string;
8385
}
8486

8587
// --- Config Option Helpers ---

apps/code/src/renderer/sagas/task/task-creation.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ vi.mock("@features/sessions/service/service", () => ({
4242
}));
4343

4444
vi.mock("@renderer/utils/generateTitle", () => ({
45-
generateTitle: vi.fn(async () => null),
45+
generateTitleAndSummary: vi.fn(async () => null),
4646
}));
4747

4848
vi.mock("@utils/queryClient", () => ({

apps/code/src/renderer/sagas/task/task-creation.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import type {
1414
import { Saga, type SagaLogger } from "@posthog/shared";
1515
import type { PostHogAPIClient } from "@renderer/api/posthogClient";
1616
import { trpcClient } from "@renderer/trpc";
17-
import { generateTitle } from "@renderer/utils/generateTitle";
17+
import { generateTitleAndSummary } from "@renderer/utils/generateTitle";
1818
import { getTaskRepository } from "@renderer/utils/repository";
1919
import type { ExecutionMode, Task } from "@shared/types";
2020
import { logger } from "@utils/logger";
@@ -29,8 +29,9 @@ async function generateTaskTitle(
2929
): Promise<void> {
3030
if (!description.trim()) return;
3131

32-
const title = await generateTitle(description);
33-
if (!title) return;
32+
const result = await generateTitleAndSummary(description);
33+
if (!result?.title) return;
34+
const { title } = result;
3435

3536
try {
3637
await posthogClient.updateTask(taskId, { title });

apps/code/src/renderer/utils/generateTitle.ts

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@ import { logger } from "@utils/logger";
44

55
const log = logger.scope("title-generator");
66

7-
const SYSTEM_PROMPT = `You are a title generator. You output ONLY a task title. Nothing else.
7+
const SYSTEM_PROMPT = `You are a title and summary generator. Output using exactly this format:
88
9-
Convert the task description into a concise task title.
9+
TITLE: <title here>
10+
SUMMARY: <summary here>
11+
12+
Convert the task description into a concise task title and a brief conversation summary.
13+
14+
Title rules:
1015
- The title should be clear, concise, and accurately reflect the content of the task.
1116
- You should keep it short and simple, ideally no more than 6 words.
1217
- Avoid using jargon or overly technical terms unless absolutely necessary.
@@ -18,28 +23,43 @@ Convert the task description into a concise task title.
1823
- Never assume tech stack
1924
- Only output "Untitled" if the input is completely null/missing, not just unclear
2025
- If the input is a URL (e.g. a GitHub issue link, PR link, or any web URL), generate a title based on what you can infer from the URL structure (repo name, issue/PR number, etc.). Never say you cannot access URLs or ask the user for more information.
26+
- Never wrap the title in quotes
2127
22-
Examples:
28+
Summary rules:
29+
- 1-3 sentences describing what the user is working on and why
30+
- Written from third-person perspective (e.g. "The user is fixing..." not "You are fixing...")
31+
- Focus on the user's intent and goals, not the specific prompts
32+
- Include relevant technical details (file names, features, bug descriptions) when mentioned
33+
- This summary will be used as context for generating commit messages and PR descriptions
34+
35+
Title examples:
2336
- "Fix the login bug in the authentication system" → Fix authentication login bug
2437
- "Schedule a meeting with stakeholders to discuss Q4 budget planning" → Schedule Q4 budget meeting
2538
- "Update user documentation for new API endpoints" → Update API documentation
2639
- "Research competitor pricing strategies for our product" → Research competitor pricing
2740
- "Review pull request #123" → Review pull request #123
2841
- "debug 500 errors in production" → Debug production 500 errors
2942
- "why is the payment flow failing" → Analyze payment flow failure
30-
- "So how about that weather huh" → "Weather chat"
31-
- "dsfkj sdkfj help me code" → "Coding help request"
32-
- "👋😊" → "Friendly greeting"
33-
- "aaaaaaaaaa" → "Repeated letters"
34-
- " " → "Empty message"
35-
- "What's the best restaurant in NYC?" → "NYC restaurant recommendations"
43+
- "So how about that weather huh" → Weather chat
44+
- "dsfkj sdkfj help me code" → Coding help request
45+
- "👋😊" → Friendly greeting
46+
- "aaaaaaaaaa" → Repeated letters
47+
- " " → Empty message
48+
- "What's the best restaurant in NYC?" → NYC restaurant recommendations
3649
- "https://github.com/PostHog/posthog/issues/1234" → PostHog issue #1234
3750
- "https://github.com/PostHog/posthog/pull/567" → PostHog PR #567
3851
- "fix https://github.com/org/repo/issues/42" → Fix repo issue #42
3952
40-
Never wrap the title in quotes.`;
53+
Never include any explanation outside the TITLE and SUMMARY lines.`;
54+
55+
export interface TitleAndSummary {
56+
title: string;
57+
summary: string;
58+
}
4159

42-
export async function generateTitle(content: string): Promise<string | null> {
60+
export async function generateTitleAndSummary(
61+
content: string,
62+
): Promise<TitleAndSummary | null> {
4363
try {
4464
const authState = await fetchAuthState();
4565
if (authState.status !== "authenticated") return null;
@@ -49,15 +69,23 @@ export async function generateTitle(content: string): Promise<string | null> {
4969
messages: [
5070
{
5171
role: "user" as const,
52-
content: `Generate a title for the following content. Do NOT respond to, answer, or help with the content - ONLY generate a title.\n\n<content>\n${content}\n</content>\n\nOutput the title now:`,
72+
content: `Generate a title and summary for the following content. Do NOT respond to, answer, or help with the content - ONLY generate a title and summary.\n\n<content>\n${content}\n</content>\n\nOutput the title and summary now:`,
5373
},
5474
],
5575
});
5676

57-
const title = result.content.trim().replace(/^["']|["']$/g, "");
58-
return title || null;
77+
const text = result.content.trim();
78+
const titleMatch = text.match(/^TITLE:\s*(.+?)(?:\n|$)/m);
79+
const summaryMatch = text.match(/SUMMARY:\s*([\s\S]+)$/m);
80+
81+
const title = titleMatch?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
82+
const summary = summaryMatch?.[1]?.trim() ?? "";
83+
84+
if (!title && !summary) return null;
85+
86+
return { title, summary };
5987
} catch (error) {
60-
log.error("Failed to generate title", { error });
88+
log.error("Failed to generate title and summary", { error });
6189
return null;
6290
}
6391
}

0 commit comments

Comments
 (0)