@mariozechner/pi-coding-agent 0.37.8 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/CHANGELOG.md +115 -4
  2. package/README.md +11 -0
  3. package/dist/cli/args.d.ts +2 -0
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +8 -0
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/core/agent-session.d.ts +23 -0
  8. package/dist/core/agent-session.d.ts.map +1 -1
  9. package/dist/core/agent-session.js +75 -35
  10. package/dist/core/agent-session.js.map +1 -1
  11. package/dist/core/bash-executor.d.ts +6 -0
  12. package/dist/core/bash-executor.d.ts.map +1 -1
  13. package/dist/core/bash-executor.js +77 -0
  14. package/dist/core/bash-executor.js.map +1 -1
  15. package/dist/core/extensions/index.d.ts +3 -3
  16. package/dist/core/extensions/index.d.ts.map +1 -1
  17. package/dist/core/extensions/index.js +1 -1
  18. package/dist/core/extensions/index.js.map +1 -1
  19. package/dist/core/extensions/loader.d.ts +8 -6
  20. package/dist/core/extensions/loader.d.ts.map +1 -1
  21. package/dist/core/extensions/loader.js +94 -211
  22. package/dist/core/extensions/loader.js.map +1 -1
  23. package/dist/core/extensions/runner.d.ts +27 -30
  24. package/dist/core/extensions/runner.d.ts.map +1 -1
  25. package/dist/core/extensions/runner.js +102 -45
  26. package/dist/core/extensions/runner.js.map +1 -1
  27. package/dist/core/extensions/types.d.ts +155 -30
  28. package/dist/core/extensions/types.d.ts.map +1 -1
  29. package/dist/core/extensions/types.js.map +1 -1
  30. package/dist/core/extensions/wrapper.d.ts +5 -3
  31. package/dist/core/extensions/wrapper.d.ts.map +1 -1
  32. package/dist/core/extensions/wrapper.js +6 -4
  33. package/dist/core/extensions/wrapper.js.map +1 -1
  34. package/dist/core/index.d.ts +2 -2
  35. package/dist/core/index.d.ts.map +1 -1
  36. package/dist/core/index.js +1 -1
  37. package/dist/core/index.js.map +1 -1
  38. package/dist/core/model-resolver.d.ts +4 -2
  39. package/dist/core/model-resolver.d.ts.map +1 -1
  40. package/dist/core/model-resolver.js +8 -9
  41. package/dist/core/model-resolver.js.map +1 -1
  42. package/dist/core/sdk.d.ts +8 -5
  43. package/dist/core/sdk.d.ts.map +1 -1
  44. package/dist/core/sdk.js +39 -87
  45. package/dist/core/sdk.js.map +1 -1
  46. package/dist/core/settings-manager.d.ts +8 -0
  47. package/dist/core/settings-manager.d.ts.map +1 -1
  48. package/dist/core/settings-manager.js +9 -1
  49. package/dist/core/settings-manager.js.map +1 -1
  50. package/dist/core/system-prompt.d.ts.map +1 -1
  51. package/dist/core/system-prompt.js +1 -5
  52. package/dist/core/system-prompt.js.map +1 -1
  53. package/dist/core/tools/bash.d.ts +25 -1
  54. package/dist/core/tools/bash.d.ts.map +1 -1
  55. package/dist/core/tools/bash.js +103 -73
  56. package/dist/core/tools/bash.js.map +1 -1
  57. package/dist/core/tools/edit.d.ts +17 -1
  58. package/dist/core/tools/edit.d.ts.map +1 -1
  59. package/dist/core/tools/edit.js +12 -5
  60. package/dist/core/tools/edit.js.map +1 -1
  61. package/dist/core/tools/find.d.ts +18 -1
  62. package/dist/core/tools/find.d.ts.map +1 -1
  63. package/dist/core/tools/find.js +68 -18
  64. package/dist/core/tools/find.js.map +1 -1
  65. package/dist/core/tools/grep.d.ts +15 -1
  66. package/dist/core/tools/grep.d.ts.map +1 -1
  67. package/dist/core/tools/grep.js +22 -10
  68. package/dist/core/tools/grep.js.map +1 -1
  69. package/dist/core/tools/index.d.ts +7 -7
  70. package/dist/core/tools/index.d.ts.map +1 -1
  71. package/dist/core/tools/index.js +1 -1
  72. package/dist/core/tools/index.js.map +1 -1
  73. package/dist/core/tools/ls.d.ts +21 -1
  74. package/dist/core/tools/ls.d.ts.map +1 -1
  75. package/dist/core/tools/ls.js +80 -72
  76. package/dist/core/tools/ls.js.map +1 -1
  77. package/dist/core/tools/read.d.ts +14 -0
  78. package/dist/core/tools/read.d.ts.map +1 -1
  79. package/dist/core/tools/read.js +12 -5
  80. package/dist/core/tools/read.js.map +1 -1
  81. package/dist/core/tools/write.d.ts +15 -1
  82. package/dist/core/tools/write.d.ts.map +1 -1
  83. package/dist/core/tools/write.js +9 -4
  84. package/dist/core/tools/write.js.map +1 -1
  85. package/dist/index.d.ts +5 -4
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +4 -2
  88. package/dist/index.js.map +1 -1
  89. package/dist/main.d.ts.map +1 -1
  90. package/dist/main.js +58 -116
  91. package/dist/main.js.map +1 -1
  92. package/dist/modes/index.d.ts +2 -2
  93. package/dist/modes/index.d.ts.map +1 -1
  94. package/dist/modes/index.js.map +1 -1
  95. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  96. package/dist/modes/interactive/components/assistant-message.js +7 -3
  97. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  98. package/dist/modes/interactive/components/countdown-timer.d.ts +14 -0
  99. package/dist/modes/interactive/components/countdown-timer.d.ts.map +1 -0
  100. package/dist/modes/interactive/components/countdown-timer.js +33 -0
  101. package/dist/modes/interactive/components/countdown-timer.js.map +1 -0
  102. package/dist/modes/interactive/components/custom-editor.d.ts +1 -1
  103. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  104. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  105. package/dist/modes/interactive/components/extension-input.d.ts +10 -2
  106. package/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
  107. package/dist/modes/interactive/components/extension-input.js +18 -14
  108. package/dist/modes/interactive/components/extension-input.js.map +1 -1
  109. package/dist/modes/interactive/components/extension-selector.d.ts +10 -2
  110. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  111. package/dist/modes/interactive/components/extension-selector.js +18 -22
  112. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  113. package/dist/modes/interactive/components/tool-execution.d.ts +6 -0
  114. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  115. package/dist/modes/interactive/components/tool-execution.js +50 -23
  116. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  117. package/dist/modes/interactive/interactive-mode.d.ts +44 -3
  118. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  119. package/dist/modes/interactive/interactive-mode.js +440 -139
  120. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  121. package/dist/modes/interactive/theme/theme.d.ts +7 -0
  122. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  123. package/dist/modes/interactive/theme/theme.js +34 -0
  124. package/dist/modes/interactive/theme/theme.js.map +1 -1
  125. package/dist/modes/print-mode.d.ts +14 -7
  126. package/dist/modes/print-mode.d.ts.map +1 -1
  127. package/dist/modes/print-mode.js +45 -21
  128. package/dist/modes/print-mode.js.map +1 -1
  129. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  130. package/dist/modes/rpc/rpc-mode.js +111 -101
  131. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  132. package/dist/modes/rpc/rpc-types.d.ts +3 -0
  133. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  134. package/dist/modes/rpc/rpc-types.js.map +1 -1
  135. package/dist/utils/clipboard-image.d.ts.map +1 -1
  136. package/dist/utils/clipboard-image.js +1 -1
  137. package/dist/utils/clipboard-image.js.map +1 -1
  138. package/dist/utils/clipboard.d.ts.map +1 -1
  139. package/dist/utils/clipboard.js +35 -7
  140. package/dist/utils/clipboard.js.map +1 -1
  141. package/docs/extensions.md +211 -15
  142. package/docs/sdk.md +68 -9
  143. package/docs/tui.md +81 -4
  144. package/examples/extensions/README.md +3 -0
  145. package/examples/extensions/claude-rules.ts +5 -2
  146. package/examples/extensions/handoff.ts +1 -1
  147. package/examples/extensions/interactive-shell.ts +196 -0
  148. package/examples/extensions/mac-system-theme.ts +25 -0
  149. package/examples/extensions/modal-editor.ts +85 -0
  150. package/examples/extensions/overlay-test.ts +145 -0
  151. package/examples/extensions/pirate.ts +7 -4
  152. package/examples/extensions/preset.ts +3 -3
  153. package/examples/extensions/qna.ts +1 -1
  154. package/examples/extensions/rainbow-editor.ts +95 -0
  155. package/examples/extensions/shutdown-command.ts +63 -0
  156. package/examples/extensions/snake.ts +1 -1
  157. package/examples/extensions/ssh.ts +220 -0
  158. package/examples/extensions/timed-confirm.ts +32 -25
  159. package/examples/extensions/todo.ts +1 -1
  160. package/examples/extensions/tool-override.ts +143 -0
  161. package/examples/extensions/tools.ts +1 -1
  162. package/examples/extensions/with-deps/package-lock.json +2 -2
  163. package/examples/extensions/with-deps/package.json +1 -1
  164. package/examples/sdk/04-skills.ts +4 -1
  165. package/package.json +6 -6
@@ -255,7 +255,7 @@ pi starts
255
255
 
256
256
  user sends prompt ─────────────────────────────────────────┐
257
257
  │ │
258
- ├─► before_agent_start (can inject message, append to system prompt)
258
+ ├─► before_agent_start (can inject message, modify system prompt)
259
259
  ├─► agent_start │
260
260
  │ │
261
261
  │ ┌─── turn (repeats while LLM calls tools) ───┐ │
@@ -414,12 +414,13 @@ pi.on("session_shutdown", async (_event, ctx) => {
414
414
 
415
415
  #### before_agent_start
416
416
 
417
- Fired after user submits prompt, before agent loop. Can inject a message and/or append to the system prompt.
417
+ Fired after user submits prompt, before agent loop. Can inject a message and/or modify the system prompt.
418
418
 
419
419
  ```typescript
420
420
  pi.on("before_agent_start", async (event, ctx) => {
421
421
  // event.prompt - user's prompt text
422
422
  // event.images - attached images (if any)
423
+ // event.systemPrompt - current system prompt
423
424
 
424
425
  return {
425
426
  // Inject a persistent message (stored in session, sent to LLM)
@@ -428,13 +429,13 @@ pi.on("before_agent_start", async (event, ctx) => {
428
429
  content: "Additional context for the LLM",
429
430
  display: true,
430
431
  },
431
- // Append to system prompt for this turn only
432
- systemPromptAppend: "Extra instructions for this turn...",
432
+ // Replace the system prompt for this turn (chained across extensions)
433
+ systemPrompt: event.systemPrompt + "\n\nExtra instructions for this turn...",
433
434
  };
434
435
  });
435
436
  ```
436
437
 
437
- **Examples:** [claude-rules.ts](../examples/extensions/claude-rules.ts), [pirate.ts](../examples/extensions/pirate.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts)
438
+ **Examples:** [claude-rules.ts](../examples/extensions/claude-rules.ts), [pirate.ts](../examples/extensions/pirate.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts), [ssh.ts](../examples/extensions/ssh.ts)
438
439
 
439
440
  #### agent_start / agent_end
440
441
 
@@ -522,6 +523,28 @@ pi.on("tool_result", async (event, ctx) => {
522
523
 
523
524
  **Examples:** [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts)
524
525
 
526
+ ### User Bash Events
527
+
528
+ #### user_bash
529
+
530
+ Fired when user executes `!` or `!!` commands. **Can intercept.**
531
+
532
+ ```typescript
533
+ pi.on("user_bash", (event, ctx) => {
534
+ // event.command - the bash command
535
+ // event.excludeFromContext - true if !! prefix
536
+ // event.cwd - working directory
537
+
538
+ // Option 1: Provide custom operations (e.g., SSH)
539
+ return { operations: remoteBashOps };
540
+
541
+ // Option 2: Full replacement - return result directly
542
+ return { result: { output: "...", exitCode: 0, cancelled: false, truncated: false } };
543
+ });
544
+ ```
545
+
546
+ **Examples:** [ssh.ts](../examples/extensions/ssh.ts), [interactive-shell.ts](../examples/extensions/interactive-shell.ts)
547
+
525
548
  ## ExtensionContext
526
549
 
527
550
  Every handler receives `ctx: ExtensionContext`:
@@ -556,6 +579,24 @@ Access to models and API keys.
556
579
 
557
580
  Control flow helpers.
558
581
 
582
+ ### ctx.shutdown()
583
+
584
+ Request a graceful shutdown of pi.
585
+
586
+ - **Interactive mode:** Deferred until the agent becomes idle (after processing all queued steering and follow-up messages).
587
+ - **RPC mode:** Deferred until the next idle state (after completing the current command response, when waiting for the next command).
588
+ - **Print mode:** No-op. The process exits automatically when all prompts are processed.
589
+
590
+ Emits `session_shutdown` event to all extensions before exiting. Available in all contexts (event handlers, tools, commands, shortcuts).
591
+
592
+ ```typescript
593
+ pi.on("tool_call", (event, ctx) => {
594
+ if (isFatal(event.input)) {
595
+ ctx.shutdown();
596
+ }
597
+ });
598
+ ```
599
+
559
600
  ## ExtensionCommandContext
560
601
 
561
602
  Command handlers receive `ExtensionCommandContext`, which extends `ExtensionContext` with session control methods. These are only available in commands because they can deadlock if called from event handlers.
@@ -924,6 +965,69 @@ pi.registerTool({
924
965
 
925
966
  **Important:** Use `StringEnum` from `@mariozechner/pi-ai` for string enums. `Type.Union`/`Type.Literal` doesn't work with Google's API.
926
967
 
968
+ ### Overriding Built-in Tools
969
+
970
+ Extensions can override built-in tools (`read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`) by registering a tool with the same name. Interactive mode displays a warning when this happens.
971
+
972
+ ```bash
973
+ # Extension's read tool replaces built-in read
974
+ pi -e ./tool-override.ts
975
+ ```
976
+
977
+ Alternatively, use `--no-tools` to start without any built-in tools:
978
+ ```bash
979
+ # No built-in tools, only extension tools
980
+ pi --no-tools -e ./my-extension.ts
981
+ ```
982
+
983
+ See [examples/extensions/tool-override.ts](../examples/extensions/tool-override.ts) for a complete example that overrides `read` with logging and access control.
984
+
985
+ **Rendering:** If your override doesn't provide custom `renderCall`/`renderResult` functions, the built-in renderer is used automatically (syntax highlighting, diffs, etc.). This lets you wrap built-in tools for logging or access control without reimplementing the UI.
986
+
987
+ **Your implementation must match the exact result shape**, including the `details` type. The UI and session logic depend on these shapes for rendering and state tracking.
988
+
989
+ Built-in tool implementations:
990
+ - [read.ts](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/tools/read.ts) - `ReadToolDetails`
991
+ - [bash.ts](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/tools/bash.ts) - `BashToolDetails`
992
+ - [edit.ts](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/tools/edit.ts)
993
+ - [write.ts](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/tools/write.ts)
994
+ - [grep.ts](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/tools/grep.ts) - `GrepToolDetails`
995
+ - [find.ts](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/tools/find.ts) - `FindToolDetails`
996
+ - [ls.ts](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/tools/ls.ts) - `LsToolDetails`
997
+
998
+ ### Remote Execution
999
+
1000
+ Built-in tools support pluggable operations for delegating to remote systems (SSH, containers, etc.):
1001
+
1002
+ ```typescript
1003
+ import { createReadTool, createBashTool, type ReadOperations } from "@mariozechner/pi-coding-agent";
1004
+
1005
+ // Create tool with custom operations
1006
+ const remoteRead = createReadTool(cwd, {
1007
+ operations: {
1008
+ readFile: (path) => sshExec(remote, `cat ${path}`),
1009
+ access: (path) => sshExec(remote, `test -r ${path}`).then(() => {}),
1010
+ }
1011
+ });
1012
+
1013
+ // Register, checking flag at execution time
1014
+ pi.registerTool({
1015
+ ...remoteRead,
1016
+ async execute(id, params, onUpdate, _ctx, signal) {
1017
+ const ssh = getSshConfig();
1018
+ if (ssh) {
1019
+ const tool = createReadTool(cwd, { operations: createRemoteOps(ssh) });
1020
+ return tool.execute(id, params, signal, onUpdate);
1021
+ }
1022
+ return localRead.execute(id, params, signal, onUpdate);
1023
+ },
1024
+ });
1025
+ ```
1026
+
1027
+ **Operations interfaces:** `ReadOperations`, `WriteOperations`, `EditOperations`, `BashOperations`, `LsOperations`, `GrepOperations`, `FindOperations`
1028
+
1029
+ See [examples/extensions/ssh.ts](../examples/extensions/ssh.ts) for a complete SSH example with `--ssh` flag.
1030
+
927
1031
  ### Output Truncation
928
1032
 
929
1033
  **Tools MUST truncate their output** to avoid overwhelming the LLM context. Large outputs can cause:
@@ -1094,9 +1198,33 @@ ctx.ui.notify("Done!", "info"); // "info" | "warning" | "error"
1094
1198
  - `ctx.ui.editor()`: [handoff.ts](../examples/extensions/handoff.ts)
1095
1199
  - `ctx.ui.setEditorText()`: [handoff.ts](../examples/extensions/handoff.ts), [qna.ts](../examples/extensions/qna.ts)
1096
1200
 
1097
- #### Auto-Dismissing Dialogs
1201
+ #### Timed Dialogs with Countdown
1098
1202
 
1099
- Dialogs can be programmatically dismissed using `AbortSignal`. This is useful for implementing timeouts:
1203
+ Dialogs support a `timeout` option that auto-dismisses with a live countdown display:
1204
+
1205
+ ```typescript
1206
+ // Dialog shows "Title (5s)" → "Title (4s)" → ... → auto-dismisses at 0
1207
+ const confirmed = await ctx.ui.confirm(
1208
+ "Timed Confirmation",
1209
+ "This dialog will auto-cancel in 5 seconds. Confirm?",
1210
+ { timeout: 5000 }
1211
+ );
1212
+
1213
+ if (confirmed) {
1214
+ // User confirmed
1215
+ } else {
1216
+ // User cancelled or timed out
1217
+ }
1218
+ ```
1219
+
1220
+ **Return values on timeout:**
1221
+ - `select()` returns `undefined`
1222
+ - `confirm()` returns `false`
1223
+ - `input()` returns `undefined`
1224
+
1225
+ #### Manual Dismissal with AbortSignal
1226
+
1227
+ For more control (e.g., to distinguish timeout from user cancel), use `AbortSignal`:
1100
1228
 
1101
1229
  ```typescript
1102
1230
  const controller = new AbortController();
@@ -1119,12 +1247,7 @@ if (confirmed) {
1119
1247
  }
1120
1248
  ```
1121
1249
 
1122
- **Return values on abort:**
1123
- - `select()` returns `undefined`
1124
- - `confirm()` returns `false`
1125
- - `input()` returns `undefined`
1126
-
1127
- See [examples/extensions/timed-confirm.ts](../examples/extensions/timed-confirm.ts) for a complete example.
1250
+ See [examples/extensions/timed-confirm.ts](../examples/extensions/timed-confirm.ts) for complete examples.
1128
1251
 
1129
1252
  ### Widgets, Status, and Footer
1130
1253
 
@@ -1151,6 +1274,20 @@ ctx.ui.setTitle("pi - my-project");
1151
1274
  // Editor text
1152
1275
  ctx.ui.setEditorText("Prefill text");
1153
1276
  const current = ctx.ui.getEditorText();
1277
+
1278
+ // Custom editor (vim mode, emacs mode, etc.)
1279
+ ctx.ui.setEditorComponent((tui, theme, keybindings) => new VimEditor(tui, theme, keybindings));
1280
+ ctx.ui.setEditorComponent(undefined); // Restore default editor
1281
+
1282
+ // Theme management
1283
+ const themes = ctx.ui.getAllThemes(); // [{ name: "dark", path: "/..." | undefined }, ...]
1284
+ const lightTheme = ctx.ui.getTheme("light"); // Load without switching
1285
+ const result = ctx.ui.setTheme("light"); // Switch by name
1286
+ if (!result.success) {
1287
+ ctx.ui.notify(`Failed: ${result.error}`, "error");
1288
+ }
1289
+ ctx.ui.setTheme(lightTheme!); // Or switch by Theme object
1290
+ ctx.ui.theme.fg("accent", "styled text"); // Access current theme
1154
1291
  ```
1155
1292
 
1156
1293
  **Examples:**
@@ -1158,6 +1295,8 @@ const current = ctx.ui.getEditorText();
1158
1295
  - `ctx.ui.setWidget()`: [plan-mode.ts](../examples/extensions/plan-mode.ts)
1159
1296
  - `ctx.ui.setFooter()`: [custom-footer.ts](../examples/extensions/custom-footer.ts)
1160
1297
  - `ctx.ui.setHeader()`: [custom-header.ts](../examples/extensions/custom-header.ts)
1298
+ - `ctx.ui.setEditorComponent()`: [modal-editor.ts](../examples/extensions/modal-editor.ts)
1299
+ - `ctx.ui.setTheme()`: [mac-system-theme.ts](../examples/extensions/mac-system-theme.ts)
1161
1300
 
1162
1301
  ### Custom Components
1163
1302
 
@@ -1166,7 +1305,7 @@ For complex UI, use `ctx.ui.custom()`. This temporarily replaces the editor with
1166
1305
  ```typescript
1167
1306
  import { Text, Component } from "@mariozechner/pi-tui";
1168
1307
 
1169
- const result = await ctx.ui.custom<boolean>((tui, theme, done) => {
1308
+ const result = await ctx.ui.custom<boolean>((tui, theme, keybindings, done) => {
1170
1309
  const text = new Text("Press Enter to confirm, Escape to cancel", 1, 1);
1171
1310
 
1172
1311
  text.onKey = (key) => {
@@ -1186,11 +1325,68 @@ if (result) {
1186
1325
  The callback receives:
1187
1326
  - `tui` - TUI instance (for screen dimensions, focus management)
1188
1327
  - `theme` - Current theme for styling
1328
+ - `keybindings` - App keybinding manager (for checking shortcuts)
1189
1329
  - `done(value)` - Call to close component and return value
1190
1330
 
1191
1331
  See [tui.md](tui.md) for the full component API.
1192
1332
 
1193
- **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)
1333
+ #### Overlay Mode (Experimental)
1334
+
1335
+ Pass `{ overlay: true }` to render the component as a floating modal on top of existing content, without clearing the screen:
1336
+
1337
+ ```typescript
1338
+ const result = await ctx.ui.custom<string | null>(
1339
+ (tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }),
1340
+ { overlay: true }
1341
+ );
1342
+ ```
1343
+
1344
+ Overlay components should define a `width` property to control their size. The overlay is centered by default. See [overlay-test.ts](../examples/extensions/overlay-test.ts) for a complete example.
1345
+
1346
+ **Examples:** [handoff.ts](../examples/extensions/handoff.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts), [qna.ts](../examples/extensions/qna.ts), [snake.ts](../examples/extensions/snake.ts), [todo.ts](../examples/extensions/todo.ts), [tools.ts](../examples/extensions/tools.ts), [overlay-test.ts](../examples/extensions/overlay-test.ts)
1347
+
1348
+ ### Custom Editor
1349
+
1350
+ Replace the main input editor with a custom implementation (vim mode, emacs mode, etc.):
1351
+
1352
+ ```typescript
1353
+ import { CustomEditor, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
1354
+ import { matchesKey } from "@mariozechner/pi-tui";
1355
+
1356
+ class VimEditor extends CustomEditor {
1357
+ private mode: "normal" | "insert" = "insert";
1358
+
1359
+ handleInput(data: string): void {
1360
+ if (matchesKey(data, "escape") && this.mode === "insert") {
1361
+ this.mode = "normal";
1362
+ return;
1363
+ }
1364
+ if (this.mode === "normal" && data === "i") {
1365
+ this.mode = "insert";
1366
+ return;
1367
+ }
1368
+ super.handleInput(data); // App keybindings + text editing
1369
+ }
1370
+ }
1371
+
1372
+ export default function (pi: ExtensionAPI) {
1373
+ pi.on("session_start", (_event, ctx) => {
1374
+ ctx.ui.setEditorComponent((_tui, theme, keybindings) =>
1375
+ new VimEditor(theme, keybindings)
1376
+ );
1377
+ });
1378
+ }
1379
+ ```
1380
+
1381
+ **Key points:**
1382
+ - Extend `CustomEditor` (not base `Editor`) to get app keybindings (escape to abort, ctrl+d, model switching)
1383
+ - Call `super.handleInput(data)` for keys you don't handle
1384
+ - Factory receives `theme` and `keybindings` from the app
1385
+ - Pass `undefined` to restore default: `ctx.ui.setEditorComponent(undefined)`
1386
+
1387
+ See [tui.md](tui.md) Pattern 7 for a complete example with mode indicator.
1388
+
1389
+ **Examples:** [modal-editor.ts](../examples/extensions/modal-editor.ts)
1194
1390
 
1195
1391
  ### Message Rendering
1196
1392
 
package/docs/sdk.md CHANGED
@@ -528,7 +528,7 @@ eventBus.on("my-extension:status", (data) => console.log(data));
528
528
  import { createAgentSession, discoverSkills, type Skill } from "@mariozechner/pi-coding-agent";
529
529
 
530
530
  // Discover and filter
531
- const allSkills = discoverSkills();
531
+ const { skills: allSkills, warnings } = discoverSkills();
532
532
  const filtered = allSkills.filter(s => s.name.includes("search"));
533
533
 
534
534
  // Custom skill
@@ -550,7 +550,7 @@ const { session } = await createAgentSession({
550
550
  });
551
551
 
552
552
  // Discovery with settings filter
553
- const skills = discoverSkills(process.cwd(), undefined, {
553
+ const { skills } = discoverSkills(process.cwd(), undefined, {
554
554
  ignoredSkills: ["browser-*"], // glob patterns to exclude
555
555
  includeSkills: ["search-*"], // glob patterns to include (empty = all)
556
556
  });
@@ -747,7 +747,7 @@ const model = modelRegistry.find("provider", "id"); // Find specific model
747
747
  const builtIn = getModel("anthropic", "claude-opus-4-5"); // Built-in only
748
748
 
749
749
  // Skills
750
- const skills = discoverSkills(cwd, agentDir, skillsSettings);
750
+ const { skills, warnings } = discoverSkills(cwd, agentDir, skillsSettings);
751
751
 
752
752
  // Hooks (async - loads TypeScript)
753
753
  // Pass eventBus to share pi.events across hooks/tools
@@ -784,15 +784,18 @@ interface CreateAgentSessionResult {
784
784
  // The session
785
785
  session: AgentSession;
786
786
 
787
- // 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
@@ -30,6 +30,8 @@ cp permission-gate.ts ~/.pi/agent/extensions/
30
30
  | `todo.ts` | Todo list tool + `/todos` command with custom rendering and state persistence |
31
31
  | `hello.ts` | Minimal custom tool example |
32
32
  | `question.ts` | Demonstrates `ctx.ui.select()` for asking the user questions |
33
+ | `tool-override.ts` | Override built-in tools (e.g., add logging/access control to `read`) |
34
+ | `ssh.ts` | Delegate all tools to a remote machine via SSH using pluggable operations |
33
35
  | `subagent/` | Delegate tasks to specialized subagents with isolated context windows |
34
36
 
35
37
  ### Commands & UI
@@ -45,6 +47,7 @@ cp permission-gate.ts ~/.pi/agent/extensions/
45
47
  | `snake.ts` | Snake game with custom UI, keyboard handling, and session persistence |
46
48
  | `send-user-message.ts` | Demonstrates `pi.sendUserMessage()` for sending user messages from extensions |
47
49
  | `timed-confirm.ts` | Demonstrates AbortSignal for auto-dismissing `ctx.ui.confirm()` and `ctx.ui.select()` dialogs |
50
+ | `modal-editor.ts` | Custom vim-like modal editor via `ctx.ui.setEditorComponent()` |
48
51
 
49
52
  ### Git Integration
50
53
 
@@ -61,7 +61,7 @@ export default function claudeRulesExtension(pi: ExtensionAPI) {
61
61
  });
62
62
 
63
63
  // Append available rules to system prompt
64
- pi.on("before_agent_start", async () => {
64
+ pi.on("before_agent_start", async (event) => {
65
65
  if (ruleFiles.length === 0) {
66
66
  return;
67
67
  }
@@ -69,7 +69,10 @@ export default function claudeRulesExtension(pi: ExtensionAPI) {
69
69
  const rulesList = ruleFiles.map((f) => `- .claude/rules/${f}`).join("\n");
70
70
 
71
71
  return {
72
- systemPromptAppend: `
72
+ systemPrompt:
73
+ event.systemPrompt +
74
+ `
75
+
73
76
  ## Project Rules
74
77
 
75
78
  The following project rules are available in .claude/rules/:
@@ -75,7 +75,7 @@ export default function (pi: ExtensionAPI) {
75
75
  const currentSessionFile = ctx.sessionManager.getSessionFile();
76
76
 
77
77
  // Generate the handoff prompt with loader UI
78
- const result = await ctx.ui.custom<string | null>((tui, theme, done) => {
78
+ const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
79
79
  const loader = new BorderedLoader(tui, theme, `Generating handoff prompt...`);
80
80
  loader.onAbort = () => done(null);
81
81