@aliou/pi-dev-kit 0.6.4 → 0.7.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.
@@ -1,6 +1,6 @@
1
1
  # Commands
2
2
 
3
- Commands are user-invoked actions triggered with `/command-name` in the input editor.
3
+ Commands are user-invoked actions triggered with `/command-name`. Register them with `pi.registerCommand(name, options)`.
4
4
 
5
5
  ## Registration
6
6
 
@@ -8,17 +8,49 @@ Commands are user-invoked actions triggered with `/command-name` in the input ed
8
8
  pi.registerCommand("my-command", {
9
9
  description: "What this command does",
10
10
  handler: async (args, ctx) => {
11
- // args: string (everything after the command name)
12
- // ctx: ExtensionContext
11
+ // args is the raw text after the command name.
12
+ // ctx is ExtensionCommandContext.
13
+ },
14
+ });
15
+ ```
16
+
17
+ If several extensions register the same command, Pi keeps all of them and assigns invocation suffixes such as `/review:1` and `/review:2`.
18
+
19
+ ## Argument Completion
20
+
21
+ Use `getArgumentCompletions` for command-specific autocomplete.
22
+
23
+ ```typescript
24
+ import type { AutocompleteItem } from "@earendil-works/pi-tui";
25
+
26
+ pi.registerCommand("deploy", {
27
+ description: "Deploy to an environment",
28
+ getArgumentCompletions(prefix: string): AutocompleteItem[] | null {
29
+ const items = ["dev", "staging", "prod"].map((env) => ({ value: env, label: env }));
30
+ const filtered = items.filter((item) => item.value.startsWith(prefix));
31
+ return filtered.length > 0 ? filtered : null;
32
+ },
33
+ handler: async (args, ctx) => {
34
+ ctx.ui.notify(`Deploying ${args}`, "info");
13
35
  },
14
36
  });
15
37
  ```
16
38
 
17
39
  ## Command Context
18
40
 
19
- The `ctx` parameter provides the same `ExtensionContext` as hooks, with access to `ctx.ui`, `ctx.hasUI`, `ctx.cwd`, etc.
41
+ Command handlers receive `ExtensionCommandContext`, which includes normal `ExtensionContext` fields plus session-control methods.
42
+
43
+ Common fields and methods:
20
44
 
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.
45
+ - `ctx.ui`, `ctx.hasUI`, `ctx.cwd`, `ctx.model`, `ctx.modelRegistry`, `ctx.sessionManager`, `ctx.signal`.
46
+ - `ctx.waitForIdle()`.
47
+ - `ctx.newSession({ setup, withSession })`.
48
+ - `ctx.fork(entryId, { position, withSession })`.
49
+ - `ctx.switchSession(path, { withSession })`.
50
+ - `ctx.navigateTree(targetId, options)`.
51
+ - `ctx.reload()`.
52
+
53
+ Session replacement invalidates captured old session-bound objects. Put post-switch work in `withSession` and use only that callback context.
22
54
 
23
55
  ## Simple Command
24
56
 
@@ -32,9 +64,24 @@ pi.registerCommand("balance", {
32
64
  });
33
65
  ```
34
66
 
35
- ## Command with Rich Display
67
+ Fire-and-forget UI calls such as `notify`, `setStatus`, and `setEditorText` are safe without `ctx.hasUI` guards.
68
+
69
+ ## Parsing Arguments
70
+
71
+ `args` is a raw string. Parse it yourself.
72
+
73
+ ```typescript
74
+ handler: async (args, ctx) => {
75
+ const [subcommand, ...rest] = args.trim().split(/\s+/);
76
+ const value = rest.join(" ");
77
+ }
78
+ ```
79
+
80
+ For complex inputs, prefer a small parser over ad hoc indexing.
36
81
 
37
- When a command needs a rich TUI display, use the three-tier pattern from `references/modes.md`:
82
+ ## Rich TUI Display
83
+
84
+ Use the three-tier pattern when a command uses `ctx.ui.custom()`.
38
85
 
39
86
  ```typescript
40
87
  pi.registerCommand("quotas", {
@@ -42,19 +89,18 @@ pi.registerCommand("quotas", {
42
89
  handler: async (_args, ctx) => {
43
90
  const quotas = await fetchQuotas();
44
91
 
45
- // Print mode
92
+ // Print/JSON mode: no UI.
46
93
  if (!ctx.hasUI) {
47
94
  console.log(formatQuotasPlain(quotas));
48
95
  return;
49
96
  }
50
97
 
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) => {
98
+ // Interactive mode: rich component. Use explicit sentinel, not undefined.
99
+ const result = await ctx.ui.custom<"closed">((_tui, theme, _keybindings, done) => {
54
100
  return new QuotasDisplay(theme, quotas, () => done("closed"));
55
101
  });
56
102
 
57
- // RPC mode: custom() returns undefined by design.
103
+ // RPC fallback: custom() returns undefined by design.
58
104
  if (result === undefined) {
59
105
  ctx.ui.notify(formatQuotasPlain(quotas), "info");
60
106
  }
@@ -62,39 +108,73 @@ pi.registerCommand("quotas", {
62
108
  });
63
109
  ```
64
110
 
65
- Do not use `done(undefined)` in normal interactive close paths if you rely on `result === undefined` to detect RPC fallback.
111
+ Do not call `done(undefined)` for normal interactive close paths if `result === undefined` detects RPC fallback.
66
112
 
67
- ## Extracting Components
113
+ ## Commands That Trigger Agent Work
68
114
 
69
- Keep command handlers thin. Extract the TUI component into a separate file:
115
+ Use `pi.sendUserMessage()` when a command should queue a user message.
70
116
 
71
- ```
72
- src/
73
- commands/
74
- quotas.ts # Handler + formatQuotasPlain
75
- components/
76
- quotas-display.ts # QuotasDisplay component class
117
+ ```typescript
118
+ pi.registerCommand("review-last", {
119
+ description: "Ask Pi to review the last change",
120
+ handler: async (_args, _ctx) => {
121
+ pi.sendUserMessage("Review the last code change for correctness.");
122
+ },
123
+ });
77
124
  ```
78
125
 
79
- The component file should export the component class. The command file imports it and wires up the handler.
126
+ When calling during streaming, specify delivery mode:
127
+
128
+ ```typescript
129
+ pi.sendUserMessage("Focus on tests next.", { deliverAs: "steer" });
130
+ pi.sendUserMessage("Then summarize.", { deliverAs: "followUp" });
131
+ ```
80
132
 
81
- ## Arguments
133
+ ## Reload Command
82
134
 
83
- The `args` parameter is the raw string after the command name. Parse it yourself:
135
+ Treat reload as terminal.
84
136
 
85
137
  ```typescript
86
- handler: async (args, ctx) => {
87
- const parts = args.trim().split(/\s+/);
88
- const subcommand = parts[0];
89
- // ...
90
- },
138
+ pi.registerCommand("reload-runtime", {
139
+ description: "Reload extensions, skills, prompts, and themes",
140
+ handler: async (_args, ctx) => {
141
+ await ctx.reload();
142
+ return;
143
+ },
144
+ });
91
145
  ```
92
146
 
147
+ Do not perform post-reload work in the same handler; it is still running in the old call frame.
148
+
93
149
  ## Command vs Tool
94
150
 
95
151
  | Aspect | Command | Tool |
96
152
  |---|---|---|
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) |
153
+ | Invoked by | User or RPC `prompt` with `/name` | LLM during a turn |
154
+ | Purpose | User-facing action, setup, settings, display | Model capability |
155
+ | UI | User is usually present; still handle RPC/print | Must avoid surprising user prompts unless designed |
156
+ | Return | `Promise<void>` | `AgentToolResult` for the LLM |
157
+ | Session methods | Yes, command-only methods available | No session replacement methods |
158
+
159
+ If the LLM should use a capability autonomously, make it a tool. If the user intentionally invokes it, make it a command. Some features expose both: a command for setup/reload and a tool that queues that command as a follow-up.
160
+
161
+ ## Component Extraction
162
+
163
+ Keep handlers thin. Extract rich components near the command that uses them.
164
+
165
+ ```
166
+ src/commands/quotas.ts
167
+ src/commands/components/quotas-display.ts
168
+ ```
169
+
170
+ Shared components can live in `src/components/`, but do not list component files in `pi.extensions`.
171
+
172
+ ## Checklist
173
+
174
+ - [ ] Command has a clear description.
175
+ - [ ] Argument parsing handles empty input.
176
+ - [ ] `getArgumentCompletions` is used when arguments have known choices.
177
+ - [ ] Rich UI uses explicit sentinels and RPC/print fallback.
178
+ - [ ] Session replacement uses `withSession` for post-switch work.
179
+ - [ ] Reload command returns immediately after `ctx.reload()`.
180
+ - [ ] Long-running or cancellable work uses available abort signals.
@@ -1,166 +1,331 @@
1
1
  # Components
2
2
 
3
- TUI components render custom UI in the terminal. They are used in `ctx.ui.custom()`, `renderResult`, and other display contexts.
3
+ TUI components render custom UI in Pi. Use them in `ctx.ui.custom()`, tool renderers, message renderers, widgets, custom footers, and custom editors.
4
4
 
5
5
  ## Component Interface
6
6
 
7
7
  ```typescript
8
- import type { Component, Theme } from "@mariozechner/pi-tui";
8
+ import type { Component } from "@earendil-works/pi-tui";
9
9
 
10
10
  class MyComponent implements Component {
11
- render(maxWidth: number, maxHeight: number): string {
12
- return "Hello from my component";
11
+ render(width: number): string[] {
12
+ return ["Hello from my component"];
13
13
  }
14
14
 
15
- // Optional: handle keyboard input
16
- handleInput?(key: string): void;
15
+ handleInput?(data: string): void;
16
+
17
+ invalidate(): void {
18
+ // Clear cached render state.
19
+ }
17
20
  }
18
21
  ```
19
22
 
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.
23
+ Rules:
24
+
25
+ - `render(width)` returns one string per line.
26
+ - Each rendered line must fit within `width`.
27
+ - Use `truncateToWidth()` or `wrapTextWithAnsi()` for long lines.
28
+ - Implement `invalidate()` and clear cached themed output.
29
+ - Use `matchesKey()` for key handling.
21
30
 
22
- ## Available Components from pi-tui
31
+ If a component displays a text cursor or embeds `Input`/`Editor`, implement `Focusable` and propagate `focused` to the child input so IME candidate windows appear in the right place.
23
32
 
24
- Before creating custom components, check if an existing one fits your need:
33
+ ## Built-in Components
25
34
 
26
- | Component | Description |
35
+ Import common components from `@earendil-works/pi-tui`:
36
+
37
+ ```typescript
38
+ import {
39
+ Box,
40
+ Container,
41
+ Image,
42
+ Input,
43
+ Markdown,
44
+ SelectList,
45
+ SettingsList,
46
+ Spacer,
47
+ Text,
48
+ } from "@earendil-works/pi-tui";
49
+ ```
50
+
51
+ Use existing components before creating your own:
52
+
53
+ | Component | Use for |
27
54
  |---|---|
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`:
55
+ | `Text` | Wrapped multi-line text. |
56
+ | `Box` | Padded/background container. |
57
+ | `Container` | Vertical grouping of child components. |
58
+ | `Spacer` | Empty vertical space. |
59
+ | `Input` | Single-line text input. |
60
+ | `Editor` | Multi-line editor. |
61
+ | `SelectList` | Searchable/scrollable pickers. |
62
+ | `SettingsList` | Toggle and settings rows. |
63
+ | `Markdown` | Markdown with syntax highlighting. |
64
+ | `Image` | Inline images in supported terminals. |
65
+
66
+ Higher-level Pi components come from `@earendil-works/pi-coding-agent`:
43
67
 
44
68
  ```typescript
45
- import { Text, Box, Container, SelectList } from "@mariozechner/pi-tui";
69
+ import {
70
+ BorderedLoader,
71
+ CustomEditor,
72
+ DynamicBorder,
73
+ getMarkdownTheme,
74
+ getSettingsListTheme,
75
+ } from "@earendil-works/pi-coding-agent";
46
76
  ```
47
77
 
48
- ## Utility Components from pi-coding-agent
78
+ ## `ctx.ui.custom()`
49
79
 
50
- These are higher-level components for common extension patterns:
80
+ `custom()` temporarily replaces the editor with your component until `done(value)` is called.
51
81
 
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 |
82
+ ```typescript
83
+ const result = await ctx.ui.custom<string | null>((tui, theme, keybindings, done) => {
84
+ const list = new SelectList(items, Math.min(items.length, 10), {
85
+ selectedPrefix: (text) => theme.fg("accent", text),
86
+ selectedText: (text) => theme.fg("accent", text),
87
+ description: (text) => theme.fg("muted", text),
88
+ });
89
+
90
+ list.onSelect = (item) => done(item.value);
91
+ list.onCancel = () => done(null);
92
+
93
+ return {
94
+ render: (width) => list.render(width),
95
+ invalidate: () => list.invalidate(),
96
+ handleInput: (data) => {
97
+ list.handleInput?.(data);
98
+ tui.requestRender();
99
+ },
100
+ };
101
+ });
102
+ ```
103
+
104
+ Callback args:
57
105
 
58
- Import from `@mariozechner/pi-coding-agent`:
106
+ - `tui`: request renders and inspect terminal state.
107
+ - `theme`: current theme. Do not import a global theme.
108
+ - `keybindings`: current app keybindings.
109
+ - `done(value)`: close and resolve.
110
+
111
+ Use explicit non-`undefined` sentinels for close/cancel paths (`null`, `false`, `"closed"`). In RPC and print modes, `custom()` returns `undefined`, so `done(undefined)` makes fallback detection ambiguous.
112
+
113
+ ## Overlay Mode
114
+
115
+ Use overlays for modal or side-panel UI without clearing existing content.
59
116
 
60
117
  ```typescript
61
- import { DynamicBorder, BorderedLoader } from "@mariozechner/pi-coding-agent";
118
+ const result = await ctx.ui.custom<string | null>(
119
+ (_tui, _theme, _keybindings, done) => new MyOverlay({ onClose: done }),
120
+ {
121
+ overlay: true,
122
+ overlayOptions: {
123
+ anchor: "right-center",
124
+ width: "50%",
125
+ maxHeight: "80%",
126
+ margin: 2,
127
+ visible: (termWidth) => termWidth >= 80,
128
+ },
129
+ },
130
+ );
62
131
  ```
63
132
 
64
- ## Using ctx.ui.custom()
133
+ Overlay components are disposed when closed. Create a fresh instance each time you show one.
134
+
135
+ ## Keyboard Handling
65
136
 
66
- `custom()` displays a full-screen component and returns when the component calls `done()`.
137
+ Use `matchesKey()` and `Key` from `pi-tui`.
67
138
 
68
139
  ```typescript
69
- const result = await ctx.ui.custom<string>((tui, theme, kb, done) => {
70
- return new MyPickerComponent(theme, items, (selected) => done(selected));
71
- });
140
+ import { Key, matchesKey } from "@earendil-works/pi-tui";
141
+
142
+ handleInput(data: string): void {
143
+ if (matchesKey(data, Key.up)) this.moveUp();
144
+ if (matchesKey(data, Key.down)) this.moveDown();
145
+ if (matchesKey(data, Key.enter)) this.confirm();
146
+ if (matchesKey(data, Key.escape)) this.cancel();
147
+ }
72
148
  ```
73
149
 
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.
150
+ Common IDs include `Key.enter`, `Key.escape`, `Key.tab`, `Key.up`, `Key.down`, `Key.left`, `Key.right`, `Key.ctrl("c")`, and `Key.ctrlShift("p")`.
151
+
152
+ ## Line Width and ANSI
153
+
154
+ Rendered lines must not exceed the provided width.
155
+
156
+ ```typescript
157
+ import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
158
+
159
+ render(width: number): string[] {
160
+ return wrapTextWithAnsi(this.theme.fg("accent", this.text), width);
161
+ }
162
+ ```
79
163
 
80
- The generic type (`<string>` above) is the type of value passed to `done()`.
164
+ Use:
81
165
 
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`.
166
+ - `visibleWidth(str)` to measure display width without ANSI codes.
167
+ - `truncateToWidth(str, width, ellipsis?)` for single-line truncation.
168
+ - `wrapTextWithAnsi(str, width)` for wrapping styled text.
86
169
 
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.).
170
+ ## Theming
88
171
 
89
- See `references/modes.md` for the three-tier pattern.
172
+ Use the `theme` passed into render/custom callbacks.
90
173
 
91
- ## Theme Styling
174
+ ```typescript
175
+ theme.fg("accent", text);
176
+ theme.fg("muted", text);
177
+ theme.fg("success", text);
178
+ theme.fg("error", text);
179
+ theme.bg("toolPendingBg", text);
180
+ theme.bold(text);
181
+ ```
92
182
 
93
- All render functions receive a `theme` object for consistent styling:
183
+ For markdown in a tool or message renderer:
94
184
 
95
185
  ```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)
186
+ const markdown = new Markdown(content, 0, 0, getMarkdownTheme());
109
187
  ```
110
188
 
111
- ## `renderResult` is not a Component
189
+ If a component caches strings that already include theme escape codes, rebuild those strings in `invalidate()` so theme changes apply correctly.
112
190
 
113
- `renderResult` is a plain render function. It returns a string (or `undefined`) and is not an interactive `Component` class.
191
+ ## Common Patterns
192
+
193
+ ### Selection dialog
194
+
195
+ Use `SelectList` with `DynamicBorder`.
114
196
 
115
197
  ```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
- },
198
+ const result = await ctx.ui.custom<string | null>((tui, theme, _keybindings, done) => {
199
+ const container = new Container();
200
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
201
+ container.addChild(new Text(theme.fg("accent", theme.bold("Pick an option")), 1, 0));
202
+
203
+ const list = new SelectList(items, Math.min(items.length, 10), {
204
+ selectedPrefix: (t) => theme.fg("accent", t),
205
+ selectedText: (t) => theme.fg("accent", t),
206
+ description: (t) => theme.fg("muted", t),
207
+ scrollInfo: (t) => theme.fg("dim", t),
208
+ noMatch: (t) => theme.fg("warning", t),
209
+ });
210
+ list.onSelect = (item) => done(item.value);
211
+ list.onCancel = () => done(null);
212
+
213
+ container.addChild(list);
214
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
215
+
216
+ return {
217
+ render: (width) => container.render(width),
218
+ invalidate: () => container.invalidate(),
219
+ handleInput: (data) => {
220
+ list.handleInput?.(data);
221
+ tui.requestRender();
222
+ },
223
+ };
224
+ });
126
225
  ```
127
226
 
128
- For full `renderCall`/`renderResult` formatting rules, follow `references/tools.md` (Tool UI Rendering Guidelines + consistency contract).
227
+ ### Async operation with cancel
129
228
 
130
- ## Keyboard Handling in custom()
229
+ Use `BorderedLoader`.
131
230
 
132
- Interactive components handle keyboard input through `handleInput`:
231
+ ```typescript
232
+ const result = await ctx.ui.custom<string | null>((_tui, theme, _keybindings, done) => {
233
+ const loader = new BorderedLoader(_tui, theme, "Fetching data...");
234
+ loader.onAbort = () => done(null);
235
+
236
+ fetchData(loader.signal)
237
+ .then((data) => done(data))
238
+ .catch(() => done(null));
239
+
240
+ return loader;
241
+ });
242
+ ```
243
+
244
+ ### Settings list
245
+
246
+ Use `SettingsList` and `getSettingsListTheme()` for toggles. For full extension settings, prefer `registerSettingsCommand` from `@aliou/pi-utils-settings`.
247
+
248
+ ### Widgets and status
133
249
 
134
250
  ```typescript
135
- class MyComponent implements Component {
136
- private done: (value: string | null) => void;
251
+ ctx.ui.setStatus("my-extension", ctx.ui.theme.fg("accent", "active"));
252
+ ctx.ui.setStatus("my-extension", undefined);
137
253
 
138
- constructor(done: (value: string | null) => void) {
139
- this.done = done;
140
- }
254
+ ctx.ui.setWidget("my-extension", ["Line 1", "Line 2"]);
255
+ ctx.ui.setWidget("my-extension", ["Below editor"], { placement: "belowEditor" });
256
+ ctx.ui.setWidget("my-extension", undefined);
257
+ ```
141
258
 
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
259
+ String-array widgets also work in RPC mode. Component widgets are TUI-only.
260
+
261
+ ### Custom footer
262
+
263
+ ```typescript
264
+ ctx.ui.setFooter((tui, theme, footerData) => ({
265
+ invalidate() {},
266
+ render(width: number): string[] {
267
+ const branch = footerData.getGitBranch() ?? "no git";
268
+ return [theme.fg("dim", `${ctx.model?.id ?? "no model"} (${branch})`)];
269
+ },
270
+ dispose: footerData.onBranchChange(() => tui.requestRender()),
271
+ }));
272
+
273
+ ctx.ui.setFooter(undefined);
274
+ ```
275
+
276
+ ### Custom editor
277
+
278
+ Extend `CustomEditor`, not the base editor, so app keybindings still work.
279
+
280
+ ```typescript
281
+ class VimEditor extends CustomEditor {
282
+ private mode: "normal" | "insert" = "insert";
283
+
284
+ handleInput(data: string): void {
285
+ if (matchesKey(data, Key.escape) && this.mode === "insert") {
286
+ this.mode = "normal";
287
+ return;
148
288
  }
289
+ super.handleInput(data);
149
290
  }
291
+ }
292
+
293
+ pi.on("session_start", (_event, ctx) => {
294
+ ctx.ui.setEditorComponent((tui, theme, keybindings) => new VimEditor(tui, theme, keybindings));
295
+ });
296
+ ```
150
297
 
151
- render(maxWidth: number, maxHeight: number): string {
152
- return "Press Enter to confirm, Esc to cancel";
298
+ Capture `ctx.ui.getEditorComponent()` before replacing the editor if you need to compose with another extension, then explicitly delegate to the previous component in your wrapper.
299
+
300
+ ## Rendering Tools and Messages
301
+
302
+ `renderCall`, `renderResult`, and message renderers return `Component | undefined`, not raw strings.
303
+
304
+ ```typescript
305
+ renderResult(result, options, theme) {
306
+ if (options.isPartial) {
307
+ return new Text(theme.fg("muted", "My Tool: loading..."), 0, 0);
153
308
  }
309
+ return new Text(theme.fg("success", "Done"), 0, 0);
154
310
  }
155
311
  ```
156
312
 
157
- ## Code Highlighting
313
+ Return `undefined` only when fallback rendering is better than custom output.
158
314
 
159
- For displaying code in renderers:
315
+ ## Mode Awareness
160
316
 
161
- ```typescript
162
- import { highlightCode, getLanguageFromPath } from "@mariozechner/pi-coding-agent";
317
+ - Interactive mode supports all TUI APIs.
318
+ - RPC mode supports dialogs and fire-and-forget string events, but `custom()` returns `undefined`.
319
+ - JSON/print modes have no UI.
163
320
 
164
- const lang = getLanguageFromPath("/path/to/file.ts"); // "typescript"
165
- const highlighted = highlightCode(code, lang, theme);
166
- ```
321
+ Read `references/modes.md` before using `ctx.ui.custom()` in commands or hooks.
322
+
323
+ ## Checklist
324
+
325
+ - [ ] Existing components checked before custom component work.
326
+ - [ ] `render(width)` returns `string[]` and respects width.
327
+ - [ ] `invalidate()` clears cached themed output.
328
+ - [ ] Key handling uses `matchesKey()`.
329
+ - [ ] `ctx.ui.custom()` uses explicit sentinels and has RPC/print fallback.
330
+ - [ ] TUI-only methods are not treated as working in RPC.
331
+ - [ ] Tool/message renderers return components.