Commit a2e2b7a0c366
Changed files (3)
dots
pi
agent
extensions
dots/pi/agent/extensions/claude-hooks.ts
@@ -7,8 +7,11 @@
* Events mapped:
* - session_start -> claude-hooks-initialize-session
* - session_shutdown -> claude-hooks-save-session
- * - tool_call -> claude-hooks-validate-git-push (PreToolUse equivalent)
- * - tool_result -> claude-hooks-capture-tool-output, claude-hooks-update-terminal-title
+ * - tool_result -> claude-hooks-capture-tool-output
+ *
+ * Migrated to native TypeScript extensions:
+ * - validate-git-push.ts (was: claude-hooks-validate-git-push)
+ * - terminal-status.ts (was: claude-hooks-update-terminal-title)
*
* Requirements:
* - claude-hooks-* binaries must be in PATH (installed via home-manager)
@@ -128,15 +131,11 @@ export default function (pi: ExtensionAPI) {
// No need to call Go binary here anymore
// Post-tool-use capture
+ // NOTE: terminal title is now handled by terminal-status.ts extension
pi.on("tool_result", async (event, _ctx) => {
- const jsonInput = toClaudePostToolUse(event);
-
- // Run hooks in parallel
- const hooks = [
- "claude-hooks-capture-tool-output",
- "claude-hooks-update-terminal-title",
- ].filter(binaryExists);
-
- await Promise.allSettled(hooks.map((hook) => runHook(hook, jsonInput)));
+ if (binaryExists("claude-hooks-capture-tool-output")) {
+ const jsonInput = toClaudePostToolUse(event);
+ await runHook("claude-hooks-capture-tool-output", jsonInput);
+ }
});
}
dots/pi/agent/extensions/notify.ts
@@ -1,102 +0,0 @@
-/**
- * Pi Notify Extension
- *
- * Sends a native terminal/desktop notification when Pi agent is done and waiting for input.
- * Supports multiple terminal protocols:
- * - OSC 777: Ghostty, iTerm2, WezTerm, rxvt-unicode
- * - OSC 99: Kitty
- * - notify-send: Linux desktop fallback (libnotify)
- * - Windows toast: Windows Terminal (WSL)
- *
- * Based on: https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/examples/extensions/notify.ts
- */
-
-import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
-
-function windowsToastScript(title: string, body: string): string {
- const type = "Windows.UI.Notifications";
- const mgr = `[${type}.ToastNotificationManager, ${type}, ContentType = WindowsRuntime]`;
- const template = `[${type}.ToastTemplateType]::ToastText01`;
- const toast = `[${type}.ToastNotification]::new($xml)`;
- return [
- `${mgr} > $null`,
- `$xml = [${type}.ToastNotificationManager]::GetTemplateContent(${template})`,
- `$xml.GetElementsByTagName('text')[0].AppendChild($xml.CreateTextNode('${body}')) > $null`,
- `[${type}.ToastNotificationManager]::CreateToastNotifier('${title}').Show(${toast})`,
- ].join("; ");
-}
-
-function notifyOSC777(title: string, body: string): void {
- process.stdout.write(`\x1b]777;notify;${title};${body}\x07`);
-}
-
-function notifyOSC99(title: string, body: string): void {
- // Kitty OSC 99: i=notification id, d=0 means not done yet, p=body for second part
- process.stdout.write(`\x1b]99;i=1:d=0;${title}\x1b\\`);
- process.stdout.write(`\x1b]99;i=1:p=body;${body}\x1b\\`);
-}
-
-function notifyWindows(title: string, body: string): void {
- const { execFile } = require("child_process");
- execFile("powershell.exe", ["-NoProfile", "-Command", windowsToastScript(title, body)]);
-}
-
-function notifyLinuxDesktop(title: string, body: string): void {
- const { execFile } = require("child_process");
- // Use notify-send (libnotify) for Linux desktop notifications
- // -u low = low urgency, -t 5000 = 5 second timeout
- execFile("notify-send", ["-u", "low", "-t", "5000", "-a", "pi", title, body], (err: Error | null) => {
- if (err) {
- // Fallback to OSC 777 if notify-send fails
- notifyOSC777(title, body);
- }
- });
-}
-
-function detectTerminal(): "kitty" | "wsl" | "osc777" | "linux-desktop" {
- // Windows Terminal (WSL)
- if (process.env.WT_SESSION) {
- return "wsl";
- }
- // Kitty terminal
- if (process.env.KITTY_WINDOW_ID) {
- return "kitty";
- }
- // Ghostty, WezTerm, iTerm2 - these support OSC 777
- if (process.env.GHOSTTY_RESOURCES_DIR || process.env.WEZTERM_PANE || process.env.ITERM_SESSION_ID) {
- return "osc777";
- }
- // Alacritty, foot, and other terminals that don't support OSC notifications
- // Use notify-send on Linux
- if (process.platform === "linux" && (process.env.ALACRITTY_WINDOW_ID || process.env.WAYLAND_DISPLAY || process.env.DISPLAY)) {
- return "linux-desktop";
- }
- // Default fallback
- return "osc777";
-}
-
-function notify(title: string, body: string): void {
- const terminal = detectTerminal();
-
- switch (terminal) {
- case "wsl":
- notifyWindows(title, body);
- break;
- case "kitty":
- notifyOSC99(title, body);
- break;
- case "linux-desktop":
- notifyLinuxDesktop(title, body);
- break;
- case "osc777":
- default:
- notifyOSC777(title, body);
- break;
- }
-}
-
-export default function (pi: ExtensionAPI) {
- pi.on("agent_end", async () => {
- notify("Pi", "Ready for input");
- });
-}
dots/pi/agent/extensions/terminal-status.ts
@@ -0,0 +1,193 @@
+/**
+ * Pi Terminal Status Extension
+ *
+ * Manages terminal status updates:
+ * 1. Terminal tab title - shows project name, current tool, and status
+ * 2. Desktop notifications - alerts when agent is ready for input
+ *
+ * Terminal title format: "π <project> • <context>"
+ * Examples:
+ * - "π home" (idle)
+ * - "π home • bash" (running bash)
+ * - "π home • Ready" (waiting for input)
+ *
+ * Notification protocols supported:
+ * - OSC 777: Ghostty, iTerm2, WezTerm, rxvt-unicode
+ * - OSC 99: Kitty
+ * - notify-send: Linux desktop fallback (libnotify)
+ * - Windows toast: Windows Terminal (WSL)
+ *
+ * Migrated from: notify.ts + Go-based claude-hooks-update-terminal-title
+ */
+
+import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
+import path from "node:path";
+
+// =============================================================================
+// Terminal Title
+// =============================================================================
+
+/**
+ * Set terminal tab/window title using ANSI escape codes
+ * OSC 0 = icon + title, OSC 2 = window title, OSC 30 = tab title
+ */
+function setTerminalTitle(title: string): void {
+ process.stderr.write(`\x1b]0;${title}\x07`);
+ process.stderr.write(`\x1b]2;${title}\x07`);
+ process.stderr.write(`\x1b]30;${title}\x07`);
+}
+
+/**
+ * Get project name from current working directory
+ */
+function getProjectName(): string {
+ try {
+ return path.basename(process.cwd());
+ } catch {
+ return "pi";
+ }
+}
+
+/**
+ * Build terminal title with optional context
+ */
+function buildTitle(context?: string): string {
+ const project = getProjectName();
+ const prefix = "π";
+
+ if (context) {
+ return `${prefix} ${project} • ${context}`;
+ }
+ return `${prefix} ${project}`;
+}
+
+// =============================================================================
+// Desktop Notifications
+// =============================================================================
+
+function windowsToastScript(title: string, body: string): string {
+ const type = "Windows.UI.Notifications";
+ const mgr = `[${type}.ToastNotificationManager, ${type}, ContentType = WindowsRuntime]`;
+ const template = `[${type}.ToastTemplateType]::ToastText01`;
+ const toast = `[${type}.ToastNotification]::new($xml)`;
+ return [
+ `${mgr} > $null`,
+ `$xml = [${type}.ToastNotificationManager]::GetTemplateContent(${template})`,
+ `$xml.GetElementsByTagName('text')[0].AppendChild($xml.CreateTextNode('${body}')) > $null`,
+ `[${type}.ToastNotificationManager]::CreateToastNotifier('${title}').Show(${toast})`,
+ ].join("; ");
+}
+
+function notifyOSC777(title: string, body: string): void {
+ process.stdout.write(`\x1b]777;notify;${title};${body}\x07`);
+}
+
+function notifyOSC99(title: string, body: string): void {
+ // Kitty OSC 99: i=notification id, d=0 means not done yet, p=body for second part
+ process.stdout.write(`\x1b]99;i=1:d=0;${title}\x1b\\`);
+ process.stdout.write(`\x1b]99;i=1:p=body;${body}\x1b\\`);
+}
+
+function notifyWindows(title: string, body: string): void {
+ const { execFile } = require("child_process");
+ execFile("powershell.exe", [
+ "-NoProfile",
+ "-Command",
+ windowsToastScript(title, body),
+ ]);
+}
+
+function notifyLinuxDesktop(title: string, body: string): void {
+ const { execFile } = require("child_process");
+ // Use notify-send (libnotify) for Linux desktop notifications
+ // -u low = low urgency, -t 5000 = 5 second timeout
+ execFile(
+ "notify-send",
+ ["-u", "low", "-t", "5000", "-a", "pi", title, body],
+ (err: Error | null) => {
+ if (err) {
+ // Fallback to OSC 777 if notify-send fails
+ notifyOSC777(title, body);
+ }
+ }
+ );
+}
+
+function detectTerminal(): "kitty" | "wsl" | "osc777" | "linux-desktop" {
+ // Windows Terminal (WSL)
+ if (process.env.WT_SESSION) {
+ return "wsl";
+ }
+ // Kitty terminal
+ if (process.env.KITTY_WINDOW_ID) {
+ return "kitty";
+ }
+ // Ghostty, WezTerm, iTerm2 - these support OSC 777
+ if (
+ process.env.GHOSTTY_RESOURCES_DIR ||
+ process.env.WEZTERM_PANE ||
+ process.env.ITERM_SESSION_ID
+ ) {
+ return "osc777";
+ }
+ // Alacritty, foot, and other terminals that don't support OSC notifications
+ // Use notify-send on Linux
+ if (
+ process.platform === "linux" &&
+ (process.env.ALACRITTY_WINDOW_ID ||
+ process.env.WAYLAND_DISPLAY ||
+ process.env.DISPLAY)
+ ) {
+ return "linux-desktop";
+ }
+ // Default fallback
+ return "osc777";
+}
+
+function notify(title: string, body: string): void {
+ const terminal = detectTerminal();
+
+ switch (terminal) {
+ case "wsl":
+ notifyWindows(title, body);
+ break;
+ case "kitty":
+ notifyOSC99(title, body);
+ break;
+ case "linux-desktop":
+ notifyLinuxDesktop(title, body);
+ break;
+ case "osc777":
+ default:
+ notifyOSC777(title, body);
+ break;
+ }
+}
+
+// =============================================================================
+// Extension Entry Point
+// =============================================================================
+
+export default function (pi: ExtensionAPI) {
+ // Set initial title on session start
+ pi.on("session_start", async () => {
+ setTerminalTitle(buildTitle());
+ });
+
+ // Update title when tool starts executing
+ pi.on("tool_call", async (event) => {
+ const toolName = event.toolName.toLowerCase();
+ setTerminalTitle(buildTitle(toolName));
+ });
+
+ // Reset title when tool finishes
+ pi.on("tool_result", async () => {
+ setTerminalTitle(buildTitle());
+ });
+
+ // Show "Ready" and send notification when agent is done
+ pi.on("agent_end", async () => {
+ setTerminalTitle(buildTitle("Ready"));
+ notify("Pi", "Ready for input");
+ });
+}