Add ExtensionAPI methods, preset example, and TUI documentation improvements

- ExtensionAPI: setModel(), getThinkingLevel(), setThinkingLevel() methods
- New preset.ts example with plan/implement presets for model/thinking/tools switching
- Export all UI components from pi-coding-agent for extension use
- docs/tui.md: Common Patterns section with copy-paste code for SelectList, BorderedLoader, SettingsList, setStatus, setWidget, setFooter
- docs/tui.md: Key Rules section for extension UI development
- docs/extensions.md: Exhaustive example links for all ExtensionAPI methods and events
- System prompt now references docs/tui.md for TUI development

Fixes #509, relates to #347
This commit is contained in:
Mario Zechner 2026-01-06 23:24:23 +01:00
parent c35a18b2b3
commit 59d8b7948c
14 changed files with 850 additions and 13 deletions

View file

@ -306,6 +306,8 @@ 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`).
@ -327,6 +329,8 @@ 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_branch / session_branch
Fired when branching via `/branch`.
@ -344,6 +348,8 @@ pi.on("session_branch", 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.
@ -371,6 +377,8 @@ pi.on("session_compact", async (event, ctx) => {
});
```
**Examples:** [custom-compaction.ts](../examples/extensions/custom-compaction.ts)
#### session_before_tree / session_tree
Fired on `/tree` navigation.
@ -388,6 +396,8 @@ 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).
@ -398,6 +408,8 @@ 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
@ -422,6 +434,8 @@ 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.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts)
#### agent_start / agent_end
Fired once per user prompt.
@ -434,6 +448,8 @@ pi.on("agent_end", async (event, ctx) => {
});
```
**Examples:** [chalk-logger.ts](../examples/extensions/chalk-logger.ts), [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts)
#### turn_start / turn_end
Fired for each turn (one LLM response + tool calls).
@ -448,6 +464,8 @@ pi.on("turn_end", async (event, ctx) => {
});
```
**Examples:** [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [status-line.ts](../examples/extensions/status-line.ts)
#### context
Fired before each LLM call. Modify messages non-destructively.
@ -460,6 +478,8 @@ pi.on("context", async (event, ctx) => {
});
```
**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts)
### Tool Events
#### tool_call
@ -478,6 +498,8 @@ pi.on("tool_call", async (event, ctx) => {
});
```
**Examples:** [chalk-logger.ts](../examples/extensions/chalk-logger.ts), [permission-gate.ts](../examples/extensions/permission-gate.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [protected-paths.ts](../examples/extensions/protected-paths.ts)
#### tool_result
Fired after tool executes. **Can modify result.**
@ -498,6 +520,8 @@ pi.on("tool_result", async (event, ctx) => {
});
```
**Examples:** [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts)
## ExtensionContext
Every handler receives `ctx: ExtensionContext`:
@ -595,7 +619,7 @@ const result = await ctx.navigateTree("entry-id-456", {
### pi.on(event, handler)
Subscribe to events. See [Events](#events).
Subscribe to events. See [Events](#events) for event types and return values.
### pi.registerTool(definition)
@ -630,9 +654,11 @@ pi.registerTool({
});
```
**Examples:** [hello.ts](../examples/extensions/hello.ts), [question.ts](../examples/extensions/question.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:
Inject a custom message into the session.
```typescript
pi.sendMessage({
@ -653,6 +679,8 @@ 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.ts](../examples/extensions/plan-mode.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.
@ -683,7 +711,7 @@ See [send-user-message.ts](../examples/extensions/send-user-message.ts) for a co
### pi.appendEntry(customType, data?)
Persist extension state (does NOT participate in LLM context):
Persist extension state (does NOT participate in LLM context).
```typescript
pi.appendEntry("my-state", { count: 42 });
@ -698,9 +726,11 @@ pi.on("session_start", async (_event, ctx) => {
});
```
**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts), [snake.ts](../examples/extensions/snake.ts), [tools.ts](../examples/extensions/tools.ts)
### pi.registerCommand(name, options)
Register a command:
Register a command.
```typescript
pi.registerCommand("stats", {
@ -712,13 +742,15 @@ pi.registerCommand("stats", {
});
```
**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.ts](../examples/extensions/plan-mode.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), [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.
```typescript
pi.registerShortcut("ctrl+shift+p", {
@ -729,9 +761,11 @@ pi.registerShortcut("ctrl+shift+p", {
});
```
**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts)
### pi.registerFlag(name, options)
Register a CLI flag:
Register a CLI flag.
```typescript
pi.registerFlag("--plan", {
@ -746,24 +780,57 @@ if (pi.getFlag("--plan")) {
}
```
**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts)
### pi.exec(command, args, options?)
Execute a shell command:
Execute a shell command.
```typescript
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:
Manage active tools.
```typescript
const active = pi.getActiveTools(); // ["read", "bash", "edit", "write"]
pi.setActiveTools(["read", "bash"]); // Switch to read-only
```
**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.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.
```typescript
const model = ctx.modelRegistry.find("anthropic", "claude-sonnet-4-5");
if (model) {
const success = await pi.setModel(model);
if (!success) {
ctx.ui.notify("No API key for this model", "error");
}
}
```
**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").
```typescript
const current = pi.getThinkingLevel(); // "off" | "minimal" | "low" | "medium" | "high" | "xhigh"
pi.setThinkingLevel("high");
```
**Examples:** [preset.ts](../examples/extensions/preset.ts)
### pi.events
Shared event bus for communication between extensions:
@ -994,6 +1061,14 @@ If `renderCall`/`renderResult` is not defined or throws:
Extensions can interact with users via `ctx.ui` methods and customize how messages/tools render.
**For custom components, see [tui.md](tui.md)** which has copy-paste patterns for:
- Selection dialogs (SelectList)
- Async operations with cancel (BorderedLoader)
- Settings toggles (SettingsList)
- Status indicators (setStatus)
- Widgets above editor (setWidget)
- Custom footers (setFooter)
### Dialogs
```typescript
@ -1013,6 +1088,12 @@ 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.ts](../examples/extensions/plan-mode.ts), [question.ts](../examples/extensions/question.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)
### Widgets, Status, and Footer
```typescript
@ -1040,6 +1121,12 @@ ctx.ui.setEditorText("Prefill text");
const current = ctx.ui.getEditorText();
```
**Examples:**
- `ctx.ui.setStatus()`: [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts), [status-line.ts](../examples/extensions/status-line.ts)
- `ctx.ui.setWidget()`: [plan-mode.ts](../examples/extensions/plan-mode.ts)
- `ctx.ui.setFooter()`: [custom-footer.ts](../examples/extensions/custom-footer.ts)
- `ctx.ui.setHeader()`: [custom-header.ts](../examples/extensions/custom-header.ts)
### Custom Components
For complex UI, use `ctx.ui.custom()`. This temporarily replaces the editor with your component until `done()` is called:
@ -1069,7 +1156,9 @@ The callback receives:
- `theme` - Current theme for styling
- `done(value)` - Call to close component and return value
See [tui.md](tui.md) for the full component API and [examples/extensions/](../examples/extensions/) for working examples (snake.ts, todo.ts, qna.ts).
See [tui.md](tui.md) for the full component API.
**Examples:** [handoff.ts](../examples/extensions/handoff.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts), [qna.ts](../examples/extensions/qna.ts), [snake.ts](../examples/extensions/snake.ts), [todo.ts](../examples/extensions/todo.ts), [tools.ts](../examples/extensions/tools.ts)
### Message Rendering