Skip to content

Commit 2845b8e

Browse files
authored
Merge branch 'main' into fix/1846-default-effort-level
2 parents 217c1a9 + b9cd2a0 commit 2845b8e

8 files changed

Lines changed: 128 additions & 33 deletions

File tree

apps/code/src/main/services/file-watcher/schemas.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const FileWatcherEvent = {
2525
FileChanged: "file-changed",
2626
FileDeleted: "file-deleted",
2727
GitStateChanged: "git-state-changed",
28+
WorkingTreeChanged: "working-tree-changed",
2829
} as const;
2930

3031
export type DirectoryChangedPayload = {
@@ -46,9 +47,14 @@ export type GitStateChangedPayload = {
4647
repoPath: string;
4748
};
4849

50+
export type WorkingTreeChangedPayload = {
51+
repoPath: string;
52+
};
53+
4954
export interface FileWatcherEvents {
5055
[FileWatcherEvent.DirectoryChanged]: DirectoryChangedPayload;
5156
[FileWatcherEvent.FileChanged]: FileChangedPayload;
5257
[FileWatcherEvent.FileDeleted]: FileDeletedPayload;
5358
[FileWatcherEvent.GitStateChanged]: GitStateChangedPayload;
59+
[FileWatcherEvent.WorkingTreeChanged]: WorkingTreeChangedPayload;
5460
}

apps/code/src/main/services/file-watcher/service.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,9 +171,11 @@ export class FileWatcherService extends TypedEventEmitter<FileWatcherEvents> {
171171

172172
const totalChanges = pending.files.size + pending.deletes.size;
173173

174-
// For bulk changes, emit a single event instead of per-file events
174+
if (totalChanges > 0) {
175+
this.emit(FileWatcherEvent.WorkingTreeChanged, { repoPath });
176+
}
177+
175178
if (totalChanges > BULK_THRESHOLD) {
176-
this.emit(FileWatcherEvent.GitStateChanged, { repoPath });
177179
pending.dirs.clear();
178180
pending.files.clear();
179181
pending.deletes.clear();

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

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -297,20 +297,29 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
297297
return getRemoteUrl(directoryPath);
298298
}
299299

300-
public async getCurrentBranch(directoryPath: string): Promise<string | null> {
301-
return getCurrentBranch(directoryPath);
300+
public async getCurrentBranch(
301+
directoryPath: string,
302+
signal?: AbortSignal,
303+
): Promise<string | null> {
304+
return getCurrentBranch(directoryPath, { abortSignal: signal });
302305
}
303306

304307
public async getDefaultBranch(directoryPath: string): Promise<string> {
305308
return getDefaultBranch(directoryPath);
306309
}
307310

308-
public async getAllBranches(directoryPath: string): Promise<string[]> {
309-
return getAllBranches(directoryPath);
311+
public async getAllBranches(
312+
directoryPath: string,
313+
signal?: AbortSignal,
314+
): Promise<string[]> {
315+
return getAllBranches(directoryPath, { abortSignal: signal });
310316
}
311317

312-
public async getGitBusyState(directoryPath: string): Promise<GitBusyState> {
313-
return getGitBusyState(directoryPath);
318+
public async getGitBusyState(
319+
directoryPath: string,
320+
signal?: AbortSignal,
321+
): Promise<GitBusyState> {
322+
return getGitBusyState(directoryPath, { abortSignal: signal });
314323
}
315324

316325
public async createBranch(
@@ -334,9 +343,11 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
334343

335344
public async getChangedFilesHead(
336345
directoryPath: string,
346+
signal?: AbortSignal,
337347
): Promise<ChangedFile[]> {
338348
const files = await getChangedFilesDetailed(directoryPath, {
339349
excludePatterns: [".claude", "CLAUDE.local.md"],
350+
abortSignal: signal,
340351
});
341352
type HeadChangedFile = Omit<ChangedFile, "patch">;
342353
const filteredFiles: Array<HeadChangedFile | null> = await Promise.all(
@@ -371,29 +382,42 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
371382
public async getFileAtHead(
372383
directoryPath: string,
373384
filePath: string,
385+
signal?: AbortSignal,
374386
): Promise<string | null> {
375-
return getFileAtHead(directoryPath, filePath);
387+
return getFileAtHead(directoryPath, filePath, { abortSignal: signal });
376388
}
377389

378390
public async getDiffHead(
379391
directoryPath: string,
380392
ignoreWhitespace?: boolean,
393+
signal?: AbortSignal,
381394
): Promise<string> {
382-
return getDiffHead(directoryPath, { ignoreWhitespace });
395+
return getDiffHead(directoryPath, {
396+
ignoreWhitespace,
397+
abortSignal: signal,
398+
});
383399
}
384400

385401
public async getDiffCached(
386402
directoryPath: string,
387403
ignoreWhitespace?: boolean,
404+
signal?: AbortSignal,
388405
): Promise<string> {
389-
return getStagedDiff(directoryPath, { ignoreWhitespace });
406+
return getStagedDiff(directoryPath, {
407+
ignoreWhitespace,
408+
abortSignal: signal,
409+
});
390410
}
391411

392412
public async getDiffUnstaged(
393413
directoryPath: string,
394414
ignoreWhitespace?: boolean,
415+
signal?: AbortSignal,
395416
): Promise<string> {
396-
return getUnstagedDiff(directoryPath, { ignoreWhitespace });
417+
return getUnstagedDiff(directoryPath, {
418+
ignoreWhitespace,
419+
abortSignal: signal,
420+
});
397421
}
398422

399423
public async stageFiles(
@@ -412,9 +436,13 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
412436
return this.getStateSnapshot(directoryPath);
413437
}
414438

415-
public async getDiffStats(directoryPath: string): Promise<DiffStats> {
439+
public async getDiffStats(
440+
directoryPath: string,
441+
signal?: AbortSignal,
442+
): Promise<DiffStats> {
416443
const stats = await getDiffStats(directoryPath, {
417444
excludePatterns: [".claude", "CLAUDE.local.md"],
445+
abortSignal: signal,
418446
});
419447
return {
420448
filesChanged: stats.filesChanged,
@@ -455,8 +483,11 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
455483

456484
public async getLatestCommit(
457485
directoryPath: string,
486+
signal?: AbortSignal,
458487
): Promise<GitCommitInfo | null> {
459-
const commit = await getLatestCommit(directoryPath);
488+
const commit = await getLatestCommit(directoryPath, {
489+
abortSignal: signal,
490+
});
460491
if (!commit) return null;
461492
return {
462493
sha: commit.sha,

apps/code/src/main/trpc/routers/file-watcher.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,5 @@ export const fileWatcherRouter = router({
4141
onFileChanged: subscribe(FileWatcherEvent.FileChanged),
4242
onFileDeleted: subscribe(FileWatcherEvent.FileDeleted),
4343
onGitStateChanged: subscribe(FileWatcherEvent.GitStateChanged),
44+
onWorkingTreeChanged: subscribe(FileWatcherEvent.WorkingTreeChanged),
4445
});

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

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -125,17 +125,23 @@ export const gitRouter = router({
125125
getCurrentBranch: publicProcedure
126126
.input(getCurrentBranchInput)
127127
.output(getCurrentBranchOutput)
128-
.query(({ input }) => getService().getCurrentBranch(input.directoryPath)),
128+
.query(({ input, signal }) =>
129+
getService().getCurrentBranch(input.directoryPath, signal),
130+
),
129131

130132
getAllBranches: publicProcedure
131133
.input(getAllBranchesInput)
132134
.output(getAllBranchesOutput)
133-
.query(({ input }) => getService().getAllBranches(input.directoryPath)),
135+
.query(({ input, signal }) =>
136+
getService().getAllBranches(input.directoryPath, signal),
137+
),
134138

135139
getGitBusyState: publicProcedure
136140
.input(getGitBusyStateInput)
137141
.output(getGitBusyStateOutput)
138-
.query(({ input }) => getService().getGitBusyState(input.directoryPath)),
142+
.query(({ input, signal }) =>
143+
getService().getGitBusyState(input.directoryPath, signal),
144+
),
139145

140146
createBranch: publicProcedure
141147
.input(createBranchInput)
@@ -154,42 +160,56 @@ export const gitRouter = router({
154160
getChangedFilesHead: publicProcedure
155161
.input(getChangedFilesHeadInput)
156162
.output(getChangedFilesHeadOutput)
157-
.query(({ input }) =>
158-
getService().getChangedFilesHead(input.directoryPath),
163+
.query(({ input, signal }) =>
164+
getService().getChangedFilesHead(input.directoryPath, signal),
159165
),
160166

161167
getFileAtHead: publicProcedure
162168
.input(getFileAtHeadInput)
163169
.output(getFileAtHeadOutput)
164-
.query(({ input }) =>
165-
getService().getFileAtHead(input.directoryPath, input.filePath),
170+
.query(({ input, signal }) =>
171+
getService().getFileAtHead(input.directoryPath, input.filePath, signal),
166172
),
167173

168174
getDiffHead: publicProcedure
169175
.input(diffInput)
170176
.output(diffOutput)
171-
.query(({ input }) =>
172-
getService().getDiffHead(input.directoryPath, input.ignoreWhitespace),
177+
.query(({ input, signal }) =>
178+
getService().getDiffHead(
179+
input.directoryPath,
180+
input.ignoreWhitespace,
181+
signal,
182+
),
173183
),
174184

175185
getDiffCached: publicProcedure
176186
.input(diffInput)
177187
.output(diffOutput)
178-
.query(({ input }) =>
179-
getService().getDiffCached(input.directoryPath, input.ignoreWhitespace),
188+
.query(({ input, signal }) =>
189+
getService().getDiffCached(
190+
input.directoryPath,
191+
input.ignoreWhitespace,
192+
signal,
193+
),
180194
),
181195

182196
getDiffUnstaged: publicProcedure
183197
.input(diffInput)
184198
.output(diffOutput)
185-
.query(({ input }) =>
186-
getService().getDiffUnstaged(input.directoryPath, input.ignoreWhitespace),
199+
.query(({ input, signal }) =>
200+
getService().getDiffUnstaged(
201+
input.directoryPath,
202+
input.ignoreWhitespace,
203+
signal,
204+
),
187205
),
188206

189207
getDiffStats: publicProcedure
190208
.input(getDiffStatsInput)
191209
.output(getDiffStatsOutput)
192-
.query(({ input }) => getService().getDiffStats(input.directoryPath)),
210+
.query(({ input, signal }) =>
211+
getService().getDiffStats(input.directoryPath, signal),
212+
),
193213

194214
stageFiles: publicProcedure
195215
.input(stageFilesInput)
@@ -233,7 +253,9 @@ export const gitRouter = router({
233253
getLatestCommit: publicProcedure
234254
.input(getLatestCommitInput)
235255
.output(getLatestCommitOutput)
236-
.query(({ input }) => getService().getLatestCommit(input.directoryPath)),
256+
.query(({ input, signal }) =>
257+
getService().getLatestCommit(input.directoryPath, signal),
258+
),
237259

238260
getGitRepoInfo: publicProcedure
239261
.input(getGitRepoInfoInput)

apps/code/src/renderer/hooks/useFileWatcher.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ export function useFileWatcher(repoPath: string | null, taskId?: string) {
4747
filePath: relativePath,
4848
}),
4949
);
50-
invalidateGitWorkingTreeQueries(repoPath);
5150
},
5251
}),
5352
);
@@ -57,7 +56,6 @@ export function useFileWatcher(repoPath: string | null, taskId?: string) {
5756
enabled: !!repoPath,
5857
onData: ({ repoPath: rp, filePath }) => {
5958
if (rp !== repoPath) return;
60-
invalidateGitWorkingTreeQueries(repoPath);
6159
if (!taskId) return;
6260
const relativePath = toRelativePath(filePath, repoPath);
6361
closeTabsForFile(taskId, relativePath);
@@ -71,6 +69,15 @@ export function useFileWatcher(repoPath: string | null, taskId?: string) {
7169
onData: ({ repoPath: rp }) => {
7270
if (rp !== repoPath) return;
7371
invalidateGitBranchQueries(repoPath);
72+
},
73+
}),
74+
);
75+
76+
useSubscription(
77+
trpc.fileWatcher.onWorkingTreeChanged.subscriptionOptions(undefined, {
78+
enabled: !!repoPath,
79+
onData: ({ repoPath: rp }) => {
80+
if (rp !== repoPath) return;
7481
invalidateGitWorkingTreeQueries(repoPath);
7582
},
7683
}),

packages/agent/src/server/agent-server.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -826,6 +826,9 @@ describe("AgentServer HTTP Mode", () => {
826826
"If the user explicitly asks you to open or update a pull request",
827827
"open a draft pull request",
828828
"unless the user explicitly asks",
829+
".github/pull_request_template.md",
830+
"gh issue list --search",
831+
"Closes #<n>",
829832
"Generated-By: PostHog Code",
830833
"Task-Id: test-task-id",
831834
],
@@ -868,6 +871,13 @@ describe("AgentServer HTTP Mode", () => {
868871
expect(prompt).toContain("Generated-By: PostHog Code");
869872
expect(prompt).toContain("Task-Id: test-task-id");
870873
expect(prompt).toContain("Created with [PostHog Code]");
874+
// PR template detection (repo first, org `.github` fallback)
875+
expect(prompt).toContain(".github/pull_request_template.md");
876+
expect(prompt).toContain("org's `.github` repo");
877+
// Related-issue linking
878+
expect(prompt).toContain("gh issue list --state open --search");
879+
expect(prompt).toContain("Closes #<n>");
880+
expect(prompt).toContain("Refs #<n>");
871881
delete process.env.POSTHOG_CODE_INTERACTION_ORIGIN;
872882
});
873883

@@ -895,6 +905,13 @@ describe("AgentServer HTTP Mode", () => {
895905
);
896906
expect(prompt).toContain("Push to the existing PR branch");
897907
expect(prompt).not.toContain("Create a draft pull request");
908+
// Review-comment thread handling: reply + resolve
909+
expect(prompt).toContain("review thread");
910+
expect(prompt).toContain("/pulls/{n}/comments/{id}/replies");
911+
expect(prompt).toContain("resolveReviewThread");
912+
expect(prompt).toContain(
913+
"Do NOT push fixes for review comments without replying to and resolving each related thread.",
914+
);
898915
delete process.env.POSTHOG_CODE_INTERACTION_ORIGIN;
899916
});
900917

packages/agent/src/server/agent-server.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1643,9 +1643,14 @@ After completing the requested changes:
16431643
1. Check out the existing PR branch with \`gh pr checkout ${prUrl}\`
16441644
2. Stage and commit all changes with a clear commit message
16451645
3. Push to the existing PR branch
1646+
4. For every PR review comment or review thread you addressed, treat the thread as done only after BOTH of these:
1647+
- Reply on the thread with a short note describing what changed (reference the commit SHA when useful) using \`gh api -X POST /repos/{owner}/{repo}/pulls/{n}/comments/{id}/replies -f body='...'\`.
1648+
- Resolve the thread via the \`resolveReviewThread\` GraphQL mutation: \`gh api graphql -f query='mutation($id:ID!){resolveReviewThread(input:{threadId:$id}){thread{isResolved}}}' -f id="<thread-node-id>"\`.
1649+
List unresolved threads first with \`gh api graphql -f query='{repository(owner:"<owner>",name:"<repo>"){pullRequest(number:<n>){reviewThreads(first:100){nodes{id isResolved comments(first:1){nodes{body}}}}}}}'\` so you can resolve each one you fixed.
16461650
16471651
Important:
16481652
- Do NOT create a new branch or a new pull request.
1653+
- Do NOT push fixes for review comments without replying to and resolving each related thread.
16491654
${attributionInstructions}
16501655
`;
16511656
}
@@ -1661,7 +1666,7 @@ When the user asks for code changes:
16611666
When the user explicitly asks to clone or work in a GitHub repository:
16621667
- Clone the repository into /tmp/workspace/repos/<owner>/<repo> using \`gh repo clone <owner>/<repo> /tmp/workspace/repos/<owner>/<repo>\`
16631668
- Work from inside that cloned repository for follow-up code changes
1664-
- If the user explicitly asks you to open or update a pull request, create a branch, commit the requested changes, push it, and open a draft pull request from inside the clone
1669+
- If the user explicitly asks you to open or update a pull request, create a branch, commit the requested changes, push it, and open a draft pull request from inside the clone. Before opening the PR, check the cloned repo for a PR template at \`.github/pull_request_template.md\` (or variants; fall back to the org's \`.github\` repo via \`gh api\`) and use it as the body structure, and search for matching open issues with \`gh issue list --search\` to include \`Closes #<n>\` / \`Refs #<n>\` links.
16651670
- Do NOT create branches, commits, push changes, or open pull requests unless the user explicitly asks for that`;
16661671

16671672
return `
@@ -1704,7 +1709,11 @@ After completing the requested changes:
17041709
1. Create a new branch prefixed with \`posthog-code/\` (e.g. \`posthog-code/fix-login-redirect\`) based on the work done
17051710
2. Stage and commit all changes with a clear commit message
17061711
3. Push the branch to origin
1707-
4. Create a draft pull request using \`gh pr create --draft${this.config.baseBranch ? ` --base ${this.config.baseBranch}` : ""}\` with a descriptive title and body. Add the following footer at the end of the PR description:
1712+
4. Before opening the PR, prepare the body:
1713+
- Check the repo for a PR template at \`.github/pull_request_template.md\` (also try \`.github/PULL_REQUEST_TEMPLATE.md\`, \`docs/pull_request_template.md\`, and root variants). If one exists, use its exact section headings as the PR body — do NOT fall back to a generic Summary/Test plan format.
1714+
- If no repo-level template exists, check the org's \`.github\` repo via \`gh api /repos/<owner>/.github/contents/.github/pull_request_template.md\` (and other common paths) and use that as a fallback.
1715+
- Search for matching open issues with \`gh issue list --state open --search '<keywords>'\` (derive keywords from the branch name, commits, and changed files; \`gh issue view <n>\` to confirm relevance). For every issue this PR would resolve, include a \`Closes #<n>\` line in the body so GitHub auto-links and auto-closes it on merge. For issues that are related but not fully resolved, use \`Refs #<n>\` instead.
1716+
5. Create a draft pull request using \`gh pr create --draft${this.config.baseBranch ? ` --base ${this.config.baseBranch}` : ""}\` with a descriptive title and the body prepared above. Add the following footer at the end of the PR description:
17081717
\`\`\`
17091718
---
17101719
*Created with [PostHog Code](https://posthog.com/code?ref=pr)*

0 commit comments

Comments
 (0)