Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/code/src/main/services/agent/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ export const AgentServiceEvent = {
PermissionRequest: "permission-request",
SessionsIdle: "sessions-idle",
SessionIdleKilled: "session-idle-killed",
AgentFileActivity: "agent-file-activity",
} as const;

export interface AgentSessionEventPayload {
Expand All @@ -229,11 +230,17 @@ export interface SessionIdleKilledPayload {
taskId: string;
}

export interface AgentFileActivityPayload {
taskId: string;
branchName: string | null;
}

export interface AgentServiceEvents {
[AgentServiceEvent.SessionEvent]: AgentSessionEventPayload;
[AgentServiceEvent.PermissionRequest]: PermissionRequestPayload;
[AgentServiceEvent.SessionsIdle]: undefined;
[AgentServiceEvent.SessionIdleKilled]: SessionIdleKilledPayload;
[AgentServiceEvent.AgentFileActivity]: AgentFileActivityPayload;
}

// Permission response input for tRPC
Expand Down
191 changes: 119 additions & 72 deletions apps/code/src/main/services/agent/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
} from "@posthog/agent/gateway-models";
import { getLlmGatewayUrl } from "@posthog/agent/posthog-api";
import type { OnLogCallback } from "@posthog/agent/types";
import { getCurrentBranch } from "@posthog/git/queries";
import { isAuthError } from "@shared/errors";
import type { AcpMessage } from "@shared/types/session-events";
import { app, powerMonitor } from "electron";
Expand Down Expand Up @@ -1124,8 +1125,8 @@ For git operations while detached:
};
emitToRenderer(acpMessage);

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

const tappedReadable = createTappedReadableStream(
Expand Down Expand Up @@ -1404,11 +1405,7 @@ For git operations while detached:
};
}

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

const toolMeta = msg.params.update._meta?.claudeCode;
const update = msg.params.update;
const toolMeta = update._meta?.claudeCode;
const toolName = toolMeta?.toolName;
if (!toolName) return;

// Only process Bash tool results
if (
!toolName ||
(!toolName.includes("Bash") && !toolName.includes("bash"))
) {
return;
const session = this.sessions.get(taskRunId);

// PR URLs only appear in Bash tool output
if (toolName.includes("Bash") || toolName.includes("bash")) {
this.detectAndAttachPrUrl(taskRunId, session, toolMeta, update.content);
}

// Extract text content from tool response or update content
let textToSearch = "";

// Check toolResponse (hook response with raw output)
const toolResponse = toolMeta?.toolResponse;
if (toolResponse) {
if (typeof toolResponse === "string") {
textToSearch = toolResponse;
} else if (typeof toolResponse === "object" && toolResponse !== null) {
// May be { stdout?: string, stderr?: string } or similar
const respObj = toolResponse as Record<string, unknown>;
textToSearch =
String(respObj.stdout || "") + String(respObj.stderr || "");
if (!textToSearch && respObj.output) {
textToSearch = String(respObj.output);
}
this.trackAgentFileActivity(taskRunId, session, toolName);
} catch (err) {
log.debug("Error in tool call update handling", {
taskRunId,
error: err,
});
}
}

/**
* Detect GitHub PR URLs in bash tool results and attach to task.
* This enables webhook tracking by populating the pr_url in TaskRun output.
*/
private detectAndAttachPrUrl(
taskRunId: string,
session: ManagedSession | undefined,
toolMeta: { toolName?: string; toolResponse?: unknown },
content?: Array<{ type?: string; text?: string }>,
): void {
let textToSearch = "";

// Check toolResponse (hook response with raw output)
const toolResponse = toolMeta?.toolResponse;
if (toolResponse) {
if (typeof toolResponse === "string") {
textToSearch = toolResponse;
} else if (typeof toolResponse === "object" && toolResponse !== null) {
// May be { stdout?: string, stderr?: string } or similar
const respObj = toolResponse as Record<string, unknown>;
textToSearch =
String(respObj.stdout || "") + String(respObj.stderr || "");
if (!textToSearch && respObj.output) {
textToSearch = String(respObj.output);
}
}
}

// Also check content array
const content = msg.params.update.content;
if (Array.isArray(content)) {
for (const item of content) {
if (item.type === "text" && item.text) {
textToSearch += ` ${item.text}`;
}
// Also check content array
if (Array.isArray(content)) {
for (const item of content) {
if (item.type === "text" && item.text) {
textToSearch += ` ${item.text}`;
}
}
}

if (!textToSearch) return;
if (!textToSearch) return;

// Match GitHub PR URLs
const prUrlMatch = textToSearch.match(
/https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+/,
);
if (!prUrlMatch) return;
// Match GitHub PR URLs
const prUrlMatch = textToSearch.match(
/https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+/,
);
if (!prUrlMatch) return;

const prUrl = prUrlMatch[0];
log.info("Detected PR URL in bash output", { taskRunId, prUrl });
const prUrl = prUrlMatch[0];
log.info("Detected PR URL in bash output", { taskRunId, prUrl });

// Find session and attach PR URL
const session = this.sessions.get(taskRunId);
if (!session) {
log.warn("Session not found for PR attachment", { taskRunId });
return;
}
// Attach PR URL
if (!session) {
log.warn("Session not found for PR attachment", { taskRunId });
return;
}

// Attach asynchronously without blocking message flow
session.agent
.attachPullRequestToTask(session.taskId, prUrl)
.then(() => {
log.info("PR URL attached to task", {
taskRunId,
taskId: session.taskId,
prUrl,
});
})
.catch((err) => {
log.error("Failed to attach PR URL to task", {
taskRunId,
taskId: session.taskId,
prUrl,
error: err,
});
// Attach asynchronously without blocking message flow
session.agent
.attachPullRequestToTask(session.taskId, prUrl)
.then(() => {
log.info("PR URL attached to task", {
taskRunId,
taskId: session.taskId,
prUrl,
});
} catch (err) {
// Don't let detection errors break message flow
log.debug("Error in PR URL detection", { taskRunId, error: err });
}
})
.catch((err) => {
log.error("Failed to attach PR URL to task", {
taskRunId,
taskId: session.taskId,
prUrl,
error: err,
});
});
}

/**
* Track agent file activity for branch association observability.
*/
private static readonly FILE_MODIFYING_TOOLS = new Set([
"Edit",
"Write",
"FileEditTool",
"FileWriteTool",
"MultiEdit",
"NotebookEdit",
]);

private trackAgentFileActivity(
taskRunId: string,
session: ManagedSession | undefined,
toolName: string,
): void {
if (!session) return;
if (!AgentService.FILE_MODIFYING_TOOLS.has(toolName)) return;

getCurrentBranch(session.repoPath)
.then((branchName) => {
this.emit(AgentServiceEvent.AgentFileActivity, {
taskId: session.taskId,
branchName,
});
})
.catch((err) => {
log.error("Failed to emit agent file activity event", {
taskRunId,
taskId: session.taskId,
toolName,
error: err,
});
});
}

async getGatewayModels(apiHost: string) {
Expand Down
10 changes: 10 additions & 0 deletions apps/code/src/main/trpc/routers/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,16 @@ export const agentRouter = router({
}
}),

onAgentFileActivity: publicProcedure.subscription(async function* (opts) {
const service = getService();
for await (const event of service.toIterable(
AgentServiceEvent.AgentFileActivity,
{ signal: opts.signal },
)) {
yield event;
}
}),

getGatewayModels: publicProcedure
.input(getGatewayModelsInput)
.output(getGatewayModelsOutput)
Expand Down
14 changes: 13 additions & 1 deletion apps/code/src/renderer/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ import { initializeConnectivityStore } from "@renderer/stores/connectivityStore"
import { useFocusStore } from "@renderer/stores/focusStore";
import { useThemeStore } from "@renderer/stores/themeStore";
import { trpcClient, useTRPC } from "@renderer/trpc/client";
import { ANALYTICS_EVENTS } from "@shared/types/analytics";
import { useQueryClient } from "@tanstack/react-query";
import { useSubscription } from "@trpc/tanstack-react-query";
import { initializePostHog } from "@utils/analytics";
import { initializePostHog, track } from "@utils/analytics";
import { logger } from "@utils/logger";
import { toast } from "@utils/toast";
import { AnimatePresence, motion } from "framer-motion";
Expand Down Expand Up @@ -108,6 +109,17 @@ function App() {
}),
);

useSubscription(
trpcReact.agent.onAgentFileActivity.subscriptionOptions(undefined, {
onData: (data) => {
track(ANALYTICS_EVENTS.AGENT_FILE_ACTIVITY, {
task_id: data.taskId,
branch_name: data.branchName,
});
},
}),
);

// Auto-unfocus when user manually checks out to a different branch
useSubscription(
trpcReact.focus.onForeignBranchCheckout.subscriptionOptions(undefined, {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getAuthenticatedClient } from "@features/auth/hooks/authClient";
import { useGitQueries } from "@features/git-interaction/hooks/useGitQueries";
import { computeGitInteractionState } from "@features/git-interaction/state/gitInteractionLogic";
import {
Expand All @@ -19,6 +20,7 @@ import { getSuggestedBranchName } from "@features/git-interaction/utils/getSugge
import { invalidateGitBranchQueries } from "@features/git-interaction/utils/gitCacheKeys";
import { partitionByStaged } from "@features/git-interaction/utils/partitionByStaged";
import { updateGitCacheFromSnapshot } from "@features/git-interaction/utils/updateGitCache";
import { useSessionStore } from "@features/sessions/stores/sessionStore";
import { trpc, trpcClient } from "@renderer/trpc";
import type { ChangedFile } from "@shared/types";
import { ANALYTICS_EVENTS } from "@shared/types/analytics";
Expand Down Expand Up @@ -115,6 +117,21 @@ function trackGitAction(
});
}

function attachPrUrlToTask(taskId: string, prUrl: string) {
const taskRunId = useSessionStore.getState().taskIdIndex[taskId];
if (!taskRunId) return;

getAuthenticatedClient()
.then((client) =>
client?.updateTaskRun(taskId, taskRunId, {
output: { pr_url: prUrl },
}),
)
.catch((err) =>
log.warn("Failed to attach PR URL to task", { taskId, prUrl, err }),
);
}

export function useGitInteraction(
taskId: string,
repoPath?: string,
Expand Down Expand Up @@ -255,6 +272,7 @@ export function useGitInteraction(

if (result.prUrl) {
await trpcClient.os.openExternal.mutate({ url: result.prUrl });
attachPrUrlToTask(taskId, result.prUrl);
}

modal.closeCreatePr();
Expand Down
7 changes: 7 additions & 0 deletions apps/code/src/shared/types/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ export interface PrCreatedProperties {
success: boolean;
}

export interface AgentFileActivityProperties {
task_id: string;
branch_name: string | null;
}

// File interactions
export interface FileOpenedProperties {
file_extension: string;
Expand Down Expand Up @@ -224,6 +229,7 @@ export const ANALYTICS_EVENTS = {
// Git operations
GIT_ACTION_EXECUTED: "Git action executed",
PR_CREATED: "PR created",
AGENT_FILE_ACTIVITY: "Agent file activity",

// File interactions
FILE_OPENED: "File opened",
Expand Down Expand Up @@ -278,6 +284,7 @@ export type EventPropertyMap = {
// Git operations
[ANALYTICS_EVENTS.GIT_ACTION_EXECUTED]: GitActionExecutedProperties;
[ANALYTICS_EVENTS.PR_CREATED]: PrCreatedProperties;
[ANALYTICS_EVENTS.AGENT_FILE_ACTIVITY]: AgentFileActivityProperties;

// File interactions
[ANALYTICS_EVENTS.FILE_OPENED]: FileOpenedProperties;
Expand Down
Loading