Skip to content

Commit 32ed95f

Browse files
committed
feat(harness): integrate pi runtime into desktop app
1 parent 0e925bf commit 32ed95f

74 files changed

Lines changed: 3165 additions & 211 deletions

File tree

Some content is hidden

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

apps/code/electron-builder.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ const config: Configuration = {
4949
".vite/build/plugins/posthog/**",
5050
".vite/build/codex-acp/**",
5151
".vite/build/grammars/**",
52+
".vite/build/rpc-host.js",
53+
".vite/build/rpc-host.js.map",
5254
...asarUnpackGlobs,
5355
],
5456

apps/code/electron.vite.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
copyCodexAcpBinaries,
2323
copyDrizzleMigrations,
2424
copyEnricherGrammars,
25+
copyPiRpcHost,
2526
copyPosthogPlugin,
2627
fixFilenameCircularRef,
2728
getBuildDate,
@@ -93,6 +94,7 @@ export default defineConfig(({ mode }) => {
9394
autoServicesPlugin(path.join(__dirname, "src/main/services")),
9495
fixFilenameCircularRef(),
9596
copyClaudeExecutable(),
97+
copyPiRpcHost(),
9698
copyPosthogPlugin(isDev),
9799
copyDrizzleMigrations(),
98100
copyCodexAcpBinaries(),

apps/code/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@
152152
"reflect-metadata": "^0.2.2",
153153
"semver": "^7.8.1",
154154
"shadcn": "^4.1.2",
155+
"superjson": "catalog:",
155156
"smol-toml": "^1.6.1",
156157
"tailwindcss-scroll-mask": "^0.0.3",
157158
"tw-animate-css": "^1.4.0",

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,10 @@ import type {
180180
} from "@posthog/workspace-server/services/local-logs/identifiers";
181181
import type { MCP_PROXY_AUTH } from "@posthog/workspace-server/services/mcp-proxy/identifiers";
182182
import type { McpProxyAuth } from "@posthog/workspace-server/services/mcp-proxy/ports";
183+
import type {
184+
PI_RPC_CLIENT_FACTORY,
185+
PiRpcClientFactory,
186+
} from "@posthog/workspace-server/services/pi-session/identifiers";
183187
import type { PosthogPluginService } from "@posthog/workspace-server/services/posthog-plugin/posthog-plugin";
184188
import type { ProcessTrackingService } from "@posthog/workspace-server/services/process-tracking/process-tracking";
185189
import type {
@@ -344,6 +348,7 @@ export interface MainBindings {
344348
[AGENT_REPO_FILES]: unknown;
345349
[AGENT_AUTH]: unknown;
346350
[AGENT_LOGGER]: RootLogger;
351+
[PI_RPC_CLIENT_FACTORY]: PiRpcClientFactory;
347352

348353
// Logger
349354
[ROOT_LOGGER]: RootLogger;

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

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,12 @@ import { OAUTH_CALLBACK_SERVER } from "@posthog/workspace-server/services/oauth-
184184
import { oauthCallbackModule } from "@posthog/workspace-server/services/oauth-callback/oauth-callback.module";
185185
import { onboardingImportModule } from "@posthog/workspace-server/services/onboarding-import/onboarding-import.module";
186186
import { osModule } from "@posthog/workspace-server/services/os/os.module";
187+
import {
188+
PI_RPC_CLIENT_FACTORY,
189+
PI_SESSION_SERVICE,
190+
} from "@posthog/workspace-server/services/pi-session/identifiers";
191+
import type { PiSessionService } from "@posthog/workspace-server/services/pi-session/pi-session";
192+
import { piSessionModule } from "@posthog/workspace-server/services/pi-session/pi-session.module";
187193
import { POSTHOG_PLUGIN_SERVICE } from "@posthog/workspace-server/services/posthog-plugin/identifiers";
188194
import { posthogPluginModule } from "@posthog/workspace-server/services/posthog-plugin/posthog-plugin.module";
189195
import { PROCESS_TRACKING_SERVICE } from "@posthog/workspace-server/services/process-tracking/identifiers";
@@ -220,6 +226,7 @@ import { workspaceModule } from "@posthog/workspace-server/services/workspace/wo
220226
import { workspaceMetadataModule } from "@posthog/workspace-server/services/workspace-metadata/workspace-metadata.module";
221227
import ExternalAppsStoreImpl from "electron-store";
222228
import type { FileWatcherBridge } from "../index";
229+
import { DesktopPiRpcClientFactory } from "../platform-adapters/desktop-pi-rpc-client-factory";
223230
import { ElectronAppLifecycle } from "../platform-adapters/electron-app-lifecycle";
224231
import { ElectronAppMeta } from "../platform-adapters/electron-app-meta";
225232
import { ElectronAppMetrics } from "../platform-adapters/electron-app-metrics";
@@ -356,10 +363,15 @@ container
356363
.bind(MAIN_DEFAULT_ADDITIONAL_DIRECTORY_REPOSITORY)
357364
.toService(DEFAULT_ADDITIONAL_DIRECTORY_REPOSITORY);
358365
container.load(agentModule);
366+
container.load(piSessionModule);
359367
container.bind(AGENT_SLEEP_COORDINATOR).toService(MAIN_SLEEP_SERVICE);
360368
container.bind(AGENT_MCP_APPS).toService(MCP_APPS_SERVICE);
361369
container.bind(AGENT_REPO_FILES).toService(MAIN_FS_SERVICE);
362370
container.bind(AGENT_AUTH).toService(MAIN_AUTH_SERVICE);
371+
container
372+
.bind(PI_RPC_CLIENT_FACTORY)
373+
.to(DesktopPiRpcClientFactory)
374+
.inSingletonScope();
363375
container.bind(AGENT_LOGGER).toConstantValue(logger);
364376
container.load(osModule);
365377
container.bind<RootLogger>(ROOT_LOGGER).toConstantValue(logger);
@@ -391,8 +403,12 @@ container.bind(MCP_PROXY_AUTH).toDynamicValue((ctx) => {
391403
});
392404
container.load(archiveModule);
393405
container.bind(ARCHIVE_SESSION_CANCELLER).toDynamicValue((ctx) => ({
394-
cancelSessionsByTaskId: (taskId: string) =>
395-
ctx.get<AgentService>(AGENT_SERVICE).cancelSessionsByTaskId(taskId),
406+
cancelSessionsByTaskId: async (taskId: string) => {
407+
await Promise.all([
408+
ctx.get<AgentService>(AGENT_SERVICE).cancelSessionsByTaskId(taskId),
409+
ctx.get<PiSessionService>(PI_SESSION_SERVICE).stop(taskId),
410+
]);
411+
},
396412
}));
397413
container.bind(ARCHIVE_FILE_WATCHER).toDynamicValue((ctx) => ({
398414
stopWatching: async (worktreePath: string) => {
@@ -403,8 +419,12 @@ container.bind(ARCHIVE_FILE_WATCHER).toDynamicValue((ctx) => ({
403419
}));
404420
container.load(suspensionModule);
405421
container.bind(SUSPENSION_SESSION_CANCELLER).toDynamicValue((ctx) => ({
406-
cancelSessionsByTaskId: (taskId: string) =>
407-
ctx.get<AgentService>(AGENT_SERVICE).cancelSessionsByTaskId(taskId),
422+
cancelSessionsByTaskId: async (taskId: string) => {
423+
await Promise.all([
424+
ctx.get<AgentService>(AGENT_SERVICE).cancelSessionsByTaskId(taskId),
425+
ctx.get<PiSessionService>(PI_SESSION_SERVICE).stop(taskId),
426+
]);
427+
},
408428
}));
409429
container.bind(SUSPENSION_FILE_WATCHER).toDynamicValue((ctx) => ({
410430
stopWatching: async (worktreePath: string) => {
@@ -675,7 +695,12 @@ container.load(workspaceModule);
675695
container.bind(WORKSPACE_AGENT).toDynamicValue((ctx): WorkspaceAgent => {
676696
const agent = ctx.get<AgentService>(AGENT_SERVICE);
677697
return {
678-
cancelSessionsByTaskId: (taskId) => agent.cancelSessionsByTaskId(taskId),
698+
cancelSessionsByTaskId: async (taskId) => {
699+
await Promise.all([
700+
agent.cancelSessionsByTaskId(taskId),
701+
ctx.get<PiSessionService>(PI_SESSION_SERVICE).stop(taskId),
702+
]);
703+
},
679704
onAgentFileActivity: (handler) =>
680705
agent.on(AgentServiceEvent.AgentFileActivity, handler),
681706
};
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type { PiRpcClient } from "@posthog/agent/pi/rpc-client";
2+
import { getLlmGatewayUrl } from "@posthog/agent/posthog-api";
3+
import { getCloudUrlFromRegion } from "@posthog/shared";
4+
import type { AgentAuth } from "@posthog/workspace-server/services/agent/ports";
5+
import type { AuthProxyService } from "@posthog/workspace-server/services/auth-proxy/auth-proxy";
6+
import { describe, expect, it, vi } from "vitest";
7+
import { DesktopPiRpcClientFactory } from "./desktop-pi-rpc-client-factory";
8+
9+
const createPiRpcClient = vi.hoisted(() => vi.fn());
10+
11+
vi.mock("@posthog/agent/pi/rpc-client", () => ({ createPiRpcClient }));
12+
13+
describe("DesktopPiRpcClientFactory", () => {
14+
it("routes Pi through the shared host auth proxy", async () => {
15+
const auth = {
16+
getOAuthCredentials: vi.fn(async () => ({
17+
access: "access-token",
18+
refresh: "refresh-token",
19+
expires: 1,
20+
region: "eu" as const,
21+
})),
22+
} as unknown as AgentAuth;
23+
const authProxy = {
24+
start: vi.fn(async () => "http://127.0.0.1:1234"),
25+
} as unknown as AuthProxyService;
26+
const client = {} as PiRpcClient;
27+
createPiRpcClient.mockReturnValue(client);
28+
const factory = new DesktopPiRpcClientFactory(auth, authProxy);
29+
30+
await expect(factory.create({ cwd: "/workspace" })).resolves.toBe(client);
31+
expect(authProxy.start).toHaveBeenCalledWith(
32+
getLlmGatewayUrl(getCloudUrlFromRegion("eu")),
33+
);
34+
expect(createPiRpcClient).toHaveBeenCalledWith({
35+
cwd: "/workspace",
36+
providerOptions: {
37+
region: "eu",
38+
baseUrl: "http://127.0.0.1:1234",
39+
apiKey: "posthog-code-auth-proxy",
40+
},
41+
env: { POSTHOG_REGION: "eu" },
42+
});
43+
});
44+
});
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import {
2+
createPiRpcClient,
3+
type PiRpcClient,
4+
} from "@posthog/agent/pi/rpc-client";
5+
import { getLlmGatewayUrl } from "@posthog/agent/posthog-api";
6+
import { type CloudRegion, getCloudUrlFromRegion } from "@posthog/shared";
7+
import { AGENT_AUTH } from "@posthog/workspace-server/services/agent/identifiers";
8+
import type { AgentAuth } from "@posthog/workspace-server/services/agent/ports";
9+
import type { AuthProxyService } from "@posthog/workspace-server/services/auth-proxy/auth-proxy";
10+
import { AUTH_PROXY_SERVICE } from "@posthog/workspace-server/services/auth-proxy/identifiers";
11+
import type { PiRpcClientFactory } from "@posthog/workspace-server/services/pi-session/identifiers";
12+
import { inject, injectable } from "inversify";
13+
14+
const PROXY_API_KEY = "posthog-code-auth-proxy";
15+
16+
@injectable()
17+
export class DesktopPiRpcClientFactory implements PiRpcClientFactory {
18+
private proxyRegion?: CloudRegion;
19+
private proxyUrlPromise?: Promise<string>;
20+
21+
constructor(
22+
@inject(AGENT_AUTH) private readonly auth: AgentAuth,
23+
@inject(AUTH_PROXY_SERVICE)
24+
private readonly authProxy: AuthProxyService,
25+
) {}
26+
27+
async create(input: {
28+
cwd: string;
29+
model?: string;
30+
sessionFile?: string;
31+
}): Promise<PiRpcClient> {
32+
const credentials = await this.auth.getOAuthCredentials();
33+
if (!credentials) {
34+
throw new Error("Pi requires PostHog authentication");
35+
}
36+
37+
const baseUrl = await this.getProxyUrl(credentials.region);
38+
39+
return createPiRpcClient({
40+
...input,
41+
providerOptions: {
42+
region: credentials.region,
43+
baseUrl,
44+
apiKey: PROXY_API_KEY,
45+
},
46+
env: { POSTHOG_REGION: credentials.region },
47+
});
48+
}
49+
50+
private getProxyUrl(region: CloudRegion): Promise<string> {
51+
if (this.proxyRegion !== region || !this.proxyUrlPromise) {
52+
this.proxyRegion = region;
53+
const gatewayUrl = getLlmGatewayUrl(getCloudUrlFromRegion(region));
54+
this.proxyUrlPromise = this.authProxy.start(gatewayUrl);
55+
}
56+
57+
return this.proxyUrlPromise;
58+
}
59+
}

apps/code/src/main/trpc/router.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { notificationRouter } from "@posthog/host-router/routers/notification.ro
3535
import { oauthRouter } from "@posthog/host-router/routers/oauth.router";
3636
import { onboardingImportRouter } from "@posthog/host-router/routers/onboarding-import.router";
3737
import { osRouter } from "@posthog/host-router/routers/os.router";
38+
import { piSessionRouter } from "@posthog/host-router/routers/pi-session.router";
3839
import { processTrackingRouter } from "@posthog/host-router/routers/process-tracking.router";
3940
import { provisioningRouter } from "@posthog/host-router/routers/provisioning.router";
4041
import { secureStoreRouter } from "@posthog/host-router/routers/secure-store.router";
@@ -93,6 +94,7 @@ export const trpcRouter = router({
9394
onboardingImport: onboardingImportRouter,
9495
logs: logsRouter,
9596
os: osRouter,
97+
piSession: piSessionRouter,
9698
processTracking: processTrackingRouter,
9799
provisioning: provisioningRouter,
98100
sleep: sleepRouter,

apps/code/src/renderer/di/bindings.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ import {
6565
GITHUB_CONNECT_CLIENT,
6666
type GithubConnectClient,
6767
} from "@posthog/core/onboarding/identifiers";
68+
import { PI_RUNNER } from "@posthog/core/pi-runtime/identifiers";
69+
import type { PiRunner } from "@posthog/core/pi-runtime/piRunner";
6870
import {
6971
type BundleLocalSkill,
7072
CLOUD_ARTIFACT_BUNDLE_LOCAL_SKILL,
@@ -276,6 +278,7 @@ export interface RendererBindings {
276278
[SHELL_PROCESS_READER]: ShellProcessReader;
277279
[ANALYTICS_TRACKER]: AnalyticsTracker;
278280
[TASK_CREATION_HOST]: ITaskCreationHost;
281+
[PI_RUNNER]: PiRunner;
279282
[TASK_CREATION_EFFECTS]: TaskCreationEffects;
280283
[RENDERER_TASK_SERVICE]: TaskService;
281284
[TASK_SERVICE]: TaskService;

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import {
3030
import { LLM_GATEWAY_SERVICE } from "@posthog/core/llm-gateway/identifiers";
3131
import type { LlmGatewayService } from "@posthog/core/llm-gateway/llm-gateway";
3232
import type { LlmMessage } from "@posthog/core/llm-gateway/schemas";
33+
import { PI_RUNNER } from "@posthog/core/pi-runtime/identifiers";
34+
import type { PiRunner } from "@posthog/core/pi-runtime/piRunner";
3335
import {
3436
CLOUD_ARTIFACT_BUNDLE_LOCAL_SKILL,
3537
CLOUD_ARTIFACT_READ_FILE_AS_BASE64,
@@ -149,6 +151,7 @@ import { trpcClient } from "@renderer/trpc";
149151
import { hostTrpcClient } from "@renderer/trpc/client";
150152
import type { TRPCClient } from "@trpc/client";
151153
import { hostLog, logger } from "@utils/logger";
154+
import { TrpcPiRunner } from "../platform-adapters/trpc-pi-runner";
152155
import type { RendererBindings } from "./bindings";
153156
import { TASK_SERVICE as RENDERER_TASK_SERVICE, TRPC_CLIENT } from "./tokens";
154157

@@ -288,6 +291,7 @@ container
288291

289292
// Bind services
290293
container.bind<ITaskCreationHost>(TASK_CREATION_HOST).to(TrpcTaskCreationHost);
294+
container.bind<PiRunner>(PI_RUNNER).to(TrpcPiRunner);
291295
container.bind(TASK_CREATION_EFFECTS).toConstantValue(taskCreationEffects);
292296
container.bind<TaskService>(RENDERER_TASK_SERVICE).to(TaskService);
293297
container.bind<TaskService>(TASK_SERVICE).toService(RENDERER_TASK_SERVICE);

0 commit comments

Comments
 (0)