Skip to content

Commit fec69bc

Browse files
authored
Merge branch 'main' into ux/project-pickers-quill
2 parents cca2f3e + 18c0502 commit fec69bc

21 files changed

Lines changed: 674 additions & 82 deletions

File tree

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import type { FsService } from "../fs/service";
5353
import type { McpAppsService } from "../mcp-apps/service";
5454
import type { PosthogPluginService } from "../posthog-plugin/service";
5555
import type { ProcessTrackingService } from "../process-tracking/service";
56+
import { loadSessionEnvOverrides } from "../session-env/loader";
5657
import type { SleepService } from "../sleep/service";
5758
import type { AgentAuthAdapter, McpToolInstallations } from "./auth-adapter";
5859
import { discoverExternalPlugins } from "./discover-plugins";
@@ -983,6 +984,27 @@ When creating pull requests, add the following footer at the end of the PR descr
983984
return taskId ? all.filter((s) => s.taskId === taskId) : all;
984985
}
985986

987+
/**
988+
* Resolve env-var overrides set by the SessionStart-style hooks of the most
989+
* recently active agent session for `taskId`.
990+
*
991+
* Used by git/gh operations triggered from the UI (Commit, Create PR) so
992+
* they pick up the same hook env the agent itself sees — most importantly
993+
* the SSH_AUTH_SOCK that Secretive's hook re-points at the Secretive agent
994+
* for commit signing. Returns an empty object when there is no session for
995+
* the task or when no hook output is available.
996+
*/
997+
public async getSessionEnvForTask(
998+
taskId: string,
999+
): Promise<Record<string, string>> {
1000+
const candidates = this.listSessions(taskId)
1001+
.filter((s) => !!s.config.sessionId)
1002+
.sort((a, b) => b.lastActivityAt - a.lastActivityAt);
1003+
const session = candidates[0];
1004+
if (!session?.config.sessionId) return {};
1005+
return loadSessionEnvOverrides(session.config.sessionId);
1006+
}
1007+
9861008
/**
9871009
* Get sessions that were interrupted for a specific reason.
9881010
* Optionally filter by repoPath to get only sessions for a specific repo.

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,11 @@ export class FileWatcherService extends TypedEventEmitter<FileWatcherEvents> {
214214
(e) =>
215215
e.path.endsWith("/HEAD") ||
216216
e.path.endsWith("/index") ||
217+
e.path.endsWith("/MERGE_HEAD") ||
218+
e.path.endsWith("/CHERRY_PICK_HEAD") ||
219+
e.path.endsWith("/REVERT_HEAD") ||
220+
e.path.includes("/rebase-merge") ||
221+
e.path.includes("/rebase-apply") ||
217222
e.path.includes("/refs/heads/"),
218223
);
219224
if (isRelevant) {

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,27 @@ export const getCurrentBranchOutput = z.string().nullable();
147147
export const getAllBranchesInput = directoryPathInput;
148148
export const getAllBranchesOutput = z.array(z.string());
149149

150+
// getGitBusyState schemas
151+
export const gitBusyOperationSchema = z.enum([
152+
"rebase",
153+
"merge",
154+
"cherry-pick",
155+
"revert",
156+
]);
157+
158+
export const gitBusyStateSchema = z.union([
159+
z.object({ busy: z.literal(false) }),
160+
z.object({
161+
busy: z.literal(true),
162+
operation: gitBusyOperationSchema,
163+
}),
164+
]);
165+
166+
export type { GitBusyOperation, GitBusyState } from "../../../shared/types";
167+
168+
export const getGitBusyStateInput = directoryPathInput;
169+
export const getGitBusyStateOutput = gitBusyStateSchema;
170+
150171
// createBranch schemas
151172
export const createBranchInput = z.object({
152173
directoryPath: z.string(),

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

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ vi.mock("../../utils/logger.js", () => ({
2323
},
2424
}));
2525

26+
import type { AgentService } from "../agent/service";
2627
import type { LlmGatewayService } from "../llm-gateway/service";
2728
import type { WorkspaceService } from "../workspace/service";
2829
import { GitService, mapPrState } from "./service";
@@ -32,7 +33,11 @@ describe("GitService.getPrChangedFiles", () => {
3233

3334
beforeEach(() => {
3435
vi.clearAllMocks();
35-
service = new GitService({} as LlmGatewayService, {} as WorkspaceService);
36+
service = new GitService(
37+
{} as LlmGatewayService,
38+
{} as WorkspaceService,
39+
{ getSessionEnvForTask: async () => ({}) } as unknown as AgentService,
40+
);
3641
});
3742

3843
it("flattens paginated GH API results and maps file statuses", async () => {
@@ -140,7 +145,11 @@ describe("GitService.getGhAuthToken", () => {
140145

141146
beforeEach(() => {
142147
vi.clearAllMocks();
143-
service = new GitService({} as LlmGatewayService, {} as WorkspaceService);
148+
service = new GitService(
149+
{} as LlmGatewayService,
150+
{} as WorkspaceService,
151+
{ getSessionEnvForTask: async () => ({}) } as unknown as AgentService,
152+
);
144153
});
145154

146155
it("returns the authenticated GitHub CLI token", async () => {
@@ -198,7 +207,11 @@ describe("GitService.getPrUrlForBranch", () => {
198207

199208
beforeEach(() => {
200209
vi.clearAllMocks();
201-
service = new GitService({} as LlmGatewayService, {} as WorkspaceService);
210+
service = new GitService(
211+
{} as LlmGatewayService,
212+
{} as WorkspaceService,
213+
{ getSessionEnvForTask: async () => ({}) } as unknown as AgentService,
214+
);
202215
});
203216

204217
it("returns the PR URL for a branch via gh pr list", async () => {

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

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
getDiffHead,
2020
getDiffStats,
2121
getFileAtHead,
22+
getGitBusyState,
2223
getLatestCommit,
2324
getRemoteUrl,
2425
getStagedDiff,
@@ -40,6 +41,7 @@ import { inject, injectable } from "inversify";
4041
import { MAIN_TOKENS } from "../../di/tokens";
4142
import { logger } from "../../utils/logger";
4243
import { TypedEventEmitter } from "../../utils/typed-event-emitter";
44+
import type { AgentService } from "../agent/service";
4345
import type { LlmGatewayService } from "../llm-gateway/service";
4446
import type { SidebarPrState } from "../workspace/schemas";
4547
import type { WorkspaceService } from "../workspace/service";
@@ -57,6 +59,7 @@ import type {
5759
GetPrTemplateOutput,
5860
GhAuthTokenOutput,
5961
GhStatusOutput,
62+
GitBusyState,
6063
GitCommitInfo,
6164
GitFileStatus,
6265
GithubRef,
@@ -134,10 +137,31 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
134137
private readonly llmGateway: LlmGatewayService,
135138
@inject(MAIN_TOKENS.WorkspaceService)
136139
private readonly workspaceService: WorkspaceService,
140+
@inject(MAIN_TOKENS.AgentService)
141+
private readonly agentService: AgentService,
137142
) {
138143
super();
139144
}
140145

146+
/**
147+
* Resolve env-var overrides set by the agent's SessionStart hooks for the
148+
* given task. Used so UI-triggered git/gh operations (Commit, Create PR)
149+
* see the same env (notably `SSH_AUTH_SOCK` re-pointed at Secretive) as
150+
* the agent's bash tool. Returns `undefined` if there's nothing to apply.
151+
*/
152+
private async getSessionEnv(
153+
taskId: string | undefined,
154+
): Promise<Record<string, string> | undefined> {
155+
if (!taskId) return undefined;
156+
try {
157+
const env = await this.agentService.getSessionEnvForTask(taskId);
158+
return Object.keys(env).length > 0 ? env : undefined;
159+
} catch (err) {
160+
log.warn("Failed to load session env for task", { taskId, err });
161+
return undefined;
162+
}
163+
}
164+
141165
private async getStateSnapshot(
142166
directoryPath: string,
143167
options?: {
@@ -285,6 +309,10 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
285309
return getAllBranches(directoryPath);
286310
}
287311

312+
public async getGitBusyState(directoryPath: string): Promise<GitBusyState> {
313+
return getGitBusyState(directoryPath);
314+
}
315+
288316
public async createBranch(
289317
directoryPath: string,
290318
branchName: string,
@@ -475,6 +503,7 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
475503
branch?: string,
476504
setUpstream = false,
477505
signal?: AbortSignal,
506+
env?: Record<string, string>,
478507
): Promise<PushOutput> {
479508
const saga = new PushSaga();
480509
const result = await saga.run({
@@ -483,6 +512,7 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
483512
branch: branch || undefined,
484513
setUpstream,
485514
signal,
515+
env,
486516
});
487517
if (!result.success) {
488518
return { success: false, message: result.error };
@@ -532,6 +562,7 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
532562
directoryPath: string,
533563
remote = "origin",
534564
signal?: AbortSignal,
565+
env?: Record<string, string>,
535566
): Promise<PublishOutput> {
536567
const currentBranch = await getCurrentBranch(directoryPath);
537568
if (!currentBranch) {
@@ -544,6 +575,7 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
544575
currentBranch,
545576
true,
546577
signal,
578+
env,
547579
);
548580
return {
549581
success: pushResult.success,
@@ -617,6 +649,8 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
617649
});
618650
};
619651

652+
const sessionEnv = await this.getSessionEnv(input.taskId);
653+
620654
const saga = new CreatePrSaga(
621655
{
622656
getCurrentBranch: (dir) => getCurrentBranch(dir),
@@ -625,14 +659,16 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
625659
getChangedFilesHead: (dir) => this.getChangedFilesHead(dir),
626660
generateCommitMessage: (dir) =>
627661
this.generateCommitMessage(dir, input.conversationContext),
628-
commit: (dir, msg, opts) => this.commit(dir, msg, opts),
662+
commit: (dir, msg, opts) =>
663+
this.commit(dir, msg, { ...opts, envOverride: sessionEnv }),
629664
getSyncStatus: (dir) => this.getGitSyncStatus(dir),
630-
push: (dir) => this.push(dir),
631-
publish: (dir) => this.publish(dir),
665+
push: (dir) =>
666+
this.push(dir, "origin", undefined, false, undefined, sessionEnv),
667+
publish: (dir) => this.publish(dir, "origin", undefined, sessionEnv),
632668
generatePrTitleAndBody: (dir) =>
633669
this.generatePrTitleAndBody(dir, input.conversationContext),
634670
createPr: (dir, title, body, draft) =>
635-
this.createPrViaGh(dir, title, body, draft),
671+
this.createPrViaGh(dir, title, body, draft, sessionEnv),
636672
onProgress: emitProgress,
637673
},
638674
log,
@@ -723,6 +759,8 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
723759
allowEmpty?: boolean;
724760
stagedOnly?: boolean;
725761
taskId?: string;
762+
/** Pre-resolved session env. Internal — used by createPr to avoid re-loading. */
763+
envOverride?: Record<string, string>;
726764
},
727765
): Promise<CommitOutput> {
728766
const fail = (msg: string): CommitOutput => ({
@@ -734,11 +772,15 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
734772

735773
if (!message.trim()) return fail("Commit message is required");
736774

775+
const { envOverride, ...sagaOptions } = options ?? {};
776+
const env = envOverride ?? (await this.getSessionEnv(options?.taskId));
777+
737778
const saga = new CommitSaga();
738779
const result = await saga.run({
739780
baseDir: directoryPath,
740781
message: message.trim(),
741-
...options,
782+
env,
783+
...sagaOptions,
742784
});
743785

744786
if (!result.success) return fail(result.error);
@@ -949,6 +991,7 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
949991
title?: string,
950992
body?: string,
951993
draft?: boolean,
994+
env?: Record<string, string>,
952995
): Promise<{ success: boolean; message: string; prUrl: string | null }> {
953996
const prFooter =
954997
"\n\n---\n*Created with [PostHog Code](https://posthog.com/code?ref=pr)*";
@@ -962,7 +1005,7 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
9621005
}
9631006
if (draft) args.push("--draft");
9641007

965-
const result = await execGh(args, { cwd: directoryPath });
1008+
const result = await execGh(args, { cwd: directoryPath, env });
9661009
if (result.exitCode !== 0) {
9671010
return {
9681011
success: false,

0 commit comments

Comments
 (0)