Skip to content

Commit cb8db4d

Browse files
OpenSource03claude
andcommitted
feat: permission prompt overhaul and queue hardening
- Rich tool detail formatting with structured labels, scrollable command preview, and bash descriptions - Double-submit prevention with loading states and request ID tracking - Permission queue extracted to pure functions with comprehensive tests - Graceful handling of stale permission requests via completed ID tracking - Analytics instrumentation across all engines and IPC handlers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e2b4a97 commit cb8db4d

23 files changed

Lines changed: 457 additions & 24 deletions

electron/src/ipc/acp-sessions.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { safeSend } from "../lib/safe-send";
88
import { getAgent } from "../lib/agent-registry";
99
import { getMcpAuthHeaders } from "../lib/mcp-oauth-flow";
1010
import { extractErrorMessage } from "../lib/error-utils";
11+
import { captureEvent } from "../lib/posthog";
1112

1213
// ACP SDK is ESM-only, must be async-imported
1314
import type { ClientSideConnection } from "@agentclientprotocol/sdk";
@@ -462,6 +463,8 @@ export function register(getMainWindow: () => BrowserWindow | null): void {
462463
// Startup succeeded — clear the pending tracker before returning
463464
pendingStartProcess = null;
464465

466+
void captureEvent("session_created", { engine: "acp" });
467+
465468
return {
466469
sessionId: internalId,
467470
agentSessionId: sessionResult.sessionId,
@@ -540,6 +543,7 @@ export function register(getMainWindow: () => BrowserWindow | null): void {
540543
}
541544

542545
const mcpStatuses = (options.mcpServers ?? []).map(s => ({ name: s.name, status: "connected" as const }));
546+
void captureEvent("session_revived", { engine: "acp", success: true });
543547
return { sessionId: internalId, agentSessionId: acpSessionId, usedLoad, configOptions, mcpStatuses };
544548
} catch (err) {
545549
// Kill process and clean up any partial session entry

electron/src/ipc/claude-sessions.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { getMcpAuthHeaders } from "../lib/mcp-oauth-flow";
1010
import { getClaudeModelsCache, setClaudeModelsCache } from "../lib/claude-model-cache";
1111
import { extractErrorMessage } from "../lib/error-utils";
1212
import { getClaudeBinaryPath, getClaudeBinaryStatus, getClaudeVersion } from "../lib/claude-binary";
13+
import { captureEvent } from "../lib/posthog";
1314

1415
/** SDK options for file checkpointing — enables Write/Edit/NotebookEdit revert support */
1516
function fileCheckpointOptions(): Record<string, unknown> {
@@ -186,6 +187,33 @@ function startEventLoop(
186187
log("EVENT_FULL", message);
187188
}
188189
safeSend(getMainWindow, "claude:event", { ...(message as object), _sessionId: sessionId });
190+
191+
// Track session completion on result events
192+
if (msgObj.type === "result") {
193+
void captureEvent("session_completed", {
194+
engine: "claude",
195+
total_cost: msgObj.total_cost_usd,
196+
num_turns: msgObj.num_turns,
197+
duration_ms: msgObj.duration_ms,
198+
is_error: !!msgObj.is_error,
199+
});
200+
}
201+
202+
// Track tool execution on tool_result user events
203+
if (msgObj.type === "user") {
204+
const userMsg = msgObj.message as { content?: unknown } | undefined;
205+
const content = userMsg?.content;
206+
if (Array.isArray(content) && content[0]?.type === "tool_result") {
207+
const isError = !!content[0].is_error;
208+
const toolMeta = msgObj.tool_use_result as Record<string, unknown> | undefined;
209+
void captureEvent("tool_executed", {
210+
engine: "claude",
211+
is_error: isError,
212+
is_mcp: false,
213+
is_async: !!toolMeta?.isAsync,
214+
});
215+
}
216+
}
189217
}
190218
} catch (err) {
191219
queryError = extractErrorMessage(err);
@@ -553,6 +581,12 @@ export function register(getMainWindow: () => BrowserWindow | null): void {
553581

554582
startEventLoop(sessionId, q, session, getMainWindow);
555583

584+
void captureEvent("session_created", {
585+
engine: "claude",
586+
model: options.model,
587+
is_resume: !!options.resume,
588+
});
589+
556590
return { sessionId, pid: 0 };
557591
} catch (err) {
558592
// getSDK() or query() threw — clean up and return error
@@ -562,6 +596,7 @@ export function register(getMainWindow: () => BrowserWindow | null): void {
562596
safeSend(getMainWindow,"claude:exit", {
563597
code: 1, _sessionId: sessionId, error: errMsg,
564598
});
599+
void captureEvent("session_error", { engine: "claude", phase: "start" });
565600
return { sessionId, pid: 0, error: errMsg };
566601
}
567602
});
@@ -662,6 +697,7 @@ export function register(getMainWindow: () => BrowserWindow | null): void {
662697
session.startOptions.model = model;
663698
}
664699
log("SET_MODEL", "session=" + sessionId.slice(0, 8) + " model=" + model);
700+
void captureEvent("model_changed", { engine: "claude", model });
665701
return { ok: true };
666702
} catch (err) {
667703
log("SET_MODEL_ERR", "session=" + sessionId.slice(0, 8) + " model=" + model + " " + extractErrorMessage(err));

electron/src/ipc/codex-sessions.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { CodexRpcClient } from "../lib/codex-rpc";
1515
import { getCodexBinaryPath, getCodexBinaryStatus, getCodexVersion } from "../lib/codex-binary";
1616
import { getAppSetting } from "../lib/app-settings";
1717
import { extractErrorMessage } from "../lib/error-utils";
18+
import { captureEvent } from "../lib/posthog";
1819

1920
import type {
2021
CodexServerNotification,
@@ -346,6 +347,8 @@ export function register(getMainWindow: () => BrowserWindow | null): void {
346347
session.threadId = threadResult.thread.id;
347348
log("codex",` Thread started: ${session.threadId}`);
348349

350+
void captureEvent("session_created", { engine: "codex", model: selectedModel });
351+
349352
return {
350353
sessionId: internalId,
351354
threadId: session.threadId,
@@ -355,6 +358,7 @@ export function register(getMainWindow: () => BrowserWindow | null): void {
355358
needsAuth: false,
356359
};
357360
} catch (err) {
361+
void captureEvent("session_error", { engine: "codex", phase: "start" });
358362
log("codex",` Start failed: ${extractErrorMessage(err)}`);
359363
// Clean up on failure
360364
const session = codexSessions.get(internalId);
@@ -716,8 +720,10 @@ export function register(getMainWindow: () => BrowserWindow | null): void {
716720
session.threadId = threadResult.thread.id;
717721
log("codex",` Thread resumed: ${session.threadId}`);
718722

723+
void captureEvent("session_revived", { engine: "codex", success: true });
719724
return { sessionId: internalId, threadId: session.threadId };
720725
} catch (err) {
726+
void captureEvent("session_revived", { engine: "codex", success: false });
721727
log("codex",` Resume failed: ${extractErrorMessage(err)}`);
722728
const session = codexSessions.get(internalId);
723729
if (session) {

electron/src/ipc/files.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import fs from "fs";
55
import { log } from "../lib/logger";
66
import { ALWAYS_SKIP } from "../lib/git-exec";
77
import { getAppSetting } from "../lib/app-settings";
8+
import { captureEvent } from "../lib/posthog";
89

910
function listFilesGit(cwd: string): Promise<string[]> {
1011
return new Promise((resolve, reject) => {
@@ -301,7 +302,9 @@ export function register(): void {
301302

302303
for (const editor of ordered) {
303304
try {
304-
return await tryEditor(editor);
305+
const result = await tryEditor(editor);
306+
void captureEvent("file_opened_in_editor", { editor });
307+
return result;
305308
} catch {
306309
// Editor not found, try next
307310
}
@@ -310,6 +313,7 @@ export function register(): void {
310313
// Fallback: OS default
311314
try {
312315
await shell.openPath(filePath);
316+
void captureEvent("file_opened_in_editor", { editor: "default" });
313317
return { ok: true, editor: "default" };
314318
} catch (err) {
315319
const errMsg = err instanceof Error ? err.message : String(err);

electron/src/ipc/git.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ipcMain } from "electron";
22
import path from "path";
33
import fs from "fs";
44
import { gitExec, ALWAYS_SKIP } from "../lib/git-exec";
5+
import { captureEvent } from "../lib/posthog";
56

67
interface DiscoveredRepo {
78
path: string;
@@ -281,6 +282,7 @@ export function register(): void {
281282
ipcMain.handle("git:commit", async (_event, { cwd, message }: { cwd: string; message: string }) => {
282283
try {
283284
const output = await gitExec(["commit", "-m", message], cwd);
285+
void captureEvent("git_commit_created", { message_length: message.length });
284286
return { ok: true, output };
285287
} catch (err) {
286288
return { error: err instanceof Error ? err.message : String(err) };
@@ -333,6 +335,7 @@ export function register(): void {
333335
try {
334336
validateRef(branch);
335337
await gitExec(["checkout", branch], cwd);
338+
void captureEvent("git_branch_switched");
336339
return { ok: true };
337340
} catch (err) {
338341
return { error: err instanceof Error ? err.message : String(err) };

electron/src/ipc/mcp.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { loadMcpServers, addMcpServer, removeMcpServer } from "../lib/mcp-store"
44
import { authenticateMcpServer } from "../lib/mcp-oauth-flow";
55
import { loadOAuthData, deleteOAuthData } from "../lib/mcp-oauth-store";
66
import { log } from "../lib/logger";
7+
import { captureEvent } from "../lib/posthog";
78
import type { McpServerConfig } from "../lib/mcp-store";
89

910
interface ProbeResult {
@@ -124,6 +125,7 @@ export function register(): void {
124125
ipcMain.handle("mcp:add", (_event, { projectId, server }: { projectId: string; server: McpServerConfig }) => {
125126
try {
126127
addMcpServer(projectId, server);
128+
void captureEvent("mcp_server_added", { transport: server.transport });
127129
return { ok: true };
128130
} catch (err) {
129131
return { error: String(err) };

electron/src/ipc/projects.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import fs from "fs";
44
import crypto from "crypto";
55
import { getDataDir } from "../lib/data-dir";
66
import { log } from "../lib/logger";
7+
import { captureEvent } from "../lib/posthog";
78

89
interface Project {
910
id: string;
@@ -66,6 +67,7 @@ export function register(getMainWindow: () => BrowserWindow | null): void {
6667
};
6768
projects.push(project);
6869
writeProjects(projects);
70+
void captureEvent("project_created");
6971
return project;
7072
} catch (err) {
7173
log("PROJECTS:CREATE_ERR", (err as Error).message);

electron/src/ipc/terminal.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { BrowserWindow, ipcMain } from "electron";
22
import crypto from "crypto";
33
import { log } from "../lib/logger";
44
import { safeSend } from "../lib/safe-send";
5+
import { captureEvent } from "../lib/posthog";
56
import {
67
appendTerminalHistory,
78
EMPTY_TERMINAL_HISTORY,
@@ -71,6 +72,7 @@ export function register(getMainWindow: () => BrowserWindow | null): void {
7172
destroyed: false,
7273
};
7374
terminals.set(terminalId, entry);
75+
void captureEvent("terminal_created");
7476

7577
ptyProcess.onData((data: string) => {
7678
if (entry.destroyed) return;

electron/src/main.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { log } from "./lib/logger";
2323
import { migrateFromOpenAcpUi } from "./lib/migration";
2424
import { glassEnabled, liquidGlass } from "./lib/glass";
2525
import { initAutoUpdater, getIsInstallingUpdate } from "./lib/updater";
26-
import { initPostHog, shutdownPostHog, reinitPostHog } from "./lib/posthog";
26+
import { initPostHog, shutdownPostHog, reinitPostHog, captureEvent } from "./lib/posthog";
2727
import { sessions } from "./ipc/claude-sessions";
2828
import { acpSessions } from "./ipc/acp-sessions";
2929
import { terminals } from "./ipc/terminal";
@@ -196,6 +196,11 @@ onSettingsChanged((settings) => {
196196
}
197197
});
198198

199+
// --- Renderer→main analytics bridge ---
200+
ipcMain.on("analytics:capture", (_event, eventName: string, properties?: Record<string, unknown>) => {
201+
captureEvent(eventName, properties).catch(() => { /* non-fatal */ });
202+
});
203+
199204
// --- DevTools in separate window via remote debugging ---
200205
let devToolsWindow: BrowserWindow | null = null;
201206

electron/src/preload.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,10 @@ contextBridge.exposeInMainWorld("claude", {
271271
transitionIssue: (params: { instanceUrl: string; issueKey: string; transitionId: string }) =>
272272
ipcRenderer.invoke("jira:transition-issue", params),
273273
},
274+
analytics: {
275+
capture: (event: string, properties?: Record<string, unknown>) =>
276+
ipcRenderer.send("analytics:capture", event, properties),
277+
},
274278
speech: {
275279
startNativeDictation: () => ipcRenderer.invoke("speech:start-native-dictation"),
276280
getPlatform: () => ipcRenderer.invoke("speech:get-platform"),

0 commit comments

Comments
 (0)