Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/code/src/main/services/git/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ export const createPrInput = z.object({
draft: z.boolean().optional(),
stagedOnly: z.boolean().optional(),
taskId: z.string().optional(),
conversationContext: z.string().optional(),
});

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

export const generateCommitMessageInput = z.object({
directoryPath: z.string(),
conversationContext: z.string().optional(),
});

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

export const generatePrTitleAndBodyInput = z.object({
directoryPath: z.string(),
conversationContext: z.string().optional(),
});

export const generatePrTitleAndBodyOutput = z.object({
Expand Down
25 changes: 21 additions & 4 deletions apps/code/src/main/services/git/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,7 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
draft?: boolean;
stagedOnly?: boolean;
taskId?: string;
conversationContext?: string;
}): Promise<CreatePrOutput> {
const { directoryPath, flowId } = input;

Expand All @@ -536,12 +537,14 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
createBranch: (dir, name) => this.createBranch(dir, name),
checkoutBranch: (dir, name) => this.checkoutBranch(dir, name),
getChangedFilesHead: (dir) => this.getChangedFilesHead(dir),
generateCommitMessage: (dir) => this.generateCommitMessage(dir),
generateCommitMessage: (dir) =>
this.generateCommitMessage(dir, input.conversationContext),
commit: (dir, msg, opts) => this.commit(dir, msg, opts),
getSyncStatus: (dir) => this.getGitSyncStatus(dir),
push: (dir) => this.push(dir),
publish: (dir) => this.publish(dir),
generatePrTitleAndBody: (dir) => this.generatePrTitleAndBody(dir),
generatePrTitleAndBody: (dir) =>
this.generatePrTitleAndBody(dir, input.conversationContext),
createPr: (dir, title, body, draft) =>
this.createPrViaGh(dir, title, body, draft),
onProgress: emitProgress,
Expand Down Expand Up @@ -960,6 +963,7 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {

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

const contextSection = conversationContext
? `\n\nConversation context (why these changes were made):\n${conversationContext}`
: "";

const userMessage = `Generate a commit message for these changes:

Changed files:
${filesSummary}

Diff:
${truncatedDiff}`;
${truncatedDiff}${contextSection}`;

log.debug("Generating commit message", {
fileCount: changedFiles.length,
diffLength: diff.length,
conventionalCommits: conventions.conventionalCommits,
hasConversationContext: !!conversationContext,
});

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

public async generatePrTitleAndBody(
directoryPath: string,
conversationContext?: string,
): Promise<{ title: string; body: string }> {
await this.fetchIfStale(directoryPath);

Expand Down Expand Up @@ -1082,13 +1093,18 @@ Rules for the title:
Rules for the body:
- Start with a TL;DR section (1-2 sentences summarizing the change)
- Include a "What changed?" section with bullet points describing the key changes
- If conversation context is provided, use it to explain WHY the changes were made in the TL;DR
- Be thorough but concise
- Use markdown formatting
- Only describe changes that are actually in the diff — do not invent or assume changes
${templateHint}

Do not include any explanation outside the TITLE and BODY sections.`;

const contextSection = conversationContext
? `\n\nConversation context (why these changes were made):\n${conversationContext}`
: "";

const userMessage = `Generate a PR title and description for these changes:

Branch: ${currentBranch ?? "unknown"} -> ${defaultBranch}
Expand All @@ -1097,12 +1113,13 @@ Commits in this PR:
${commitsSummary || "(no commits yet - changes are uncommitted)"}

Diff:
${truncatedDiff || "(no diff available)"}`;
${truncatedDiff || "(no diff available)"}${contextSection}`;

log.debug("Generating PR title and body", {
commitCount: commits.length,
diffLength: fullDiff.length,
hasTemplate: !!prTemplate.template,
hasConversationContext: !!conversationContext,
});

const response = await this.llmGateway.prompt(
Expand Down
10 changes: 8 additions & 2 deletions apps/code/src/main/trpc/routers/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,14 +307,20 @@ export const gitRouter = router({
.input(generateCommitMessageInput)
.output(generateCommitMessageOutput)
.mutation(({ input }) =>
getService().generateCommitMessage(input.directoryPath),
getService().generateCommitMessage(
input.directoryPath,
input.conversationContext,
),
),

generatePrTitleAndBody: publicProcedure
.input(generatePrTitleAndBodyInput)
.output(generatePrTitleAndBodyOutput)
.mutation(({ input }) =>
getService().generatePrTitleAndBody(input.directoryPath),
getService().generatePrTitleAndBody(
input.directoryPath,
input.conversationContext,
),
),

searchGithubIssues: publicProcedure
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ const log = logger.scope("git-interaction");

export type { GitMenuAction, GitMenuActionId };

function getConversationContext(taskId: string): string | undefined {
const state = useSessionStore.getState();
const taskRunId = state.taskIdIndex[taskId];
if (!taskRunId) return undefined;
return state.sessions[taskRunId]?.conversationSummary;
}

interface GitInteractionState {
primaryAction: GitMenuAction;
actions: GitMenuAction[];
Expand Down Expand Up @@ -248,6 +255,7 @@ export function useGitInteraction(
draft: store.createPrDraft || undefined,
stagedOnly: stagedOnly || undefined,
taskId,
conversationContext: getConversationContext(taskId),
});

if (!result.success) {
Expand Down Expand Up @@ -336,6 +344,7 @@ export function useGitInteraction(
try {
const generated = await trpcClient.git.generateCommitMessage.mutate({
directoryPath: repoPath,
conversationContext: getConversationContext(taskId),
});

if (!generated.message) {
Expand Down Expand Up @@ -442,6 +451,7 @@ export function useGitInteraction(
try {
const result = await trpcClient.git.generateCommitMessage.mutate({
directoryPath: repoPath,
conversationContext: getConversationContext(taskId),
});

if (result.message) {
Expand Down Expand Up @@ -472,6 +482,7 @@ export function useGitInteraction(
try {
const result = await trpcClient.git.generatePrTitleAndBody.mutate({
directoryPath: repoPath,
conversationContext: getConversationContext(taskId),
});

if (result.title || result.body) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { getAuthenticatedClient } from "@features/auth/hooks/authClient";
import { getSessionService } from "@features/sessions/service/service";
import { useSessionStore } from "@features/sessions/stores/sessionStore";
import {
sessionStoreSetters,
useSessionStore,
} from "@features/sessions/stores/sessionStore";
import type { Task } from "@shared/types";
import { generateTitle } from "@utils/generateTitle";
import { generateTitleAndSummary } from "@utils/generateTitle";
import { logger } from "@utils/logger";
import { queryClient } from "@utils/queryClient";
import { extractUserPromptsFromEvents } from "@utils/session";
Expand Down Expand Up @@ -69,22 +72,37 @@ export function useChatTitleGenerator(taskId: string): void {
return;
}

const title = await generateTitle(content);
if (title) {
const client = await getAuthenticatedClient();
if (client) {
await client.updateTask(taskId, { title });
queryClient.setQueriesData<Task[]>(
{ queryKey: ["tasks", "list"] },
(old) =>
old?.map((task) =>
task.id === taskId ? { ...task, title } : task,
),
);
getSessionService().updateSessionTaskTitle(taskId, title);
log.debug("Updated task title from conversation", {
const result = await generateTitleAndSummary(content);
if (result) {
const { title, summary } = result;
if (title) {
const client = await getAuthenticatedClient();
if (client) {
await client.updateTask(taskId, { title });
queryClient.setQueriesData<Task[]>(
{ queryKey: ["tasks", "list"] },
(old) =>
old?.map((task) =>
task.id === taskId ? { ...task, title } : task,
),
);
getSessionService().updateSessionTaskTitle(taskId, title);
log.debug("Updated task title from conversation", {
taskId,
title,
promptCount,
});
}
}

if (summary) {
sessionStoreSetters.updateSession(taskRunId, {
conversationSummary: result.summary,
});

log.debug("Updated task summary from conversation", {
taskId,
title,
summary,
promptCount,
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ export interface AgentSession {
contextUsed?: number;
/** Context window total size in tokens (from usage_update) */
contextSize?: number;
/** Pre-computed conversation summary for commit/PR generation context */
conversationSummary?: string;
}

// --- Config Option Helpers ---
Expand Down
2 changes: 1 addition & 1 deletion apps/code/src/renderer/sagas/task/task-creation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ vi.mock("@features/sessions/service/service", () => ({
}));

vi.mock("@renderer/utils/generateTitle", () => ({
generateTitle: vi.fn(async () => null),
generateTitleAndSummary: vi.fn(async () => null),
}));

vi.mock("@utils/queryClient", () => ({
Expand Down
7 changes: 4 additions & 3 deletions apps/code/src/renderer/sagas/task/task-creation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type {
import { Saga, type SagaLogger } from "@posthog/shared";
import type { PostHogAPIClient } from "@renderer/api/posthogClient";
import { trpcClient } from "@renderer/trpc";
import { generateTitle } from "@renderer/utils/generateTitle";
import { generateTitleAndSummary } from "@renderer/utils/generateTitle";
import { getTaskRepository } from "@renderer/utils/repository";
import type { ExecutionMode, Task } from "@shared/types";
import { logger } from "@utils/logger";
Expand All @@ -29,8 +29,9 @@ async function generateTaskTitle(
): Promise<void> {
if (!description.trim()) return;

const title = await generateTitle(description);
if (!title) return;
const result = await generateTitleAndSummary(description);
if (!result?.title) return;
const { title } = result;

try {
await posthogClient.updateTask(taskId, { title });
Expand Down
58 changes: 43 additions & 15 deletions apps/code/src/renderer/utils/generateTitle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@ import { logger } from "@utils/logger";

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

const SYSTEM_PROMPT = `You are a title generator. You output ONLY a task title. Nothing else.
const SYSTEM_PROMPT = `You are a title and summary generator. Output using exactly this format:

Convert the task description into a concise task title.
TITLE: <title here>
SUMMARY: <summary here>

Convert the task description into a concise task title and a brief conversation summary.

Title rules:
- The title should be clear, concise, and accurately reflect the content of the task.
- You should keep it short and simple, ideally no more than 6 words.
- Avoid using jargon or overly technical terms unless absolutely necessary.
Expand All @@ -18,28 +23,43 @@ Convert the task description into a concise task title.
- Never assume tech stack
- Only output "Untitled" if the input is completely null/missing, not just unclear
- 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.
- Never wrap the title in quotes

Examples:
Summary rules:
- 1-3 sentences describing what the user is working on and why
- Written from third-person perspective (e.g. "The user is fixing..." not "You are fixing...")
- Focus on the user's intent and goals, not the specific prompts
- Include relevant technical details (file names, features, bug descriptions) when mentioned
- This summary will be used as context for generating commit messages and PR descriptions

Title examples:
- "Fix the login bug in the authentication system" → Fix authentication login bug
- "Schedule a meeting with stakeholders to discuss Q4 budget planning" → Schedule Q4 budget meeting
- "Update user documentation for new API endpoints" → Update API documentation
- "Research competitor pricing strategies for our product" → Research competitor pricing
- "Review pull request #123" → Review pull request #123
- "debug 500 errors in production" → Debug production 500 errors
- "why is the payment flow failing" → Analyze payment flow failure
- "So how about that weather huh" → "Weather chat"
- "dsfkj sdkfj help me code" → "Coding help request"
- "👋😊" → "Friendly greeting"
- "aaaaaaaaaa" → "Repeated letters"
- " " → "Empty message"
- "What's the best restaurant in NYC?" → "NYC restaurant recommendations"
- "So how about that weather huh" → Weather chat
- "dsfkj sdkfj help me code" → Coding help request
- "👋😊" → Friendly greeting
- "aaaaaaaaaa" → Repeated letters
- " " → Empty message
- "What's the best restaurant in NYC?" → NYC restaurant recommendations
- "https://github.com/PostHog/posthog/issues/1234" → PostHog issue #1234
- "https://github.com/PostHog/posthog/pull/567" → PostHog PR #567
- "fix https://github.com/org/repo/issues/42" → Fix repo issue #42

Never wrap the title in quotes.`;
Never include any explanation outside the TITLE and SUMMARY lines.`;

export interface TitleAndSummary {
title: string;
summary: string;
}

export async function generateTitle(content: string): Promise<string | null> {
export async function generateTitleAndSummary(
content: string,
): Promise<TitleAndSummary | null> {
try {
const authState = await fetchAuthState();
if (authState.status !== "authenticated") return null;
Expand All @@ -49,15 +69,23 @@ export async function generateTitle(content: string): Promise<string | null> {
messages: [
{
role: "user" as const,
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:`,
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:`,
},
],
});

const title = result.content.trim().replace(/^["']|["']$/g, "");
return title || null;
const text = result.content.trim();
const titleMatch = text.match(/^TITLE:\s*(.+?)(?:\n|$)/m);
const summaryMatch = text.match(/SUMMARY:\s*([\s\S]+)$/m);

const title = titleMatch?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
const summary = summaryMatch?.[1]?.trim() ?? "";

if (!title && !summary) return null;

return { title, summary };
} catch (error) {
log.error("Failed to generate title", { error });
log.error("Failed to generate title and summary", { error });
return null;
}
}
Loading