Skip to content

Commit 7e2736d

Browse files
authored
Merge branch 'main' into feat/cloud-agent/user-authored-prs
2 parents c7aba3f + bc8fae9 commit 7e2736d

42 files changed

Lines changed: 1917 additions & 531 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CONTRIBUTING.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ We love contributions big and small. PostHog Code is the IDE for understanding h
99
3. Create a branch (`feat/my-change`, `fix/that-bug`)
1010
4. Make your changes and open a pull request
1111

12-
We recommend creating an issue first if one doesn't already exist so we can align on the approach before you invest time.
12+
Issues labeled [`good first issue`](https://github.com/PostHog/code/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20label%3A%22good%20first%20issue%22) are a great place to start!
13+
14+
If an issue does not yet exist for your change, please create one first so we can align on the approach before you invest time.
1315

1416
## Development setup
1517

apps/code/scripts/download-binaries.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const DEST_DIR = join(__dirname, "..", "resources", "codex-acp");
1919
const BINARIES = [
2020
{
2121
name: "codex-acp",
22-
version: "0.9.5",
22+
version: "0.11.1",
2323
getUrl: (version, target) => {
2424
const ext = target.includes("windows") ? "zip" : "tar.gz";
2525
return `https://github.com/zed-industries/codex-acp/releases/download/v${version}/codex-acp-${version}-${target}.${ext}`;

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

Lines changed: 96 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@ import { isMcpToolReadOnly } from "@posthog/agent";
1616
import { hydrateSessionJsonl } from "@posthog/agent/adapters/claude/session/jsonl-hydration";
1717
import { getEffortOptions } from "@posthog/agent/adapters/claude/session/models";
1818
import { Agent } from "@posthog/agent/agent";
19-
import { getAvailableModes } from "@posthog/agent/execution-mode";
2019
import {
20+
getAvailableCodexModes,
21+
getAvailableModes,
22+
} from "@posthog/agent/execution-mode";
23+
import {
24+
DEFAULT_CODEX_MODEL,
2125
DEFAULT_GATEWAY_MODEL,
2226
fetchGatewayModels,
2327
formatGatewayModelName,
@@ -178,6 +182,56 @@ const onAgentLog: OnLogCallback = (level, scope, message, data) => {
178182
}
179183
};
180184

185+
const HAIKU_EXPLORE_AGENT_OVERRIDE = {
186+
description:
187+
'Fast agent for exploring and understanding codebases. Use this when you need to find files by pattern (eg. "src/components/**/*.tsx"), search for code or keywords (eg. "where is the auth middleware?"), or answer questions about how the codebase works (eg. "how does the session service handle reconnects?"). When calling this agent, specify a thoroughness level: "quick" for targeted lookups, "medium" for broader exploration, or "very thorough" for comprehensive analysis across multiple locations.',
188+
model: "haiku",
189+
prompt: `You are a fast, read-only codebase exploration agent.
190+
191+
Your job is to find files, search code, read the most relevant sources, and report findings clearly.
192+
193+
Rules:
194+
- Never create, modify, delete, move, or copy files.
195+
- Never use shell redirection or any command that changes system state.
196+
- Use Glob for broad file pattern matching.
197+
- Use Grep for searching file contents.
198+
- Use Read when you know the exact file path to inspect.
199+
- Use Bash only for safe read-only commands like ls, git status, git log, git diff, find, cat, head, and tail.
200+
- Adapt your search approach based on the thoroughness level specified by the caller.
201+
- Return file paths as absolute paths in your final response.
202+
- Avoid using emojis.
203+
- Wherever possible, spawn multiple parallel tool calls for grepping and reading files.
204+
- Search efficiently, then read only the most relevant files.
205+
- Return findings directly in your final response — do not create files.`,
206+
tools: [
207+
"Bash",
208+
"Glob",
209+
"Grep",
210+
"Read",
211+
"WebFetch",
212+
"WebSearch",
213+
"NotebookRead",
214+
"TodoWrite",
215+
],
216+
};
217+
218+
function buildClaudeCodeOptions(args: {
219+
additionalDirectories?: string[];
220+
effort?: EffortLevel;
221+
plugins: { type: "local"; path: string }[];
222+
}) {
223+
return {
224+
...(args.additionalDirectories?.length && {
225+
additionalDirectories: args.additionalDirectories,
226+
}),
227+
...(args.effort && { effort: args.effort }),
228+
plugins: args.plugins,
229+
agents: {
230+
"ph-explore": HAIKU_EXPLORE_AGENT_OVERRIDE,
231+
},
232+
};
233+
}
234+
181235
interface SessionConfig {
182236
taskId: string;
183237
taskRunId: string;
@@ -550,10 +604,18 @@ When creating pull requests, add the following footer at the end of the PR descr
550604
});
551605

552606
try {
607+
const systemPrompt = this.buildSystemPrompt(
608+
credentials,
609+
taskId,
610+
customInstructions,
611+
);
612+
553613
const acpConnection = await agent.run(taskId, taskRunId, {
554614
adapter,
555615
gatewayUrl: proxyUrl,
556616
codexBinaryPath: adapter === "codex" ? getCodexBinaryPath() : undefined,
617+
model,
618+
instructions: adapter === "codex" ? systemPrompt.append : undefined,
557619
processCallbacks: {
558620
onProcessSpawned: (info) => {
559621
this.processTracking.register(
@@ -631,40 +693,37 @@ When creating pull requests, add the following footer at the end of the PR descr
631693
},
632694
...externalPlugins,
633695
];
696+
const claudeCodeOptions = buildClaudeCodeOptions({
697+
additionalDirectories,
698+
effort,
699+
plugins,
700+
});
634701

635702
let configOptions: SessionConfigOption[] | undefined;
636703
let agentSessionId: string;
637704

638-
if (isReconnect && adapter === "codex" && config.sessionId) {
639-
const existingSessionId = config.sessionId;
640-
const loadResponse = await connection.loadSession({
641-
sessionId: existingSessionId,
642-
cwd: repoPath,
643-
mcpServers,
644-
});
645-
configOptions = loadResponse.configOptions ?? undefined;
646-
agentSessionId = existingSessionId;
647-
} else if (isReconnect && adapter === "claude" && config.sessionId) {
705+
if (isReconnect && config.sessionId) {
648706
const existingSessionId = config.sessionId;
649707

650-
const posthogAPI = agent.getPosthogAPI();
651-
if (posthogAPI) {
652-
await hydrateSessionJsonl({
653-
sessionId: existingSessionId,
654-
cwd: repoPath,
655-
taskId,
656-
runId: taskRunId,
657-
permissionMode: config.permissionMode,
658-
posthogAPI,
659-
log,
660-
});
708+
// Claude-specific: hydrate session JSONL from PostHog before resuming
709+
if (adapter !== "codex") {
710+
const posthogAPI = agent.getPosthogAPI();
711+
if (posthogAPI) {
712+
await hydrateSessionJsonl({
713+
sessionId: existingSessionId,
714+
cwd: repoPath,
715+
taskId,
716+
runId: taskRunId,
717+
permissionMode: config.permissionMode,
718+
posthogAPI,
719+
log,
720+
});
721+
}
661722
}
662723

663-
const systemPrompt = this.buildSystemPrompt(
664-
credentials,
665-
taskId,
666-
customInstructions,
667-
);
724+
// Both adapters implement unstable_resumeSession:
725+
// - Claude: delegates to SDK's resumeSession with JSONL hydration
726+
// - Codex: delegates to codex-acp's loadSession internally
668727
const resumeResponse = await connection.unstable_resumeSession({
669728
sessionId: existingSessionId,
670729
cwd: repoPath,
@@ -679,13 +738,7 @@ When creating pull requests, add the following footer at the end of the PR descr
679738
...(permissionMode && { permissionMode }),
680739
...(model != null && { model }),
681740
claudeCode: {
682-
options: {
683-
...(additionalDirectories?.length && {
684-
additionalDirectories,
685-
}),
686-
...(effort && { effort }),
687-
plugins,
688-
},
741+
options: claudeCodeOptions,
689742
},
690743
},
691744
});
@@ -698,11 +751,6 @@ When creating pull requests, add the following footer at the end of the PR descr
698751
taskRunId,
699752
});
700753
}
701-
const systemPrompt = this.buildSystemPrompt(
702-
credentials,
703-
taskId,
704-
customInstructions,
705-
);
706754
const newSessionResponse = await connection.newSession({
707755
cwd: repoPath,
708756
mcpServers,
@@ -712,11 +760,7 @@ When creating pull requests, add the following footer at the end of the PR descr
712760
...(permissionMode && { permissionMode }),
713761
...(model != null && { model }),
714762
claudeCode: {
715-
options: {
716-
...(additionalDirectories?.length && { additionalDirectories }),
717-
...(effort && { effort }),
718-
plugins,
719-
},
763+
options: claudeCodeOptions,
720764
},
721765
},
722766
});
@@ -1622,7 +1666,9 @@ For git operations while detached:
16221666

16231667
const defaultModel =
16241668
adapter === "codex"
1625-
? (modelOptions[0]?.value ?? "")
1669+
? (modelOptions.find((o) => o.value === DEFAULT_CODEX_MODEL)?.value ??
1670+
modelOptions[0]?.value ??
1671+
"")
16261672
: DEFAULT_GATEWAY_MODEL;
16271673

16281674
const resolvedModelId = modelOptions.some((o) => o.value === defaultModel)
@@ -1637,18 +1683,21 @@ For git operations while detached:
16371683
});
16381684
}
16391685

1640-
const modeOptions = getAvailableModes().map((mode) => ({
1686+
const modes =
1687+
adapter === "codex" ? getAvailableCodexModes() : getAvailableModes();
1688+
const modeOptions = modes.map((mode) => ({
16411689
value: mode.id,
16421690
name: mode.name,
16431691
description: mode.description ?? undefined,
16441692
}));
1693+
const defaultMode = adapter === "codex" ? "auto" : "plan";
16451694

16461695
const configOptions: SessionConfigOption[] = [
16471696
{
16481697
id: "mode",
16491698
name: "Approval Preset",
16501699
type: "select",
1651-
currentValue: "plan",
1700+
currentValue: defaultMode,
16521701
options: modeOptions,
16531702
category: "mode",
16541703
description:

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ export const createPrInput = z.object({
260260
draft: z.boolean().optional(),
261261
stagedOnly: z.boolean().optional(),
262262
taskId: z.string().optional(),
263+
conversationContext: z.string().optional(),
263264
});
264265

265266
export type CreatePrInput = z.infer<typeof createPrInput>;
@@ -331,6 +332,7 @@ export const getBranchChangedFilesOutput = z.array(changedFileSchema);
331332

332333
export const generateCommitMessageInput = z.object({
333334
directoryPath: z.string(),
335+
conversationContext: z.string().optional(),
334336
});
335337

336338
export const generateCommitMessageOutput = z.object({
@@ -339,6 +341,7 @@ export const generateCommitMessageOutput = z.object({
339341

340342
export const generatePrTitleAndBodyInput = z.object({
341343
directoryPath: z.string(),
344+
conversationContext: z.string().optional(),
342345
});
343346

344347
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
@@ -515,6 +515,7 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
515515
draft?: boolean;
516516
stagedOnly?: boolean;
517517
taskId?: string;
518+
conversationContext?: string;
518519
}): Promise<CreatePrOutput> {
519520
const { directoryPath, flowId } = input;
520521

@@ -537,12 +538,14 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
537538
createBranch: (dir, name) => this.createBranch(dir, name),
538539
checkoutBranch: (dir, name) => this.checkoutBranch(dir, name),
539540
getChangedFilesHead: (dir) => this.getChangedFilesHead(dir),
540-
generateCommitMessage: (dir) => this.generateCommitMessage(dir),
541+
generateCommitMessage: (dir) =>
542+
this.generateCommitMessage(dir, input.conversationContext),
541543
commit: (dir, msg, opts) => this.commit(dir, msg, opts),
542544
getSyncStatus: (dir) => this.getGitSyncStatus(dir),
543545
push: (dir) => this.push(dir),
544546
publish: (dir) => this.publish(dir),
545-
generatePrTitleAndBody: (dir) => this.generatePrTitleAndBody(dir),
547+
generatePrTitleAndBody: (dir) =>
548+
this.generatePrTitleAndBody(dir, input.conversationContext),
546549
createPr: (dir, title, body, draft) =>
547550
this.createPrViaGh(dir, title, body, draft),
548551
onProgress: emitProgress,
@@ -988,6 +991,7 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
988991

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

1039+
const contextSection = conversationContext
1040+
? `\n\nConversation context (why these changes were made):\n${conversationContext}`
1041+
: "";
1042+
10341043
const userMessage = `Generate a commit message for these changes:
10351044
10361045
Changed files:
10371046
${filesSummary}
10381047
10391048
Diff:
1040-
${truncatedDiff}`;
1049+
${truncatedDiff}${contextSection}`;
10411050

10421051
log.debug("Generating commit message", {
10431052
fileCount: changedFiles.length,
10441053
diffLength: diff.length,
10451054
conventionalCommits: conventions.conventionalCommits,
1055+
hasConversationContext: !!conversationContext,
10461056
});
10471057

10481058
const response = await this.llmGateway.prompt(
@@ -1055,6 +1065,7 @@ ${truncatedDiff}`;
10551065

10561066
public async generatePrTitleAndBody(
10571067
directoryPath: string,
1068+
conversationContext?: string,
10581069
): Promise<{ title: string; body: string }> {
10591070
await this.fetchIfStale(directoryPath);
10601071

@@ -1110,13 +1121,18 @@ Rules for the title:
11101121
Rules for the body:
11111122
- Start with a TL;DR section (1-2 sentences summarizing the change)
11121123
- Include a "What changed?" section with bullet points describing the key changes
1124+
- If conversation context is provided, use it to explain WHY the changes were made in the TL;DR
11131125
- Be thorough but concise
11141126
- Use markdown formatting
11151127
- Only describe changes that are actually in the diff — do not invent or assume changes
11161128
${templateHint}
11171129
11181130
Do not include any explanation outside the TITLE and BODY sections.`;
11191131

1132+
const contextSection = conversationContext
1133+
? `\n\nConversation context (why these changes were made):\n${conversationContext}`
1134+
: "";
1135+
11201136
const userMessage = `Generate a PR title and description for these changes:
11211137
11221138
Branch: ${currentBranch ?? "unknown"} -> ${defaultBranch}
@@ -1125,12 +1141,13 @@ Commits in this PR:
11251141
${commitsSummary || "(no commits yet - changes are uncommitted)"}
11261142
11271143
Diff:
1128-
${truncatedDiff || "(no diff available)"}`;
1144+
${truncatedDiff || "(no diff available)"}${contextSection}`;
11291145

11301146
log.debug("Generating PR title and body", {
11311147
commitCount: commits.length,
11321148
diffLength: fullDiff.length,
11331149
hasTemplate: !!prTemplate.template,
1150+
hasConversationContext: !!conversationContext,
11341151
});
11351152

11361153
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
@@ -312,14 +312,20 @@ export const gitRouter = router({
312312
.input(generateCommitMessageInput)
313313
.output(generateCommitMessageOutput)
314314
.mutation(({ input }) =>
315-
getService().generateCommitMessage(input.directoryPath),
315+
getService().generateCommitMessage(
316+
input.directoryPath,
317+
input.conversationContext,
318+
),
316319
),
317320

318321
generatePrTitleAndBody: publicProcedure
319322
.input(generatePrTitleAndBodyInput)
320323
.output(generatePrTitleAndBodyOutput)
321324
.mutation(({ input }) =>
322-
getService().generatePrTitleAndBody(input.directoryPath),
325+
getService().generatePrTitleAndBody(
326+
input.directoryPath,
327+
input.conversationContext,
328+
),
323329
),
324330

325331
searchGithubIssues: publicProcedure

0 commit comments

Comments
 (0)