-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat(desktop): wire pi terminal agent to notification hooks #4083
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
Merged
Kitenite
merged 2 commits into
superset-sh:main
from
garritfra:implement-superset-pi-hooks-prd
May 5, 2026
Merged
Changes from 1 commit
Commits
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
64 changes: 64 additions & 0 deletions
64
apps/desktop/src/main/lib/agent-setup/agent-wrappers-pi.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,64 @@ | ||
| import fs from "node:fs"; | ||
| import os from "node:os"; | ||
| import path from "node:path"; | ||
| import { writeFileIfChanged } from "./agent-wrappers-common"; | ||
|
|
||
| export const PI_EXTENSION_FILE = "superset-hooks.ts"; | ||
|
|
||
| const PI_EXTENSION_SIGNATURE = "// Superset pi extension"; | ||
| const PI_EXTENSION_VERSION = "v1"; | ||
| export const PI_EXTENSION_MARKER = `${PI_EXTENSION_SIGNATURE} ${PI_EXTENSION_VERSION}`; | ||
|
|
||
| const PI_EXTENSION_TEMPLATE_PATH = path.join( | ||
| __dirname, | ||
| "templates", | ||
| "pi-extension.template.ts", | ||
| ); | ||
|
|
||
| /** | ||
| * Returns the global pi extensions directory used by pi's auto-discovery. | ||
| * | ||
| * Decision (see PRD): we install into the user's global `~/.pi/agent/extensions/` | ||
| * rather than an env-scoped Superset-private path. Pi reads | ||
| * `PI_CODING_AGENT_DIR` exclusively when set, so an env-scoped install would | ||
| * shadow user-installed extensions. Cursor-agent is the precedent for | ||
| * "global install, no env override." | ||
| */ | ||
| export function getPiExtensionPath(): string { | ||
| return path.join( | ||
| os.homedir(), | ||
| ".pi", | ||
| "agent", | ||
| "extensions", | ||
| PI_EXTENSION_FILE, | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Renders the pi extension content with the marker substituted. | ||
| * | ||
| * The template is environment-independent: it computes the notify.sh path at | ||
| * runtime from `SUPERSET_HOME_DIR` (which is set in every Superset terminal | ||
| * for both dev and prod installs). | ||
| */ | ||
| export function getPiExtensionContent(): string { | ||
| const template = fs.readFileSync(PI_EXTENSION_TEMPLATE_PATH, "utf-8"); | ||
| return template.replace("{{MARKER}}", PI_EXTENSION_MARKER); | ||
| } | ||
|
|
||
| /** | ||
| * Writes the Superset-managed pi extension into the global pi extensions | ||
| * directory. Idempotent via `writeFileIfChanged`. | ||
| * | ||
| * Pi auto-discovers extensions in this directory at session start, so no | ||
| * registration step is required. The install is unconditional on whether | ||
| * pi itself is installed: if the user later installs pi via npm, hooks | ||
| * start working with no further setup. | ||
| */ | ||
| export function createPiExtension(): void { | ||
| const extensionPath = getPiExtensionPath(); | ||
| const content = getPiExtensionContent(); | ||
| fs.mkdirSync(path.dirname(extensionPath), { recursive: true }); | ||
| const changed = writeFileIfChanged(extensionPath, content, 0o644); | ||
| console.log(`[agent-setup] ${changed ? "Updated" : "Verified"} pi extension`); | ||
| } |
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
98 changes: 98 additions & 0 deletions
98
apps/desktop/src/main/lib/agent-setup/templates/pi-extension.template.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,98 @@ | ||
| // {{MARKER}} | ||
| /** | ||
| * Superset Notification Extension for pi | ||
| * | ||
| * Emits Claude-Code-compatible lifecycle hooks to Superset's notify.sh so | ||
| * the host UI gets a "working" indicator (and completion chime) for pi | ||
| * sessions, the same way it does for Claude Code, Codex, etc. | ||
| * | ||
| * Mapping: | ||
| * pi `before_agent_start` → Claude `UserPromptSubmit` → Superset `Start` | ||
| * pi `tool_execution_end` → Claude `PostToolUse` → progress signal | ||
| * pi `agent_end` → Claude `Stop` → completion / chime | ||
| * pi `session_shutdown` → Claude `Stop` → cleanup on quit/reload | ||
| * | ||
| * Activates only when running inside a Superset terminal (detected via | ||
| * SUPERSET_TERMINAL_ID / SUPERSET_TAB_ID / SUPERSET_PANE_ID). Outside | ||
| * Superset it's a complete no-op. If notify.sh is missing it's also a | ||
| * no-op (Superset uninstalled / never installed). | ||
| * | ||
| * Hook dispatch is fire-and-forget: failures to spawn or curl never | ||
| * affect the agent loop. notify.sh has its own connect/max timeouts. | ||
| */ | ||
|
|
||
| import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; | ||
| import { spawn } from "node:child_process"; | ||
| import { existsSync } from "node:fs"; | ||
| import { homedir } from "node:os"; | ||
| import { join } from "node:path"; | ||
|
|
||
| export default function (pi: ExtensionAPI) { | ||
| // Only activate inside a Superset terminal. Both v2 (host-service) and | ||
| // v1 (electron localhost) shells set at least one of these. | ||
| const insideSuperset = Boolean( | ||
| process.env.SUPERSET_TERMINAL_ID || | ||
| process.env.SUPERSET_TAB_ID || | ||
| process.env.SUPERSET_PANE_ID, | ||
| ); | ||
| if (!insideSuperset) return; | ||
|
|
||
| const supersetHome = | ||
| process.env.SUPERSET_HOME_DIR || join(homedir(), ".superset"); | ||
| const notifyScript = join(supersetHome, "hooks", "notify.sh"); | ||
| if (!existsSync(notifyScript)) return; | ||
|
|
||
| const fire = (eventName: string) => { | ||
| try { | ||
| const child = spawn(notifyScript, [], { | ||
| stdio: ["pipe", "ignore", "ignore"], | ||
| detached: true, | ||
| env: process.env, | ||
| }); | ||
| child.on("error", () => { | ||
| /* swallow — never let hook failures affect pi */ | ||
| }); | ||
| child.stdin?.on("error", () => { | ||
| /* swallow — happens if notify.sh exits before we finish writing */ | ||
| }); | ||
| child.stdin?.end(JSON.stringify({ hook_event_name: eventName })); | ||
| child.unref(); | ||
| } catch { | ||
| // spawn() can throw synchronously (EACCES, ENOENT). Stay silent. | ||
| } | ||
| }; | ||
|
|
||
| // Gate every hook on ctx.hasUI: when this is explicitly false (print | ||
| // mode `-p`, JSON mode), pi is running as a subagent or non-interactive | ||
| // helper and should NOT drive Superset's working indicator. Interactive | ||
| // and RPC sessions (the user-facing ones) have hasUI=true. | ||
| // | ||
| // We deliberately check `=== false` rather than `!ctx.hasUI` so that pi | ||
| // versions older than 0.38.0 (where `hasUI` did not yet exist) still | ||
| // fire hooks. On those older versions subagent flicker is possible, but | ||
| // that's a niche regression; on >=0.38.0 the gate works precisely. | ||
| const skip = (ctx: { hasUI?: boolean }) => ctx.hasUI === false; | ||
|
|
||
| pi.on("before_agent_start", (_event, ctx) => { | ||
| if (skip(ctx)) return; | ||
| fire("UserPromptSubmit"); | ||
| }); | ||
|
|
||
| pi.on("tool_execution_end", (_event, ctx) => { | ||
| if (skip(ctx)) return; | ||
| fire("PostToolUse"); | ||
| }); | ||
|
|
||
| pi.on("agent_end", (_event, ctx) => { | ||
| if (skip(ctx)) return; | ||
| fire("Stop"); | ||
| }); | ||
|
|
||
| // Ensure we mark the agent as "stopped" if pi is killed mid-run, so the | ||
| // Superset working indicator doesn't get stuck on. Fires on Ctrl+C, | ||
| // SIGTERM, SIGHUP, /quit, /reload, /new, /resume, /fork. | ||
| pi.on("session_shutdown", (_event, ctx) => { | ||
| if (skip(ctx)) return; | ||
| fire("Stop"); | ||
| }); | ||
| } | ||
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
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
//prefix in installed marker linePI_EXTENSION_MARKERis"// Superset pi extension v1"(already starts with//), but the template wraps the placeholder in another comment:// {{MARKER}}. After substitution the installed extension begins with// // Superset pi extension v1— an unintended double-comment prefix.It is functionally harmless (substring detection still matches), but inconsistent with the opencode plugin template which places
{{MARKER}}on a bare line so the marker renders cleanly. Fix is either://from the template line (rely on the biome exclusion already added):{{MARKER}}//fromPI_EXTENSION_SIGNATUREso the marker is"Superset pi extension v1"and the installed line becomes// Superset pi extension v1Prompt To Fix With AI
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 9ce69be — dropped the
//from the template line so it now reads bare{{MARKER}}, matching opencode's pattern. After substitution the installed file's first line is the clean// Superset pi extension v1. Tests still pass (29/29), biome clean.