Skip to content

Commit 38cd461

Browse files
committed
feat(code): persist PR URLs to backend
1 parent fa9a488 commit 38cd461

File tree

6 files changed

+174
-73
lines changed

6 files changed

+174
-73
lines changed

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

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

215216
export interface AgentSessionEventPayload {
@@ -230,11 +231,17 @@ export interface SessionIdleKilledPayload {
230231
taskId: string;
231232
}
232233

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

240247
// 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
@@ -27,6 +27,7 @@ import {
2727
} from "@posthog/agent/gateway-models";
2828
import { getLlmGatewayUrl } from "@posthog/agent/posthog-api";
2929
import type { OnLogCallback } from "@posthog/agent/types";
30+
import { getCurrentBranch } from "@posthog/git/queries";
3031
import { isAuthError } from "@shared/errors";
3132
import type { AcpMessage } from "@shared/types/session-events";
3233
import { app, powerMonitor } from "electron";
@@ -1134,8 +1135,8 @@ For git operations while detached:
11341135
};
11351136
emitToRenderer(acpMessage);
11361137

1137-
// Detect PR URLs in bash tool results and attach to task
1138-
this.detectAndAttachPrUrl(taskRunId, message as AcpMessage["message"]);
1138+
// Inspect tool call updates for PR URLs and file activity
1139+
this.handleToolCallUpdate(taskRunId, message as AcpMessage["message"]);
11391140
};
11401141

11411142
const tappedReadable = createTappedReadableStream(
@@ -1415,11 +1416,7 @@ For git operations while detached:
14151416
};
14161417
}
14171418

1418-
/**
1419-
* Detect GitHub PR URLs in bash tool results and attach to task.
1420-
* This enables webhook tracking by populating the pr_url in TaskRun output.
1421-
*/
1422-
private detectAndAttachPrUrl(taskRunId: string, message: unknown): void {
1419+
private handleToolCallUpdate(taskRunId: string, message: unknown): void {
14231420
try {
14241421
const msg = message as {
14251422
method?: string;
@@ -1441,86 +1438,136 @@ For git operations while detached:
14411438
if (msg.method !== "session/update") return;
14421439
if (msg.params?.update?.sessionUpdate !== "tool_call_update") return;
14431440

1444-
const toolMeta = msg.params.update._meta?.claudeCode;
1441+
const update = msg.params.update;
1442+
const toolMeta = update._meta?.claudeCode;
14451443
const toolName = toolMeta?.toolName;
1444+
if (!toolName) return;
14461445

1447-
// Only process Bash tool results
1448-
if (
1449-
!toolName ||
1450-
(!toolName.includes("Bash") && !toolName.includes("bash"))
1451-
) {
1452-
return;
1446+
const session = this.sessions.get(taskRunId);
1447+
1448+
// PR URLs only appear in Bash tool output
1449+
if (toolName.includes("Bash") || toolName.includes("bash")) {
1450+
this.detectAndAttachPrUrl(taskRunId, session, toolMeta, update.content);
14531451
}
14541452

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

1474-
// Also check content array
1475-
const content = msg.params.update.content;
1476-
if (Array.isArray(content)) {
1477-
for (const item of content) {
1478-
if (item.type === "text" && item.text) {
1479-
textToSearch += ` ${item.text}`;
1480-
}
1490+
// Also check content array
1491+
if (Array.isArray(content)) {
1492+
for (const item of content) {
1493+
if (item.type === "text" && item.text) {
1494+
textToSearch += ` ${item.text}`;
14811495
}
14821496
}
1497+
}
14831498

1484-
if (!textToSearch) return;
1499+
if (!textToSearch) return;
14851500

1486-
// Match GitHub PR URLs
1487-
const prUrlMatch = textToSearch.match(
1488-
/https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+/,
1489-
);
1490-
if (!prUrlMatch) return;
1501+
// Match GitHub PR URLs
1502+
const prUrlMatch = textToSearch.match(
1503+
/https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+/,
1504+
);
1505+
if (!prUrlMatch) return;
14911506

1492-
const prUrl = prUrlMatch[0];
1493-
log.info("Detected PR URL in bash output", { taskRunId, prUrl });
1507+
const prUrl = prUrlMatch[0];
1508+
log.info("Detected PR URL in bash output", { taskRunId, prUrl });
14941509

1495-
// Find session and attach PR URL
1496-
const session = this.sessions.get(taskRunId);
1497-
if (!session) {
1498-
log.warn("Session not found for PR attachment", { taskRunId });
1499-
return;
1500-
}
1510+
// Attach PR URL
1511+
if (!session) {
1512+
log.warn("Session not found for PR attachment", { taskRunId });
1513+
return;
1514+
}
15011515

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

15261573
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
@@ -191,6 +191,16 @@ export const agentRouter = router({
191191
}
192192
}),
193193

194+
onAgentFileActivity: publicProcedure.subscription(async function* (opts) {
195+
const service = getService();
196+
for await (const event of service.toIterable(
197+
AgentServiceEvent.AgentFileActivity,
198+
{ signal: opts.signal },
199+
)) {
200+
yield event;
201+
}
202+
}),
203+
194204
getGatewayModels: publicProcedure
195205
.input(getGatewayModelsInput)
196206
.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)