@mariozechner/pi-coding-agent 0.37.8 → 0.38.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 +84 -4
- package/README.md +10 -0
- package/dist/cli/args.d.ts +1 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +4 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +13 -1
- package/dist/core/agent-session.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 +24 -28
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +58 -38
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +116 -27
- 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 +1 -1
- package/dist/core/index.d.ts.map +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 +3 -3
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +19 -75
- 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/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +47 -115
- 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/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/interactive-mode.d.ts +44 -3
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +289 -95
- package/dist/modes/interactive/interactive-mode.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 +101 -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/docs/extensions.md +110 -9
- package/docs/sdk.md +65 -6
- package/docs/tui.md +81 -4
- package/examples/extensions/README.md +1 -0
- package/examples/extensions/handoff.ts +1 -1
- package/examples/extensions/modal-editor.ts +85 -0
- package/examples/extensions/preset.ts +1 -1
- 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/timed-confirm.ts +32 -25
- package/examples/extensions/todo.ts +1 -1
- 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/package.json +5 -5
package/docs/sdk.md
CHANGED
|
@@ -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
|
|
@@ -45,6 +45,7 @@ cp permission-gate.ts ~/.pi/agent/extensions/
|
|
|
45
45
|
| `snake.ts` | Snake game with custom UI, keyboard handling, and session persistence |
|
|
46
46
|
| `send-user-message.ts` | Demonstrates `pi.sendUserMessage()` for sending user messages from extensions |
|
|
47
47
|
| `timed-confirm.ts` | Demonstrates AbortSignal for auto-dismissing `ctx.ui.confirm()` and `ctx.ui.select()` dialogs |
|
|
48
|
+
| `modal-editor.ts` | Custom vim-like modal editor via `ctx.ui.setEditorComponent()` |
|
|
48
49
|
|
|
49
50
|
### Git Integration
|
|
50
51
|
|
|
@@ -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
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modal Editor - vim-like modal editing example
|
|
3
|
+
*
|
|
4
|
+
* Usage: pi --extension ./examples/extensions/modal-editor.ts
|
|
5
|
+
*
|
|
6
|
+
* - Escape: insert → normal mode (in normal mode, aborts agent)
|
|
7
|
+
* - i: normal → insert mode
|
|
8
|
+
* - hjkl: navigation in normal mode
|
|
9
|
+
* - ctrl+c, ctrl+d, etc. work in both modes
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { CustomEditor, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
13
|
+
import { matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
14
|
+
|
|
15
|
+
// Normal mode key mappings: key -> escape sequence (or null for mode switch)
|
|
16
|
+
const NORMAL_KEYS: Record<string, string | null> = {
|
|
17
|
+
h: "\x1b[D", // left
|
|
18
|
+
j: "\x1b[B", // down
|
|
19
|
+
k: "\x1b[A", // up
|
|
20
|
+
l: "\x1b[C", // right
|
|
21
|
+
"0": "\x01", // line start
|
|
22
|
+
$: "\x05", // line end
|
|
23
|
+
x: "\x1b[3~", // delete char
|
|
24
|
+
i: null, // insert mode
|
|
25
|
+
a: null, // append (insert + right)
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
class ModalEditor extends CustomEditor {
|
|
29
|
+
private mode: "normal" | "insert" = "insert";
|
|
30
|
+
|
|
31
|
+
handleInput(data: string): void {
|
|
32
|
+
// Escape toggles to normal mode, or passes through for app handling
|
|
33
|
+
if (matchesKey(data, "escape")) {
|
|
34
|
+
if (this.mode === "insert") {
|
|
35
|
+
this.mode = "normal";
|
|
36
|
+
} else {
|
|
37
|
+
super.handleInput(data); // abort agent, etc.
|
|
38
|
+
}
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Insert mode: pass everything through
|
|
43
|
+
if (this.mode === "insert") {
|
|
44
|
+
super.handleInput(data);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Normal mode: check mapped keys
|
|
49
|
+
if (data in NORMAL_KEYS) {
|
|
50
|
+
const seq = NORMAL_KEYS[data];
|
|
51
|
+
if (data === "i") {
|
|
52
|
+
this.mode = "insert";
|
|
53
|
+
} else if (data === "a") {
|
|
54
|
+
this.mode = "insert";
|
|
55
|
+
super.handleInput("\x1b[C"); // move right first
|
|
56
|
+
} else if (seq) {
|
|
57
|
+
super.handleInput(seq);
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Pass control sequences (ctrl+c, etc.) to super, ignore printable chars
|
|
63
|
+
if (data.length === 1 && data.charCodeAt(0) >= 32) return;
|
|
64
|
+
super.handleInput(data);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
render(width: number): string[] {
|
|
68
|
+
const lines = super.render(width);
|
|
69
|
+
if (lines.length === 0) return lines;
|
|
70
|
+
|
|
71
|
+
// Add mode indicator to bottom border
|
|
72
|
+
const label = this.mode === "normal" ? " NORMAL " : " INSERT ";
|
|
73
|
+
const last = lines.length - 1;
|
|
74
|
+
if (visibleWidth(lines[last]!) >= label.length) {
|
|
75
|
+
lines[last] = truncateToWidth(lines[last]!, width - label.length, "") + label;
|
|
76
|
+
}
|
|
77
|
+
return lines;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export default function (pi: ExtensionAPI) {
|
|
82
|
+
pi.on("session_start", (_event, ctx) => {
|
|
83
|
+
ctx.ui.setEditorComponent((_tui, theme, kb) => new ModalEditor(theme, kb));
|
|
84
|
+
});
|
|
85
|
+
}
|
|
@@ -206,7 +206,7 @@ export default function presetExtension(pi: ExtensionAPI) {
|
|
|
206
206
|
description: "Clear active preset, restore defaults",
|
|
207
207
|
});
|
|
208
208
|
|
|
209
|
-
const result = await ctx.ui.custom<string | null>((tui, theme, done) => {
|
|
209
|
+
const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
|
|
210
210
|
const container = new Container();
|
|
211
211
|
container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
|
|
212
212
|
|
|
@@ -71,7 +71,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
// Run extraction with loader UI
|
|
74
|
-
const result = await ctx.ui.custom<string | null>((tui, theme, done) => {
|
|
74
|
+
const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
|
|
75
75
|
const loader = new BorderedLoader(tui, theme, `Extracting questions using ${ctx.model!.id}...`);
|
|
76
76
|
loader.onAbort = () => done(null);
|
|
77
77
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rainbow Editor - highlights "ultrathink" with animated shine effect
|
|
3
|
+
*
|
|
4
|
+
* Usage: pi --extension ./examples/extensions/rainbow-editor.ts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { CustomEditor, type ExtensionAPI, type KeybindingsManager } from "@mariozechner/pi-coding-agent";
|
|
8
|
+
import type { EditorTheme, TUI } from "@mariozechner/pi-tui";
|
|
9
|
+
|
|
10
|
+
// Base colors (coral → yellow → green → teal → blue → purple → pink)
|
|
11
|
+
const COLORS: [number, number, number][] = [
|
|
12
|
+
[233, 137, 115], // coral
|
|
13
|
+
[228, 186, 103], // yellow
|
|
14
|
+
[141, 192, 122], // green
|
|
15
|
+
[102, 194, 179], // teal
|
|
16
|
+
[121, 157, 207], // blue
|
|
17
|
+
[157, 134, 195], // purple
|
|
18
|
+
[206, 130, 172], // pink
|
|
19
|
+
];
|
|
20
|
+
const RESET = "\x1b[0m";
|
|
21
|
+
|
|
22
|
+
function brighten(rgb: [number, number, number], factor: number): string {
|
|
23
|
+
const [r, g, b] = rgb.map((c) => Math.round(c + (255 - c) * factor));
|
|
24
|
+
return `\x1b[38;2;${r};${g};${b}m`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function colorize(text: string, shinePos: number): string {
|
|
28
|
+
return (
|
|
29
|
+
[...text]
|
|
30
|
+
.map((c, i) => {
|
|
31
|
+
const baseColor = COLORS[i % COLORS.length]!;
|
|
32
|
+
// 3-letter shine: center bright, adjacent dimmer
|
|
33
|
+
let factor = 0;
|
|
34
|
+
if (shinePos >= 0) {
|
|
35
|
+
const dist = Math.abs(i - shinePos);
|
|
36
|
+
if (dist === 0) factor = 0.7;
|
|
37
|
+
else if (dist === 1) factor = 0.35;
|
|
38
|
+
}
|
|
39
|
+
return `${brighten(baseColor, factor)}${c}`;
|
|
40
|
+
})
|
|
41
|
+
.join("") + RESET
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class RainbowEditor extends CustomEditor {
|
|
46
|
+
private animationTimer?: ReturnType<typeof setInterval>;
|
|
47
|
+
private tui: TUI;
|
|
48
|
+
private frame = 0;
|
|
49
|
+
|
|
50
|
+
constructor(tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) {
|
|
51
|
+
super(theme, keybindings);
|
|
52
|
+
this.tui = tui;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private hasUltrathink(): boolean {
|
|
56
|
+
return /ultrathink/i.test(this.getText());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private startAnimation(): void {
|
|
60
|
+
if (this.animationTimer) return;
|
|
61
|
+
this.animationTimer = setInterval(() => {
|
|
62
|
+
this.frame++;
|
|
63
|
+
this.tui.requestRender();
|
|
64
|
+
}, 60);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private stopAnimation(): void {
|
|
68
|
+
if (this.animationTimer) {
|
|
69
|
+
clearInterval(this.animationTimer);
|
|
70
|
+
this.animationTimer = undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
handleInput(data: string): void {
|
|
75
|
+
super.handleInput(data);
|
|
76
|
+
if (this.hasUltrathink()) {
|
|
77
|
+
this.startAnimation();
|
|
78
|
+
} else {
|
|
79
|
+
this.stopAnimation();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
render(width: number): string[] {
|
|
84
|
+
// Cycle: 10 shine positions + 10 pause frames
|
|
85
|
+
const cycle = this.frame % 20;
|
|
86
|
+
const shinePos = cycle < 10 ? cycle : -1; // -1 means no shine (pause)
|
|
87
|
+
return super.render(width).map((line) => line.replace(/ultrathink/gi, (m) => colorize(m, shinePos)));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export default function (pi: ExtensionAPI) {
|
|
92
|
+
pi.on("session_start", (_event, ctx) => {
|
|
93
|
+
ctx.ui.setEditorComponent((tui, theme, kb) => new RainbowEditor(tui, theme, kb));
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shutdown Command Extension
|
|
3
|
+
*
|
|
4
|
+
* Adds a /quit command that allows extensions to trigger clean shutdown.
|
|
5
|
+
* Demonstrates how extensions can use ctx.shutdown() to exit pi cleanly.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import { Type } from "@sinclair/typebox";
|
|
10
|
+
|
|
11
|
+
export default function (pi: ExtensionAPI) {
|
|
12
|
+
// Register a /quit command that cleanly exits pi
|
|
13
|
+
pi.registerCommand("quit", {
|
|
14
|
+
description: "Exit pi cleanly",
|
|
15
|
+
handler: async (_args, ctx) => {
|
|
16
|
+
ctx.shutdown();
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// You can also create a tool that shuts down after completing work
|
|
21
|
+
pi.registerTool({
|
|
22
|
+
name: "finish_and_exit",
|
|
23
|
+
label: "Finish and Exit",
|
|
24
|
+
description: "Complete a task and exit pi",
|
|
25
|
+
parameters: Type.Object({}),
|
|
26
|
+
async execute(_toolCallId, _params, _onUpdate, ctx, _signal) {
|
|
27
|
+
// Do any final work here...
|
|
28
|
+
// Request graceful shutdown (deferred until agent is idle)
|
|
29
|
+
ctx.shutdown();
|
|
30
|
+
|
|
31
|
+
// This return is sent to the LLM before shutdown occurs
|
|
32
|
+
return {
|
|
33
|
+
content: [{ type: "text", text: "Shutdown requested. Exiting after this response." }],
|
|
34
|
+
details: {},
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// You could also create a more complex tool with parameters
|
|
40
|
+
pi.registerTool({
|
|
41
|
+
name: "deploy_and_exit",
|
|
42
|
+
label: "Deploy and Exit",
|
|
43
|
+
description: "Deploy the application and exit pi",
|
|
44
|
+
parameters: Type.Object({
|
|
45
|
+
environment: Type.String({ description: "Target environment (e.g., production, staging)" }),
|
|
46
|
+
}),
|
|
47
|
+
async execute(_toolCallId, params, onUpdate, ctx, _signal) {
|
|
48
|
+
onUpdate?.({ content: [{ type: "text", text: `Deploying to ${params.environment}...` }], details: {} });
|
|
49
|
+
|
|
50
|
+
// Example deployment logic
|
|
51
|
+
// const result = await pi.exec("npm", ["run", "deploy", params.environment], { signal });
|
|
52
|
+
|
|
53
|
+
// On success, request graceful shutdown
|
|
54
|
+
onUpdate?.({ content: [{ type: "text", text: "Deployment complete, exiting..." }], details: {} });
|
|
55
|
+
ctx.shutdown();
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
content: [{ type: "text", text: "Done! Shutdown requested." }],
|
|
59
|
+
details: { environment: params.environment },
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -1,62 +1,69 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Example extension demonstrating
|
|
2
|
+
* Example extension demonstrating timed dialogs with live countdown.
|
|
3
3
|
*
|
|
4
4
|
* Commands:
|
|
5
|
-
* - /timed - Shows confirm dialog that auto-cancels after 5 seconds
|
|
6
|
-
* - /timed-select - Shows select dialog that auto-cancels after 10 seconds
|
|
5
|
+
* - /timed - Shows confirm dialog that auto-cancels after 5 seconds with countdown
|
|
6
|
+
* - /timed-select - Shows select dialog that auto-cancels after 10 seconds with countdown
|
|
7
|
+
* - /timed-signal - Shows confirm using AbortSignal (manual approach)
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
10
11
|
|
|
11
12
|
export default function (pi: ExtensionAPI) {
|
|
13
|
+
// Simple approach: use timeout option (recommended)
|
|
12
14
|
pi.registerCommand("timed", {
|
|
13
|
-
description: "Show a timed confirmation dialog (auto-cancels in 5s)",
|
|
15
|
+
description: "Show a timed confirmation dialog (auto-cancels in 5s with countdown)",
|
|
14
16
|
handler: async (_args, ctx) => {
|
|
15
|
-
const controller = new AbortController();
|
|
16
|
-
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
17
|
-
|
|
18
|
-
ctx.ui.notify("Dialog will auto-cancel in 5 seconds...", "info");
|
|
19
|
-
|
|
20
17
|
const confirmed = await ctx.ui.confirm(
|
|
21
18
|
"Timed Confirmation",
|
|
22
19
|
"This dialog will auto-cancel in 5 seconds. Confirm?",
|
|
23
|
-
{
|
|
20
|
+
{ timeout: 5000 },
|
|
24
21
|
);
|
|
25
22
|
|
|
26
|
-
clearTimeout(timeoutId);
|
|
27
|
-
|
|
28
23
|
if (confirmed) {
|
|
29
24
|
ctx.ui.notify("Confirmed by user!", "info");
|
|
30
|
-
} else if (controller.signal.aborted) {
|
|
31
|
-
ctx.ui.notify("Dialog timed out (auto-cancelled)", "warning");
|
|
32
25
|
} else {
|
|
33
|
-
ctx.ui.notify("Cancelled
|
|
26
|
+
ctx.ui.notify("Cancelled or timed out", "info");
|
|
34
27
|
}
|
|
35
28
|
},
|
|
36
29
|
});
|
|
37
30
|
|
|
38
31
|
pi.registerCommand("timed-select", {
|
|
39
|
-
description: "Show a timed select dialog (auto-cancels in 10s)",
|
|
32
|
+
description: "Show a timed select dialog (auto-cancels in 10s with countdown)",
|
|
33
|
+
handler: async (_args, ctx) => {
|
|
34
|
+
const choice = await ctx.ui.select("Pick an option", ["Option A", "Option B", "Option C"], { timeout: 10000 });
|
|
35
|
+
|
|
36
|
+
if (choice) {
|
|
37
|
+
ctx.ui.notify(`Selected: ${choice}`, "info");
|
|
38
|
+
} else {
|
|
39
|
+
ctx.ui.notify("Selection cancelled or timed out", "info");
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Manual approach: use AbortSignal for more control
|
|
45
|
+
pi.registerCommand("timed-signal", {
|
|
46
|
+
description: "Show a timed confirm using AbortSignal (manual approach)",
|
|
40
47
|
handler: async (_args, ctx) => {
|
|
41
48
|
const controller = new AbortController();
|
|
42
|
-
const timeoutId = setTimeout(() => controller.abort(),
|
|
49
|
+
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
43
50
|
|
|
44
|
-
ctx.ui.notify("
|
|
51
|
+
ctx.ui.notify("Dialog will auto-cancel in 5 seconds...", "info");
|
|
45
52
|
|
|
46
|
-
const
|
|
47
|
-
"
|
|
48
|
-
|
|
53
|
+
const confirmed = await ctx.ui.confirm(
|
|
54
|
+
"Timed Confirmation",
|
|
55
|
+
"This dialog will auto-cancel in 5 seconds. Confirm?",
|
|
49
56
|
{ signal: controller.signal },
|
|
50
57
|
);
|
|
51
58
|
|
|
52
59
|
clearTimeout(timeoutId);
|
|
53
60
|
|
|
54
|
-
if (
|
|
55
|
-
ctx.ui.notify(
|
|
61
|
+
if (confirmed) {
|
|
62
|
+
ctx.ui.notify("Confirmed by user!", "info");
|
|
56
63
|
} else if (controller.signal.aborted) {
|
|
57
|
-
ctx.ui.notify("
|
|
64
|
+
ctx.ui.notify("Dialog timed out (auto-cancelled)", "warning");
|
|
58
65
|
} else {
|
|
59
|
-
ctx.ui.notify("
|
|
66
|
+
ctx.ui.notify("Cancelled by user", "info");
|
|
60
67
|
}
|
|
61
68
|
},
|
|
62
69
|
});
|
|
@@ -291,7 +291,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
291
291
|
return;
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
-
await ctx.ui.custom<void>((_tui, theme, done) => {
|
|
294
|
+
await ctx.ui.custom<void>((_tui, theme, _kb, done) => {
|
|
295
295
|
return new TodoListComponent(todos, theme, () => done());
|
|
296
296
|
});
|
|
297
297
|
},
|
|
@@ -69,7 +69,7 @@ export default function toolsExtension(pi: ExtensionAPI) {
|
|
|
69
69
|
// Refresh tool list
|
|
70
70
|
allTools = pi.getAllTools();
|
|
71
71
|
|
|
72
|
-
await ctx.ui.custom((tui, theme, done) => {
|
|
72
|
+
await ctx.ui.custom((tui, theme, _kb, done) => {
|
|
73
73
|
// Build settings items for each tool
|
|
74
74
|
const items: SettingItem[] = allTools.map((tool) => ({
|
|
75
75
|
id: tool,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-extension-with-deps",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pi-extension-with-deps",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.2.0",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"ms": "^2.1.3"
|
|
12
12
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mariozechner/pi-coding-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"piConfig": {
|
|
@@ -38,10 +38,10 @@
|
|
|
38
38
|
"prepublishOnly": "npm run clean && npm run build"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@
|
|
42
|
-
"@mariozechner/pi-agent-core": "^0.
|
|
43
|
-
"@mariozechner/pi-ai": "^0.
|
|
44
|
-
"@mariozechner/pi-tui": "^0.
|
|
41
|
+
"@mariozechner/clipboard": "^0.3.0",
|
|
42
|
+
"@mariozechner/pi-agent-core": "^0.38.0",
|
|
43
|
+
"@mariozechner/pi-ai": "^0.38.0",
|
|
44
|
+
"@mariozechner/pi-tui": "^0.38.0",
|
|
45
45
|
"chalk": "^5.5.0",
|
|
46
46
|
"cli-highlight": "^2.1.11",
|
|
47
47
|
"diff": "^8.0.2",
|