Skip to content

Commit b05db93

Browse files
committed
feat(code): persist PR URLs to backend
1 parent fea8dfe commit b05db93

6 files changed

Lines changed: 174 additions & 73 deletions

File tree

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ export const AgentServiceEvent = {
209209
PermissionRequest: "permission-request",
210210
SessionsIdle: "sessions-idle",
211211
SessionIdleKilled: "session-idle-killed",
212+
AgentFileActivity: "agent-file-activity",
212213
} as const;
213214

214215
export interface AgentSessionEventPayload {
@@ -229,11 +230,17 @@ export interface SessionIdleKilledPayload {
229230
taskId: string;
230231
}
231232

233+
export interface AgentFileActivityPayload {
234+
taskId: string;
235+
branchName: string | null;
236+
}
237+
232238
export interface AgentServiceEvents {
233239
[AgentServiceEvent.SessionEvent]: AgentSessionEventPayload;
234240
[AgentServiceEvent.PermissionRequest]: PermissionRequestPayload;
235241
[AgentServiceEvent.SessionsIdle]: undefined;
236242
[AgentServiceEvent.SessionIdleKilled]: SessionIdleKilledPayload;
243+
[AgentServiceEvent.AgentFileActivity]: AgentFileActivityPayload;
237244
}
238245

239246
// Permission response input for tRPC

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

Lines changed: 119 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
} from "@posthog/agent/gateway-models";
2323
import { getLlmGatewayUrl } from "@posthog/agent/posthog-api";
2424
import type { OnLogCallback } from "@posthog/agent/types";
25+
import { getCurrentBranch } from "@posthog/git/queries";
2526
import { isAuthError } from "@shared/errors";
2627
import type { AcpMessage } from "@shared/types/session-events";
2728
import { app, powerMonitor } from "electron";
@@ -1124,8 +1125,8 @@ For git operations while detached:
11241125
};
11251126
emitToRenderer(acpMessage);
11261127

1127-
// Detect PR URLs in bash tool results and attach to task
1128-
this.detectAndAttachPrUrl(taskRunId, message as AcpMessage["message"]);
1128+
// Inspect tool call updates for PR URLs and file activity
1129+
this.handleToolCallUpdate(taskRunId, message as AcpMessage["message"]);
11291130
};
11301131

11311132
const tappedReadable = createTappedReadableStream(
@@ -1404,11 +1405,7 @@ For git operations while detached:
14041405
};
14051406
}
14061407

1407-
/**
1408-
* Detect GitHub PR URLs in bash tool results and attach to task.
1409-
* This enables webhook tracking by populating the pr_url in TaskRun output.
1410-
*/
1411-
private detectAndAttachPrUrl(taskRunId: string, message: unknown): void {
1408+
private handleToolCallUpdate(taskRunId: string, message: unknown): void {
14121409
try {
14131410
const msg = message as {
14141411
method?: string;
@@ -1430,86 +1427,136 @@ For git operations while detached:
14301427
if (msg.method !== "session/update") return;
14311428
if (msg.params?.update?.sessionUpdate !== "tool_call_update") return;
14321429

1433-
const toolMeta = msg.params.update._meta?.claudeCode;
1430+
const update = msg.params.update;
1431+
const toolMeta = update._meta?.claudeCode;
14341432
const toolName = toolMeta?.toolName;
1433+
if (!toolName) return;
14351434

1436-
// Only process Bash tool results
1437-
if (
1438-
!toolName ||
1439-
(!toolName.includes("Bash") && !toolName.includes("bash"))
1440-
) {
1441-
return;
1435+
const session = this.sessions.get(taskRunId);
1436+
1437+
// PR URLs only appear in Bash tool output
1438+
if (toolName.includes("Bash") || toolName.includes("bash")) {
1439+
this.detectAndAttachPrUrl(taskRunId, session, toolMeta, update.content);
14421440
}
14431441

1444-
// Extract text content from tool response or update content
1445-
let textToSearch = "";
1446-
1447-
// Check toolResponse (hook response with raw output)
1448-
const toolResponse = toolMeta?.toolResponse;
1449-
if (toolResponse) {
1450-
if (typeof toolResponse === "string") {
1451-
textToSearch = toolResponse;
1452-
} else if (typeof toolResponse === "object" && toolResponse !== null) {
1453-
// May be { stdout?: string, stderr?: string } or similar
1454-
const respObj = toolResponse as Record<string, unknown>;
1455-
textToSearch =
1456-
String(respObj.stdout || "") + String(respObj.stderr || "");
1457-
if (!textToSearch && respObj.output) {
1458-
textToSearch = String(respObj.output);
1459-
}
1442+
this.trackAgentFileActivity(taskRunId, session, toolName);
1443+
} catch (err) {
1444+
log.debug("Error in tool call update handling", {
1445+
taskRunId,
1446+
error: err,
1447+
});
1448+
}
1449+
}
1450+
1451+
/**
1452+
* Detect GitHub PR URLs in bash tool results and attach to task.
1453+
* This enables webhook tracking by populating the pr_url in TaskRun output.
1454+
*/
1455+
private detectAndAttachPrUrl(
1456+
taskRunId: string,
1457+
session: ManagedSession | undefined,
1458+
toolMeta: { toolName?: string; toolResponse?: unknown },
1459+
content?: Array<{ type?: string; text?: string }>,
1460+
): void {
1461+
let textToSearch = "";
1462+
1463+
// Check toolResponse (hook response with raw output)
1464+
const toolResponse = toolMeta?.toolResponse;
1465+
if (toolResponse) {
1466+
if (typeof toolResponse === "string") {
1467+
textToSearch = toolResponse;
1468+
} else if (typeof toolResponse === "object" && toolResponse !== null) {
1469+
// May be { stdout?: string, stderr?: string } or similar
1470+
const respObj = toolResponse as Record<string, unknown>;
1471+
textToSearch =
1472+
String(respObj.stdout || "") + String(respObj.stderr || "");
1473+
if (!textToSearch && respObj.output) {
1474+
textToSearch = String(respObj.output);
14601475
}
14611476
}
1477+
}
14621478

1463-
// Also check content array
1464-
const content = msg.params.update.content;
1465-
if (Array.isArray(content)) {
1466-
for (const item of content) {
1467-
if (item.type === "text" && item.text) {
1468-
textToSearch += ` ${item.text}`;
1469-
}
1479+
// Also check content array
1480+
if (Array.isArray(content)) {
1481+
for (const item of content) {
1482+
if (item.type === "text" && item.text) {
1483+
textToSearch += ` ${item.text}`;
14701484
}
14711485
}
1486+
}
14721487

1473-
if (!textToSearch) return;
1488+
if (!textToSearch) return;
14741489

1475-
// Match GitHub PR URLs
1476-
const prUrlMatch = textToSearch.match(
1477-
/https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+/,
1478-
);
1479-
if (!prUrlMatch) return;
1490+
// Match GitHub PR URLs
1491+
const prUrlMatch = textToSearch.match(
1492+
/https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+/,
1493+
);
1494+
if (!prUrlMatch) return;
14801495

1481-
const prUrl = prUrlMatch[0];
1482-
log.info("Detected PR URL in bash output", { taskRunId, prUrl });
1496+
const prUrl = prUrlMatch[0];
1497+
log.info("Detected PR URL in bash output", { taskRunId, prUrl });
14831498

1484-
// Find session and attach PR URL
1485-
const session = this.sessions.get(taskRunId);
1486-
if (!session) {
1487-
log.warn("Session not found for PR attachment", { taskRunId });
1488-
return;
1489-
}
1499+
// Attach PR URL
1500+
if (!session) {
1501+
log.warn("Session not found for PR attachment", { taskRunId });
1502+
return;
1503+
}
14901504

1491-
// Attach asynchronously without blocking message flow
1492-
session.agent
1493-
.attachPullRequestToTask(session.taskId, prUrl)
1494-
.then(() => {
1495-
log.info("PR URL attached to task", {
1496-
taskRunId,
1497-
taskId: session.taskId,
1498-
prUrl,
1499-
});
1500-
})
1501-
.catch((err) => {
1502-
log.error("Failed to attach PR URL to task", {
1503-
taskRunId,
1504-
taskId: session.taskId,
1505-
prUrl,
1506-
error: err,
1507-
});
1505+
// Attach asynchronously without blocking message flow
1506+
session.agent
1507+
.attachPullRequestToTask(session.taskId, prUrl)
1508+
.then(() => {
1509+
log.info("PR URL attached to task", {
1510+
taskRunId,
1511+
taskId: session.taskId,
1512+
prUrl,
15081513
});
1509-
} catch (err) {
1510-
// Don't let detection errors break message flow
1511-
log.debug("Error in PR URL detection", { taskRunId, error: err });
1512-
}
1514+
})
1515+
.catch((err) => {
1516+
log.error("Failed to attach PR URL to task", {
1517+
taskRunId,
1518+
taskId: session.taskId,
1519+
prUrl,
1520+
error: err,
1521+
});
1522+
});
1523+
}
1524+
1525+
/**
1526+
* Track agent file activity for branch association observability.
1527+
*/
1528+
private static readonly FILE_MODIFYING_TOOLS = new Set([
1529+
"Edit",
1530+
"Write",
1531+
"FileEditTool",
1532+
"FileWriteTool",
1533+
"MultiEdit",
1534+
"NotebookEdit",
1535+
]);
1536+
1537+
private trackAgentFileActivity(
1538+
taskRunId: string,
1539+
session: ManagedSession | undefined,
1540+
toolName: string,
1541+
): void {
1542+
if (!session) return;
1543+
if (!AgentService.FILE_MODIFYING_TOOLS.has(toolName)) return;
1544+
1545+
getCurrentBranch(session.repoPath)
1546+
.then((branchName) => {
1547+
this.emit(AgentServiceEvent.AgentFileActivity, {
1548+
taskId: session.taskId,
1549+
branchName,
1550+
});
1551+
})
1552+
.catch((err) => {
1553+
log.error("Failed to emit agent file activity event", {
1554+
taskRunId,
1555+
taskId: session.taskId,
1556+
toolName,
1557+
error: err,
1558+
});
1559+
});
15131560
}
15141561

15151562
async getGatewayModels(apiHost: string) {

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,16 @@ export const agentRouter = router({
189189
}
190190
}),
191191

192+
onAgentFileActivity: publicProcedure.subscription(async function* (opts) {
193+
const service = getService();
194+
for await (const event of service.toIterable(
195+
AgentServiceEvent.AgentFileActivity,
196+
{ signal: opts.signal },
197+
)) {
198+
yield event;
199+
}
200+
}),
201+
192202
getGatewayModels: publicProcedure
193203
.input(getGatewayModelsInput)
194204
.output(getGatewayModelsOutput)

apps/code/src/renderer/App.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ import { initializeConnectivityStore } from "@renderer/stores/connectivityStore"
1414
import { useFocusStore } from "@renderer/stores/focusStore";
1515
import { useThemeStore } from "@renderer/stores/themeStore";
1616
import { trpcClient, useTRPC } from "@renderer/trpc/client";
17+
import { ANALYTICS_EVENTS } from "@shared/types/analytics";
1718
import { useQueryClient } from "@tanstack/react-query";
1819
import { useSubscription } from "@trpc/tanstack-react-query";
19-
import { initializePostHog } from "@utils/analytics";
20+
import { initializePostHog, track } from "@utils/analytics";
2021
import { logger } from "@utils/logger";
2122
import { toast } from "@utils/toast";
2223
import { AnimatePresence, motion } from "framer-motion";
@@ -108,6 +109,17 @@ function App() {
108109
}),
109110
);
110111

112+
useSubscription(
113+
trpcReact.agent.onAgentFileActivity.subscriptionOptions(undefined, {
114+
onData: (data) => {
115+
track(ANALYTICS_EVENTS.AGENT_FILE_ACTIVITY, {
116+
task_id: data.taskId,
117+
branch_name: data.branchName,
118+
});
119+
},
120+
}),
121+
);
122+
111123
// Auto-unfocus when user manually checks out to a different branch
112124
useSubscription(
113125
trpcReact.focus.onForeignBranchCheckout.subscriptionOptions(undefined, {

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { getAuthenticatedClient } from "@features/auth/hooks/authClient";
12
import { useGitQueries } from "@features/git-interaction/hooks/useGitQueries";
23
import { computeGitInteractionState } from "@features/git-interaction/state/gitInteractionLogic";
34
import {
@@ -19,6 +20,7 @@ import { getSuggestedBranchName } from "@features/git-interaction/utils/getSugge
1920
import { invalidateGitBranchQueries } from "@features/git-interaction/utils/gitCacheKeys";
2021
import { partitionByStaged } from "@features/git-interaction/utils/partitionByStaged";
2122
import { updateGitCacheFromSnapshot } from "@features/git-interaction/utils/updateGitCache";
23+
import { useSessionStore } from "@features/sessions/stores/sessionStore";
2224
import { trpc, trpcClient } from "@renderer/trpc";
2325
import type { ChangedFile } from "@shared/types";
2426
import { ANALYTICS_EVENTS } from "@shared/types/analytics";
@@ -115,6 +117,21 @@ function trackGitAction(
115117
});
116118
}
117119

120+
function attachPrUrlToTask(taskId: string, prUrl: string) {
121+
const taskRunId = useSessionStore.getState().taskIdIndex[taskId];
122+
if (!taskRunId) return;
123+
124+
getAuthenticatedClient()
125+
.then((client) =>
126+
client?.updateTaskRun(taskId, taskRunId, {
127+
output: { pr_url: prUrl },
128+
}),
129+
)
130+
.catch((err) =>
131+
log.warn("Failed to attach PR URL to task", { taskId, prUrl, err }),
132+
);
133+
}
134+
118135
export function useGitInteraction(
119136
taskId: string,
120137
repoPath?: string,
@@ -255,6 +272,7 @@ export function useGitInteraction(
255272

256273
if (result.prUrl) {
257274
await trpcClient.os.openExternal.mutate({ url: result.prUrl });
275+
attachPrUrlToTask(taskId, result.prUrl);
258276
}
259277

260278
modal.closeCreatePr();

apps/code/src/shared/types/analytics.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ export interface PrCreatedProperties {
111111
success: boolean;
112112
}
113113

114+
export interface AgentFileActivityProperties {
115+
task_id: string;
116+
branch_name: string | null;
117+
}
118+
114119
// File interactions
115120
export interface FileOpenedProperties {
116121
file_extension: string;
@@ -224,6 +229,7 @@ export const ANALYTICS_EVENTS = {
224229
// Git operations
225230
GIT_ACTION_EXECUTED: "Git action executed",
226231
PR_CREATED: "PR created",
232+
AGENT_FILE_ACTIVITY: "Agent file activity",
227233

228234
// File interactions
229235
FILE_OPENED: "File opened",
@@ -278,6 +284,7 @@ export type EventPropertyMap = {
278284
// Git operations
279285
[ANALYTICS_EVENTS.GIT_ACTION_EXECUTED]: GitActionExecutedProperties;
280286
[ANALYTICS_EVENTS.PR_CREATED]: PrCreatedProperties;
287+
[ANALYTICS_EVENTS.AGENT_FILE_ACTIVITY]: AgentFileActivityProperties;
281288

282289
// File interactions
283290
[ANALYTICS_EVENTS.FILE_OPENED]: FileOpenedProperties;

0 commit comments

Comments
 (0)