@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.
- package/README.md +2 -2
- package/package.json +15 -10
- package/src/commands/index.ts +5 -1
- package/src/commands/update.ts +139 -91
- package/src/skills/pi-extension/SKILL.md +108 -131
- package/src/skills/pi-extension/references/additional-apis.md +256 -173
- package/src/skills/pi-extension/references/commands.md +113 -33
- package/src/skills/pi-extension/references/components.md +267 -102
- package/src/skills/pi-extension/references/hooks.md +229 -156
- package/src/skills/pi-extension/references/messages.md +97 -92
- package/src/skills/pi-extension/references/modes.md +80 -90
- package/src/skills/pi-extension/references/providers.md +255 -96
- package/src/skills/pi-extension/references/publish.md +76 -62
- package/src/skills/pi-extension/references/state.md +80 -33
- package/src/skills/pi-extension/references/structure.md +156 -245
- package/src/skills/pi-extension/references/testing.md +1 -1
- package/src/skills/pi-extension/references/tools.md +212 -816
- package/src/tools/changelog-tool.ts +237 -230
- package/src/tools/docs-tool.ts +127 -130
- package/src/tools/index.ts +5 -1
- package/src/tools/package-manager-tool.ts +152 -147
- package/src/tools/utils.ts +33 -23
- package/src/tools/version-tool.ts +51 -51
- package/src/index.ts +0 -8
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Commands
|
|
2
2
|
|
|
3
|
-
Commands are user-invoked actions triggered with `/command-name
|
|
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
|
|
12
|
-
// ctx
|
|
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
|
-
|
|
41
|
+
Command handlers receive `ExtensionCommandContext`, which includes normal `ExtensionContext` fields plus session-control methods.
|
|
42
|
+
|
|
43
|
+
Common fields and methods:
|
|
20
44
|
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
52
|
-
|
|
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
|
|
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
|
|
111
|
+
Do not call `done(undefined)` for normal interactive close paths if `result === undefined` detects RPC fallback.
|
|
66
112
|
|
|
67
|
-
##
|
|
113
|
+
## Commands That Trigger Agent Work
|
|
68
114
|
|
|
69
|
-
|
|
115
|
+
Use `pi.sendUserMessage()` when a command should queue a user message.
|
|
70
116
|
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
133
|
+
## Reload Command
|
|
82
134
|
|
|
83
|
-
|
|
135
|
+
Treat reload as terminal.
|
|
84
136
|
|
|
85
137
|
```typescript
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
98
|
-
| Purpose | User-facing
|
|
99
|
-
| UI
|
|
100
|
-
| Return
|
|
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
|
|
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
|
|
8
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
9
9
|
|
|
10
10
|
class MyComponent implements Component {
|
|
11
|
-
render(
|
|
12
|
-
return "Hello from my component";
|
|
11
|
+
render(width: number): string[] {
|
|
12
|
+
return ["Hello from my component"];
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
handleInput?(data: string): void;
|
|
16
|
+
|
|
17
|
+
invalidate(): void {
|
|
18
|
+
// Clear cached render state.
|
|
19
|
+
}
|
|
17
20
|
}
|
|
18
21
|
```
|
|
19
22
|
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
33
|
+
## Built-in Components
|
|
25
34
|
|
|
26
|
-
|
|
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` |
|
|
29
|
-
| `Box` |
|
|
30
|
-
| `Container` | Vertical
|
|
31
|
-
| `Spacer` | Empty space |
|
|
32
|
-
| `Input` |
|
|
33
|
-
| `Editor` | Multi-line
|
|
34
|
-
| `SelectList` |
|
|
35
|
-
| `SettingsList` |
|
|
36
|
-
| `
|
|
37
|
-
| `
|
|
38
|
-
|
|
39
|
-
|
|
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 {
|
|
69
|
+
import {
|
|
70
|
+
BorderedLoader,
|
|
71
|
+
CustomEditor,
|
|
72
|
+
DynamicBorder,
|
|
73
|
+
getMarkdownTheme,
|
|
74
|
+
getSettingsListTheme,
|
|
75
|
+
} from "@earendil-works/pi-coding-agent";
|
|
46
76
|
```
|
|
47
77
|
|
|
48
|
-
##
|
|
78
|
+
## `ctx.ui.custom()`
|
|
49
79
|
|
|
50
|
-
|
|
80
|
+
`custom()` temporarily replaces the editor with your component until `done(value)` is called.
|
|
51
81
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
133
|
+
Overlay components are disposed when closed. Create a fresh instance each time you show one.
|
|
134
|
+
|
|
135
|
+
## Keyboard Handling
|
|
65
136
|
|
|
66
|
-
`
|
|
137
|
+
Use `matchesKey()` and `Key` from `pi-tui`.
|
|
67
138
|
|
|
68
139
|
```typescript
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
164
|
+
Use:
|
|
81
165
|
|
|
82
|
-
|
|
83
|
-
-
|
|
84
|
-
-
|
|
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
|
-
|
|
170
|
+
## Theming
|
|
88
171
|
|
|
89
|
-
|
|
172
|
+
Use the `theme` passed into render/custom callbacks.
|
|
90
173
|
|
|
91
|
-
|
|
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
|
-
|
|
183
|
+
For markdown in a tool or message renderer:
|
|
94
184
|
|
|
95
185
|
```typescript
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
191
|
+
## Common Patterns
|
|
192
|
+
|
|
193
|
+
### Selection dialog
|
|
194
|
+
|
|
195
|
+
Use `SelectList` with `DynamicBorder`.
|
|
114
196
|
|
|
115
197
|
```typescript
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
theme.fg("
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
227
|
+
### Async operation with cancel
|
|
129
228
|
|
|
130
|
-
|
|
229
|
+
Use `BorderedLoader`.
|
|
131
230
|
|
|
132
|
-
|
|
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
|
-
|
|
136
|
-
|
|
251
|
+
ctx.ui.setStatus("my-extension", ctx.ui.theme.fg("accent", "active"));
|
|
252
|
+
ctx.ui.setStatus("my-extension", undefined);
|
|
137
253
|
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
152
|
-
|
|
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
|
-
|
|
313
|
+
Return `undefined` only when fallback rendering is better than custom output.
|
|
158
314
|
|
|
159
|
-
|
|
315
|
+
## Mode Awareness
|
|
160
316
|
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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.
|