@mariozechner/pi-coding-agent 0.37.4 → 0.37.5
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 +15 -0
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +33 -3
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +4 -1
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +3 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +13 -1
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +1 -1
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/index.d.ts +1 -1
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +1 -0
- package/dist/core/tools/index.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/modes/interactive/components/index.d.ts +28 -0
- package/dist/modes/interactive/components/index.d.ts.map +1 -0
- package/dist/modes/interactive/components/index.js +29 -0
- package/dist/modes/interactive/components/index.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +9 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +9 -0
- 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 +9 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/docs/extensions.md +149 -9
- package/docs/tui.md +220 -2
- package/examples/extensions/README.md +1 -0
- package/examples/extensions/preset.ts +398 -0
- package/examples/extensions/truncated-tool.ts +192 -0
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
package/docs/extensions.md
CHANGED
|
@@ -306,6 +306,8 @@ pi.on("session_start", async (_event, ctx) => {
|
|
|
306
306
|
});
|
|
307
307
|
```
|
|
308
308
|
|
|
309
|
+
**Examples:** [claude-rules.ts](../examples/extensions/claude-rules.ts), [custom-header.ts](../examples/extensions/custom-header.ts), [file-trigger.ts](../examples/extensions/file-trigger.ts), [status-line.ts](../examples/extensions/status-line.ts), [todo.ts](../examples/extensions/todo.ts), [tools.ts](../examples/extensions/tools.ts)
|
|
310
|
+
|
|
309
311
|
#### session_before_switch / session_switch
|
|
310
312
|
|
|
311
313
|
Fired when starting a new session (`/new`) or switching sessions (`/resume`).
|
|
@@ -327,6 +329,8 @@ pi.on("session_switch", async (event, ctx) => {
|
|
|
327
329
|
});
|
|
328
330
|
```
|
|
329
331
|
|
|
332
|
+
**Examples:** [confirm-destructive.ts](../examples/extensions/confirm-destructive.ts), [dirty-repo-guard.ts](../examples/extensions/dirty-repo-guard.ts), [status-line.ts](../examples/extensions/status-line.ts), [todo.ts](../examples/extensions/todo.ts)
|
|
333
|
+
|
|
330
334
|
#### session_before_branch / session_branch
|
|
331
335
|
|
|
332
336
|
Fired when branching via `/branch`.
|
|
@@ -344,6 +348,8 @@ pi.on("session_branch", async (event, ctx) => {
|
|
|
344
348
|
});
|
|
345
349
|
```
|
|
346
350
|
|
|
351
|
+
**Examples:** [confirm-destructive.ts](../examples/extensions/confirm-destructive.ts), [dirty-repo-guard.ts](../examples/extensions/dirty-repo-guard.ts), [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [todo.ts](../examples/extensions/todo.ts), [tools.ts](../examples/extensions/tools.ts)
|
|
352
|
+
|
|
347
353
|
#### session_before_compact / session_compact
|
|
348
354
|
|
|
349
355
|
Fired on compaction. See [compaction.md](compaction.md) for details.
|
|
@@ -371,6 +377,8 @@ pi.on("session_compact", async (event, ctx) => {
|
|
|
371
377
|
});
|
|
372
378
|
```
|
|
373
379
|
|
|
380
|
+
**Examples:** [custom-compaction.ts](../examples/extensions/custom-compaction.ts)
|
|
381
|
+
|
|
374
382
|
#### session_before_tree / session_tree
|
|
375
383
|
|
|
376
384
|
Fired on `/tree` navigation.
|
|
@@ -388,6 +396,8 @@ pi.on("session_tree", async (event, ctx) => {
|
|
|
388
396
|
});
|
|
389
397
|
```
|
|
390
398
|
|
|
399
|
+
**Examples:** [todo.ts](../examples/extensions/todo.ts), [tools.ts](../examples/extensions/tools.ts)
|
|
400
|
+
|
|
391
401
|
#### session_shutdown
|
|
392
402
|
|
|
393
403
|
Fired on exit (Ctrl+C, Ctrl+D, SIGTERM).
|
|
@@ -398,6 +408,8 @@ pi.on("session_shutdown", async (_event, ctx) => {
|
|
|
398
408
|
});
|
|
399
409
|
```
|
|
400
410
|
|
|
411
|
+
**Examples:** [auto-commit-on-exit.ts](../examples/extensions/auto-commit-on-exit.ts)
|
|
412
|
+
|
|
401
413
|
### Agent Events
|
|
402
414
|
|
|
403
415
|
#### before_agent_start
|
|
@@ -422,6 +434,8 @@ pi.on("before_agent_start", async (event, ctx) => {
|
|
|
422
434
|
});
|
|
423
435
|
```
|
|
424
436
|
|
|
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
|
+
|
|
425
439
|
#### agent_start / agent_end
|
|
426
440
|
|
|
427
441
|
Fired once per user prompt.
|
|
@@ -434,6 +448,8 @@ pi.on("agent_end", async (event, ctx) => {
|
|
|
434
448
|
});
|
|
435
449
|
```
|
|
436
450
|
|
|
451
|
+
**Examples:** [chalk-logger.ts](../examples/extensions/chalk-logger.ts), [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts)
|
|
452
|
+
|
|
437
453
|
#### turn_start / turn_end
|
|
438
454
|
|
|
439
455
|
Fired for each turn (one LLM response + tool calls).
|
|
@@ -448,6 +464,8 @@ pi.on("turn_end", async (event, ctx) => {
|
|
|
448
464
|
});
|
|
449
465
|
```
|
|
450
466
|
|
|
467
|
+
**Examples:** [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [status-line.ts](../examples/extensions/status-line.ts)
|
|
468
|
+
|
|
451
469
|
#### context
|
|
452
470
|
|
|
453
471
|
Fired before each LLM call. Modify messages non-destructively.
|
|
@@ -460,6 +478,8 @@ pi.on("context", async (event, ctx) => {
|
|
|
460
478
|
});
|
|
461
479
|
```
|
|
462
480
|
|
|
481
|
+
**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts)
|
|
482
|
+
|
|
463
483
|
### Tool Events
|
|
464
484
|
|
|
465
485
|
#### tool_call
|
|
@@ -478,6 +498,8 @@ pi.on("tool_call", async (event, ctx) => {
|
|
|
478
498
|
});
|
|
479
499
|
```
|
|
480
500
|
|
|
501
|
+
**Examples:** [chalk-logger.ts](../examples/extensions/chalk-logger.ts), [permission-gate.ts](../examples/extensions/permission-gate.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [protected-paths.ts](../examples/extensions/protected-paths.ts)
|
|
502
|
+
|
|
481
503
|
#### tool_result
|
|
482
504
|
|
|
483
505
|
Fired after tool executes. **Can modify result.**
|
|
@@ -498,6 +520,8 @@ pi.on("tool_result", async (event, ctx) => {
|
|
|
498
520
|
});
|
|
499
521
|
```
|
|
500
522
|
|
|
523
|
+
**Examples:** [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts)
|
|
524
|
+
|
|
501
525
|
## ExtensionContext
|
|
502
526
|
|
|
503
527
|
Every handler receives `ctx: ExtensionContext`:
|
|
@@ -595,7 +619,7 @@ const result = await ctx.navigateTree("entry-id-456", {
|
|
|
595
619
|
|
|
596
620
|
### pi.on(event, handler)
|
|
597
621
|
|
|
598
|
-
Subscribe to events. See [Events](#events).
|
|
622
|
+
Subscribe to events. See [Events](#events) for event types and return values.
|
|
599
623
|
|
|
600
624
|
### pi.registerTool(definition)
|
|
601
625
|
|
|
@@ -630,9 +654,11 @@ pi.registerTool({
|
|
|
630
654
|
});
|
|
631
655
|
```
|
|
632
656
|
|
|
657
|
+
**Examples:** [hello.ts](../examples/extensions/hello.ts), [question.ts](../examples/extensions/question.ts), [todo.ts](../examples/extensions/todo.ts), [truncated-tool.ts](../examples/extensions/truncated-tool.ts)
|
|
658
|
+
|
|
633
659
|
### pi.sendMessage(message, options?)
|
|
634
660
|
|
|
635
|
-
Inject a custom message into the session
|
|
661
|
+
Inject a custom message into the session.
|
|
636
662
|
|
|
637
663
|
```typescript
|
|
638
664
|
pi.sendMessage({
|
|
@@ -653,6 +679,8 @@ pi.sendMessage({
|
|
|
653
679
|
- `"nextTurn"` - Queued for next user prompt. Does not interrupt or trigger anything.
|
|
654
680
|
- `triggerTurn: true` - If agent is idle, trigger an LLM response immediately. Only applies to `"steer"` and `"followUp"` modes (ignored for `"nextTurn"`).
|
|
655
681
|
|
|
682
|
+
**Examples:** [file-trigger.ts](../examples/extensions/file-trigger.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts)
|
|
683
|
+
|
|
656
684
|
### pi.sendUserMessage(content, options?)
|
|
657
685
|
|
|
658
686
|
Send a user message to the agent. Unlike `sendMessage()` which sends custom messages, this sends an actual user message that appears as if typed by the user. Always triggers a turn.
|
|
@@ -683,7 +711,7 @@ See [send-user-message.ts](../examples/extensions/send-user-message.ts) for a co
|
|
|
683
711
|
|
|
684
712
|
### pi.appendEntry(customType, data?)
|
|
685
713
|
|
|
686
|
-
Persist extension state (does NOT participate in LLM context)
|
|
714
|
+
Persist extension state (does NOT participate in LLM context).
|
|
687
715
|
|
|
688
716
|
```typescript
|
|
689
717
|
pi.appendEntry("my-state", { count: 42 });
|
|
@@ -698,9 +726,11 @@ pi.on("session_start", async (_event, ctx) => {
|
|
|
698
726
|
});
|
|
699
727
|
```
|
|
700
728
|
|
|
729
|
+
**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts), [snake.ts](../examples/extensions/snake.ts), [tools.ts](../examples/extensions/tools.ts)
|
|
730
|
+
|
|
701
731
|
### pi.registerCommand(name, options)
|
|
702
732
|
|
|
703
|
-
Register a command
|
|
733
|
+
Register a command.
|
|
704
734
|
|
|
705
735
|
```typescript
|
|
706
736
|
pi.registerCommand("stats", {
|
|
@@ -712,13 +742,15 @@ pi.registerCommand("stats", {
|
|
|
712
742
|
});
|
|
713
743
|
```
|
|
714
744
|
|
|
745
|
+
**Examples:** [custom-footer.ts](../examples/extensions/custom-footer.ts), [custom-header.ts](../examples/extensions/custom-header.ts), [handoff.ts](../examples/extensions/handoff.ts), [pirate.ts](../examples/extensions/pirate.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts), [qna.ts](../examples/extensions/qna.ts), [send-user-message.ts](../examples/extensions/send-user-message.ts), [snake.ts](../examples/extensions/snake.ts), [todo.ts](../examples/extensions/todo.ts), [tools.ts](../examples/extensions/tools.ts)
|
|
746
|
+
|
|
715
747
|
### pi.registerMessageRenderer(customType, renderer)
|
|
716
748
|
|
|
717
749
|
Register a custom TUI renderer for messages with your `customType`. See [Custom UI](#custom-ui).
|
|
718
750
|
|
|
719
751
|
### pi.registerShortcut(shortcut, options)
|
|
720
752
|
|
|
721
|
-
Register a keyboard shortcut
|
|
753
|
+
Register a keyboard shortcut.
|
|
722
754
|
|
|
723
755
|
```typescript
|
|
724
756
|
pi.registerShortcut("ctrl+shift+p", {
|
|
@@ -729,9 +761,11 @@ pi.registerShortcut("ctrl+shift+p", {
|
|
|
729
761
|
});
|
|
730
762
|
```
|
|
731
763
|
|
|
764
|
+
**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts)
|
|
765
|
+
|
|
732
766
|
### pi.registerFlag(name, options)
|
|
733
767
|
|
|
734
|
-
Register a CLI flag
|
|
768
|
+
Register a CLI flag.
|
|
735
769
|
|
|
736
770
|
```typescript
|
|
737
771
|
pi.registerFlag("--plan", {
|
|
@@ -746,24 +780,57 @@ if (pi.getFlag("--plan")) {
|
|
|
746
780
|
}
|
|
747
781
|
```
|
|
748
782
|
|
|
783
|
+
**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts)
|
|
784
|
+
|
|
749
785
|
### pi.exec(command, args, options?)
|
|
750
786
|
|
|
751
|
-
Execute a shell command
|
|
787
|
+
Execute a shell command.
|
|
752
788
|
|
|
753
789
|
```typescript
|
|
754
790
|
const result = await pi.exec("git", ["status"], { signal, timeout: 5000 });
|
|
755
791
|
// result.stdout, result.stderr, result.code, result.killed
|
|
756
792
|
```
|
|
757
793
|
|
|
794
|
+
**Examples:** [auto-commit-on-exit.ts](../examples/extensions/auto-commit-on-exit.ts), [dirty-repo-guard.ts](../examples/extensions/dirty-repo-guard.ts), [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts)
|
|
795
|
+
|
|
758
796
|
### pi.getActiveTools() / pi.getAllTools() / pi.setActiveTools(names)
|
|
759
797
|
|
|
760
|
-
Manage active tools
|
|
798
|
+
Manage active tools.
|
|
761
799
|
|
|
762
800
|
```typescript
|
|
763
801
|
const active = pi.getActiveTools(); // ["read", "bash", "edit", "write"]
|
|
764
802
|
pi.setActiveTools(["read", "bash"]); // Switch to read-only
|
|
765
803
|
```
|
|
766
804
|
|
|
805
|
+
**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts), [tools.ts](../examples/extensions/tools.ts)
|
|
806
|
+
|
|
807
|
+
### pi.setModel(model)
|
|
808
|
+
|
|
809
|
+
Set the current model. Returns `false` if no API key is available for the model.
|
|
810
|
+
|
|
811
|
+
```typescript
|
|
812
|
+
const model = ctx.modelRegistry.find("anthropic", "claude-sonnet-4-5");
|
|
813
|
+
if (model) {
|
|
814
|
+
const success = await pi.setModel(model);
|
|
815
|
+
if (!success) {
|
|
816
|
+
ctx.ui.notify("No API key for this model", "error");
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
**Examples:** [preset.ts](../examples/extensions/preset.ts)
|
|
822
|
+
|
|
823
|
+
### pi.getThinkingLevel() / pi.setThinkingLevel(level)
|
|
824
|
+
|
|
825
|
+
Get or set the thinking level. Level is clamped to model capabilities (non-reasoning models always use "off").
|
|
826
|
+
|
|
827
|
+
```typescript
|
|
828
|
+
const current = pi.getThinkingLevel(); // "off" | "minimal" | "low" | "medium" | "high" | "xhigh"
|
|
829
|
+
pi.setThinkingLevel("high");
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
**Examples:** [preset.ts](../examples/extensions/preset.ts)
|
|
833
|
+
|
|
767
834
|
### pi.events
|
|
768
835
|
|
|
769
836
|
Shared event bus for communication between extensions:
|
|
@@ -857,6 +924,57 @@ pi.registerTool({
|
|
|
857
924
|
|
|
858
925
|
**Important:** Use `StringEnum` from `@mariozechner/pi-ai` for string enums. `Type.Union`/`Type.Literal` doesn't work with Google's API.
|
|
859
926
|
|
|
927
|
+
### Output Truncation
|
|
928
|
+
|
|
929
|
+
**Tools MUST truncate their output** to avoid overwhelming the LLM context. Large outputs can cause:
|
|
930
|
+
- Context overflow errors (prompt too long)
|
|
931
|
+
- Compaction failures
|
|
932
|
+
- Degraded model performance
|
|
933
|
+
|
|
934
|
+
The built-in limit is **50KB** (~10k tokens) and **2000 lines**, whichever is hit first. Use the exported truncation utilities:
|
|
935
|
+
|
|
936
|
+
```typescript
|
|
937
|
+
import {
|
|
938
|
+
truncateHead, // Keep first N lines/bytes (good for file reads, search results)
|
|
939
|
+
truncateTail, // Keep last N lines/bytes (good for logs, command output)
|
|
940
|
+
formatSize, // Human-readable size (e.g., "50KB", "1.5MB")
|
|
941
|
+
DEFAULT_MAX_BYTES, // 50KB
|
|
942
|
+
DEFAULT_MAX_LINES, // 2000
|
|
943
|
+
} from "@mariozechner/pi-coding-agent";
|
|
944
|
+
|
|
945
|
+
async execute(toolCallId, params, onUpdate, ctx, signal) {
|
|
946
|
+
const output = await runCommand();
|
|
947
|
+
|
|
948
|
+
// Apply truncation
|
|
949
|
+
const truncation = truncateHead(output, {
|
|
950
|
+
maxLines: DEFAULT_MAX_LINES,
|
|
951
|
+
maxBytes: DEFAULT_MAX_BYTES,
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
let result = truncation.content;
|
|
955
|
+
|
|
956
|
+
if (truncation.truncated) {
|
|
957
|
+
// Write full output to temp file
|
|
958
|
+
const tempFile = writeTempFile(output);
|
|
959
|
+
|
|
960
|
+
// Inform the LLM where to find complete output
|
|
961
|
+
result += `\n\n[Output truncated: ${truncation.outputLines} of ${truncation.totalLines} lines`;
|
|
962
|
+
result += ` (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}).`;
|
|
963
|
+
result += ` Full output saved to: ${tempFile}]`;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
return { content: [{ type: "text", text: result }] };
|
|
967
|
+
}
|
|
968
|
+
```
|
|
969
|
+
|
|
970
|
+
**Key points:**
|
|
971
|
+
- Use `truncateHead` for content where the beginning matters (search results, file reads)
|
|
972
|
+
- Use `truncateTail` for content where the end matters (logs, command output)
|
|
973
|
+
- Always inform the LLM when output is truncated and where to find the full version
|
|
974
|
+
- Document the truncation limits in your tool's description
|
|
975
|
+
|
|
976
|
+
See [examples/extensions/truncated-tool.ts](../examples/extensions/truncated-tool.ts) for a complete example wrapping `rg` (ripgrep) with proper truncation.
|
|
977
|
+
|
|
860
978
|
### Multiple Tools
|
|
861
979
|
|
|
862
980
|
One extension can register multiple tools with shared state:
|
|
@@ -943,6 +1061,14 @@ If `renderCall`/`renderResult` is not defined or throws:
|
|
|
943
1061
|
|
|
944
1062
|
Extensions can interact with users via `ctx.ui` methods and customize how messages/tools render.
|
|
945
1063
|
|
|
1064
|
+
**For custom components, see [tui.md](tui.md)** which has copy-paste patterns for:
|
|
1065
|
+
- Selection dialogs (SelectList)
|
|
1066
|
+
- Async operations with cancel (BorderedLoader)
|
|
1067
|
+
- Settings toggles (SettingsList)
|
|
1068
|
+
- Status indicators (setStatus)
|
|
1069
|
+
- Widgets above editor (setWidget)
|
|
1070
|
+
- Custom footers (setFooter)
|
|
1071
|
+
|
|
946
1072
|
### Dialogs
|
|
947
1073
|
|
|
948
1074
|
```typescript
|
|
@@ -962,6 +1088,12 @@ const text = await ctx.ui.editor("Edit:", "prefilled text");
|
|
|
962
1088
|
ctx.ui.notify("Done!", "info"); // "info" | "warning" | "error"
|
|
963
1089
|
```
|
|
964
1090
|
|
|
1091
|
+
**Examples:**
|
|
1092
|
+
- `ctx.ui.select()`: [confirm-destructive.ts](../examples/extensions/confirm-destructive.ts), [dirty-repo-guard.ts](../examples/extensions/dirty-repo-guard.ts), [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [permission-gate.ts](../examples/extensions/permission-gate.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [question.ts](../examples/extensions/question.ts)
|
|
1093
|
+
- `ctx.ui.confirm()`: [confirm-destructive.ts](../examples/extensions/confirm-destructive.ts)
|
|
1094
|
+
- `ctx.ui.editor()`: [handoff.ts](../examples/extensions/handoff.ts)
|
|
1095
|
+
- `ctx.ui.setEditorText()`: [handoff.ts](../examples/extensions/handoff.ts), [qna.ts](../examples/extensions/qna.ts)
|
|
1096
|
+
|
|
965
1097
|
### Widgets, Status, and Footer
|
|
966
1098
|
|
|
967
1099
|
```typescript
|
|
@@ -989,6 +1121,12 @@ ctx.ui.setEditorText("Prefill text");
|
|
|
989
1121
|
const current = ctx.ui.getEditorText();
|
|
990
1122
|
```
|
|
991
1123
|
|
|
1124
|
+
**Examples:**
|
|
1125
|
+
- `ctx.ui.setStatus()`: [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts), [status-line.ts](../examples/extensions/status-line.ts)
|
|
1126
|
+
- `ctx.ui.setWidget()`: [plan-mode.ts](../examples/extensions/plan-mode.ts)
|
|
1127
|
+
- `ctx.ui.setFooter()`: [custom-footer.ts](../examples/extensions/custom-footer.ts)
|
|
1128
|
+
- `ctx.ui.setHeader()`: [custom-header.ts](../examples/extensions/custom-header.ts)
|
|
1129
|
+
|
|
992
1130
|
### Custom Components
|
|
993
1131
|
|
|
994
1132
|
For complex UI, use `ctx.ui.custom()`. This temporarily replaces the editor with your component until `done()` is called:
|
|
@@ -1018,7 +1156,9 @@ The callback receives:
|
|
|
1018
1156
|
- `theme` - Current theme for styling
|
|
1019
1157
|
- `done(value)` - Call to close component and return value
|
|
1020
1158
|
|
|
1021
|
-
See [tui.md](tui.md) for the full component API
|
|
1159
|
+
See [tui.md](tui.md) for the full component API.
|
|
1160
|
+
|
|
1161
|
+
**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)
|
|
1022
1162
|
|
|
1023
1163
|
### Message Rendering
|
|
1024
1164
|
|
package/docs/tui.md
CHANGED
|
@@ -340,7 +340,225 @@ class CachedComponent {
|
|
|
340
340
|
|
|
341
341
|
Call `invalidate()` when state changes, then `handle.requestRender()` to trigger re-render.
|
|
342
342
|
|
|
343
|
+
## Common Patterns
|
|
344
|
+
|
|
345
|
+
These patterns cover the most common UI needs in extensions. **Copy these patterns instead of building from scratch.**
|
|
346
|
+
|
|
347
|
+
### Pattern 1: Selection Dialog (SelectList)
|
|
348
|
+
|
|
349
|
+
For letting users pick from a list of options. Use `SelectList` from `@mariozechner/pi-tui` with `DynamicBorder` for framing.
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
353
|
+
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
|
354
|
+
import { Container, type SelectItem, SelectList, Text } from "@mariozechner/pi-tui";
|
|
355
|
+
|
|
356
|
+
pi.registerCommand("pick", {
|
|
357
|
+
handler: async (_args, ctx) => {
|
|
358
|
+
const items: SelectItem[] = [
|
|
359
|
+
{ value: "opt1", label: "Option 1", description: "First option" },
|
|
360
|
+
{ value: "opt2", label: "Option 2", description: "Second option" },
|
|
361
|
+
{ value: "opt3", label: "Option 3" }, // description is optional
|
|
362
|
+
];
|
|
363
|
+
|
|
364
|
+
const result = await ctx.ui.custom<string | null>((tui, theme, done) => {
|
|
365
|
+
const container = new Container();
|
|
366
|
+
|
|
367
|
+
// Top border
|
|
368
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
369
|
+
|
|
370
|
+
// Title
|
|
371
|
+
container.addChild(new Text(theme.fg("accent", theme.bold("Pick an Option")), 1, 0));
|
|
372
|
+
|
|
373
|
+
// SelectList with theme
|
|
374
|
+
const selectList = new SelectList(items, Math.min(items.length, 10), {
|
|
375
|
+
selectedPrefix: (t) => theme.fg("accent", t),
|
|
376
|
+
selectedText: (t) => theme.fg("accent", t),
|
|
377
|
+
description: (t) => theme.fg("muted", t),
|
|
378
|
+
scrollInfo: (t) => theme.fg("dim", t),
|
|
379
|
+
noMatch: (t) => theme.fg("warning", t),
|
|
380
|
+
});
|
|
381
|
+
selectList.onSelect = (item) => done(item.value);
|
|
382
|
+
selectList.onCancel = () => done(null);
|
|
383
|
+
container.addChild(selectList);
|
|
384
|
+
|
|
385
|
+
// Help text
|
|
386
|
+
container.addChild(new Text(theme.fg("dim", "↑↓ navigate • enter select • esc cancel"), 1, 0));
|
|
387
|
+
|
|
388
|
+
// Bottom border
|
|
389
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
render: (w) => container.render(w),
|
|
393
|
+
invalidate: () => container.invalidate(),
|
|
394
|
+
handleInput: (data) => { selectList.handleInput(data); tui.requestRender(); },
|
|
395
|
+
};
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
if (result) {
|
|
399
|
+
ctx.ui.notify(`Selected: ${result}`, "info");
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
**Examples:** [preset.ts](../examples/extensions/preset.ts), [tools.ts](../examples/extensions/tools.ts)
|
|
406
|
+
|
|
407
|
+
### Pattern 2: Async Operation with Cancel (BorderedLoader)
|
|
408
|
+
|
|
409
|
+
For operations that take time and should be cancellable. `BorderedLoader` shows a spinner and handles escape to cancel.
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
import { BorderedLoader } from "@mariozechner/pi-coding-agent";
|
|
413
|
+
|
|
414
|
+
pi.registerCommand("fetch", {
|
|
415
|
+
handler: async (_args, ctx) => {
|
|
416
|
+
const result = await ctx.ui.custom<string | null>((tui, theme, done) => {
|
|
417
|
+
const loader = new BorderedLoader(tui, theme, "Fetching data...");
|
|
418
|
+
loader.onAbort = () => done(null);
|
|
419
|
+
|
|
420
|
+
// Do async work
|
|
421
|
+
fetchData(loader.signal)
|
|
422
|
+
.then((data) => done(data))
|
|
423
|
+
.catch(() => done(null));
|
|
424
|
+
|
|
425
|
+
return loader;
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
if (result === null) {
|
|
429
|
+
ctx.ui.notify("Cancelled", "info");
|
|
430
|
+
} else {
|
|
431
|
+
ctx.ui.setEditorText(result);
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
**Examples:** [qna.ts](../examples/extensions/qna.ts), [handoff.ts](../examples/extensions/handoff.ts)
|
|
438
|
+
|
|
439
|
+
### Pattern 3: Settings/Toggles (SettingsList)
|
|
440
|
+
|
|
441
|
+
For toggling multiple settings. Use `SettingsList` from `@mariozechner/pi-tui` with `getSettingsListTheme()`.
|
|
442
|
+
|
|
443
|
+
```typescript
|
|
444
|
+
import { getSettingsListTheme } from "@mariozechner/pi-coding-agent";
|
|
445
|
+
import { Container, type SettingItem, SettingsList, Text } from "@mariozechner/pi-tui";
|
|
446
|
+
|
|
447
|
+
pi.registerCommand("settings", {
|
|
448
|
+
handler: async (_args, ctx) => {
|
|
449
|
+
const items: SettingItem[] = [
|
|
450
|
+
{ id: "verbose", label: "Verbose mode", currentValue: "off", values: ["on", "off"] },
|
|
451
|
+
{ id: "color", label: "Color output", currentValue: "on", values: ["on", "off"] },
|
|
452
|
+
];
|
|
453
|
+
|
|
454
|
+
await ctx.ui.custom((_tui, theme, done) => {
|
|
455
|
+
const container = new Container();
|
|
456
|
+
container.addChild(new Text(theme.fg("accent", theme.bold("Settings")), 1, 1));
|
|
457
|
+
|
|
458
|
+
const settingsList = new SettingsList(
|
|
459
|
+
items,
|
|
460
|
+
Math.min(items.length + 2, 15),
|
|
461
|
+
getSettingsListTheme(),
|
|
462
|
+
(id, newValue) => {
|
|
463
|
+
// Handle value change
|
|
464
|
+
ctx.ui.notify(`${id} = ${newValue}`, "info");
|
|
465
|
+
},
|
|
466
|
+
() => done(undefined), // On close
|
|
467
|
+
);
|
|
468
|
+
container.addChild(settingsList);
|
|
469
|
+
|
|
470
|
+
return {
|
|
471
|
+
render: (w) => container.render(w),
|
|
472
|
+
invalidate: () => container.invalidate(),
|
|
473
|
+
handleInput: (data) => settingsList.handleInput?.(data),
|
|
474
|
+
};
|
|
475
|
+
});
|
|
476
|
+
},
|
|
477
|
+
});
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
**Examples:** [tools.ts](../examples/extensions/tools.ts)
|
|
481
|
+
|
|
482
|
+
### Pattern 4: Persistent Status Indicator
|
|
483
|
+
|
|
484
|
+
Show status in the footer that persists across renders. Good for mode indicators.
|
|
485
|
+
|
|
486
|
+
```typescript
|
|
487
|
+
// Set status (shown in footer)
|
|
488
|
+
ctx.ui.setStatus("my-ext", ctx.ui.theme.fg("accent", "● active"));
|
|
489
|
+
|
|
490
|
+
// Clear status
|
|
491
|
+
ctx.ui.setStatus("my-ext", undefined);
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
**Examples:** [status-line.ts](../examples/extensions/status-line.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts)
|
|
495
|
+
|
|
496
|
+
### Pattern 5: Widget Above Editor
|
|
497
|
+
|
|
498
|
+
Show persistent content above the input editor. Good for todo lists, progress.
|
|
499
|
+
|
|
500
|
+
```typescript
|
|
501
|
+
// Simple string array
|
|
502
|
+
ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"]);
|
|
503
|
+
|
|
504
|
+
// Or with theme
|
|
505
|
+
ctx.ui.setWidget("my-widget", (_tui, theme) => {
|
|
506
|
+
const lines = items.map((item, i) =>
|
|
507
|
+
item.done
|
|
508
|
+
? theme.fg("success", "✓ ") + theme.fg("muted", item.text)
|
|
509
|
+
: theme.fg("dim", "○ ") + item.text
|
|
510
|
+
);
|
|
511
|
+
return {
|
|
512
|
+
render: () => lines,
|
|
513
|
+
invalidate: () => {},
|
|
514
|
+
};
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// Clear
|
|
518
|
+
ctx.ui.setWidget("my-widget", undefined);
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts)
|
|
522
|
+
|
|
523
|
+
### Pattern 6: Custom Footer
|
|
524
|
+
|
|
525
|
+
Replace the entire footer with custom content.
|
|
526
|
+
|
|
527
|
+
```typescript
|
|
528
|
+
ctx.ui.setFooter((_tui, theme) => ({
|
|
529
|
+
render(width: number): string[] {
|
|
530
|
+
const left = theme.fg("dim", "custom footer");
|
|
531
|
+
const right = theme.fg("accent", "status");
|
|
532
|
+
const padding = " ".repeat(Math.max(1, width - visibleWidth(left) - visibleWidth(right)));
|
|
533
|
+
return [truncateToWidth(left + padding + right, width)];
|
|
534
|
+
},
|
|
535
|
+
invalidate() {},
|
|
536
|
+
}));
|
|
537
|
+
|
|
538
|
+
// Restore default
|
|
539
|
+
ctx.ui.setFooter(undefined);
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
**Examples:** [custom-footer.ts](../examples/extensions/custom-footer.ts)
|
|
543
|
+
|
|
544
|
+
## Key Rules
|
|
545
|
+
|
|
546
|
+
1. **Always use theme from callback** - Don't import theme directly. Use `theme` from the `ctx.ui.custom((tui, theme, done) => ...)` callback.
|
|
547
|
+
|
|
548
|
+
2. **Always type DynamicBorder color param** - Write `(s: string) => theme.fg("accent", s)`, not `(s) => theme.fg("accent", s)`.
|
|
549
|
+
|
|
550
|
+
3. **Call tui.requestRender() after state changes** - In `handleInput`, call `tui.requestRender()` after updating state.
|
|
551
|
+
|
|
552
|
+
4. **Return the three-method object** - Custom components need `{ render, invalidate, handleInput }`.
|
|
553
|
+
|
|
554
|
+
5. **Use existing components** - `SelectList`, `SettingsList`, `BorderedLoader` cover 90% of cases. Don't rebuild them.
|
|
555
|
+
|
|
343
556
|
## Examples
|
|
344
557
|
|
|
345
|
-
- **
|
|
346
|
-
- **
|
|
558
|
+
- **Selection UI**: [examples/extensions/preset.ts](../examples/extensions/preset.ts) - SelectList with DynamicBorder framing
|
|
559
|
+
- **Async with cancel**: [examples/extensions/qna.ts](../examples/extensions/qna.ts) - BorderedLoader for LLM calls
|
|
560
|
+
- **Settings toggles**: [examples/extensions/tools.ts](../examples/extensions/tools.ts) - SettingsList for tool enable/disable
|
|
561
|
+
- **Status indicators**: [examples/extensions/plan-mode.ts](../examples/extensions/plan-mode.ts) - setStatus and setWidget
|
|
562
|
+
- **Custom footer**: [examples/extensions/custom-footer.ts](../examples/extensions/custom-footer.ts) - setFooter with stats
|
|
563
|
+
- **Snake game**: [examples/extensions/snake.ts](../examples/extensions/snake.ts) - Full game with keyboard input, game loop
|
|
564
|
+
- **Custom tool rendering**: [examples/extensions/todo.ts](../examples/extensions/todo.ts) - renderCall and renderResult
|
|
@@ -36,6 +36,7 @@ cp permission-gate.ts ~/.pi/agent/extensions/
|
|
|
36
36
|
|
|
37
37
|
| Extension | Description |
|
|
38
38
|
|-----------|-------------|
|
|
39
|
+
| `preset.ts` | Named presets for model, thinking level, tools, and instructions via `--preset` flag and `/preset` command |
|
|
39
40
|
| `plan-mode.ts` | Claude Code-style plan mode for read-only exploration with `/plan` command |
|
|
40
41
|
| `tools.ts` | Interactive `/tools` command to enable/disable tools with session persistence |
|
|
41
42
|
| `handoff.ts` | Transfer context to a new focused session via `/handoff <goal>` |
|