diff --git a/packages/coding-agent/docs/extensions.md b/packages/coding-agent/docs/extensions.md index 82ed5d68..d76eb88b 100644 --- a/packages/coding-agent/docs/extensions.md +++ b/packages/coding-agent/docs/extensions.md @@ -46,6 +46,7 @@ See [examples/extensions/](../examples/extensions/) for working implementations. - [Custom UI](#custom-ui) - [Error Handling](#error-handling) - [Mode Behavior](#mode-behavior) +- [Examples Reference](#examples-reference) ## Quick Start @@ -407,6 +408,8 @@ exit (Ctrl+C, Ctrl+D) ### Session Events +See [session.md](session.md) for session storage internals and the SessionManager API. + #### session_start Fired on initial session load. @@ -417,8 +420,6 @@ pi.on("session_start", async (_event, ctx) => { }); ``` -**Examples:** [claude-rules.ts](../examples/extensions/claude-rules.ts), [custom-header.ts](../examples/extensions/custom-header.ts), [file-trigger.ts](../examples/extensions/file-trigger.ts), [status-line.ts](../examples/extensions/status-line.ts), [todo.ts](../examples/extensions/todo.ts), [tools.ts](../examples/extensions/tools.ts) - #### session_before_switch / session_switch Fired when starting a new session (`/new`) or switching sessions (`/resume`). @@ -440,8 +441,6 @@ pi.on("session_switch", async (event, ctx) => { }); ``` -**Examples:** [confirm-destructive.ts](../examples/extensions/confirm-destructive.ts), [dirty-repo-guard.ts](../examples/extensions/dirty-repo-guard.ts), [status-line.ts](../examples/extensions/status-line.ts), [todo.ts](../examples/extensions/todo.ts) - #### session_before_fork / session_fork Fired when forking via `/fork`. @@ -459,8 +458,6 @@ pi.on("session_fork", async (event, ctx) => { }); ``` -**Examples:** [confirm-destructive.ts](../examples/extensions/confirm-destructive.ts), [dirty-repo-guard.ts](../examples/extensions/dirty-repo-guard.ts), [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [todo.ts](../examples/extensions/todo.ts), [tools.ts](../examples/extensions/tools.ts) - #### session_before_compact / session_compact Fired on compaction. See [compaction.md](compaction.md) for details. @@ -488,8 +485,6 @@ pi.on("session_compact", async (event, ctx) => { }); ``` -**Examples:** [custom-compaction.ts](../examples/extensions/custom-compaction.ts), [trigger-compact.ts](../examples/extensions/trigger-compact.ts) - #### session_before_tree / session_tree Fired on `/tree` navigation. @@ -507,8 +502,6 @@ pi.on("session_tree", async (event, ctx) => { }); ``` -**Examples:** [todo.ts](../examples/extensions/todo.ts), [tools.ts](../examples/extensions/tools.ts) - #### session_shutdown Fired on exit (Ctrl+C, Ctrl+D, SIGTERM). @@ -519,8 +512,6 @@ pi.on("session_shutdown", async (_event, ctx) => { }); ``` -**Examples:** [auto-commit-on-exit.ts](../examples/extensions/auto-commit-on-exit.ts) - ### Agent Events #### before_agent_start @@ -546,8 +537,6 @@ pi.on("before_agent_start", async (event, ctx) => { }); ``` -**Examples:** [claude-rules.ts](../examples/extensions/claude-rules.ts), [pirate.ts](../examples/extensions/pirate.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts), [ssh.ts](../examples/extensions/ssh.ts) - #### agent_start / agent_end Fired once per user prompt. @@ -560,8 +549,6 @@ pi.on("agent_end", async (event, ctx) => { }); ``` -**Examples:** [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts) - #### turn_start / turn_end Fired for each turn (one LLM response + tool calls). @@ -576,8 +563,6 @@ pi.on("turn_end", async (event, ctx) => { }); ``` -**Examples:** [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [status-line.ts](../examples/extensions/status-line.ts) - #### context Fired before each LLM call. Modify messages non-destructively. @@ -590,8 +575,6 @@ pi.on("context", async (event, ctx) => { }); ``` -**Examples:** [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts) - ### Model Events #### model_select @@ -615,8 +598,6 @@ pi.on("model_select", async (event, ctx) => { Use this to update UI elements (status bars, footers) or perform model-specific initialization when the active model changes. -**Examples:** [model-status.ts](../examples/extensions/model-status.ts) - ### Tool Events #### tool_call @@ -635,8 +616,6 @@ pi.on("tool_call", async (event, ctx) => { }); ``` -**Examples:** [permission-gate.ts](../examples/extensions/permission-gate.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [protected-paths.ts](../examples/extensions/protected-paths.ts) - #### tool_result Fired after tool executes. **Can modify result.** @@ -657,8 +636,6 @@ pi.on("tool_result", async (event, ctx) => { }); ``` -**Examples:** [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts) - ### User Bash Events #### user_bash @@ -679,8 +656,6 @@ pi.on("user_bash", (event, ctx) => { }); ``` -**Examples:** [ssh.ts](../examples/extensions/ssh.ts), [interactive-shell.ts](../examples/extensions/interactive-shell.ts) - ### Input Events #### input @@ -915,8 +890,6 @@ pi.registerTool({ }); ``` -**Examples:** [hello.ts](../examples/extensions/hello.ts), [question.ts](../examples/extensions/question.ts), [questionnaire.ts](../examples/extensions/questionnaire.ts), [todo.ts](../examples/extensions/todo.ts), [truncated-tool.ts](../examples/extensions/truncated-tool.ts) - ### pi.sendMessage(message, options?) Inject a custom message into the session. @@ -940,8 +913,6 @@ pi.sendMessage({ - `"nextTurn"` - Queued for next user prompt. Does not interrupt or trigger anything. - `triggerTurn: true` - If agent is idle, trigger an LLM response immediately. Only applies to `"steer"` and `"followUp"` modes (ignored for `"nextTurn"`). -**Examples:** [file-trigger.ts](../examples/extensions/file-trigger.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts) - ### pi.sendUserMessage(content, options?) Send a user message to the agent. Unlike `sendMessage()` which sends custom messages, this sends an actual user message that appears as if typed by the user. Always triggers a turn. @@ -987,8 +958,6 @@ pi.on("session_start", async (_event, ctx) => { }); ``` -**Examples:** [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts), [snake.ts](../examples/extensions/snake.ts), [tools.ts](../examples/extensions/tools.ts) - ### pi.setSessionName(name) Set the session display name (shown in session selector instead of first message). @@ -1058,15 +1027,13 @@ pi.registerCommand("deploy", { }); ``` -**Examples:** [custom-footer.ts](../examples/extensions/custom-footer.ts), [custom-header.ts](../examples/extensions/custom-header.ts), [handoff.ts](../examples/extensions/handoff.ts), [pirate.ts](../examples/extensions/pirate.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts), [qna.ts](../examples/extensions/qna.ts), [send-user-message.ts](../examples/extensions/send-user-message.ts), [snake.ts](../examples/extensions/snake.ts), [summarize.ts](../examples/extensions/summarize.ts), [todo.ts](../examples/extensions/todo.ts), [tools.ts](../examples/extensions/tools.ts) - ### pi.registerMessageRenderer(customType, renderer) Register a custom TUI renderer for messages with your `customType`. See [Custom UI](#custom-ui). ### pi.registerShortcut(shortcut, options) -Register a keyboard shortcut. +Register a keyboard shortcut. See [keybindings.md](keybindings.md) for the shortcut format and built-in keybindings. ```typescript pi.registerShortcut("ctrl+shift+p", { @@ -1077,8 +1044,6 @@ pi.registerShortcut("ctrl+shift+p", { }); ``` -**Examples:** [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts) - ### pi.registerFlag(name, options) Register a CLI flag. @@ -1096,8 +1061,6 @@ if (pi.getFlag("--plan")) { } ``` -**Examples:** [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts) - ### pi.exec(command, args, options?) Execute a shell command. @@ -1107,8 +1070,6 @@ const result = await pi.exec("git", ["status"], { signal, timeout: 5000 }); // result.stdout, result.stderr, result.code, result.killed ``` -**Examples:** [auto-commit-on-exit.ts](../examples/extensions/auto-commit-on-exit.ts), [dirty-repo-guard.ts](../examples/extensions/dirty-repo-guard.ts), [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts) - ### pi.getActiveTools() / pi.getAllTools() / pi.setActiveTools(names) Manage active tools. @@ -1120,8 +1081,6 @@ const names = all.map(t => t.name); // Just names if needed pi.setActiveTools(["read", "bash"]); // Switch to read-only ``` -**Examples:** [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts), [tools.ts](../examples/extensions/tools.ts) - ### pi.setModel(model) Set the current model. Returns `false` if no API key is available for the model. @@ -1136,8 +1095,6 @@ if (model) { } ``` -**Examples:** [preset.ts](../examples/extensions/preset.ts) - ### pi.getThinkingLevel() / pi.setThinkingLevel(level) Get or set the thinking level. Level is clamped to model capabilities (non-reasoning models always use "off"). @@ -1147,8 +1104,6 @@ const current = pi.getThinkingLevel(); // "off" | "minimal" | "low" | "medium" pi.setThinkingLevel("high"); ``` -**Examples:** [preset.ts](../examples/extensions/preset.ts) - ### pi.events Shared event bus for communication between extensions: @@ -1382,6 +1337,7 @@ The built-in limit is **50KB** (~10k tokens) and **2000 lines**, whichever is hi import { truncateHead, // Keep first N lines/bytes (good for file reads, search results) truncateTail, // Keep last N lines/bytes (good for logs, command output) + truncateLine, // Truncate a single line to maxBytes with ellipsis formatSize, // Human-readable size (e.g., "50KB", "1.5MB") DEFAULT_MAX_BYTES, // 50KB DEFAULT_MAX_LINES, // 2000 @@ -1556,12 +1512,6 @@ const text = await ctx.ui.editor("Edit:", "prefilled text"); ctx.ui.notify("Done!", "info"); // "info" | "warning" | "error" ``` -**Examples:** -- `ctx.ui.select()`: [confirm-destructive.ts](../examples/extensions/confirm-destructive.ts), [dirty-repo-guard.ts](../examples/extensions/dirty-repo-guard.ts), [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [permission-gate.ts](../examples/extensions/permission-gate.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [question.ts](../examples/extensions/question.ts), [questionnaire.ts](../examples/extensions/questionnaire.ts) -- `ctx.ui.confirm()`: [confirm-destructive.ts](../examples/extensions/confirm-destructive.ts) -- `ctx.ui.editor()`: [handoff.ts](../examples/extensions/handoff.ts) -- `ctx.ui.setEditorText()`: [handoff.ts](../examples/extensions/handoff.ts), [qna.ts](../examples/extensions/qna.ts) - #### Timed Dialogs with Countdown Dialogs support a `timeout` option that auto-dismisses with a live countdown display: @@ -1649,7 +1599,7 @@ const current = ctx.ui.getEditorText(); ctx.ui.setEditorComponent((tui, theme, keybindings) => new VimEditor(tui, theme, keybindings)); ctx.ui.setEditorComponent(undefined); // Restore default editor -// Theme management +// Theme management (see themes.md for creating themes) const themes = ctx.ui.getAllThemes(); // [{ name: "dark", path: "/..." | undefined }, ...] const lightTheme = ctx.ui.getTheme("light"); // Load without switching const result = ctx.ui.setTheme("light"); // Switch by name @@ -1660,14 +1610,6 @@ ctx.ui.setTheme(lightTheme!); // Or switch by Theme object ctx.ui.theme.fg("accent", "styled text"); // Access current theme ``` -**Examples:** -- `ctx.ui.setStatus()`: [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts), [status-line.ts](../examples/extensions/status-line.ts) -- `ctx.ui.setWidget()`: [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts) -- `ctx.ui.setFooter()`: [custom-footer.ts](../examples/extensions/custom-footer.ts) -- `ctx.ui.setHeader()`: [custom-header.ts](../examples/extensions/custom-header.ts) -- `ctx.ui.setEditorComponent()`: [modal-editor.ts](../examples/extensions/modal-editor.ts) -- `ctx.ui.setTheme()`: [mac-system-theme.ts](../examples/extensions/mac-system-theme.ts) - ### Custom Components For complex UI, use `ctx.ui.custom()`. This temporarily replaces the editor with your component until `done()` is called: @@ -1726,8 +1668,6 @@ const result = await ctx.ui.custom( See [tui.md](tui.md) for the full `OverlayOptions` API and [overlay-qa-tests.ts](../examples/extensions/overlay-qa-tests.ts) for examples. -**Examples:** [handoff.ts](../examples/extensions/handoff.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts), [qna.ts](../examples/extensions/qna.ts), [snake.ts](../examples/extensions/snake.ts), [summarize.ts](../examples/extensions/summarize.ts), [todo.ts](../examples/extensions/todo.ts), [tools.ts](../examples/extensions/tools.ts), [overlay-test.ts](../examples/extensions/overlay-test.ts) - ### Custom Editor Replace the main input editor with a custom implementation (vim mode, emacs mode, etc.): @@ -1769,8 +1709,6 @@ export default function (pi: ExtensionAPI) { See [tui.md](tui.md) Pattern 7 for a complete example with mode indicator. -**Examples:** [modal-editor.ts](../examples/extensions/modal-editor.ts) - ### Message Rendering Register a custom renderer for messages with your `customType`: @@ -1822,6 +1760,19 @@ theme.italic(text) theme.strikethrough(text) ``` +For syntax highlighting in custom tool renderers: + +```typescript +import { highlightCode, getLanguageFromPath } from "@mariozechner/pi-coding-agent"; + +// Highlight code with explicit language +const highlighted = highlightCode("const x = 1;", "typescript", theme); + +// Auto-detect language from file path +const lang = getLanguageFromPath("/path/to/file.rs"); // "rust" +const highlighted = highlightCode(code, lang, theme); +``` + ## Error Handling - Extension errors are logged, agent continues @@ -1837,3 +1788,76 @@ theme.strikethrough(text) | Print (`-p`) | No-op | Extensions run but can't prompt | In print mode, check `ctx.hasUI` before using UI methods. + +## Examples Reference + +All examples in [examples/extensions/](../examples/extensions/). + +| Example | Description | Key APIs | +|---------|-------------|----------| +| **Tools** ||| +| `hello.ts` | Minimal tool registration | `registerTool` | +| `question.ts` | Tool with user interaction | `registerTool`, `ui.select` | +| `questionnaire.ts` | Multi-step wizard tool | `registerTool`, `ui.custom` | +| `todo.ts` | Stateful tool with persistence | `registerTool`, `appendEntry`, `renderResult`, session events | +| `truncated-tool.ts` | Output truncation example | `registerTool`, `truncateHead` | +| `tool-override.ts` | Override built-in read tool | `registerTool` (same name as built-in) | +| **Commands** ||| +| `pirate.ts` | Modify system prompt per-turn | `registerCommand`, `before_agent_start` | +| `summarize.ts` | Conversation summary command | `registerCommand`, `ui.custom` | +| `handoff.ts` | Cross-provider model handoff | `registerCommand`, `ui.editor`, `ui.custom` | +| `qna.ts` | Q&A with custom UI | `registerCommand`, `ui.custom`, `setEditorText` | +| `send-user-message.ts` | Inject user messages | `registerCommand`, `sendUserMessage` | +| `shutdown-command.ts` | Graceful shutdown command | `registerCommand`, `shutdown()` | +| **Events & Gates** ||| +| `permission-gate.ts` | Block dangerous commands | `on("tool_call")`, `ui.confirm` | +| `protected-paths.ts` | Block writes to specific paths | `on("tool_call")` | +| `confirm-destructive.ts` | Confirm session changes | `on("session_before_switch")`, `on("session_before_fork")` | +| `dirty-repo-guard.ts` | Warn on dirty git repo | `on("session_before_*")`, `exec` | +| `input-transform.ts` | Transform user input | `on("input")` | +| `model-status.ts` | React to model changes | `on("model_select")`, `setStatus` | +| `claude-rules.ts` | Load rules from files | `on("session_start")`, `on("before_agent_start")` | +| `file-trigger.ts` | File watcher triggers messages | `sendMessage` | +| **Compaction & Sessions** ||| +| `custom-compaction.ts` | Custom compaction summary | `on("session_before_compact")` | +| `trigger-compact.ts` | Trigger compaction manually | `compact()` | +| `git-checkpoint.ts` | Git stash on turns | `on("turn_end")`, `on("session_fork")`, `exec` | +| `auto-commit-on-exit.ts` | Commit on shutdown | `on("session_shutdown")`, `exec` | +| **UI Components** ||| +| `status-line.ts` | Footer status indicator | `setStatus`, session events | +| `custom-footer.ts` | Replace footer entirely | `registerCommand`, `setFooter` | +| `custom-header.ts` | Replace startup header | `on("session_start")`, `setHeader` | +| `modal-editor.ts` | Vim-style modal editor | `setEditorComponent`, `CustomEditor` | +| `rainbow-editor.ts` | Custom editor styling | `setEditorComponent` | +| `widget-placement.ts` | Widget above/below editor | `setWidget` | +| `overlay-test.ts` | Overlay components | `ui.custom` with overlay options | +| `overlay-qa-tests.ts` | Comprehensive overlay tests | `ui.custom`, all overlay options | +| `notify.ts` | Simple notifications | `ui.notify` | +| `timed-confirm.ts` | Dialogs with timeout | `ui.confirm` with timeout/signal | +| `mac-system-theme.ts` | Auto-switch theme | `setTheme`, `exec` | +| **Complex Extensions** ||| +| `plan-mode/` | Full plan mode implementation | All event types, `registerCommand`, `registerShortcut`, `registerFlag`, `setStatus`, `setWidget`, `sendMessage`, `setActiveTools` | +| `preset.ts` | Saveable presets (model, tools, thinking) | `registerCommand`, `registerShortcut`, `registerFlag`, `setModel`, `setActiveTools`, `setThinkingLevel`, `appendEntry` | +| `tools.ts` | Toggle tools on/off UI | `registerCommand`, `setActiveTools`, `SettingsList`, session events | +| **Remote & Sandbox** ||| +| `ssh.ts` | SSH remote execution | `registerFlag`, `on("user_bash")`, `on("before_agent_start")`, tool operations | +| `interactive-shell.ts` | Persistent shell session | `on("user_bash")` | +| `sandbox/` | Sandboxed tool execution | Tool operations | +| `subagent/` | Spawn sub-agents | `registerTool`, `exec` | +| **Games** ||| +| `snake.ts` | Snake game | `registerCommand`, `ui.custom`, keyboard handling | +| `space-invaders.ts` | Space Invaders game | `registerCommand`, `ui.custom` | +| `doom-overlay/` | Doom in overlay | `ui.custom` with overlay | +| **Providers** ||| +| `custom-provider-anthropic/` | Custom Anthropic proxy | `registerProvider` | +| `custom-provider-gitlab-duo/` | GitLab Duo integration | `registerProvider` with OAuth | +| **Messages & Communication** ||| +| `message-renderer.ts` | Custom message rendering | `registerMessageRenderer`, `sendMessage` | +| `event-bus.ts` | Inter-extension events | `pi.events` | +| **Session Metadata** ||| +| `session-name.ts` | Name sessions for selector | `setSessionName`, `getSessionName` | +| `bookmark.ts` | Bookmark entries for /tree | `setLabel` | +| **Misc** ||| +| `antigravity-image-gen.ts` | Image generation tool | `registerTool`, Google Antigravity | +| `inline-bash.ts` | Inline bash in tool calls | `on("tool_call")` | +| `with-deps/` | Extension with npm dependencies | Package structure with `package.json` | diff --git a/packages/coding-agent/examples/extensions/README.md b/packages/coding-agent/examples/extensions/README.md index 60a2afe2..38921014 100644 --- a/packages/coding-agent/examples/extensions/README.md +++ b/packages/coding-agent/examples/extensions/README.md @@ -88,6 +88,20 @@ cp permission-gate.ts ~/.pi/agent/extensions/ |-----------|-------------| | `mac-system-theme.ts` | Syncs pi theme with macOS dark/light mode | +### Messages & Communication + +| Extension | Description | +|-----------|-------------| +| `message-renderer.ts` | Custom message rendering with colors and expandable details via `registerMessageRenderer` | +| `event-bus.ts` | Inter-extension communication via `pi.events` | + +### Session Metadata + +| Extension | Description | +|-----------|-------------| +| `session-name.ts` | Name sessions for the session selector via `setSessionName` | +| `bookmark.ts` | Bookmark entries with labels for `/tree` navigation via `setLabel` | + ### Custom Providers | Extension | Description | diff --git a/packages/coding-agent/examples/extensions/bookmark.ts b/packages/coding-agent/examples/extensions/bookmark.ts new file mode 100644 index 00000000..bd67fbd3 --- /dev/null +++ b/packages/coding-agent/examples/extensions/bookmark.ts @@ -0,0 +1,50 @@ +/** + * Entry bookmarking example. + * + * Shows setLabel to mark entries with labels for easy navigation in /tree. + * Labels appear in the tree view and help you find important points. + * + * Usage: /bookmark [label] - bookmark the last assistant message + */ + +import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; + +export default function (pi: ExtensionAPI) { + pi.registerCommand("bookmark", { + description: "Bookmark last message (usage: /bookmark [label])", + handler: async (args, ctx) => { + const label = args.trim() || `bookmark-${Date.now()}`; + + // Find the last assistant message entry + const entries = ctx.sessionManager.getEntries(); + for (let i = entries.length - 1; i >= 0; i--) { + const entry = entries[i]; + if (entry.type === "message" && entry.message.role === "assistant") { + pi.setLabel(entry.id, label); + ctx.ui.notify(`Bookmarked as: ${label}`, "info"); + return; + } + } + + ctx.ui.notify("No assistant message to bookmark", "warning"); + }, + }); + + // Remove bookmark + pi.registerCommand("unbookmark", { + description: "Remove bookmark from last labeled entry", + handler: async (_args, ctx) => { + const entries = ctx.sessionManager.getEntries(); + for (let i = entries.length - 1; i >= 0; i--) { + const entry = entries[i]; + const label = ctx.sessionManager.getLabel(entry.id); + if (label) { + pi.setLabel(entry.id, undefined); + ctx.ui.notify(`Removed bookmark: ${label}`, "info"); + return; + } + } + ctx.ui.notify("No bookmarked entry found", "warning"); + }, + }); +} diff --git a/packages/coding-agent/examples/extensions/event-bus.ts b/packages/coding-agent/examples/extensions/event-bus.ts new file mode 100644 index 00000000..0a4b8c10 --- /dev/null +++ b/packages/coding-agent/examples/extensions/event-bus.ts @@ -0,0 +1,43 @@ +/** + * Inter-extension event bus example. + * + * Shows pi.events for communication between extensions. One extension + * can emit events that other extensions listen to. + * + * Usage: /emit [event-name] [data] - emit an event on the bus + */ + +import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent"; + +export default function (pi: ExtensionAPI) { + // Store ctx for use in event handler + let currentCtx: ExtensionContext | undefined; + + pi.on("session_start", async (_event, ctx) => { + currentCtx = ctx; + }); + + // Listen for events from other extensions + pi.events.on("my:notification", (data) => { + const { message, from } = data as { message: string; from: string }; + currentCtx?.ui.notify(`Event from ${from}: ${message}`, "info"); + }); + + // Command to emit events (emits "my:notification" which the listener above receives) + pi.registerCommand("emit", { + description: "Emit my:notification event (usage: /emit message)", + handler: async (args, _ctx) => { + const message = args.trim() || "hello"; + pi.events.emit("my:notification", { message, from: "/emit command" }); + // Listener above will show the notification + }, + }); + + // Example: emit on session start + pi.on("session_start", async () => { + pi.events.emit("my:notification", { + message: "Session started", + from: "event-bus-example", + }); + }); +} diff --git a/packages/coding-agent/examples/extensions/message-renderer.ts b/packages/coding-agent/examples/extensions/message-renderer.ts new file mode 100644 index 00000000..47a061d3 --- /dev/null +++ b/packages/coding-agent/examples/extensions/message-renderer.ts @@ -0,0 +1,59 @@ +/** + * Custom message rendering example. + * + * Shows how to use registerMessageRenderer to control how custom messages + * appear in the TUI, with colors, formatting, and expandable details. + * + * Usage: /status [message] - sends a status message with custom rendering + */ + +import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; +import { Box, Text } from "@mariozechner/pi-tui"; + +export default function (pi: ExtensionAPI) { + // Register custom renderer for "status-update" messages + pi.registerMessageRenderer("status-update", (message, { expanded }, theme) => { + const details = message.details as { level: string; timestamp: number } | undefined; + const level = details?.level ?? "info"; + + // Color based on level + const color = level === "error" ? "error" : level === "warn" ? "warning" : "success"; + const prefix = theme.fg(color, `[${level.toUpperCase()}]`); + + let text = `${prefix} ${message.content}`; + + // Show timestamp when expanded + if (expanded && details?.timestamp) { + const time = new Date(details.timestamp).toLocaleTimeString(); + text += `\n${theme.fg("dim", ` at ${time}`)}`; + } + + // Use Box with customMessageBg for consistent styling + const box = new Box(1, 1, (t) => theme.bg("customMessageBg", t)); + box.addChild(new Text(text, 0, 0)); + return box; + }); + + // Command to send status messages + pi.registerCommand("status", { + description: "Send a status message (usage: /status [warn|error] message)", + handler: async (args, _ctx) => { + const parts = args.trim().split(/\s+/); + let level = "info"; + let content = args.trim(); + + // Check for level prefix + if (parts[0] === "warn" || parts[0] === "error") { + level = parts[0]; + content = parts.slice(1).join(" ") || "Status update"; + } + + pi.sendMessage({ + customType: "status-update", + content, + display: true, + details: { level, timestamp: Date.now() }, + }); + }, + }); +} diff --git a/packages/coding-agent/examples/extensions/session-name.ts b/packages/coding-agent/examples/extensions/session-name.ts new file mode 100644 index 00000000..8ff1c378 --- /dev/null +++ b/packages/coding-agent/examples/extensions/session-name.ts @@ -0,0 +1,27 @@ +/** + * Session naming example. + * + * Shows setSessionName/getSessionName to give sessions friendly names + * that appear in the session selector instead of the first message. + * + * Usage: /session-name [name] - set or show session name + */ + +import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; + +export default function (pi: ExtensionAPI) { + pi.registerCommand("session-name", { + description: "Set or show session name (usage: /session-name [new name])", + handler: async (args, ctx) => { + const name = args.trim(); + + if (name) { + pi.setSessionName(name); + ctx.ui.notify(`Session named: ${name}`, "info"); + } else { + const current = pi.getSessionName(); + ctx.ui.notify(current ? `Session: ${current}` : "No session name set", "info"); + } + }, + }); +}