Skip to content

Commit 4962bc8

Browse files
committed
dev toolbar
1 parent 6f783e1 commit 4962bc8

48 files changed

Lines changed: 4326 additions & 55 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/code/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@
190190
"react-hotkeys-hook": "^4.4.4",
191191
"react-markdown": "^10.1.0",
192192
"react-resizable-panels": "^3.0.6",
193+
"react-scan": "^0.5.6",
193194
"reflect-metadata": "^0.2.2",
194195
"rehype-raw": "^7.0.0",
195196
"rehype-sanitize": "^6.0.0",

apps/code/src/main/di/container.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { WorktreeRepository } from "../db/repositories/worktree-repository";
1212
import { DatabaseService } from "../db/service";
1313
import { ElectronAppLifecycle } from "../platform-adapters/electron-app-lifecycle";
1414
import { ElectronAppMeta } from "../platform-adapters/electron-app-meta";
15+
import { ElectronAppMetrics } from "../platform-adapters/electron-app-metrics";
1516
import { ElectronBundledResources } from "../platform-adapters/electron-bundled-resources";
1617
import { ElectronClipboard } from "../platform-adapters/electron-clipboard";
1718
import { ElectronContextMenu } from "../platform-adapters/electron-context-menu";
@@ -35,6 +36,11 @@ import { CloudTaskService } from "../services/cloud-task/service";
3536
import { ConnectivityService } from "../services/connectivity/service";
3637
import { ContextMenuService } from "../services/context-menu/service";
3738
import { DeepLinkService } from "../services/deep-link/service";
39+
import { DevActionsService } from "../services/dev-actions/service";
40+
import { DevFlagsService } from "../services/dev-flags/service";
41+
import { DevLogsService } from "../services/dev-logs/service";
42+
import { DevMetricsService } from "../services/dev-metrics/service";
43+
import { DevNetworkService } from "../services/dev-network/service";
3844
import { EnrichmentService } from "../services/enrichment/service";
3945
import { EnvironmentService } from "../services/environment/service";
4046
import { ExternalAppsService } from "../services/external-apps/service";
@@ -76,6 +82,18 @@ export const container = new Container({
7682
defaultScope: "Singleton",
7783
});
7884

85+
export function getService<T>(token: symbol): T {
86+
return container.get<T>(token);
87+
}
88+
89+
export function tryGetService<T>(token: symbol): T | null {
90+
try {
91+
return container.get<T>(token);
92+
} catch {
93+
return null;
94+
}
95+
}
96+
7997
container.bind(MAIN_TOKENS.UrlLauncher).to(ElectronUrlLauncher);
8098
container.bind(MAIN_TOKENS.StoragePaths).to(ElectronStoragePaths);
8199
container.bind(MAIN_TOKENS.AppMeta).to(ElectronAppMeta);
@@ -91,6 +109,7 @@ container.bind(MAIN_TOKENS.Notifier).to(ElectronNotifier);
91109
container.bind(MAIN_TOKENS.ContextMenu).to(ElectronContextMenu);
92110
container.bind(MAIN_TOKENS.BundledResources).to(ElectronBundledResources);
93111
container.bind(MAIN_TOKENS.ImageProcessor).to(ElectronImageProcessor);
112+
container.bind(MAIN_TOKENS.AppMetrics).to(ElectronAppMetrics);
94113

95114
container.bind(MAIN_TOKENS.DatabaseService).to(DatabaseService);
96115
container
@@ -156,3 +175,9 @@ container.bind(MAIN_TOKENS.WatcherRegistryService).to(WatcherRegistryService);
156175
container.bind(MAIN_TOKENS.WorkspaceService).to(WorkspaceService);
157176

158177
container.bind(MAIN_TOKENS.SettingsStore).toConstantValue(settingsStore);
178+
179+
container.bind(MAIN_TOKENS.DevFlagsService).to(DevFlagsService);
180+
container.bind(MAIN_TOKENS.DevMetricsService).to(DevMetricsService);
181+
container.bind(MAIN_TOKENS.DevNetworkService).to(DevNetworkService);
182+
container.bind(MAIN_TOKENS.DevLogsService).to(DevLogsService);
183+
container.bind(MAIN_TOKENS.DevActionsService).to(DevActionsService);

apps/code/src/main/di/tokens.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const MAIN_TOKENS = Object.freeze({
2121
ContextMenu: Symbol.for("Platform.ContextMenu"),
2222
BundledResources: Symbol.for("Platform.BundledResources"),
2323
ImageProcessor: Symbol.for("Platform.ImageProcessor"),
24+
AppMetrics: Symbol.for("Platform.AppMetrics"),
2425

2526
// Stores
2627
SettingsStore: Symbol.for("Main.SettingsStore"),
@@ -84,4 +85,9 @@ export const MAIN_TOKENS = Object.freeze({
8485
WorkspaceService: Symbol.for("Main.WorkspaceService"),
8586
EnrichmentService: Symbol.for("Main.EnrichmentService"),
8687
UsageMonitorService: Symbol.for("Main.UsageMonitorService"),
88+
DevFlagsService: Symbol.for("Main.DevFlagsService"),
89+
DevMetricsService: Symbol.for("Main.DevMetricsService"),
90+
DevNetworkService: Symbol.for("Main.DevNetworkService"),
91+
DevLogsService: Symbol.for("Main.DevLogsService"),
92+
DevActionsService: Symbol.for("Main.DevActionsService"),
8793
});

apps/code/src/main/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { MAIN_TOKENS } from "./di/tokens";
1212
import { registerMcpSandboxProtocol } from "./protocols/mcp-sandbox";
1313
import type { AppLifecycleService } from "./services/app-lifecycle/service";
1414
import type { AuthService } from "./services/auth/service";
15+
import type { DevLogsService } from "./services/dev-logs/service";
16+
import type { DevNetworkService } from "./services/dev-network/service";
1517
import type { ExternalAppsService } from "./services/external-apps/service";
1618
import type { GitHubIntegrationService } from "./services/github-integration/service";
1719
import type { InboxLinkService } from "./services/inbox-link/service";
@@ -144,6 +146,9 @@ app.on("child-process-gone", (_event, details) => {
144146
});
145147

146148
async function initializeServices(): Promise<void> {
149+
container.get<DevNetworkService>(MAIN_TOKENS.DevNetworkService).install();
150+
container.get<DevLogsService>(MAIN_TOKENS.DevLogsService).install();
151+
147152
container.get<DatabaseService>(MAIN_TOKENS.DatabaseService);
148153
container.get<OAuthService>(MAIN_TOKENS.OAuthService);
149154
const authService = container.get<AuthService>(MAIN_TOKENS.AuthService);
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type {
2+
AppProcessMetric,
3+
IAppMetrics,
4+
} from "@posthog/platform/app-metrics";
5+
import { app } from "electron";
6+
import { injectable } from "inversify";
7+
8+
@injectable()
9+
export class ElectronAppMetrics implements IAppMetrics {
10+
public getAppMetrics(): AppProcessMetric[] {
11+
return app.getAppMetrics().map((m) => ({
12+
pid: m.pid,
13+
type: m.type,
14+
name: m.name,
15+
cpu: m.cpu ? { percentCPUUsage: m.cpu.percentCPUUsage } : undefined,
16+
memory: m.memory
17+
? { workingSetSize: m.memory.workingSetSize }
18+
: undefined,
19+
}));
20+
}
21+
}

apps/code/src/main/preload.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,28 @@ import { exposeElectronTRPC } from "@posthog/electron-trpc/main";
22
import { contextBridge, webUtils } from "electron";
33
import "electron-log/preload";
44

5+
const DEV_FLAGS_CLI_PREFIX = "--posthog-code-flags=";
6+
7+
function readDevFlags(): { devMode: boolean } {
8+
const arg = process.argv.find((a) => a.startsWith(DEV_FLAGS_CLI_PREFIX));
9+
if (!arg) return { devMode: false };
10+
try {
11+
const payload = decodeURIComponent(arg.slice(DEV_FLAGS_CLI_PREFIX.length));
12+
const parsed = JSON.parse(payload);
13+
return { devMode: parsed?.devMode === true };
14+
} catch {
15+
return { devMode: false };
16+
}
17+
}
18+
19+
const devFlags = readDevFlags();
20+
521
contextBridge.exposeInMainWorld("electronUtils", {
622
getPathForFile: (file: File) => webUtils.getPathForFile(file),
723
});
824

25+
contextBridge.exposeInMainWorld("__posthogCodeDevFlags", devFlags);
26+
927
if (process.argv.includes("--posthog-code-dev")) {
1028
contextBridge.exposeInMainWorld("__posthogCodeTest", {
1129
crash: () => {

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -978,6 +978,49 @@ When creating pull requests, add the following footer at the end of the PR descr
978978
return this.sessions.get(taskRunId);
979979
}
980980

981+
getDebugSnapshot(): {
982+
sessions: Array<{
983+
taskRunId: string;
984+
taskId: string;
985+
repoPath: string;
986+
adapter: string;
987+
model: string | null;
988+
sessionId: string | null;
989+
channel: string;
990+
createdAt: number;
991+
lastActivityAt: number;
992+
promptPending: boolean;
993+
inFlightToolCalls: number;
994+
idleDeadline: number | null;
995+
}>;
996+
pendingPermissions: Array<{
997+
taskRunId: string;
998+
toolCallId: string;
999+
}>;
1000+
} {
1001+
const sessions = [...this.sessions.values()].map((session) => ({
1002+
taskRunId: session.taskRunId,
1003+
taskId: session.taskId,
1004+
repoPath: session.repoPath,
1005+
adapter: session.config.adapter ?? "claude",
1006+
model: session.config.model ?? null,
1007+
sessionId: session.config.sessionId ?? null,
1008+
channel: session.channel,
1009+
createdAt: session.createdAt,
1010+
lastActivityAt: session.lastActivityAt,
1011+
promptPending: session.promptPending,
1012+
inFlightToolCalls: session.inFlightMcpToolCalls.size,
1013+
idleDeadline: this.idleTimeouts.get(session.taskRunId)?.deadline ?? null,
1014+
}));
1015+
const pendingPermissions = [...this.pendingPermissions.values()].map(
1016+
(perm) => ({
1017+
taskRunId: perm.taskRunId,
1018+
toolCallId: perm.toolCallId,
1019+
}),
1020+
);
1021+
return { sessions, pendingPermissions };
1022+
}
1023+
9811024
async setSessionConfigOption(
9821025
sessionId: string,
9831026
configId: string,
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { z } from "zod";
2+
3+
export const devToastInput = z.object({
4+
variant: z.enum(["info", "error"]),
5+
message: z.string(),
6+
});
7+
8+
export const devToastSchema = z.object({
9+
id: z.number(),
10+
variant: z.enum(["info", "error"]),
11+
message: z.string(),
12+
});
13+
14+
export type DevToast = z.infer<typeof devToastSchema>;
15+
16+
export const DevActionsEvent = {
17+
Toast: "toast",
18+
} as const;
19+
20+
export interface DevActionsEvents {
21+
[DevActionsEvent.Toast]: DevToast;
22+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { app, BrowserWindow, shell } from "electron";
2+
import { inject, injectable } from "inversify";
3+
import { MAIN_TOKENS } from "../../di/tokens";
4+
import { getUserDataDir } from "../../utils/env";
5+
import { getLogFilePath, logger } from "../../utils/logger";
6+
import { TypedEventEmitter } from "../../utils/typed-event-emitter";
7+
import type { DevNetworkService } from "../dev-network/service";
8+
import {
9+
DevActionsEvent,
10+
type DevActionsEvents,
11+
type DevToast,
12+
} from "./schemas";
13+
14+
const log = logger.scope("dev-actions");
15+
16+
@injectable()
17+
export class DevActionsService extends TypedEventEmitter<DevActionsEvents> {
18+
private nextToastId = 1;
19+
20+
constructor(
21+
@inject(MAIN_TOKENS.DevNetworkService)
22+
private readonly network: DevNetworkService,
23+
) {
24+
super();
25+
}
26+
27+
async openUserDataDir(): Promise<void> {
28+
await shell.openPath(getUserDataDir());
29+
}
30+
31+
async openLogFile(): Promise<void> {
32+
await shell.openPath(getLogFilePath());
33+
}
34+
35+
reloadRenderer(): void {
36+
for (const window of BrowserWindow.getAllWindows()) {
37+
window.webContents.reload();
38+
}
39+
}
40+
41+
restartMain(): void {
42+
log.warn("Restarting main process from dev toolbar");
43+
app.relaunch();
44+
app.exit(0);
45+
}
46+
47+
crashMain(): void {
48+
log.warn("Crashing main process from dev toolbar");
49+
process.crash();
50+
}
51+
52+
triggerToast(variant: "info" | "error", message: string): DevToast {
53+
const toast: DevToast = {
54+
id: this.nextToastId++,
55+
variant,
56+
message,
57+
};
58+
this.emit(DevActionsEvent.Toast, toast);
59+
return toast;
60+
}
61+
62+
setOffline(offline: boolean): void {
63+
this.network.setSim({ offline });
64+
}
65+
66+
setSlowDelay(slowDelayMs: number): void {
67+
this.network.setSim({ slowDelayMs });
68+
}
69+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { z } from "zod";
2+
3+
export const devFlagsSchema = z.object({
4+
devMode: z.boolean(),
5+
});
6+
7+
export type DevFlags = z.infer<typeof devFlagsSchema>;
8+
9+
export const DEFAULT_DEV_FLAGS: DevFlags = {
10+
devMode: false,
11+
};
12+
13+
export const DevFlagsEvent = {
14+
Changed: "changed",
15+
} as const;
16+
17+
export interface DevFlagsEvents {
18+
[DevFlagsEvent.Changed]: DevFlags;
19+
}

0 commit comments

Comments
 (0)