@aliou/pi-dev-kit 0.4.9 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -0
- package/package.json +59 -2
- package/src/commands/index.ts +6 -0
- package/src/commands/update.ts +144 -0
- package/src/index.ts +8 -0
- package/src/prompts/setup-demo.md +35 -0
- package/src/skills/demo-setup/SKILL.md +217 -0
- package/src/skills/pi-extension/SKILL.md +140 -0
- package/src/skills/pi-extension/references/additional-apis.md +264 -0
- package/src/skills/pi-extension/references/commands.md +100 -0
- package/src/skills/pi-extension/references/components.md +166 -0
- package/src/skills/pi-extension/references/documentation.md +54 -0
- package/src/skills/pi-extension/references/hooks.md +244 -0
- package/src/skills/pi-extension/references/messages.md +169 -0
- package/src/skills/pi-extension/references/modes.md +156 -0
- package/src/skills/pi-extension/references/providers.md +134 -0
- package/src/skills/pi-extension/references/publish.md +139 -0
- package/src/skills/pi-extension/references/state.md +56 -0
- package/src/skills/pi-extension/references/structure.md +408 -0
- package/src/skills/pi-extension/references/testing.md +54 -0
- package/src/skills/pi-extension/references/tools.md +430 -0
- package/src/tools/changelog-tool.ts +596 -0
- package/src/tools/docs-tool.ts +240 -0
- package/src/tools/index.ts +12 -0
- package/src/tools/package-manager-tool.ts +223 -0
- package/src/tools/utils.ts +62 -0
- package/src/tools/version-tool.ts +77 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Documentation
|
|
2
|
+
|
|
3
|
+
Every published extension should have a README that explains what it does, how to set it up, and what it provides.
|
|
4
|
+
|
|
5
|
+
## README Template
|
|
6
|
+
|
|
7
|
+
```markdown
|
|
8
|
+
# pi-my-extension
|
|
9
|
+
|
|
10
|
+
Brief description of the extension.
|
|
11
|
+
|
|
12
|
+
## Setup
|
|
13
|
+
|
|
14
|
+
\`\`\`bash
|
|
15
|
+
pi install @scope/pi-my-extension
|
|
16
|
+
\`\`\`
|
|
17
|
+
|
|
18
|
+
### Environment Variables
|
|
19
|
+
|
|
20
|
+
| Variable | Required | Description |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| `MY_API_KEY` | Yes | API key from [provider](https://...) |
|
|
23
|
+
|
|
24
|
+
## Tools
|
|
25
|
+
|
|
26
|
+
| Tool | Description |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `my_tool` | What it does |
|
|
29
|
+
|
|
30
|
+
## Commands
|
|
31
|
+
|
|
32
|
+
| Command | Description |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `/my-command` | What it does |
|
|
35
|
+
|
|
36
|
+
## Providers
|
|
37
|
+
|
|
38
|
+
| Provider | Models |
|
|
39
|
+
|---|---|
|
|
40
|
+
| `my-provider` | model-a, model-b |
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## What to Document
|
|
44
|
+
|
|
45
|
+
- **Installation**: `pi install` command.
|
|
46
|
+
- **Environment variables**: Every required and optional env var, with links to where to get them.
|
|
47
|
+
- **Tools**: Name and description of each registered tool. Include example usage if non-obvious.
|
|
48
|
+
- **Commands**: Name and description of each registered command.
|
|
49
|
+
- **Providers**: Provider name and available models (if the extension registers a provider).
|
|
50
|
+
- **Limitations**: Known limitations, unsupported modes, or missing features.
|
|
51
|
+
|
|
52
|
+
## Changelog
|
|
53
|
+
|
|
54
|
+
If using changesets, the CHANGELOG.md is generated automatically. Each changeset entry should describe what changed from the user's perspective, not implementation details.
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# Hooks (Events)
|
|
2
|
+
|
|
3
|
+
Hooks let extensions react to lifecycle events. They are registered with `pi.on(eventName, handler)`.
|
|
4
|
+
|
|
5
|
+
## Event Reference
|
|
6
|
+
|
|
7
|
+
### Session Events
|
|
8
|
+
|
|
9
|
+
| Event | When | Can Cancel | Payload |
|
|
10
|
+
|---|---|---|---|
|
|
11
|
+
| `session_start` | New session created | No | `{}` |
|
|
12
|
+
| `session_switch` | Switched to different session | No | `{ reason: "new" \| "switch" \| "fork" }` |
|
|
13
|
+
| `session_before_switch` | Before switching sessions | Yes (`{ cancel: true }`) | `{ reason: "new" \| "switch" \| "fork" }` |
|
|
14
|
+
| `session_before_fork` | Before forking a session | Yes (`{ cancel: true }`) | `{}` |
|
|
15
|
+
| `session_fork` | After session was forked | No | `{}` |
|
|
16
|
+
| `session_shutdown` | Pi is shutting down | No | `{}` |
|
|
17
|
+
| `session_before_compact` | Before compaction | Yes (return custom summary string) | `{ summary: string }` |
|
|
18
|
+
|
|
19
|
+
### Agent Events
|
|
20
|
+
|
|
21
|
+
| Event | When | Payload |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| `before_agent_start` | Before agent turn starts | `{}` |
|
|
24
|
+
| `agent_start` | Agent turn started | `{}` |
|
|
25
|
+
| `turn_start` | Turn begins processing | `{}` |
|
|
26
|
+
| `turn_end` | Turn finishes processing | `{}` |
|
|
27
|
+
| `model_select` | User changed the model | `{ model: string }` |
|
|
28
|
+
|
|
29
|
+
### Tool Events
|
|
30
|
+
|
|
31
|
+
| Event | When | Can Block | Payload |
|
|
32
|
+
|---|---|---|---|
|
|
33
|
+
| `tool_call` | Before a tool executes | Yes (`{ block: true, reason }`) | `{ toolName, toolCallId, input }` |
|
|
34
|
+
|
|
35
|
+
### Input Events
|
|
36
|
+
|
|
37
|
+
| Event | When | Can Transform | Payload |
|
|
38
|
+
|---|---|---|---|
|
|
39
|
+
| `input` | User submitted a message | Yes (return transformed text) | `{ text: string }` |
|
|
40
|
+
|
|
41
|
+
### Bash Events
|
|
42
|
+
|
|
43
|
+
| Event | When | Can Modify | Payload |
|
|
44
|
+
|---|---|---|---|
|
|
45
|
+
| `user_bash` | Before bash command runs | Yes (return modified command/cwd/env) | `{ command, cwd }` |
|
|
46
|
+
|
|
47
|
+
## Handler Signature
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
pi.on("event_name", async (event, ctx) => {
|
|
51
|
+
// event: event-specific payload
|
|
52
|
+
// ctx: ExtensionContext (hasUI, ui methods, cwd, model, etc.)
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The handler receives the event payload and an `ExtensionContext`. The context provides access to UI methods, the current working directory, model info, and more.
|
|
57
|
+
|
|
58
|
+
## Blocking and Cancelling
|
|
59
|
+
|
|
60
|
+
Some events let you prevent the default behavior by returning an object.
|
|
61
|
+
|
|
62
|
+
### Blocking Tool Calls
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
66
|
+
if (event.toolName === "bash" && event.input.command.includes("rm -rf /")) {
|
|
67
|
+
// Check ctx.hasUI before prompting -- see references/modes.md
|
|
68
|
+
if (!ctx.hasUI) {
|
|
69
|
+
return { block: true, reason: "Blocked: dangerous command (no UI to confirm)" };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const confirmed = await ctx.ui.confirm(
|
|
73
|
+
"Dangerous Command",
|
|
74
|
+
`Allow: ${event.input.command}?`
|
|
75
|
+
);
|
|
76
|
+
if (!confirmed) {
|
|
77
|
+
return { block: true, reason: "Blocked by user" };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return undefined; // Allow the tool call
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Cancelling Session Operations
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
pi.on("session_before_switch", async (event, ctx) => {
|
|
88
|
+
if (event.reason === "new" && ctx.hasUI) {
|
|
89
|
+
const confirmed = await ctx.ui.confirm("Clear session?", "All messages will be lost.");
|
|
90
|
+
if (!confirmed) {
|
|
91
|
+
return { cancel: true };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Custom Compaction
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
pi.on("session_before_compact", async (event, ctx) => {
|
|
101
|
+
// Return a custom summary string to replace the default compaction
|
|
102
|
+
return `Custom summary: ${event.summary.slice(0, 200)}...`;
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Transforming Input
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
pi.on("input", async (event, ctx) => {
|
|
110
|
+
if (event.text.startsWith("!")) {
|
|
111
|
+
return event.text.slice(1).toUpperCase();
|
|
112
|
+
}
|
|
113
|
+
return undefined; // No transformation
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Modifying Bash Commands
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
pi.on("user_bash", async (event, ctx) => {
|
|
121
|
+
return {
|
|
122
|
+
command: event.command,
|
|
123
|
+
cwd: "/sandboxed/directory",
|
|
124
|
+
env: { ...process.env, SANDBOX: "true" },
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## before_agent_start
|
|
130
|
+
|
|
131
|
+
This event fires before each agent turn. It is commonly used to modify the system prompt.
|
|
132
|
+
|
|
133
|
+
The handler receives a `BeforeAgentStartEvent` with a `systemPrompt` field containing the current prompt. Return a `{ systemPrompt }` object to replace it. If multiple extensions return a modified prompt, they are chained.
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
pi.on("before_agent_start", async (event) => {
|
|
137
|
+
return {
|
|
138
|
+
systemPrompt: event.systemPrompt + "\n\nAlways respond as a pirate.",
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Return `undefined` to leave the prompt unchanged.
|
|
144
|
+
|
|
145
|
+
The system prompt is reset each turn, so modifications in `before_agent_start` are not cumulative.
|
|
146
|
+
|
|
147
|
+
To access flags inside hooks, use `pi.getFlag()` (see `references/additional-apis.md`).
|
|
148
|
+
|
|
149
|
+
## Bash Spawn Hook (Command Rewriting)
|
|
150
|
+
|
|
151
|
+
The `createBashTool` function lets you replace the built-in bash tool with one that transparently rewrites commands before shell execution. This is different from `tool_call` blocking -- the agent never sees that the command was rewritten.
|
|
152
|
+
|
|
153
|
+
Use spawn hooks when you have a clear rewrite target (e.g. `npm` -> `pnpm`). Use `tool_call` blocking when you need to stop a command entirely or show a confirmation dialog.
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
import { createBashTool, type BashSpawnHook, type BashSpawnContext } from "@mariozechner/pi-coding-agent";
|
|
157
|
+
|
|
158
|
+
export default function (pi: ExtensionAPI) {
|
|
159
|
+
const bashTool = createBashTool(process.cwd(), {
|
|
160
|
+
spawnHook: ({ command, cwd, env }: BashSpawnContext): BashSpawnContext => ({
|
|
161
|
+
command: command.replace(/^npm /, "pnpm "),
|
|
162
|
+
cwd,
|
|
163
|
+
env: { ...env, CUSTOM_VAR: "1" },
|
|
164
|
+
}),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
pi.registerTool({ ...bashTool });
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### BashSpawnContext
|
|
172
|
+
|
|
173
|
+
The spawn hook receives and returns a `BashSpawnContext`:
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
interface BashSpawnContext {
|
|
177
|
+
command: string; // The shell command to execute
|
|
178
|
+
cwd: string; // Working directory
|
|
179
|
+
env: NodeJS.ProcessEnv; // Environment variables
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
You can modify any of these fields. The hook runs after pi's own processing but before the shell spawns.
|
|
184
|
+
|
|
185
|
+
### Key Points
|
|
186
|
+
|
|
187
|
+
- **Replaces the built-in bash tool.** When you call `pi.registerTool()` with a tool named `"bash"`, it replaces the default. Only one extension should do this.
|
|
188
|
+
- **Transparent to the agent.** The agent sees the original command in the tool call UI but gets the output of the rewritten command.
|
|
189
|
+
- **Execution order with tool_call hooks.** `tool_call` event hooks (blockers) run first. If a blocker returns `{ block: true }`, the spawn hook never fires. This means you can combine blocking hooks for commands that should be stopped entirely with spawn hooks for commands that should be rewritten.
|
|
190
|
+
- **Prefer AST-based rewrites over regex.** A false positive rewrite corrupts a command silently. Use `@aliou/sh` or similar shell parsers to identify command names in the AST, then do surgical string replacement at the identified positions. If the parse fails, return the command unchanged.
|
|
191
|
+
- **Compose multiple rewriters.** Chain rewriter functions that each transform the context:
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
const rewriters: ((ctx: BashSpawnContext) => BashSpawnContext)[] = [
|
|
195
|
+
createPackageManagerRewriter(config),
|
|
196
|
+
createGitRebaseRewriter(),
|
|
197
|
+
];
|
|
198
|
+
|
|
199
|
+
const spawnHook = (ctx: BashSpawnContext) => {
|
|
200
|
+
let result = ctx;
|
|
201
|
+
for (const rewrite of rewriters) {
|
|
202
|
+
result = rewrite(result);
|
|
203
|
+
}
|
|
204
|
+
return result;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const bashTool = createBashTool(process.cwd(), { spawnHook });
|
|
208
|
+
pi.registerTool({ ...bashTool });
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### When to Use What
|
|
212
|
+
|
|
213
|
+
| Pattern | Use When | Agent Sees |
|
|
214
|
+
|---|---|---|
|
|
215
|
+
| `tool_call` hook + `{ block: true }` | Command must be stopped entirely | Block reason (retries with correct command) |
|
|
216
|
+
| `tool_call` hook + `ctx.ui.confirm()` | User confirmation needed | Block reason if denied |
|
|
217
|
+
| Spawn hook (command rewrite) | Clear 1:1 rewrite target exists | Output of rewritten command (transparent) |
|
|
218
|
+
| Spawn hook (env injection) | Need to set env vars for specific commands | Output with injected env (transparent) |
|
|
219
|
+
|
|
220
|
+
## Multiple Handlers
|
|
221
|
+
|
|
222
|
+
Multiple extensions can register handlers for the same event. They execute in registration order. For blocking events (`tool_call`, `session_before_switch`, etc.), the first handler to return a blocking/cancelling result wins.
|
|
223
|
+
|
|
224
|
+
## Mode Awareness in Hooks
|
|
225
|
+
|
|
226
|
+
Always consider what happens in Print mode when your hook uses dialog methods. See `references/modes.md` for the full behavior matrix.
|
|
227
|
+
|
|
228
|
+
Common pattern for `tool_call` handlers:
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
232
|
+
if (shouldBlock(event)) {
|
|
233
|
+
if (!ctx.hasUI) {
|
|
234
|
+
return { block: true, reason: "No UI to confirm" };
|
|
235
|
+
}
|
|
236
|
+
// Safe to use dialogs here
|
|
237
|
+
const choice = await ctx.ui.select("Allow?", ["Yes", "No"]);
|
|
238
|
+
if (choice !== "Yes") {
|
|
239
|
+
return { block: true, reason: "Blocked by user" };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return undefined;
|
|
243
|
+
});
|
|
244
|
+
```
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# Messages
|
|
2
|
+
|
|
3
|
+
Pi provides several ways to display information to the user. Choose based on the UX goal.
|
|
4
|
+
|
|
5
|
+
## When to Use What
|
|
6
|
+
|
|
7
|
+
| Method | Persistence | Interactivity | Use When |
|
|
8
|
+
|---|---|---|---|
|
|
9
|
+
| `ctx.ui.notify()` | Transient (fades) | None | Quick feedback: "Saved", "API key missing" |
|
|
10
|
+
| `ctx.ui.custom()` | Until dismissed | Full keyboard | Rich interactive display: pickers, dashboards |
|
|
11
|
+
| `pi.sendMessage()` | In session history | Via renderer | Persistent results that should survive compaction |
|
|
12
|
+
| `pi.appendEntry()` | In session history | Via renderer | State tracking entries (see `references/state.md`) |
|
|
13
|
+
|
|
14
|
+
## sendMessage
|
|
15
|
+
|
|
16
|
+
Sends a message into the session conversation. It appears as an assistant message and is persisted in session history.
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
pi.sendMessage({
|
|
20
|
+
customType: "balance-result", // Identifier for the message renderer
|
|
21
|
+
content: "Balance: $42.50", // Plain text content (LLM sees this)
|
|
22
|
+
display: true, // Show in TUI
|
|
23
|
+
details: { balance: 42.50 }, // Rich data for custom rendering
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
| Field | Type | Description |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| `customType` | `string` | Identifies the message type. Paired with `registerMessageRenderer`. |
|
|
30
|
+
| `content` | `string` | Plain text content. This is what the LLM sees if the message is in context. |
|
|
31
|
+
| `display` | `boolean` | Whether to show the message in the TUI. |
|
|
32
|
+
| `details` | `object` | Arbitrary data passed to the message renderer. |
|
|
33
|
+
|
|
34
|
+
## registerMessageRenderer
|
|
35
|
+
|
|
36
|
+
Registers a custom renderer for messages with a specific `customType`:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
pi.registerMessageRenderer("balance-result", (message, theme) => {
|
|
40
|
+
const { balance } = message.details;
|
|
41
|
+
return [
|
|
42
|
+
theme.bold("Account Balance"),
|
|
43
|
+
"",
|
|
44
|
+
theme.fg("success", ` $${balance.toFixed(2)}`),
|
|
45
|
+
].join("\n");
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The renderer receives the full message object and the theme. It returns a string for display in the TUI.
|
|
50
|
+
|
|
51
|
+
If no renderer is registered for a `customType`, the message's `content` field is displayed as plain text.
|
|
52
|
+
|
|
53
|
+
## Custom Message Design Guide (breadcrumbs-style)
|
|
54
|
+
|
|
55
|
+
`breadcrumbs` is a good reference for custom entries/messages (`../pi-extensions/extensions/breadcrumbs/lib/session-link.ts`, plus `commands/handoff.ts` and `commands/spawn.ts`).
|
|
56
|
+
|
|
57
|
+
### 1) Prefer paired entries for links/handovers
|
|
58
|
+
|
|
59
|
+
For cross-session workflows, use two custom message types:
|
|
60
|
+
- **Marker** in source session: short line (`Handed off to X` / `Continues in X`).
|
|
61
|
+
- **Source** in new session: header + optional expanded context.
|
|
62
|
+
|
|
63
|
+
This gives both directions of navigation and keeps history readable.
|
|
64
|
+
|
|
65
|
+
### 2) Collapsed vs expanded behavior
|
|
66
|
+
|
|
67
|
+
Keep collapsed view minimal and scannable:
|
|
68
|
+
- one semantic line
|
|
69
|
+
- optional hint (`Press Ctrl+O to expand`) only when extra content exists
|
|
70
|
+
|
|
71
|
+
Use expanded view for rich content:
|
|
72
|
+
- markdown body
|
|
73
|
+
- multi-line context
|
|
74
|
+
- file lists / instructions
|
|
75
|
+
|
|
76
|
+
### 3) Renderer resilience
|
|
77
|
+
|
|
78
|
+
Message renderers should degrade safely:
|
|
79
|
+
- missing `details` => fallback to plain `content`
|
|
80
|
+
- markdown render failure => fallback to plain text
|
|
81
|
+
- unknown fields => ignore, don't throw
|
|
82
|
+
|
|
83
|
+
### 4) Keep details small and durable
|
|
84
|
+
|
|
85
|
+
`details` should contain stable identifiers and routing data, not large blobs:
|
|
86
|
+
- session IDs
|
|
87
|
+
- link type (`handoff`, `continue`)
|
|
88
|
+
- short metadata (goal/title)
|
|
89
|
+
|
|
90
|
+
Put large human-readable content in `content` (for expansion/LLM visibility), not deep nested `details`.
|
|
91
|
+
|
|
92
|
+
### 5) Use visual hierarchy consistently
|
|
93
|
+
|
|
94
|
+
For message UIs:
|
|
95
|
+
- muted label + accent target/value (`Continues in <session-name>`)
|
|
96
|
+
- subtle container background for custom message blocks
|
|
97
|
+
- avoid decorative noise; optimize for fast scan in session history
|
|
98
|
+
|
|
99
|
+
## Pattern: Command with sendMessage Fallback
|
|
100
|
+
|
|
101
|
+
This combines with the three-tier pattern from `references/modes.md`. Use `sendMessage` as the RPC fallback for commands that use `custom()`:
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
// Register the renderer once at load time
|
|
105
|
+
pi.registerMessageRenderer("my-results", (message, theme) => {
|
|
106
|
+
const { items } = message.details;
|
|
107
|
+
return [
|
|
108
|
+
theme.bold(`Results (${items.length})`),
|
|
109
|
+
...items.map((item: string) => ` ${theme.fg("accent", item)}`),
|
|
110
|
+
].join("\n");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
pi.registerCommand("results", {
|
|
114
|
+
description: "Show results",
|
|
115
|
+
handler: async (_args, ctx) => {
|
|
116
|
+
const items = await fetchItems();
|
|
117
|
+
|
|
118
|
+
if (!ctx.hasUI) {
|
|
119
|
+
console.log(items.join("\n"));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const result = await ctx.ui.custom<"closed">((tui, theme, _kb, done) => {
|
|
124
|
+
return new ResultsDisplay(theme, items, () => done("closed"));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// RPC fallback only: custom() returns undefined in RPC/Print.
|
|
128
|
+
if (result === undefined) {
|
|
129
|
+
pi.sendMessage({
|
|
130
|
+
customType: "my-results",
|
|
131
|
+
content: items.join("\n"),
|
|
132
|
+
display: true,
|
|
133
|
+
details: { items },
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## notify
|
|
141
|
+
|
|
142
|
+
For transient feedback that does not need to persist:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
ctx.ui.notify("Operation complete", "info");
|
|
146
|
+
ctx.ui.notify("Something went wrong", "error");
|
|
147
|
+
ctx.ui.notify("Proceed with caution", "warning");
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The second argument is the notification type: `"info"`, `"error"`, or `"warning"`. It affects the color/icon.
|
|
151
|
+
|
|
152
|
+
`notify` is fire-and-forget. It works in Interactive and RPC modes, and is a no-op in Print mode.
|
|
153
|
+
|
|
154
|
+
## Writing custom entries in a new session
|
|
155
|
+
|
|
156
|
+
When using `ctx.newSession({ setup })`, write custom entries directly through the setup `SessionManager`:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
await ctx.newSession({
|
|
160
|
+
setup: async (sm) => {
|
|
161
|
+
sm.appendCustomMessageEntry("my-source-type", "Context text", true, {
|
|
162
|
+
parentSessionId: "...",
|
|
163
|
+
linkType: "handoff",
|
|
164
|
+
});
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Use this pattern for handoff/spawn-like workflows where the new session must start with structured context.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Mode Awareness
|
|
2
|
+
|
|
3
|
+
Pi runs in different modes. Extensions must handle all of them gracefully.
|
|
4
|
+
|
|
5
|
+
## Modes
|
|
6
|
+
|
|
7
|
+
| Mode | `ctx.hasUI` | Description |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| **Interactive** | `true` | Full TUI. Normal terminal usage. |
|
|
10
|
+
| **RPC** (`--mode rpc`) | `true` | JSON protocol. A host application handles UI. Dialogs work via request/response. |
|
|
11
|
+
| **Print** (`-p`, `--mode json`) | `false` | No UI. Extensions run but cannot prompt the user. |
|
|
12
|
+
|
|
13
|
+
Important nuance: in RPC mode, `ctx.hasUI` is `true`, but TUI-only APIs are degraded.
|
|
14
|
+
|
|
15
|
+
## Method Behavior by Mode
|
|
16
|
+
|
|
17
|
+
### Dialog Methods (return a value)
|
|
18
|
+
|
|
19
|
+
These methods prompt the user and return a result. Their behavior varies by mode.
|
|
20
|
+
|
|
21
|
+
| Method | Interactive | RPC | Print |
|
|
22
|
+
|---|---|---|---|
|
|
23
|
+
| `ctx.ui.select()` | TUI picker | JSON request to host | Returns `undefined` |
|
|
24
|
+
| `ctx.ui.confirm()` | TUI dialog | JSON request to host | Returns `false` |
|
|
25
|
+
| `ctx.ui.input()` | TUI text input | JSON request to host | Returns `undefined` |
|
|
26
|
+
| `ctx.ui.editor()` | TUI editor | JSON request to host | Returns `undefined` |
|
|
27
|
+
| `ctx.ui.custom()` | TUI component | Returns `undefined` | Returns `undefined` |
|
|
28
|
+
|
|
29
|
+
Key observation: `custom()` returns `undefined` in both RPC and Print modes. All other dialog methods work in RPC (the host presents them to the user).
|
|
30
|
+
|
|
31
|
+
Second key observation: `custom()` can also resolve to `undefined` in Interactive mode if your component calls `done(undefined)`. So `result === undefined` is not a reliable mode detector by itself.
|
|
32
|
+
|
|
33
|
+
### Fire-and-Forget Methods (no return value)
|
|
34
|
+
|
|
35
|
+
These methods are safe to call unconditionally in any mode. In modes that do not support them, they are silently ignored.
|
|
36
|
+
|
|
37
|
+
| Method | Interactive | RPC | Print |
|
|
38
|
+
|---|---|---|---|
|
|
39
|
+
| `ctx.ui.notify()` | TUI notification | JSON event to host | No-op |
|
|
40
|
+
| `ctx.ui.setStatus()` | Status bar | JSON event to host | No-op |
|
|
41
|
+
| `ctx.ui.setWidget()` | Widget area | JSON event to host (string arrays only) | No-op |
|
|
42
|
+
| `ctx.ui.setTitle()` | Window title | JSON event to host | No-op |
|
|
43
|
+
| `ctx.ui.setEditorText()` | Sets editor content | JSON event to host | No-op |
|
|
44
|
+
| `ctx.ui.setFooter()` | Footer area | No-op | No-op |
|
|
45
|
+
| `ctx.ui.setHeader()` | Header area | No-op | No-op |
|
|
46
|
+
| `ctx.ui.setWorkingMessage()` | Loader text | No-op | No-op |
|
|
47
|
+
| `ctx.ui.setEditorComponent()` | Custom editor | No-op | No-op |
|
|
48
|
+
|
|
49
|
+
You never need to check `ctx.hasUI` before calling fire-and-forget methods.
|
|
50
|
+
|
|
51
|
+
## When to Check ctx.hasUI
|
|
52
|
+
|
|
53
|
+
Check `ctx.hasUI` when a dialog method gates behavior. If the dialog result determines what happens next (for example, blocking a tool call or cancelling a session switch), you must handle the case where the dialog cannot run.
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
// tool_call handler: must decide to block or allow
|
|
57
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
58
|
+
if (isDangerous(event)) {
|
|
59
|
+
if (!ctx.hasUI) {
|
|
60
|
+
// Print mode: no way to ask the user, block by default
|
|
61
|
+
return { block: true, reason: "Dangerous command blocked (no UI)" };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const choice = await ctx.ui.select("Dangerous command detected", ["Allow", "Block"]);
|
|
65
|
+
if (choice !== "Allow") {
|
|
66
|
+
return { block: true, reason: "Blocked by user" };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
You do not need to check `ctx.hasUI` for:
|
|
74
|
+
- Fire-and-forget calls (`notify`, `setStatus`, `setWidget`, etc.).
|
|
75
|
+
- Dialogs where the default return value is acceptable (for example, a non-critical confirm that defaults to `false`).
|
|
76
|
+
|
|
77
|
+
## The Three-Tier Pattern for Custom Components
|
|
78
|
+
|
|
79
|
+
When a command uses `ctx.ui.custom()` for a rich TUI display, handle three tiers:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
pi.registerCommand("quotas", {
|
|
83
|
+
description: "Show API quotas",
|
|
84
|
+
handler: async (_args, ctx) => {
|
|
85
|
+
const data = await fetchQuotas();
|
|
86
|
+
|
|
87
|
+
// Tier 1: Print mode -- no UI at all
|
|
88
|
+
if (!ctx.hasUI) {
|
|
89
|
+
console.log(formatPlain(data));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Tier 2: Interactive mode -- full TUI component.
|
|
94
|
+
// Use an explicit non-undefined sentinel for close/cancel.
|
|
95
|
+
const result = await ctx.ui.custom<"closed">((tui, theme, _kb, done) => {
|
|
96
|
+
return new QuotasDisplay(theme, data, () => done("closed"));
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Tier 3: RPC mode -- custom() returns undefined by design.
|
|
100
|
+
if (result === undefined) {
|
|
101
|
+
ctx.ui.notify(formatPlain(data), "info");
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Since `select`, `confirm`, `input`, and `notify` all work in RPC mode (forwarded to the host via JSON protocol), use them as the RPC fallback. Choose based on UX:
|
|
108
|
+
|
|
109
|
+
- **`notify`**: Transient feedback or displaying data. Best for most display-only commands.
|
|
110
|
+
- **`select`**: When the custom component is a picker/selector. The RPC host presents a list.
|
|
111
|
+
- **`confirm`**: When the custom component is a confirmation dialog (for example, permission gate).
|
|
112
|
+
- **Notify "requires interactive mode"**: When the custom component is too complex to reduce (for example, settings editor, process manager).
|
|
113
|
+
|
|
114
|
+
Use `sendMessage` + `registerMessageRenderer` only when the result must persist in session history. See `references/messages.md`.
|
|
115
|
+
|
|
116
|
+
### Example: Selector Fallback
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
const result = await ctx.ui.custom<string | null>((_tui, _theme, _kb, done) => {
|
|
120
|
+
return new FancyPicker(items, done); // done(value) or done(null)
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// RPC fallback: use select dialog
|
|
124
|
+
if (result === undefined) {
|
|
125
|
+
const selected = await ctx.ui.select("Pick an item", items.map((i) => i.label));
|
|
126
|
+
// ... handle selected
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Example: Confirmation Fallback
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
// In a tool_call handler:
|
|
134
|
+
if (!ctx.hasUI) {
|
|
135
|
+
return { block: true, reason: "No UI to confirm" };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const proceed = await ctx.ui.custom<boolean>((_tui, theme, _kb, done) => {
|
|
139
|
+
return new ConfirmDialog(theme, message, done); // done(true|false)
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// RPC fallback: custom() returns undefined, so treat as "not approved".
|
|
143
|
+
if (proceed !== true) {
|
|
144
|
+
return { block: true, reason: "Blocked" };
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Guidelines
|
|
149
|
+
|
|
150
|
+
1. Never assume Interactive mode. Always consider what happens in RPC and Print.
|
|
151
|
+
2. Fire-and-forget methods are always safe. Use them freely.
|
|
152
|
+
3. Guard dialog methods that gate behavior with `ctx.hasUI` checks.
|
|
153
|
+
4. Always provide a fallback for `ctx.ui.custom()` because it returns `undefined` in RPC and Print.
|
|
154
|
+
5. Do not use `done(undefined)` for normal interactive close paths if you need to detect RPC fallback.
|
|
155
|
+
6. For `tool_call` handlers, decide a safe default when there is no UI (usually block).
|
|
156
|
+
7. Test your extension in at least Interactive and Print modes. If you use `custom()`, test RPC fallback explicitly.
|