@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.
Files changed (102) hide show
  1. package/CHANGELOG.md +84 -4
  2. package/README.md +10 -0
  3. package/dist/cli/args.d.ts +1 -0
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +4 -0
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/core/agent-session.d.ts.map +1 -1
  8. package/dist/core/agent-session.js +13 -1
  9. package/dist/core/agent-session.js.map +1 -1
  10. package/dist/core/extensions/index.d.ts +3 -3
  11. package/dist/core/extensions/index.d.ts.map +1 -1
  12. package/dist/core/extensions/index.js +1 -1
  13. package/dist/core/extensions/index.js.map +1 -1
  14. package/dist/core/extensions/loader.d.ts +8 -6
  15. package/dist/core/extensions/loader.d.ts.map +1 -1
  16. package/dist/core/extensions/loader.js +94 -211
  17. package/dist/core/extensions/loader.js.map +1 -1
  18. package/dist/core/extensions/runner.d.ts +24 -28
  19. package/dist/core/extensions/runner.d.ts.map +1 -1
  20. package/dist/core/extensions/runner.js +58 -38
  21. package/dist/core/extensions/runner.js.map +1 -1
  22. package/dist/core/extensions/types.d.ts +116 -27
  23. package/dist/core/extensions/types.d.ts.map +1 -1
  24. package/dist/core/extensions/types.js.map +1 -1
  25. package/dist/core/extensions/wrapper.d.ts +5 -3
  26. package/dist/core/extensions/wrapper.d.ts.map +1 -1
  27. package/dist/core/extensions/wrapper.js +6 -4
  28. package/dist/core/extensions/wrapper.js.map +1 -1
  29. package/dist/core/index.d.ts +1 -1
  30. package/dist/core/index.d.ts.map +1 -1
  31. package/dist/core/index.js.map +1 -1
  32. package/dist/core/model-resolver.d.ts +4 -2
  33. package/dist/core/model-resolver.d.ts.map +1 -1
  34. package/dist/core/model-resolver.js +8 -9
  35. package/dist/core/model-resolver.js.map +1 -1
  36. package/dist/core/sdk.d.ts +3 -3
  37. package/dist/core/sdk.d.ts.map +1 -1
  38. package/dist/core/sdk.js +19 -75
  39. package/dist/core/sdk.js.map +1 -1
  40. package/dist/core/settings-manager.d.ts +8 -0
  41. package/dist/core/settings-manager.d.ts.map +1 -1
  42. package/dist/core/settings-manager.js +9 -1
  43. package/dist/core/settings-manager.js.map +1 -1
  44. package/dist/index.d.ts +3 -2
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +3 -1
  47. package/dist/index.js.map +1 -1
  48. package/dist/main.d.ts.map +1 -1
  49. package/dist/main.js +47 -115
  50. package/dist/main.js.map +1 -1
  51. package/dist/modes/index.d.ts +2 -2
  52. package/dist/modes/index.d.ts.map +1 -1
  53. package/dist/modes/index.js.map +1 -1
  54. package/dist/modes/interactive/components/countdown-timer.d.ts +14 -0
  55. package/dist/modes/interactive/components/countdown-timer.d.ts.map +1 -0
  56. package/dist/modes/interactive/components/countdown-timer.js +33 -0
  57. package/dist/modes/interactive/components/countdown-timer.js.map +1 -0
  58. package/dist/modes/interactive/components/custom-editor.d.ts +1 -1
  59. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  60. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  61. package/dist/modes/interactive/components/extension-input.d.ts +10 -2
  62. package/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
  63. package/dist/modes/interactive/components/extension-input.js +18 -14
  64. package/dist/modes/interactive/components/extension-input.js.map +1 -1
  65. package/dist/modes/interactive/components/extension-selector.d.ts +10 -2
  66. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  67. package/dist/modes/interactive/components/extension-selector.js +18 -22
  68. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  69. package/dist/modes/interactive/interactive-mode.d.ts +44 -3
  70. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  71. package/dist/modes/interactive/interactive-mode.js +289 -95
  72. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  73. package/dist/modes/print-mode.d.ts +14 -7
  74. package/dist/modes/print-mode.d.ts.map +1 -1
  75. package/dist/modes/print-mode.js +45 -21
  76. package/dist/modes/print-mode.js.map +1 -1
  77. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  78. package/dist/modes/rpc/rpc-mode.js +101 -101
  79. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  80. package/dist/modes/rpc/rpc-types.d.ts +3 -0
  81. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  82. package/dist/modes/rpc/rpc-types.js.map +1 -1
  83. package/dist/utils/clipboard-image.d.ts.map +1 -1
  84. package/dist/utils/clipboard-image.js +1 -1
  85. package/dist/utils/clipboard-image.js.map +1 -1
  86. package/docs/extensions.md +110 -9
  87. package/docs/sdk.md +65 -6
  88. package/docs/tui.md +81 -4
  89. package/examples/extensions/README.md +1 -0
  90. package/examples/extensions/handoff.ts +1 -1
  91. package/examples/extensions/modal-editor.ts +85 -0
  92. package/examples/extensions/preset.ts +1 -1
  93. package/examples/extensions/qna.ts +1 -1
  94. package/examples/extensions/rainbow-editor.ts +95 -0
  95. package/examples/extensions/shutdown-command.ts +63 -0
  96. package/examples/extensions/snake.ts +1 -1
  97. package/examples/extensions/timed-confirm.ts +32 -25
  98. package/examples/extensions/todo.ts +1 -1
  99. package/examples/extensions/tools.ts +1 -1
  100. package/examples/extensions/with-deps/package-lock.json +2 -2
  101. package/examples/extensions/with-deps/package.json +1 -1
  102. 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
- // Custom tools (for UI setup)
788
- customToolsResult: {
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, use RPC mode instead of the SDK:
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
+ }
@@ -327,7 +327,7 @@ export default function (pi: ExtensionAPI) {
327
327
  }
328
328
  }
329
329
 
330
- await ctx.ui.custom((tui, _theme, done) => {
330
+ await ctx.ui.custom((tui, _theme, _kb, done) => {
331
331
  return new SnakeComponent(
332
332
  tui,
333
333
  () => done(undefined),
@@ -1,62 +1,69 @@
1
1
  /**
2
- * Example extension demonstrating AbortSignal for auto-dismissing dialogs.
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
- { signal: controller.signal },
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 by user", "info");
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(), 10000);
49
+ const timeoutId = setTimeout(() => controller.abort(), 5000);
43
50
 
44
- ctx.ui.notify("Select dialog will auto-cancel in 10 seconds...", "info");
51
+ ctx.ui.notify("Dialog will auto-cancel in 5 seconds...", "info");
45
52
 
46
- const choice = await ctx.ui.select(
47
- "Pick an option (auto-cancels in 10s)",
48
- ["Option A", "Option B", "Option C"],
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 (choice) {
55
- ctx.ui.notify(`Selected: ${choice}`, "info");
61
+ if (confirmed) {
62
+ ctx.ui.notify("Confirmed by user!", "info");
56
63
  } else if (controller.signal.aborted) {
57
- ctx.ui.notify("Selection timed out", "warning");
64
+ ctx.ui.notify("Dialog timed out (auto-cancelled)", "warning");
58
65
  } else {
59
- ctx.ui.notify("Selection cancelled", "info");
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.1.8",
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.1.8",
9
+ "version": "1.2.0",
10
10
  "dependencies": {
11
11
  "ms": "^2.1.3"
12
12
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-extension-with-deps",
3
3
  "private": true,
4
- "version": "1.1.8",
4
+ "version": "1.2.0",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mariozechner/pi-coding-agent",
3
- "version": "0.37.8",
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
- "@crosscopy/clipboard": "^0.2.8",
42
- "@mariozechner/pi-agent-core": "^0.37.8",
43
- "@mariozechner/pi-ai": "^0.37.8",
44
- "@mariozechner/pi-tui": "^0.37.8",
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",