@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.
@@ -0,0 +1,140 @@
1
+ ---
2
+ name: pi-extension
3
+ description: Create, update, and publish Pi extensions. Use when working on extensions in this repository.
4
+ ---
5
+
6
+ # Pi Extension Development
7
+
8
+ Guide for creating and maintaining Pi extensions. Read the relevant reference files before implementing.
9
+
10
+ ## Key Imports
11
+
12
+ ```typescript
13
+ // Core types
14
+ import type { ExtensionAPI, ExtensionContext, ToolDefinition, ProviderDefinition } from "@mariozechner/pi-coding-agent";
15
+
16
+ // Schema (TypeBox)
17
+ import { Type } from "@mariozechner/pi-coding-agent";
18
+
19
+ // TUI components
20
+ import type { Component, Theme } from "@mariozechner/pi-tui";
21
+ import { Text, Box, Container, Markdown, SelectList } from "@mariozechner/pi-tui";
22
+
23
+ // Tool UI components (from @aliou/pi-utils-ui)
24
+ import { ToolCallHeader, ToolBody, ToolFooter } from "@aliou/pi-utils-ui";
25
+
26
+ // Rendering utilities
27
+ import { getMarkdownTheme, keyHint } from "@mariozechner/pi-coding-agent";
28
+
29
+ // General utilities
30
+ import { truncateHead, highlightCode, getLanguageFromPath, DynamicBorder, BorderedLoader } from "@mariozechner/pi-coding-agent";
31
+ ```
32
+
33
+ ## Workflow
34
+
35
+ ### Creating a New Extension
36
+
37
+ 1. Read `references/structure.md` for the project layout and package.json template.
38
+ 2. Create the entry point (`src/index.ts`) with a default export function.
39
+ 3. Decide what the extension provides:
40
+ - **Tools** (LLM-callable): Read `references/tools.md`.
41
+ - **Commands** (user-invoked): Read `references/commands.md`.
42
+ - **Providers** (LLM backends): Read `references/providers.md`.
43
+ - **Hooks** (event handlers): Read `references/hooks.md`. Includes both `tool_call` blocking hooks and spawn hooks for transparent command rewriting via `createBashTool`.
44
+ 4. Read `references/modes.md` for mode-awareness guidelines. Every extension must handle Interactive, RPC, and Print modes.
45
+ 5. If the extension displays rich UI: Read `references/components.md` for TUI components and `references/messages.md` for message display patterns.
46
+ 6. If the extension tracks state: Read `references/state.md`.
47
+ 7. For less common APIs: Read `references/additional-apis.md`.
48
+ 8. If the extension adds a tool that competes with a natural bash fallback (process managers, CI watchers, search tools): inject system prompt guidance. Read the **Guidance Injection Pattern** section in `references/additional-apis.md`.
49
+ 9. Before publishing: Read `references/publish.md` and `references/documentation.md`.
50
+
51
+ ### Modifying an Existing Extension
52
+
53
+ 1. Read the extension's `index.ts` to understand its structure.
54
+ 2. Read the relevant reference file for the area you are modifying.
55
+ 3. Check `references/modes.md` if adding any UI interaction.
56
+ 4. Run type checking after changes.
57
+
58
+ ## Reference Files
59
+
60
+ | File | Content |
61
+ |---|---|
62
+ | `references/structure.md` | Project layout, package.json, tsconfig, biome.json, config.ts, entry point patterns (including acceptable exceptions), API key pattern, imports |
63
+ | `references/tools.md` | Tool registration, execute signature, parameters, streaming, rendering, naming, renderCall/renderResult UI guidelines |
64
+ | `references/hooks.md` | Events, blocking/cancelling, input transformation, system prompt modification, bash spawn hooks (command rewriting) |
65
+ | `references/commands.md` | Command registration, three-tier pattern, component extraction |
66
+ | `references/components.md` | TUI components (pi-tui + pi-coding-agent), custom(), theme styling, keyboard handling |
67
+ | `references/providers.md` | Provider registration, model definition, compat field, API key gating |
68
+ | `references/modes.md` | Mode behavior matrix, ctx.hasUI, dialog vs fire-and-forget, three-tier pattern |
69
+ | `references/messages.md` | sendMessage, registerMessageRenderer, notify, when to use each |
70
+ | `references/state.md` | appendEntry, state reconstruction, appendEntry vs sendMessage |
71
+ | `references/additional-apis.md` | Shortcuts, flags, exec, sendUserMessage, session name, labels, model control, EventBus, theme, UI customization, system prompt guidance injection |
72
+ | `references/publish.md` | npm publishing, changesets (manual file format + CI automation), GitHub Actions publish workflow, first-time setup, NPM_TOKEN, pre-publish checklist |
73
+ | `references/testing.md` | Local development, type checking, manual testing, debugging |
74
+ | `references/documentation.md` | README template, what to document, changelog |
75
+
76
+ ## Reference Extensions
77
+
78
+ When implementing, look at these existing extensions for patterns:
79
+
80
+ **Standalone repos (recommended structure):**
81
+ - `pi-linkup` (`/Users/alioudiallo/code/src/github.com/aliou/pi-linkup/`): Tools wrapping a third-party API. Has tools, a command, custom message rendering, API key gating, system prompt guidance injection (`src/guidance.ts` + `src/hooks/system-prompt.ts`).
82
+ - `pi-synthetic` (`/Users/alioudiallo/code/src/github.com/aliou/pi-synthetic/`): Provider + tools. Has a provider with models, a command with `custom()` component, API key gating, async entry point.
83
+ - `pi-processes` (`/Users/alioudiallo/code/src/github.com/aliou/pi-processes/`): Multi-action tool with system prompt guidance injection (`src/guidance.ts` + `src/hooks/system-prompt.ts`), config toggle, background blocker hook.
84
+
85
+ **Monorepo extensions (simpler structure):**
86
+ - `extensions/defaults/` in this repo: Simple tool registration (get_current_time).
87
+ - `extensions/guardrails/` in this repo: Event hooks (tool_call blocking). Has `hooks/`, `commands/`, `components/`, `utils/` directories with config types in `config.ts`.
88
+ - `extensions/toolchain/` in this repo: Bash spawn hooks (command rewriting via `createBashTool`) combined with tool_call blockers. Has `blockers/`, `rewriters/`, `commands/`, `utils/` directories.
89
+ - `extensions/processes/` in this repo: Multi-action tool with StringEnum parameters.
90
+
91
+ ## Critical Rules
92
+
93
+ 1. **Execute parameter order**: `(toolCallId, params, signal, onUpdate, ctx)`. Signal before onUpdate.
94
+ 2. **Always use `onUpdate?.()`**: Optional chaining. The parameter can be `undefined`.
95
+ 3. **No `.js` in imports**: Use bare module paths (`./tools/my-tool`, not `./tools/my-tool.js`).
96
+ 4. **Mode awareness**: Every `ctx.ui.custom()` call needs an RPC fallback (use `select`/`confirm`/`notify` -- they work in RPC). Do not use `done(undefined)` for normal interactive close paths when you detect fallback with `result === undefined`; use explicit sentinels (`null`, `"closed"`, boolean). Every `tool_call` hook with dialogs needs a `ctx.hasUI` check.
97
+ 5. **API key gating**: Check before registering tools that require the key. Providers handle missing keys internally via their `models()` function.
98
+ 6. **Tool naming**: Prefix with API name for third-party integrations (`linkup_web_search`). No prefix for internal tools (`get_current_time`).
99
+ 7. **Tool call header pattern**: Keep `renderCall` consistent: first line `[Tool Name]: [Action] [Main arg] [Option args]`, extra lines for long args. Use display names, not raw tool IDs.
100
+ 8. **Deterministic call rendering**: Build `renderCall` with a stable extraction order (action → main arg → option args → long args), process-style. Same input should produce same header layout.
101
+ 9. **Long args placement**: Put long prompt/task/question/context strings on following lines. Keep first line scannable.
102
+ 10. **Result layout consistency**: In `renderResult`, handle `isPartial` first, start final output with a clear state summary, use `expanded` for compact-vs-full detail, and keep one blank line before any footer.
103
+ 11. **peerDependencies**: Any package Pi already ships (`@mariozechner/pi-coding-agent`, `@mariozechner/pi-tui`, `@mariozechner/pi-ai`) must be listed in `peerDependencies` with `optional: true` in `peerDependenciesMeta` if imported at runtime. Without `optional: true`, npm 7+ auto-installs peers, adding hundreds of packages on every install even though Pi already provides them. Keep them in `devDependencies` too for local type checking — `pnpm install` installs peers, so development is unaffected. Use `>=CURRENT_VERSION` range, not `*`.
104
+ 12. **Check existing components**: Before creating a new TUI component, check if `pi-tui` or `pi-coding-agent` already exports one that fits.
105
+ 13. **Forward abort signals**: Always pass `signal` through to `fetch()`, `pi.exec()`, and API client methods. A tool that ignores its signal prevents cancellation from reaching the underlying operation. Never prefix with `_signal` unless the tool truly has no async work to cancel.
106
+ 14. **Never use Node child_process APIs**: Do not use `child_process.exec`, `execSync`, `spawn`, `spawnSync`, `execFile`, or `execFileSync` to run binaries or shell scripts. Always use `pi.exec()`. `pi.exec` handles CWD, signal propagation, and output capture consistently. The only exception is if you need a long-lived streaming process with stdin/stdout piping that `pi.exec` cannot support — document the reason in code comments.
107
+ 15. **Never use `homedir()` for pi paths**: Use the SDK helpers from `@mariozechner/pi-coding-agent` instead. They respect the `PI_CODING_AGENT_DIR` env var which is used for testing and custom setups. Key functions: `getAgentDir()`, `getSettingsPath()`, `getSessionsDir()`, `getPromptsDir()`, `getToolsDir()`, `getCustomThemesDir()`, `getModelsPath()`, `getAuthPath()`, `getBinDir()`, `getDebugLogPath()`. All exported from the main package entry point.
108
+ 16. **Config uses the interface pattern**: `config.ts` defines two TypeScript interfaces (`RawConfig` with all fields optional, `ResolvedConfig` with all fields required) and a `ConfigLoader<Raw, Resolved>` instance. Do not use TypeBox schemas for config types.
109
+ 17. **Entry point deviations must be documented**: The standard entry point pattern is load config → check `enabled` → register. Deviations (no config, API-key-first ordering, no `enabled` toggle) are acceptable when justified, but must be noted in `AGENTS.md`.
110
+
111
+ ## Checklist
112
+
113
+ Before considering an extension complete:
114
+
115
+ - [ ] Entry point has correct default export signature.
116
+ - [ ] All tools have correct execute parameter order.
117
+ - [ ] All `onUpdate` calls use optional chaining.
118
+ - [ ] No `.js` file extensions in imports.
119
+ - [ ] `renderCall` uses a consistent first-line pattern (tool, action if any, main arg, options).
120
+ - [ ] `renderCall` arg extraction is deterministic (action → main arg → option args → long args).
121
+ - [ ] Long call arguments are moved to follow-up lines, not crammed into first line.
122
+ - [ ] `renderResult` handles `isPartial`, starts with a clear state summary, and uses `expanded` for compact-vs-full detail.
123
+ - [ ] If result includes a footer, there is a blank line above it.
124
+ - [ ] `renderResult` handles errors from thrown exceptions: checks for missing expected fields in `details` (framework passes `{}`, not `undefined`), extracts error message from `content`.
125
+ - [ ] `ctx.ui.custom()` calls have RPC fallback, and interactive close/cancel paths do not rely on `done(undefined)` when fallback detection uses `result === undefined`.
126
+ - [ ] `tool_call` hooks check `ctx.hasUI` before dialog methods.
127
+ - [ ] Fire-and-forget methods (notify, setStatus, etc.) are used without hasUI guards.
128
+ - [ ] If using custom message renderers: collapsed view is scannable, expanded view adds depth, and renderer has plain-text fallback when `details` is missing.
129
+ - [ ] `signal` is forwarded to all async operations (fetch, `pi.exec`, API clients). No unused `_signal`.
130
+ - [ ] Missing API keys produce a notification, not a crash.
131
+ - [ ] If in a monorepo: package doesn't depend on private workspace packages (run `pnpm run check:public-deps` if available).
132
+ - [ ] `pnpm typecheck` passes.
133
+ - [ ] No `child_process` imports -- uses `pi.exec()` for shell commands.
134
+ - [ ] No `homedir()` calls for pi paths -- uses SDK helpers (`getAgentDir()`, etc.).
135
+ - [ ] README documents tools, commands, env vars.
136
+ - [ ] `@mariozechner/pi-tui` (and any other Pi-provided package) is in `peerDependencies` with `optional: true` if imported at runtime, not just `devDependencies`.
137
+ - [ ] `prepare` script is `"[ -d .git ] && husky || true"`, not bare `"husky"`.
138
+ - [ ] `config.ts` uses `ConfigLoader<Raw, Resolved>` with TypeScript interfaces, not TypeBox schemas.
139
+ - [ ] If deviating from the standard entry point pattern (load-config → check-enabled → register), the reason is documented in `AGENTS.md`.
140
+ - [ ] If the extension adds a tool that competes with a bash fallback: guidance is injected via `src/guidance.ts` + `src/hooks/system-prompt.ts`, with a `systemPromptGuidance` config toggle defaulting to `true`.
@@ -0,0 +1,264 @@
1
+ # Additional APIs
2
+
3
+ These APIs are available on `ExtensionAPI` and `ExtensionContext` but are less commonly used. Each is shown with a minimal example.
4
+
5
+ When you implement something using one of these APIs, update this skill reference with a fuller example based on your actual usage.
6
+
7
+ ## Shortcuts
8
+
9
+ Register global keyboard shortcuts:
10
+
11
+ ```typescript
12
+ pi.registerShortcut("ctrl+shift+p", {
13
+ description: "Toggle plan mode",
14
+ handler: async (ctx) => {
15
+ planModeEnabled = !planModeEnabled;
16
+ ctx.ui.setStatus("plan", planModeEnabled ? "Plan Mode" : "");
17
+ },
18
+ });
19
+ ```
20
+
21
+ Shortcuts work only in Interactive mode.
22
+
23
+ ## Flags
24
+
25
+ Register boolean flags that persist across sessions:
26
+
27
+ ```typescript
28
+ // Register
29
+ pi.registerFlag("auto-commit", {
30
+ description: "Auto-commit after each turn",
31
+ default: false,
32
+ });
33
+
34
+ // Read (in any handler)
35
+ const autoCommit = pi.getFlag("auto-commit");
36
+ ```
37
+
38
+ Users toggle flags with `/flag auto-commit` in the input editor.
39
+
40
+ ## sendUserMessage
41
+
42
+ Inject a user message into the conversation programmatically:
43
+
44
+ ```typescript
45
+ pi.sendUserMessage("Please summarize what we just discussed");
46
+ ```
47
+
48
+ This triggers a full agent turn as if the user typed the message. Useful for file watchers, timers, or other automated triggers.
49
+
50
+ ## Session Name
51
+
52
+ Set or get a name for the current session (shown in the session selector):
53
+
54
+ ```typescript
55
+ pi.setSessionName("Feature: Auth Refactor");
56
+ const name = pi.getSessionName();
57
+ ```
58
+
59
+ ## Labels
60
+
61
+ Set a label on a specific session entry (shown in `/tree` view):
62
+
63
+ ```typescript
64
+ pi.setLabel(entryId, "checkpoint: before refactor");
65
+ ```
66
+
67
+ ## exec
68
+
69
+ Run a shell command and get the result. This is the **only** way to run external binaries or shell scripts from an extension.
70
+
71
+ ```typescript
72
+ const result = await pi.exec("git status --porcelain", { cwd: process.cwd() });
73
+ // result: { stdout, stderr, exitCode }
74
+ ```
75
+
76
+ Useful for git operations, environment checks, running CLI tools, etc.
77
+
78
+ **Do not use Node `child_process` APIs** (`exec`, `execSync`, `spawn`, `spawnSync`, `execFile`, `execFileSync`). `pi.exec` handles CWD resolution, output capture, and integrates with the extension lifecycle. Using `child_process` directly bypasses these guarantees and creates inconsistent behavior across environments.
79
+
80
+ The only exception is a long-lived streaming process that requires direct stdin/stdout piping — document the reason in code comments if this applies.
81
+
82
+ ## Active Tools
83
+
84
+ Get or set which tools are currently active:
85
+
86
+ ```typescript
87
+ const tools = pi.getActiveTools(); // string[]
88
+ pi.setActiveTools(["bash", "read", "write", "my_custom_tool"]);
89
+ ```
90
+
91
+ Setting active tools restricts which tools the LLM can use.
92
+
93
+ ## Model Control
94
+
95
+ ```typescript
96
+ // Set the active model
97
+ pi.setModel("anthropic/claude-sonnet-4-20250514");
98
+
99
+ // Get/set thinking level
100
+ const level = pi.getThinkingLevel(); // "none" | "low" | "medium" | "high"
101
+ pi.setThinkingLevel("high");
102
+ ```
103
+
104
+ ## System Prompt
105
+
106
+ Read or modify the system prompt (typically in `before_agent_start`):
107
+
108
+ ```typescript
109
+ pi.on("before_agent_start", async (_event, ctx) => {
110
+ const prompt = ctx.getSystemPrompt();
111
+ ctx.setSystemPrompt(prompt + "\n\nExtra instructions.");
112
+ });
113
+ ```
114
+
115
+ The system prompt resets each turn, so modifications are not cumulative.
116
+
117
+ ### Guidance Injection Pattern
118
+
119
+ Extensions that add tools or behavioral patterns the agent may not know how to use correctly should inject guidance into the system prompt. Without it, agents fall back to bash workarounds even when a better tool is available.
120
+
121
+ **When to inject guidance:**
122
+ - Your extension adds a tool that competes with a natural bash fallback (e.g. a process manager, a CI watcher, a search tool)
123
+ - Correct usage depends on subtle conditions (alert flags, when-not-to-use, alert vs. poll)
124
+ - You have observed agents ignoring the tool or reaching for `bash` instead
125
+
126
+ **When not to:**
127
+ - The tool description alone is self-explanatory
128
+ - The tool has no plausible bash alternative
129
+
130
+ **Structure: three files**
131
+
132
+ `src/guidance.ts` — the guidance text as a named export:
133
+
134
+ ```typescript
135
+ export const MY_EXTENSION_GUIDANCE = `
136
+ ## My Extension
137
+
138
+ Use the \`my_tool\` tool for X. Don't use bash for X.
139
+
140
+ **Use \`my_tool\` when:**
141
+ - Situation A
142
+ - Situation B
143
+
144
+ **Use \`bash\` when:**
145
+ - You need the result immediately to proceed (quick commands that finish in seconds)
146
+
147
+ **Never do this:**
148
+ \`\`\`bash
149
+ workaround_command # loses observability
150
+ \`\`\`
151
+
152
+ **Do this instead:**
153
+ \`\`\`
154
+ my_tool({ action: "start", ... })
155
+ \`\`\`
156
+ `;
157
+ ```
158
+
159
+ `src/hooks/system-prompt.ts` — the hook:
160
+
161
+ ```typescript
162
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
163
+ import { configLoader } from "../config";
164
+ import { MY_EXTENSION_GUIDANCE } from "../guidance";
165
+
166
+ export function registerGuidance(pi: ExtensionAPI): void {
167
+ pi.on("before_agent_start", async (event) => {
168
+ const config = configLoader.getConfig();
169
+ if (!config.systemPromptGuidance) return;
170
+
171
+ return {
172
+ systemPrompt: `${event.systemPrompt}\n${MY_EXTENSION_GUIDANCE}`,
173
+ };
174
+ });
175
+ }
176
+ ```
177
+
178
+ `src/config.ts` — add the toggle (default `true`):
179
+
180
+ ```typescript
181
+ export interface MyExtensionConfig {
182
+ // ...
183
+ /** Inject tool guidance into the system prompt each turn. Default: true. */
184
+ systemPromptGuidance?: boolean;
185
+ }
186
+ ```
187
+
188
+ Call `registerGuidance(pi)` from your hooks setup function.
189
+
190
+ **What makes guidance effective:**
191
+
192
+ - Lead with the decision rule: **when to use AND when not to use**. The when-not-to-use is as important — it gives the agent permission to keep using `bash` for quick tasks and prevents overcorrection.
193
+ - Name anti-patterns explicitly by their exact form (`cmd &`, `nohup`, `sleep 30 &&`). Abstract descriptions ("don't use bash workarounds") are ignored.
194
+ - Use 2–3 tight code examples. More than that dilutes attention; fewer leave the pattern underspecified.
195
+ - Keep the guidance section header (`## My Extension`) so it reads as a named capability, not a restriction.
196
+ - Avoid stacking emphasis markers (`NEVER`, `ALWAYS`, `IMPORTANT`). One or two land; more are ignored.
197
+
198
+ **Reference implementations:** `pi-linkup` (`src/hooks/system-prompt.ts`, `src/guidance.ts`) and `pi-processes` (`src/hooks/system-prompt.ts`, `src/guidance.ts`).
199
+
200
+ ## Compaction
201
+
202
+ Trigger compaction programmatically:
203
+
204
+ ```typescript
205
+ await pi.compact();
206
+ ```
207
+
208
+ ## Shutdown
209
+
210
+ Shut down pi gracefully:
211
+
212
+ ```typescript
213
+ pi.shutdown();
214
+ ```
215
+
216
+ ## EventBus
217
+
218
+ Inter-extension communication via a shared event bus:
219
+
220
+ ```typescript
221
+ // Extension A: emit
222
+ pi.events.emit("my-extension:data-ready", { items: [...] });
223
+
224
+ // Extension B: listen
225
+ pi.events.on("my-extension:data-ready", (data) => {
226
+ console.log("Received:", data.items.length, "items");
227
+ });
228
+ ```
229
+
230
+ Namespace event names with your extension name to avoid collisions. The event bus is supplementary -- most extensions do not need it. Use it when two extensions need to coordinate.
231
+
232
+ ## Theme Control
233
+
234
+ ```typescript
235
+ // Get current and available themes
236
+ const current = ctx.ui.getTheme();
237
+ const all = ctx.ui.getAllThemes();
238
+
239
+ // Set theme
240
+ const result = ctx.ui.setTheme("catppuccin-mocha");
241
+ // result: { success: boolean, error?: string }
242
+ ```
243
+
244
+ ## UI Customization
245
+
246
+ ```typescript
247
+ // Replace the footer
248
+ ctx.ui.setFooter((maxWidth, theme) => {
249
+ return theme.fg("muted", "Custom footer content");
250
+ });
251
+
252
+ // Replace the startup header
253
+ ctx.ui.setHeader((maxWidth, theme) => {
254
+ return theme.fg("accent", "My Custom Header");
255
+ });
256
+
257
+ // Set the editor component
258
+ ctx.ui.setEditorComponent((tui, theme, kb) => {
259
+ return new CustomEditor(tui, theme, kb);
260
+ });
261
+
262
+ // Prefill the editor
263
+ ctx.ui.setEditorText("Prefilled content");
264
+ ```
@@ -0,0 +1,100 @@
1
+ # Commands
2
+
3
+ Commands are user-invoked actions triggered with `/command-name` in the input editor.
4
+
5
+ ## Registration
6
+
7
+ ```typescript
8
+ pi.registerCommand("my-command", {
9
+ description: "What this command does",
10
+ handler: async (args, ctx) => {
11
+ // args: string (everything after the command name)
12
+ // ctx: ExtensionContext
13
+ },
14
+ });
15
+ ```
16
+
17
+ ## Command Context
18
+
19
+ The `ctx` parameter provides the same `ExtensionContext` as hooks, with access to `ctx.ui`, `ctx.hasUI`, `ctx.cwd`, etc.
20
+
21
+ Commands are interactive by nature (the user typed them), so `ctx.hasUI` is usually `true`. However, commands can also be invoked programmatically (for example via RPC), so the three-tier pattern still applies.
22
+
23
+ ## Simple Command
24
+
25
+ ```typescript
26
+ pi.registerCommand("balance", {
27
+ description: "Check API balance",
28
+ handler: async (_args, ctx) => {
29
+ const balance = await fetchBalance();
30
+ ctx.ui.notify(`Balance: $${balance.toFixed(2)}`, "info");
31
+ },
32
+ });
33
+ ```
34
+
35
+ ## Command with Rich Display
36
+
37
+ When a command needs a rich TUI display, use the three-tier pattern from `references/modes.md`:
38
+
39
+ ```typescript
40
+ pi.registerCommand("quotas", {
41
+ description: "Show API quotas",
42
+ handler: async (_args, ctx) => {
43
+ const quotas = await fetchQuotas();
44
+
45
+ // Print mode
46
+ if (!ctx.hasUI) {
47
+ console.log(formatQuotasPlain(quotas));
48
+ return;
49
+ }
50
+
51
+ // Interactive mode: full TUI component.
52
+ // Use explicit sentinel value for close/cancel, not undefined.
53
+ const result = await ctx.ui.custom<"closed">((tui, theme, _kb, done) => {
54
+ return new QuotasDisplay(theme, quotas, () => done("closed"));
55
+ });
56
+
57
+ // RPC mode: custom() returns undefined by design.
58
+ if (result === undefined) {
59
+ ctx.ui.notify(formatQuotasPlain(quotas), "info");
60
+ }
61
+ },
62
+ });
63
+ ```
64
+
65
+ Do not use `done(undefined)` in normal interactive close paths if you rely on `result === undefined` to detect RPC fallback.
66
+
67
+ ## Extracting Components
68
+
69
+ Keep command handlers thin. Extract the TUI component into a separate file:
70
+
71
+ ```
72
+ src/
73
+ commands/
74
+ quotas.ts # Handler + formatQuotasPlain
75
+ components/
76
+ quotas-display.ts # QuotasDisplay component class
77
+ ```
78
+
79
+ The component file should export the component class. The command file imports it and wires up the handler.
80
+
81
+ ## Arguments
82
+
83
+ The `args` parameter is the raw string after the command name. Parse it yourself:
84
+
85
+ ```typescript
86
+ handler: async (args, ctx) => {
87
+ const parts = args.trim().split(/\s+/);
88
+ const subcommand = parts[0];
89
+ // ...
90
+ },
91
+ ```
92
+
93
+ ## Command vs Tool
94
+
95
+ | Aspect | Command | Tool |
96
+ |---|---|---|
97
+ | Invoked by | User (typing `/name`) | LLM (during a turn) |
98
+ | Purpose | User-facing actions, settings, displays | LLM capabilities |
99
+ | UI access | Full (user is present) | Limited (LLM is driving) |
100
+ | Return value | void | `AgentToolResult` (output for LLM) |
@@ -0,0 +1,166 @@
1
+ # Components
2
+
3
+ TUI components render custom UI in the terminal. They are used in `ctx.ui.custom()`, `renderResult`, and other display contexts.
4
+
5
+ ## Component Interface
6
+
7
+ ```typescript
8
+ import type { Component, Theme } from "@mariozechner/pi-tui";
9
+
10
+ class MyComponent implements Component {
11
+ render(maxWidth: number, maxHeight: number): string {
12
+ return "Hello from my component";
13
+ }
14
+
15
+ // Optional: handle keyboard input
16
+ handleInput?(key: string): void;
17
+ }
18
+ ```
19
+
20
+ `render` is called whenever the TUI needs to repaint. Return a string (can include ANSI codes via theme helpers). `maxWidth` and `maxHeight` are the available terminal dimensions.
21
+
22
+ ## Available Components from pi-tui
23
+
24
+ Before creating custom components, check if an existing one fits your need:
25
+
26
+ | Component | Description |
27
+ |---|---|
28
+ | `Text` | Styled text with wrapping |
29
+ | `Box` | Container with borders and padding |
30
+ | `Container` | Vertical/horizontal layout |
31
+ | `Spacer` | Empty space |
32
+ | `Input` | Text input field |
33
+ | `Editor` | Multi-line text editor |
34
+ | `SelectList` | Scrollable selection list |
35
+ | `SettingsList` | Key-value settings display |
36
+ | `Loader` | Loading spinner |
37
+ | `CancellableLoader` | Loader with cancel support |
38
+ | `Markdown` | Markdown rendering |
39
+ | `Image` | Image rendering (kitty/sixel protocol) |
40
+ | `TruncatedText` | Text with line limit and expand/collapse |
41
+
42
+ Import from `@mariozechner/pi-tui`:
43
+
44
+ ```typescript
45
+ import { Text, Box, Container, SelectList } from "@mariozechner/pi-tui";
46
+ ```
47
+
48
+ ## Utility Components from pi-coding-agent
49
+
50
+ These are higher-level components for common extension patterns:
51
+
52
+ | Component | Description |
53
+ |---|---|
54
+ | `DynamicBorder` | Border that adjusts to content width |
55
+ | `BorderedLoader` | Loader inside a bordered box with optional cancel |
56
+ | `ToolExecutionComponent` | Standard tool execution display |
57
+
58
+ Import from `@mariozechner/pi-coding-agent`:
59
+
60
+ ```typescript
61
+ import { DynamicBorder, BorderedLoader } from "@mariozechner/pi-coding-agent";
62
+ ```
63
+
64
+ ## Using ctx.ui.custom()
65
+
66
+ `custom()` displays a full-screen component and returns when the component calls `done()`.
67
+
68
+ ```typescript
69
+ const result = await ctx.ui.custom<string>((tui, theme, kb, done) => {
70
+ return new MyPickerComponent(theme, items, (selected) => done(selected));
71
+ });
72
+ ```
73
+
74
+ Parameters passed to the factory:
75
+ - `tui`: The TUI instance (rarely needed directly).
76
+ - `theme`: Current theme for styling.
77
+ - `kb`: Keybinding configuration.
78
+ - `done(value)`: Call to close the component and return the value.
79
+
80
+ The generic type (`<string>` above) is the type of value passed to `done()`.
81
+
82
+ Mode behavior:
83
+ - Interactive: returns the value passed to `done(value)`.
84
+ - RPC: returns `undefined` (by design; no local TUI).
85
+ - Print: returns `undefined`.
86
+
87
+ Important: `undefined` is ambiguous. In Interactive mode you can also produce it yourself by calling `done(undefined)`. If you need to detect RPC fallback, use explicit non-undefined sentinels for interactive close paths (`null`, `"closed"`, `false`, etc.).
88
+
89
+ See `references/modes.md` for the three-tier pattern.
90
+
91
+ ## Theme Styling
92
+
93
+ All render functions receive a `theme` object for consistent styling:
94
+
95
+ ```typescript
96
+ // Foreground colors
97
+ theme.fg("toolTitle", text) // Tool names
98
+ theme.fg("accent", text) // Highlights
99
+ theme.fg("success", text) // Green
100
+ theme.fg("error", text) // Red
101
+ theme.fg("warning", text) // Yellow
102
+ theme.fg("muted", text) // Secondary text
103
+ theme.fg("dim", text) // Tertiary text
104
+
105
+ // Text styles
106
+ theme.bold(text)
107
+ theme.italic(text)
108
+ theme.strikethrough(text)
109
+ ```
110
+
111
+ ## `renderResult` is not a Component
112
+
113
+ `renderResult` is a plain render function. It returns a string (or `undefined`) and is not an interactive `Component` class.
114
+
115
+ ```typescript
116
+ renderResult(result, { expanded, isPartial }, theme) {
117
+ if (isPartial) return theme.fg("muted", "Loading...");
118
+
119
+ const items = result.details?.items ?? [];
120
+ const visible = expanded ? items : items.slice(0, 5);
121
+ return [
122
+ theme.fg("success", `Found ${items.length} results`),
123
+ ...visible.map((item: string) => ` ${theme.fg("accent", item)}`),
124
+ ].join("\n");
125
+ },
126
+ ```
127
+
128
+ For full `renderCall`/`renderResult` formatting rules, follow `references/tools.md` (Tool UI Rendering Guidelines + consistency contract).
129
+
130
+ ## Keyboard Handling in custom()
131
+
132
+ Interactive components handle keyboard input through `handleInput`:
133
+
134
+ ```typescript
135
+ class MyComponent implements Component {
136
+ private done: (value: string | null) => void;
137
+
138
+ constructor(done: (value: string | null) => void) {
139
+ this.done = done;
140
+ }
141
+
142
+ handleInput(key: string) {
143
+ if (key === "escape" || key === "q") {
144
+ this.done(null); // Cancel (explicit sentinel)
145
+ }
146
+ if (key === "return") {
147
+ this.done("selected"); // Confirm
148
+ }
149
+ }
150
+
151
+ render(maxWidth: number, maxHeight: number): string {
152
+ return "Press Enter to confirm, Esc to cancel";
153
+ }
154
+ }
155
+ ```
156
+
157
+ ## Code Highlighting
158
+
159
+ For displaying code in renderers:
160
+
161
+ ```typescript
162
+ import { highlightCode, getLanguageFromPath } from "@mariozechner/pi-coding-agent";
163
+
164
+ const lang = getLanguageFromPath("/path/to/file.ts"); // "typescript"
165
+ const highlighted = highlightCode(code, lang, theme);
166
+ ```