-
Notifications
You must be signed in to change notification settings - Fork 3.1k
feat(desktop): add Pi extension support #76416
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
marandaneto
wants to merge
15
commits into
master
Choose a base branch
from
feat/pi-extension-system
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+3,905
−233
Open
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
0aba6d4
feat(desktop): add Pi extension support
marandaneto 5ddcc69
fix(desktop): recreate sessions after failed stop
marandaneto ee6c62c
fix(desktop): address Pi extension review feedback
marandaneto a6d76af
Merge remote-tracking branch 'origin/master' into feat/pi-extension-s…
marandaneto 0d65ca0
test(desktop): cover Pi extension RPC flow
marandaneto 1f60778
Merge branch 'master' into feat/pi-extension-system
marandaneto b1a7bce
use vendor Pi extension types
marandaneto 6647aba
derive Pi transport types from vendor contracts
marandaneto bceeaee
address React Doctor Pi session warnings
marandaneto 946744b
type Pi extension RPC events
marandaneto e18142e
buffer startup Pi extension dialogs
marandaneto cc54793
cancel orphaned Pi extension dialogs
marandaneto 1e4c6cd
cancel pending Pi dialogs on disconnect
marandaneto ab63b18
track queued Pi extension dialogs
marandaneto e9696f1
Merge branch 'master' into feat/pi-extension-system
marandaneto File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
176 changes: 176 additions & 0 deletions
176
products/desktop/apps/code/tests/e2e/tests/pi-extension.spec.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,176 @@ | ||
| import { existsSync } from "node:fs"; | ||
| import { mkdir, writeFile } from "node:fs/promises"; | ||
| import path from "node:path"; | ||
| import type { PiRpcClient } from "@posthog/agent/pi/rpc-client"; | ||
| import type { PiExtensionEvent } from "@posthog/agent/pi/types"; | ||
| import { expect, test } from "../fixtures/electron"; | ||
|
|
||
| const EXTENSION_SOURCE = ` | ||
| export default function (pi) { | ||
| pi.registerCommand("extension-e2e", { | ||
| description: "Exercise the PostHog Desktop extension RPC UI", | ||
| handler: async (_args, ctx) => { | ||
| ctx.ui.setTitle("Extension E2E"); | ||
| ctx.ui.setStatus("extension-e2e", "Extension ready"); | ||
| ctx.ui.setWidget("extension-e2e", ["Extension widget ready"]); | ||
| const confirmed = await ctx.ui.confirm( | ||
| "Extension confirmation", | ||
| "Confirm the extension RPC round trip.", | ||
| ); | ||
| ctx.ui.notify( | ||
| confirmed ? "Extension E2E passed" : "Extension E2E cancelled", | ||
| confirmed ? "info" : "warning", | ||
| ); | ||
| }, | ||
| }); | ||
| } | ||
| `; | ||
|
|
||
| const HOME_ENVIRONMENT_KEYS = [ | ||
| "APPDATA", | ||
| "HOME", | ||
| "LOCALAPPDATA", | ||
| "USERPROFILE", | ||
| "XDG_CONFIG_HOME", | ||
| ] as const; | ||
|
|
||
| test.describe("Pi extensions", () => { | ||
| test("loads a real extension and completes its UI round trip", async ({ | ||
| electronApp, | ||
| }) => { | ||
| const { e2eHome, resourcesPath } = await electronApp.evaluate( | ||
| async ({ app }) => ({ | ||
| e2eHome: process.env.HOME ?? app.getPath("home"), | ||
| resourcesPath: process.resourcesPath, | ||
| }), | ||
| ); | ||
| const rpcHostPath = path.join( | ||
| resourcesPath, | ||
| "app.asar.unpacked", | ||
| ".vite", | ||
| "build", | ||
| "rpc-host.js", | ||
| ); | ||
| expect(existsSync(rpcHostPath)).toBe(true); | ||
|
|
||
| const extensionsDirectory = path.join( | ||
| e2eHome, | ||
| ".pi", | ||
| "agent", | ||
| "extensions", | ||
| ); | ||
| await mkdir(extensionsDirectory, { recursive: true }); | ||
| await writeFile( | ||
| path.join(extensionsDirectory, "extension-e2e.ts"), | ||
| EXTENSION_SOURCE, | ||
| ); | ||
|
|
||
| const previousEnvironment = new Map( | ||
| HOME_ENVIRONMENT_KEYS.map((key) => [key, process.env[key]]), | ||
| ); | ||
| for (const key of HOME_ENVIRONMENT_KEYS) { | ||
| process.env[key] = e2eHome; | ||
| } | ||
|
|
||
| const events: PiExtensionEvent[] = []; | ||
| let client: PiRpcClient | undefined; | ||
|
|
||
| try { | ||
| const { createPiRpcClient } = await import( | ||
| "@posthog/agent/pi/rpc-client" | ||
| ); | ||
| client = createPiRpcClient({ | ||
| cliPath: rpcHostPath, | ||
| cwd: e2eHome, | ||
| projectTrusted: false, | ||
| providerOptions: { apiKey: "unused-e2e-key" }, | ||
| }); | ||
| client.onEvent((event) => { | ||
| if ( | ||
| event.type === "extension_ui_request" || | ||
| event.type === "extension_error" | ||
| ) { | ||
| events.push(event); | ||
| } | ||
| }); | ||
| await client.start(); | ||
|
|
||
| const prompt = client.prompt("/extension-e2e"); | ||
|
|
||
| await expect | ||
| .poll(() => events) | ||
| .toContainEqual( | ||
| expect.objectContaining({ | ||
| type: "extension_ui_request", | ||
| method: "setTitle", | ||
| title: "Extension E2E", | ||
| }), | ||
| ); | ||
| await expect | ||
| .poll(() => events) | ||
| .toContainEqual( | ||
| expect.objectContaining({ | ||
| type: "extension_ui_request", | ||
| method: "setStatus", | ||
| statusKey: "extension-e2e", | ||
| statusText: "Extension ready", | ||
| }), | ||
| ); | ||
| await expect | ||
| .poll(() => events) | ||
| .toContainEqual( | ||
| expect.objectContaining({ | ||
| type: "extension_ui_request", | ||
| method: "setWidget", | ||
| widgetKey: "extension-e2e", | ||
| widgetLines: ["Extension widget ready"], | ||
| }), | ||
| ); | ||
| await expect | ||
| .poll(() => events) | ||
| .toContainEqual( | ||
| expect.objectContaining({ | ||
| type: "extension_ui_request", | ||
| method: "confirm", | ||
| title: "Extension confirmation", | ||
| message: "Confirm the extension RPC round trip.", | ||
| }), | ||
| ); | ||
|
|
||
| const confirmation = events.find( | ||
| (event) => | ||
| event.type === "extension_ui_request" && event.method === "confirm", | ||
| ); | ||
| if (!confirmation) { | ||
| throw new Error("Extension confirmation request was not emitted"); | ||
| } | ||
|
|
||
| await client.respondToExtensionUI({ | ||
| type: "extension_ui_response", | ||
| id: confirmation.id, | ||
| confirmed: true, | ||
| }); | ||
| await prompt; | ||
|
|
||
| await expect | ||
| .poll(() => events) | ||
| .toContainEqual( | ||
| expect.objectContaining({ | ||
| type: "extension_ui_request", | ||
| method: "notify", | ||
| message: "Extension E2E passed", | ||
| notifyType: "info", | ||
| }), | ||
| ); | ||
| } finally { | ||
| await client?.stop(); | ||
| for (const [key, value] of previousEnvironment) { | ||
| if (value === undefined) { | ||
| delete process.env[key]; | ||
| } else { | ||
| process.env[key] = value; | ||
| } | ||
| } | ||
| } | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| # Pi extensions in PostHog Desktop | ||
|
|
||
| PostHog Desktop supports Pi extensions and packages in local Pi sessions through Pi's RPC mode. Cloud Pi sessions do not load local extensions. | ||
|
|
||
| For installation, discovery, authoring, and security guidance, see the [Pi documentation](https://pi.dev/docs/latest/) and [Pi extension documentation](https://pi.dev/docs/latest/extensions). | ||
|
|
||
| ## Repository trust | ||
|
|
||
| Project-local Pi resources remain disabled until the repository is explicitly trusted. When PostHog Desktop detects project resources, it shows a trust control above the composer. | ||
|
|
||
| Trust decisions use Pi's native project trust store. A decision made for the registered main repository also applies to PostHog Desktop's managed worktrees for that repository. Trusting or revoking a repository restarts the local Pi runtime so Pi can rebuild its resource set while preserving the native task session. | ||
|
|
||
| Pi extensions execute inside the local Pi subprocess with the current user's permissions. Review a repository's Pi resources before trusting it. | ||
|
|
||
| ## Desktop RPC behavior | ||
|
|
||
| PostHog Desktop supports extension tools, lifecycle hooks, slash commands, custom text messages, and the UI methods represented by Pi's RPC protocol. RPC dialogs, notifications, statuses, text widgets, titles, and editor text updates are rendered using Desktop UI. | ||
|
|
||
| Extension UI state is ephemeral. PostHog Desktop does not replay UI requests or maintain a separate snapshot across RPC reconnects or runtime replacement. Extensions that need to restore meaningful state should persist it in the Pi session and construct fresh UI from that state. | ||
|
|
||
| PostHog Desktop does not support terminal-only behavior such as arbitrary TUI components, rendering factories, raw terminal input handlers, custom headers or footers, themes, custom tool-call renderers, editor component replacement, editor autocomplete providers, or synchronous reads of the current composer text. Widgets are limited to text lines. | ||
|
|
||
| Extensions that support both environments should use Pi's RPC-compatible UI methods in Desktop and keep terminal-specific behavior behind Pi's TUI mode checks. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.