@mariozechner/pi-coding-agent 0.37.8 → 0.39.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/CHANGELOG.md +115 -4
- package/README.md +11 -0
- package/dist/cli/args.d.ts +2 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +8 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-session.d.ts +23 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +75 -35
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/bash-executor.d.ts +6 -0
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +77 -0
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/extensions/index.d.ts +3 -3
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts +8 -6
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +94 -211
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +27 -30
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +102 -45
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +155 -30
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/extensions/wrapper.d.ts +5 -3
- package/dist/core/extensions/wrapper.d.ts.map +1 -1
- package/dist/core/extensions/wrapper.js +6 -4
- package/dist/core/extensions/wrapper.js.map +1 -1
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/model-resolver.d.ts +4 -2
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +8 -9
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/sdk.d.ts +8 -5
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +39 -87
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/settings-manager.d.ts +8 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +9 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +1 -5
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/bash.d.ts +25 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +103 -73
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/edit.d.ts +17 -1
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +12 -5
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/find.d.ts +18 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +68 -18
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts +15 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +22 -10
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts +7 -7
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +1 -1
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/ls.d.ts +21 -1
- package/dist/core/tools/ls.d.ts.map +1 -1
- package/dist/core/tools/ls.js +80 -72
- package/dist/core/tools/ls.js.map +1 -1
- package/dist/core/tools/read.d.ts +14 -0
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +12 -5
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/write.d.ts +15 -1
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +9 -4
- package/dist/core/tools/write.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +58 -116
- package/dist/main.js.map +1 -1
- package/dist/modes/index.d.ts +2 -2
- package/dist/modes/index.d.ts.map +1 -1
- package/dist/modes/index.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +7 -3
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/countdown-timer.d.ts +14 -0
- package/dist/modes/interactive/components/countdown-timer.d.ts.map +1 -0
- package/dist/modes/interactive/components/countdown-timer.js +33 -0
- package/dist/modes/interactive/components/countdown-timer.js.map +1 -0
- package/dist/modes/interactive/components/custom-editor.d.ts +1 -1
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-editor.js.map +1 -1
- package/dist/modes/interactive/components/extension-input.d.ts +10 -2
- package/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-input.js +18 -14
- package/dist/modes/interactive/components/extension-input.js.map +1 -1
- package/dist/modes/interactive/components/extension-selector.d.ts +10 -2
- package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-selector.js +18 -22
- package/dist/modes/interactive/components/extension-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +6 -0
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +50 -23
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +44 -3
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +440 -139
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts +7 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +34 -0
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/print-mode.d.ts +14 -7
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +45 -21
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +111 -101
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +3 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/utils/clipboard-image.d.ts.map +1 -1
- package/dist/utils/clipboard-image.js +1 -1
- package/dist/utils/clipboard-image.js.map +1 -1
- package/dist/utils/clipboard.d.ts.map +1 -1
- package/dist/utils/clipboard.js +35 -7
- package/dist/utils/clipboard.js.map +1 -1
- package/docs/extensions.md +211 -15
- package/docs/sdk.md +68 -9
- package/docs/tui.md +81 -4
- package/examples/extensions/README.md +3 -0
- package/examples/extensions/claude-rules.ts +5 -2
- package/examples/extensions/handoff.ts +1 -1
- package/examples/extensions/interactive-shell.ts +196 -0
- package/examples/extensions/mac-system-theme.ts +25 -0
- package/examples/extensions/modal-editor.ts +85 -0
- package/examples/extensions/overlay-test.ts +145 -0
- package/examples/extensions/pirate.ts +7 -4
- package/examples/extensions/preset.ts +3 -3
- package/examples/extensions/qna.ts +1 -1
- package/examples/extensions/rainbow-editor.ts +95 -0
- package/examples/extensions/shutdown-command.ts +63 -0
- package/examples/extensions/snake.ts +1 -1
- package/examples/extensions/ssh.ts +220 -0
- package/examples/extensions/timed-confirm.ts +32 -25
- package/examples/extensions/todo.ts +1 -1
- package/examples/extensions/tool-override.ts +143 -0
- package/examples/extensions/tools.ts +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/examples/sdk/04-skills.ts +4 -1
- package/package.json +6 -6
package/docs/extensions.md
CHANGED
|
@@ -255,7 +255,7 @@ pi starts
|
|
|
255
255
|
▼
|
|
256
256
|
user sends prompt ─────────────────────────────────────────┐
|
|
257
257
|
│ │
|
|
258
|
-
├─► before_agent_start (can inject message,
|
|
258
|
+
├─► before_agent_start (can inject message, modify system prompt)
|
|
259
259
|
├─► agent_start │
|
|
260
260
|
│ │
|
|
261
261
|
│ ┌─── turn (repeats while LLM calls tools) ───┐ │
|
|
@@ -414,12 +414,13 @@ pi.on("session_shutdown", async (_event, ctx) => {
|
|
|
414
414
|
|
|
415
415
|
#### before_agent_start
|
|
416
416
|
|
|
417
|
-
Fired after user submits prompt, before agent loop. Can inject a message and/or
|
|
417
|
+
Fired after user submits prompt, before agent loop. Can inject a message and/or modify the system prompt.
|
|
418
418
|
|
|
419
419
|
```typescript
|
|
420
420
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
421
421
|
// event.prompt - user's prompt text
|
|
422
422
|
// event.images - attached images (if any)
|
|
423
|
+
// event.systemPrompt - current system prompt
|
|
423
424
|
|
|
424
425
|
return {
|
|
425
426
|
// Inject a persistent message (stored in session, sent to LLM)
|
|
@@ -428,13 +429,13 @@ pi.on("before_agent_start", async (event, ctx) => {
|
|
|
428
429
|
content: "Additional context for the LLM",
|
|
429
430
|
display: true,
|
|
430
431
|
},
|
|
431
|
-
//
|
|
432
|
-
|
|
432
|
+
// Replace the system prompt for this turn (chained across extensions)
|
|
433
|
+
systemPrompt: event.systemPrompt + "\n\nExtra instructions for this turn...",
|
|
433
434
|
};
|
|
434
435
|
});
|
|
435
436
|
```
|
|
436
437
|
|
|
437
|
-
**Examples:** [claude-rules.ts](../examples/extensions/claude-rules.ts), [pirate.ts](../examples/extensions/pirate.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts)
|
|
438
|
+
**Examples:** [claude-rules.ts](../examples/extensions/claude-rules.ts), [pirate.ts](../examples/extensions/pirate.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts), [ssh.ts](../examples/extensions/ssh.ts)
|
|
438
439
|
|
|
439
440
|
#### agent_start / agent_end
|
|
440
441
|
|
|
@@ -522,6 +523,28 @@ pi.on("tool_result", async (event, ctx) => {
|
|
|
522
523
|
|
|
523
524
|
**Examples:** [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts)
|
|
524
525
|
|
|
526
|
+
### User Bash Events
|
|
527
|
+
|
|
528
|
+
#### user_bash
|
|
529
|
+
|
|
530
|
+
Fired when user executes `!` or `!!` commands. **Can intercept.**
|
|
531
|
+
|
|
532
|
+
```typescript
|
|
533
|
+
pi.on("user_bash", (event, ctx) => {
|
|
534
|
+
// event.command - the bash command
|
|
535
|
+
// event.excludeFromContext - true if !! prefix
|
|
536
|
+
// event.cwd - working directory
|
|
537
|
+
|
|
538
|
+
// Option 1: Provide custom operations (e.g., SSH)
|
|
539
|
+
return { operations: remoteBashOps };
|
|
540
|
+
|
|
541
|
+
// Option 2: Full replacement - return result directly
|
|
542
|
+
return { result: { output: "...", exitCode: 0, cancelled: false, truncated: false } };
|
|
543
|
+
});
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
**Examples:** [ssh.ts](../examples/extensions/ssh.ts), [interactive-shell.ts](../examples/extensions/interactive-shell.ts)
|
|
547
|
+
|
|
525
548
|
## ExtensionContext
|
|
526
549
|
|
|
527
550
|
Every handler receives `ctx: ExtensionContext`:
|
|
@@ -556,6 +579,24 @@ Access to models and API keys.
|
|
|
556
579
|
|
|
557
580
|
Control flow helpers.
|
|
558
581
|
|
|
582
|
+
### ctx.shutdown()
|
|
583
|
+
|
|
584
|
+
Request a graceful shutdown of pi.
|
|
585
|
+
|
|
586
|
+
- **Interactive mode:** Deferred until the agent becomes idle (after processing all queued steering and follow-up messages).
|
|
587
|
+
- **RPC mode:** Deferred until the next idle state (after completing the current command response, when waiting for the next command).
|
|
588
|
+
- **Print mode:** No-op. The process exits automatically when all prompts are processed.
|
|
589
|
+
|
|
590
|
+
Emits `session_shutdown` event to all extensions before exiting. Available in all contexts (event handlers, tools, commands, shortcuts).
|
|
591
|
+
|
|
592
|
+
```typescript
|
|
593
|
+
pi.on("tool_call", (event, ctx) => {
|
|
594
|
+
if (isFatal(event.input)) {
|
|
595
|
+
ctx.shutdown();
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
```
|
|
599
|
+
|
|
559
600
|
## ExtensionCommandContext
|
|
560
601
|
|
|
561
602
|
Command handlers receive `ExtensionCommandContext`, which extends `ExtensionContext` with session control methods. These are only available in commands because they can deadlock if called from event handlers.
|
|
@@ -924,6 +965,69 @@ pi.registerTool({
|
|
|
924
965
|
|
|
925
966
|
**Important:** Use `StringEnum` from `@mariozechner/pi-ai` for string enums. `Type.Union`/`Type.Literal` doesn't work with Google's API.
|
|
926
967
|
|
|
968
|
+
### Overriding Built-in Tools
|
|
969
|
+
|
|
970
|
+
Extensions can override built-in tools (`read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`) by registering a tool with the same name. Interactive mode displays a warning when this happens.
|
|
971
|
+
|
|
972
|
+
```bash
|
|
973
|
+
# Extension's read tool replaces built-in read
|
|
974
|
+
pi -e ./tool-override.ts
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
Alternatively, use `--no-tools` to start without any built-in tools:
|
|
978
|
+
```bash
|
|
979
|
+
# No built-in tools, only extension tools
|
|
980
|
+
pi --no-tools -e ./my-extension.ts
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
See [examples/extensions/tool-override.ts](../examples/extensions/tool-override.ts) for a complete example that overrides `read` with logging and access control.
|
|
984
|
+
|
|
985
|
+
**Rendering:** If your override doesn't provide custom `renderCall`/`renderResult` functions, the built-in renderer is used automatically (syntax highlighting, diffs, etc.). This lets you wrap built-in tools for logging or access control without reimplementing the UI.
|
|
986
|
+
|
|
987
|
+
**Your implementation must match the exact result shape**, including the `details` type. The UI and session logic depend on these shapes for rendering and state tracking.
|
|
988
|
+
|
|
989
|
+
Built-in tool implementations:
|
|
990
|
+
- [read.ts](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/tools/read.ts) - `ReadToolDetails`
|
|
991
|
+
- [bash.ts](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/tools/bash.ts) - `BashToolDetails`
|
|
992
|
+
- [edit.ts](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/tools/edit.ts)
|
|
993
|
+
- [write.ts](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/tools/write.ts)
|
|
994
|
+
- [grep.ts](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/tools/grep.ts) - `GrepToolDetails`
|
|
995
|
+
- [find.ts](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/tools/find.ts) - `FindToolDetails`
|
|
996
|
+
- [ls.ts](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/tools/ls.ts) - `LsToolDetails`
|
|
997
|
+
|
|
998
|
+
### Remote Execution
|
|
999
|
+
|
|
1000
|
+
Built-in tools support pluggable operations for delegating to remote systems (SSH, containers, etc.):
|
|
1001
|
+
|
|
1002
|
+
```typescript
|
|
1003
|
+
import { createReadTool, createBashTool, type ReadOperations } from "@mariozechner/pi-coding-agent";
|
|
1004
|
+
|
|
1005
|
+
// Create tool with custom operations
|
|
1006
|
+
const remoteRead = createReadTool(cwd, {
|
|
1007
|
+
operations: {
|
|
1008
|
+
readFile: (path) => sshExec(remote, `cat ${path}`),
|
|
1009
|
+
access: (path) => sshExec(remote, `test -r ${path}`).then(() => {}),
|
|
1010
|
+
}
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
// Register, checking flag at execution time
|
|
1014
|
+
pi.registerTool({
|
|
1015
|
+
...remoteRead,
|
|
1016
|
+
async execute(id, params, onUpdate, _ctx, signal) {
|
|
1017
|
+
const ssh = getSshConfig();
|
|
1018
|
+
if (ssh) {
|
|
1019
|
+
const tool = createReadTool(cwd, { operations: createRemoteOps(ssh) });
|
|
1020
|
+
return tool.execute(id, params, signal, onUpdate);
|
|
1021
|
+
}
|
|
1022
|
+
return localRead.execute(id, params, signal, onUpdate);
|
|
1023
|
+
},
|
|
1024
|
+
});
|
|
1025
|
+
```
|
|
1026
|
+
|
|
1027
|
+
**Operations interfaces:** `ReadOperations`, `WriteOperations`, `EditOperations`, `BashOperations`, `LsOperations`, `GrepOperations`, `FindOperations`
|
|
1028
|
+
|
|
1029
|
+
See [examples/extensions/ssh.ts](../examples/extensions/ssh.ts) for a complete SSH example with `--ssh` flag.
|
|
1030
|
+
|
|
927
1031
|
### Output Truncation
|
|
928
1032
|
|
|
929
1033
|
**Tools MUST truncate their output** to avoid overwhelming the LLM context. Large outputs can cause:
|
|
@@ -1094,9 +1198,33 @@ ctx.ui.notify("Done!", "info"); // "info" | "warning" | "error"
|
|
|
1094
1198
|
- `ctx.ui.editor()`: [handoff.ts](../examples/extensions/handoff.ts)
|
|
1095
1199
|
- `ctx.ui.setEditorText()`: [handoff.ts](../examples/extensions/handoff.ts), [qna.ts](../examples/extensions/qna.ts)
|
|
1096
1200
|
|
|
1097
|
-
####
|
|
1201
|
+
#### Timed Dialogs with Countdown
|
|
1098
1202
|
|
|
1099
|
-
Dialogs
|
|
1203
|
+
Dialogs support a `timeout` option that auto-dismisses with a live countdown display:
|
|
1204
|
+
|
|
1205
|
+
```typescript
|
|
1206
|
+
// Dialog shows "Title (5s)" → "Title (4s)" → ... → auto-dismisses at 0
|
|
1207
|
+
const confirmed = await ctx.ui.confirm(
|
|
1208
|
+
"Timed Confirmation",
|
|
1209
|
+
"This dialog will auto-cancel in 5 seconds. Confirm?",
|
|
1210
|
+
{ timeout: 5000 }
|
|
1211
|
+
);
|
|
1212
|
+
|
|
1213
|
+
if (confirmed) {
|
|
1214
|
+
// User confirmed
|
|
1215
|
+
} else {
|
|
1216
|
+
// User cancelled or timed out
|
|
1217
|
+
}
|
|
1218
|
+
```
|
|
1219
|
+
|
|
1220
|
+
**Return values on timeout:**
|
|
1221
|
+
- `select()` returns `undefined`
|
|
1222
|
+
- `confirm()` returns `false`
|
|
1223
|
+
- `input()` returns `undefined`
|
|
1224
|
+
|
|
1225
|
+
#### Manual Dismissal with AbortSignal
|
|
1226
|
+
|
|
1227
|
+
For more control (e.g., to distinguish timeout from user cancel), use `AbortSignal`:
|
|
1100
1228
|
|
|
1101
1229
|
```typescript
|
|
1102
1230
|
const controller = new AbortController();
|
|
@@ -1119,12 +1247,7 @@ if (confirmed) {
|
|
|
1119
1247
|
}
|
|
1120
1248
|
```
|
|
1121
1249
|
|
|
1122
|
-
|
|
1123
|
-
- `select()` returns `undefined`
|
|
1124
|
-
- `confirm()` returns `false`
|
|
1125
|
-
- `input()` returns `undefined`
|
|
1126
|
-
|
|
1127
|
-
See [examples/extensions/timed-confirm.ts](../examples/extensions/timed-confirm.ts) for a complete example.
|
|
1250
|
+
See [examples/extensions/timed-confirm.ts](../examples/extensions/timed-confirm.ts) for complete examples.
|
|
1128
1251
|
|
|
1129
1252
|
### Widgets, Status, and Footer
|
|
1130
1253
|
|
|
@@ -1151,6 +1274,20 @@ ctx.ui.setTitle("pi - my-project");
|
|
|
1151
1274
|
// Editor text
|
|
1152
1275
|
ctx.ui.setEditorText("Prefill text");
|
|
1153
1276
|
const current = ctx.ui.getEditorText();
|
|
1277
|
+
|
|
1278
|
+
// Custom editor (vim mode, emacs mode, etc.)
|
|
1279
|
+
ctx.ui.setEditorComponent((tui, theme, keybindings) => new VimEditor(tui, theme, keybindings));
|
|
1280
|
+
ctx.ui.setEditorComponent(undefined); // Restore default editor
|
|
1281
|
+
|
|
1282
|
+
// Theme management
|
|
1283
|
+
const themes = ctx.ui.getAllThemes(); // [{ name: "dark", path: "/..." | undefined }, ...]
|
|
1284
|
+
const lightTheme = ctx.ui.getTheme("light"); // Load without switching
|
|
1285
|
+
const result = ctx.ui.setTheme("light"); // Switch by name
|
|
1286
|
+
if (!result.success) {
|
|
1287
|
+
ctx.ui.notify(`Failed: ${result.error}`, "error");
|
|
1288
|
+
}
|
|
1289
|
+
ctx.ui.setTheme(lightTheme!); // Or switch by Theme object
|
|
1290
|
+
ctx.ui.theme.fg("accent", "styled text"); // Access current theme
|
|
1154
1291
|
```
|
|
1155
1292
|
|
|
1156
1293
|
**Examples:**
|
|
@@ -1158,6 +1295,8 @@ const current = ctx.ui.getEditorText();
|
|
|
1158
1295
|
- `ctx.ui.setWidget()`: [plan-mode.ts](../examples/extensions/plan-mode.ts)
|
|
1159
1296
|
- `ctx.ui.setFooter()`: [custom-footer.ts](../examples/extensions/custom-footer.ts)
|
|
1160
1297
|
- `ctx.ui.setHeader()`: [custom-header.ts](../examples/extensions/custom-header.ts)
|
|
1298
|
+
- `ctx.ui.setEditorComponent()`: [modal-editor.ts](../examples/extensions/modal-editor.ts)
|
|
1299
|
+
- `ctx.ui.setTheme()`: [mac-system-theme.ts](../examples/extensions/mac-system-theme.ts)
|
|
1161
1300
|
|
|
1162
1301
|
### Custom Components
|
|
1163
1302
|
|
|
@@ -1166,7 +1305,7 @@ For complex UI, use `ctx.ui.custom()`. This temporarily replaces the editor with
|
|
|
1166
1305
|
```typescript
|
|
1167
1306
|
import { Text, Component } from "@mariozechner/pi-tui";
|
|
1168
1307
|
|
|
1169
|
-
const result = await ctx.ui.custom<boolean>((tui, theme, done) => {
|
|
1308
|
+
const result = await ctx.ui.custom<boolean>((tui, theme, keybindings, done) => {
|
|
1170
1309
|
const text = new Text("Press Enter to confirm, Escape to cancel", 1, 1);
|
|
1171
1310
|
|
|
1172
1311
|
text.onKey = (key) => {
|
|
@@ -1186,11 +1325,68 @@ if (result) {
|
|
|
1186
1325
|
The callback receives:
|
|
1187
1326
|
- `tui` - TUI instance (for screen dimensions, focus management)
|
|
1188
1327
|
- `theme` - Current theme for styling
|
|
1328
|
+
- `keybindings` - App keybinding manager (for checking shortcuts)
|
|
1189
1329
|
- `done(value)` - Call to close component and return value
|
|
1190
1330
|
|
|
1191
1331
|
See [tui.md](tui.md) for the full component API.
|
|
1192
1332
|
|
|
1193
|
-
|
|
1333
|
+
#### Overlay Mode (Experimental)
|
|
1334
|
+
|
|
1335
|
+
Pass `{ overlay: true }` to render the component as a floating modal on top of existing content, without clearing the screen:
|
|
1336
|
+
|
|
1337
|
+
```typescript
|
|
1338
|
+
const result = await ctx.ui.custom<string | null>(
|
|
1339
|
+
(tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }),
|
|
1340
|
+
{ overlay: true }
|
|
1341
|
+
);
|
|
1342
|
+
```
|
|
1343
|
+
|
|
1344
|
+
Overlay components should define a `width` property to control their size. The overlay is centered by default. See [overlay-test.ts](../examples/extensions/overlay-test.ts) for a complete example.
|
|
1345
|
+
|
|
1346
|
+
**Examples:** [handoff.ts](../examples/extensions/handoff.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts), [qna.ts](../examples/extensions/qna.ts), [snake.ts](../examples/extensions/snake.ts), [todo.ts](../examples/extensions/todo.ts), [tools.ts](../examples/extensions/tools.ts), [overlay-test.ts](../examples/extensions/overlay-test.ts)
|
|
1347
|
+
|
|
1348
|
+
### Custom Editor
|
|
1349
|
+
|
|
1350
|
+
Replace the main input editor with a custom implementation (vim mode, emacs mode, etc.):
|
|
1351
|
+
|
|
1352
|
+
```typescript
|
|
1353
|
+
import { CustomEditor, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
1354
|
+
import { matchesKey } from "@mariozechner/pi-tui";
|
|
1355
|
+
|
|
1356
|
+
class VimEditor extends CustomEditor {
|
|
1357
|
+
private mode: "normal" | "insert" = "insert";
|
|
1358
|
+
|
|
1359
|
+
handleInput(data: string): void {
|
|
1360
|
+
if (matchesKey(data, "escape") && this.mode === "insert") {
|
|
1361
|
+
this.mode = "normal";
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
if (this.mode === "normal" && data === "i") {
|
|
1365
|
+
this.mode = "insert";
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
super.handleInput(data); // App keybindings + text editing
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
export default function (pi: ExtensionAPI) {
|
|
1373
|
+
pi.on("session_start", (_event, ctx) => {
|
|
1374
|
+
ctx.ui.setEditorComponent((_tui, theme, keybindings) =>
|
|
1375
|
+
new VimEditor(theme, keybindings)
|
|
1376
|
+
);
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
```
|
|
1380
|
+
|
|
1381
|
+
**Key points:**
|
|
1382
|
+
- Extend `CustomEditor` (not base `Editor`) to get app keybindings (escape to abort, ctrl+d, model switching)
|
|
1383
|
+
- Call `super.handleInput(data)` for keys you don't handle
|
|
1384
|
+
- Factory receives `theme` and `keybindings` from the app
|
|
1385
|
+
- Pass `undefined` to restore default: `ctx.ui.setEditorComponent(undefined)`
|
|
1386
|
+
|
|
1387
|
+
See [tui.md](tui.md) Pattern 7 for a complete example with mode indicator.
|
|
1388
|
+
|
|
1389
|
+
**Examples:** [modal-editor.ts](../examples/extensions/modal-editor.ts)
|
|
1194
1390
|
|
|
1195
1391
|
### Message Rendering
|
|
1196
1392
|
|
package/docs/sdk.md
CHANGED
|
@@ -528,7 +528,7 @@ eventBus.on("my-extension:status", (data) => console.log(data));
|
|
|
528
528
|
import { createAgentSession, discoverSkills, type Skill } from "@mariozechner/pi-coding-agent";
|
|
529
529
|
|
|
530
530
|
// Discover and filter
|
|
531
|
-
const allSkills = discoverSkills();
|
|
531
|
+
const { skills: allSkills, warnings } = discoverSkills();
|
|
532
532
|
const filtered = allSkills.filter(s => s.name.includes("search"));
|
|
533
533
|
|
|
534
534
|
// Custom skill
|
|
@@ -550,7 +550,7 @@ const { session } = await createAgentSession({
|
|
|
550
550
|
});
|
|
551
551
|
|
|
552
552
|
// Discovery with settings filter
|
|
553
|
-
const skills = discoverSkills(process.cwd(), undefined, {
|
|
553
|
+
const { skills } = discoverSkills(process.cwd(), undefined, {
|
|
554
554
|
ignoredSkills: ["browser-*"], // glob patterns to exclude
|
|
555
555
|
includeSkills: ["search-*"], // glob patterns to include (empty = all)
|
|
556
556
|
});
|
|
@@ -747,7 +747,7 @@ const model = modelRegistry.find("provider", "id"); // Find specific model
|
|
|
747
747
|
const builtIn = getModel("anthropic", "claude-opus-4-5"); // Built-in only
|
|
748
748
|
|
|
749
749
|
// Skills
|
|
750
|
-
const skills = discoverSkills(cwd, agentDir, skillsSettings);
|
|
750
|
+
const { skills, warnings } = discoverSkills(cwd, agentDir, skillsSettings);
|
|
751
751
|
|
|
752
752
|
// Hooks (async - loads TypeScript)
|
|
753
753
|
// Pass eventBus to share pi.events across hooks/tools
|
|
@@ -784,15 +784,18 @@ interface CreateAgentSessionResult {
|
|
|
784
784
|
// The session
|
|
785
785
|
session: AgentSession;
|
|
786
786
|
|
|
787
|
-
//
|
|
788
|
-
|
|
789
|
-
tools: LoadedCustomTool[];
|
|
790
|
-
setUIContext: (ctx, hasUI) => void;
|
|
791
|
-
};
|
|
787
|
+
// Extensions result (for runner setup)
|
|
788
|
+
extensionsResult: LoadExtensionsResult;
|
|
792
789
|
|
|
793
790
|
// Warning if session model couldn't be restored
|
|
794
791
|
modelFallbackMessage?: string;
|
|
795
792
|
}
|
|
793
|
+
|
|
794
|
+
interface LoadExtensionsResult {
|
|
795
|
+
extensions: Extension[];
|
|
796
|
+
errors: Array<{ path: string; error: string }>;
|
|
797
|
+
runtime: ExtensionRuntime;
|
|
798
|
+
}
|
|
796
799
|
```
|
|
797
800
|
|
|
798
801
|
## Complete Example
|
|
@@ -883,9 +886,65 @@ session.subscribe((event) => {
|
|
|
883
886
|
await session.prompt("Get status and list files.");
|
|
884
887
|
```
|
|
885
888
|
|
|
889
|
+
## Run Modes
|
|
890
|
+
|
|
891
|
+
The SDK exports run mode utilities for building custom interfaces on top of `createAgentSession()`:
|
|
892
|
+
|
|
893
|
+
### InteractiveMode
|
|
894
|
+
|
|
895
|
+
Full TUI interactive mode with editor, chat history, and all built-in commands:
|
|
896
|
+
|
|
897
|
+
```typescript
|
|
898
|
+
import { createAgentSession, InteractiveMode } from "@mariozechner/pi-coding-agent";
|
|
899
|
+
|
|
900
|
+
const { session } = await createAgentSession({ /* ... */ });
|
|
901
|
+
|
|
902
|
+
const mode = new InteractiveMode(session, {
|
|
903
|
+
// All optional
|
|
904
|
+
migratedProviders: [], // Show migration warnings
|
|
905
|
+
modelFallbackMessage: undefined, // Show model restore warning
|
|
906
|
+
initialMessage: "Hello", // Send on startup
|
|
907
|
+
initialImages: [], // Images with initial message
|
|
908
|
+
initialMessages: [], // Additional startup prompts
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
await mode.run(); // Blocks until exit
|
|
912
|
+
```
|
|
913
|
+
|
|
914
|
+
### runPrintMode
|
|
915
|
+
|
|
916
|
+
Single-shot mode: send prompts, output result, exit:
|
|
917
|
+
|
|
918
|
+
```typescript
|
|
919
|
+
import { createAgentSession, runPrintMode } from "@mariozechner/pi-coding-agent";
|
|
920
|
+
|
|
921
|
+
const { session } = await createAgentSession({ /* ... */ });
|
|
922
|
+
|
|
923
|
+
await runPrintMode(session, {
|
|
924
|
+
mode: "text", // "text" for final response, "json" for all events
|
|
925
|
+
initialMessage: "Hello", // First message (can include @file content)
|
|
926
|
+
initialImages: [], // Images with initial message
|
|
927
|
+
messages: ["Follow up"], // Additional prompts
|
|
928
|
+
});
|
|
929
|
+
```
|
|
930
|
+
|
|
931
|
+
### runRpcMode
|
|
932
|
+
|
|
933
|
+
JSON-RPC mode for subprocess integration:
|
|
934
|
+
|
|
935
|
+
```typescript
|
|
936
|
+
import { createAgentSession, runRpcMode } from "@mariozechner/pi-coding-agent";
|
|
937
|
+
|
|
938
|
+
const { session } = await createAgentSession({ /* ... */ });
|
|
939
|
+
|
|
940
|
+
await runRpcMode(session); // Reads JSON commands from stdin, writes to stdout
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
See [RPC documentation](rpc.md) for the JSON protocol.
|
|
944
|
+
|
|
886
945
|
## RPC Mode Alternative
|
|
887
946
|
|
|
888
|
-
For subprocess-based integration
|
|
947
|
+
For subprocess-based integration without building with the SDK, use the CLI directly:
|
|
889
948
|
|
|
890
949
|
```bash
|
|
891
950
|
pi --mode rpc --no-session
|
package/docs/tui.md
CHANGED
|
@@ -361,7 +361,7 @@ pi.registerCommand("pick", {
|
|
|
361
361
|
{ value: "opt3", label: "Option 3" }, // description is optional
|
|
362
362
|
];
|
|
363
363
|
|
|
364
|
-
const result = await ctx.ui.custom<string | null>((tui, theme, done) => {
|
|
364
|
+
const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
|
|
365
365
|
const container = new Container();
|
|
366
366
|
|
|
367
367
|
// Top border
|
|
@@ -413,7 +413,7 @@ import { BorderedLoader } from "@mariozechner/pi-coding-agent";
|
|
|
413
413
|
|
|
414
414
|
pi.registerCommand("fetch", {
|
|
415
415
|
handler: async (_args, ctx) => {
|
|
416
|
-
const result = await ctx.ui.custom<string | null>((tui, theme, done) => {
|
|
416
|
+
const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
|
|
417
417
|
const loader = new BorderedLoader(tui, theme, "Fetching data...");
|
|
418
418
|
loader.onAbort = () => done(null);
|
|
419
419
|
|
|
@@ -451,7 +451,7 @@ pi.registerCommand("settings", {
|
|
|
451
451
|
{ id: "color", label: "Color output", currentValue: "on", values: ["on", "off"] },
|
|
452
452
|
];
|
|
453
453
|
|
|
454
|
-
await ctx.ui.custom((_tui, theme, done) => {
|
|
454
|
+
await ctx.ui.custom((_tui, theme, _kb, done) => {
|
|
455
455
|
const container = new Container();
|
|
456
456
|
container.addChild(new Text(theme.fg("accent", theme.bold("Settings")), 1, 1));
|
|
457
457
|
|
|
@@ -541,9 +541,85 @@ ctx.ui.setFooter(undefined);
|
|
|
541
541
|
|
|
542
542
|
**Examples:** [custom-footer.ts](../examples/extensions/custom-footer.ts)
|
|
543
543
|
|
|
544
|
+
### Pattern 7: Custom Editor (vim mode, etc.)
|
|
545
|
+
|
|
546
|
+
Replace the main input editor with a custom implementation. Useful for modal editing (vim), different keybindings (emacs), or specialized input handling.
|
|
547
|
+
|
|
548
|
+
```typescript
|
|
549
|
+
import { CustomEditor, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
550
|
+
import { matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
|
|
551
|
+
|
|
552
|
+
type Mode = "normal" | "insert";
|
|
553
|
+
|
|
554
|
+
class VimEditor extends CustomEditor {
|
|
555
|
+
private mode: Mode = "insert";
|
|
556
|
+
|
|
557
|
+
handleInput(data: string): void {
|
|
558
|
+
// Escape: switch to normal mode, or pass through for app handling
|
|
559
|
+
if (matchesKey(data, "escape")) {
|
|
560
|
+
if (this.mode === "insert") {
|
|
561
|
+
this.mode = "normal";
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
// In normal mode, escape aborts agent (handled by CustomEditor)
|
|
565
|
+
super.handleInput(data);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Insert mode: pass everything to CustomEditor
|
|
570
|
+
if (this.mode === "insert") {
|
|
571
|
+
super.handleInput(data);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Normal mode: vim-style navigation
|
|
576
|
+
switch (data) {
|
|
577
|
+
case "i": this.mode = "insert"; return;
|
|
578
|
+
case "h": super.handleInput("\x1b[D"); return; // Left
|
|
579
|
+
case "j": super.handleInput("\x1b[B"); return; // Down
|
|
580
|
+
case "k": super.handleInput("\x1b[A"); return; // Up
|
|
581
|
+
case "l": super.handleInput("\x1b[C"); return; // Right
|
|
582
|
+
}
|
|
583
|
+
// Pass unhandled keys to super (ctrl+c, etc.), but filter printable chars
|
|
584
|
+
if (data.length === 1 && data.charCodeAt(0) >= 32) return;
|
|
585
|
+
super.handleInput(data);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
render(width: number): string[] {
|
|
589
|
+
const lines = super.render(width);
|
|
590
|
+
// Add mode indicator to bottom border (use truncateToWidth for ANSI-safe truncation)
|
|
591
|
+
if (lines.length > 0) {
|
|
592
|
+
const label = this.mode === "normal" ? " NORMAL " : " INSERT ";
|
|
593
|
+
const lastLine = lines[lines.length - 1]!;
|
|
594
|
+
// Pass "" as ellipsis to avoid adding "..." when truncating
|
|
595
|
+
lines[lines.length - 1] = truncateToWidth(lastLine, width - label.length, "") + label;
|
|
596
|
+
}
|
|
597
|
+
return lines;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
export default function (pi: ExtensionAPI) {
|
|
602
|
+
pi.on("session_start", (_event, ctx) => {
|
|
603
|
+
// Factory receives theme and keybindings from the app
|
|
604
|
+
ctx.ui.setEditorComponent((tui, theme, keybindings) =>
|
|
605
|
+
new VimEditor(theme, keybindings)
|
|
606
|
+
);
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
**Key points:**
|
|
612
|
+
|
|
613
|
+
- **Extend `CustomEditor`** (not base `Editor`) to get app keybindings (escape to abort, ctrl+d to exit, model switching, etc.)
|
|
614
|
+
- **Call `super.handleInput(data)`** for keys you don't handle
|
|
615
|
+
- **Factory pattern**: `setEditorComponent` receives a factory function that gets `tui`, `theme`, and `keybindings`
|
|
616
|
+
- **Pass `undefined`** to restore the default editor: `ctx.ui.setEditorComponent(undefined)`
|
|
617
|
+
|
|
618
|
+
**Examples:** [modal-editor.ts](../examples/extensions/modal-editor.ts)
|
|
619
|
+
|
|
544
620
|
## Key Rules
|
|
545
621
|
|
|
546
|
-
1. **Always use theme from callback** - Don't import theme directly. Use `theme` from the `ctx.ui.custom((tui, theme, done) => ...)` callback.
|
|
622
|
+
1. **Always use theme from callback** - Don't import theme directly. Use `theme` from the `ctx.ui.custom((tui, theme, keybindings, done) => ...)` callback.
|
|
547
623
|
|
|
548
624
|
2. **Always type DynamicBorder color param** - Write `(s: string) => theme.fg("accent", s)`, not `(s) => theme.fg("accent", s)`.
|
|
549
625
|
|
|
@@ -560,5 +636,6 @@ ctx.ui.setFooter(undefined);
|
|
|
560
636
|
- **Settings toggles**: [examples/extensions/tools.ts](../examples/extensions/tools.ts) - SettingsList for tool enable/disable
|
|
561
637
|
- **Status indicators**: [examples/extensions/plan-mode.ts](../examples/extensions/plan-mode.ts) - setStatus and setWidget
|
|
562
638
|
- **Custom footer**: [examples/extensions/custom-footer.ts](../examples/extensions/custom-footer.ts) - setFooter with stats
|
|
639
|
+
- **Custom editor**: [examples/extensions/modal-editor.ts](../examples/extensions/modal-editor.ts) - Vim-like modal editing
|
|
563
640
|
- **Snake game**: [examples/extensions/snake.ts](../examples/extensions/snake.ts) - Full game with keyboard input, game loop
|
|
564
641
|
- **Custom tool rendering**: [examples/extensions/todo.ts](../examples/extensions/todo.ts) - renderCall and renderResult
|
|
@@ -30,6 +30,8 @@ cp permission-gate.ts ~/.pi/agent/extensions/
|
|
|
30
30
|
| `todo.ts` | Todo list tool + `/todos` command with custom rendering and state persistence |
|
|
31
31
|
| `hello.ts` | Minimal custom tool example |
|
|
32
32
|
| `question.ts` | Demonstrates `ctx.ui.select()` for asking the user questions |
|
|
33
|
+
| `tool-override.ts` | Override built-in tools (e.g., add logging/access control to `read`) |
|
|
34
|
+
| `ssh.ts` | Delegate all tools to a remote machine via SSH using pluggable operations |
|
|
33
35
|
| `subagent/` | Delegate tasks to specialized subagents with isolated context windows |
|
|
34
36
|
|
|
35
37
|
### Commands & UI
|
|
@@ -45,6 +47,7 @@ cp permission-gate.ts ~/.pi/agent/extensions/
|
|
|
45
47
|
| `snake.ts` | Snake game with custom UI, keyboard handling, and session persistence |
|
|
46
48
|
| `send-user-message.ts` | Demonstrates `pi.sendUserMessage()` for sending user messages from extensions |
|
|
47
49
|
| `timed-confirm.ts` | Demonstrates AbortSignal for auto-dismissing `ctx.ui.confirm()` and `ctx.ui.select()` dialogs |
|
|
50
|
+
| `modal-editor.ts` | Custom vim-like modal editor via `ctx.ui.setEditorComponent()` |
|
|
48
51
|
|
|
49
52
|
### Git Integration
|
|
50
53
|
|
|
@@ -61,7 +61,7 @@ export default function claudeRulesExtension(pi: ExtensionAPI) {
|
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
// Append available rules to system prompt
|
|
64
|
-
pi.on("before_agent_start", async () => {
|
|
64
|
+
pi.on("before_agent_start", async (event) => {
|
|
65
65
|
if (ruleFiles.length === 0) {
|
|
66
66
|
return;
|
|
67
67
|
}
|
|
@@ -69,7 +69,10 @@ export default function claudeRulesExtension(pi: ExtensionAPI) {
|
|
|
69
69
|
const rulesList = ruleFiles.map((f) => `- .claude/rules/${f}`).join("\n");
|
|
70
70
|
|
|
71
71
|
return {
|
|
72
|
-
|
|
72
|
+
systemPrompt:
|
|
73
|
+
event.systemPrompt +
|
|
74
|
+
`
|
|
75
|
+
|
|
73
76
|
## Project Rules
|
|
74
77
|
|
|
75
78
|
The following project rules are available in .claude/rules/:
|
|
@@ -75,7 +75,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
75
75
|
const currentSessionFile = ctx.sessionManager.getSessionFile();
|
|
76
76
|
|
|
77
77
|
// Generate the handoff prompt with loader UI
|
|
78
|
-
const result = await ctx.ui.custom<string | null>((tui, theme, done) => {
|
|
78
|
+
const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
|
|
79
79
|
const loader = new BorderedLoader(tui, theme, `Generating handoff prompt...`);
|
|
80
80
|
loader.onAbort = () => done(null);
|
|
81
81
|
|