@mariozechner/pi-coding-agent 0.30.1 → 0.31.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 (301) hide show
  1. package/CHANGELOG.md +251 -2
  2. package/README.md +105 -84
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +5 -1
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/cli/file-processor.d.ts +3 -3
  7. package/dist/cli/file-processor.d.ts.map +1 -1
  8. package/dist/cli/file-processor.js +7 -10
  9. package/dist/cli/file-processor.js.map +1 -1
  10. package/dist/config.d.ts +9 -0
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +18 -0
  13. package/dist/config.js.map +1 -1
  14. package/dist/core/agent-session.d.ts +73 -34
  15. package/dist/core/agent-session.d.ts.map +1 -1
  16. package/dist/core/agent-session.js +464 -210
  17. package/dist/core/agent-session.js.map +1 -1
  18. package/dist/core/auth-storage.d.ts +2 -7
  19. package/dist/core/auth-storage.d.ts.map +1 -1
  20. package/dist/core/auth-storage.js +4 -52
  21. package/dist/core/auth-storage.js.map +1 -1
  22. package/dist/core/bash-executor.d.ts +2 -2
  23. package/dist/core/bash-executor.d.ts.map +1 -1
  24. package/dist/core/bash-executor.js +2 -2
  25. package/dist/core/bash-executor.js.map +1 -1
  26. package/dist/core/compaction/branch-summarization.d.ts +84 -0
  27. package/dist/core/compaction/branch-summarization.d.ts.map +1 -0
  28. package/dist/core/compaction/branch-summarization.js +233 -0
  29. package/dist/core/compaction/branch-summarization.js.map +1 -0
  30. package/dist/core/{compaction.d.ts → compaction/compaction.d.ts} +38 -19
  31. package/dist/core/compaction/compaction.d.ts.map +1 -0
  32. package/dist/core/compaction/compaction.js +558 -0
  33. package/dist/core/compaction/compaction.js.map +1 -0
  34. package/dist/core/compaction/index.d.ts +7 -0
  35. package/dist/core/compaction/index.d.ts.map +1 -0
  36. package/dist/core/compaction/index.js +7 -0
  37. package/dist/core/compaction/index.js.map +1 -0
  38. package/dist/core/compaction/utils.d.ts +35 -0
  39. package/dist/core/compaction/utils.d.ts.map +1 -0
  40. package/dist/core/compaction/utils.js +138 -0
  41. package/dist/core/compaction/utils.js.map +1 -0
  42. package/dist/core/custom-tools/index.d.ts +2 -1
  43. package/dist/core/custom-tools/index.d.ts.map +1 -1
  44. package/dist/core/custom-tools/index.js +1 -0
  45. package/dist/core/custom-tools/index.js.map +1 -1
  46. package/dist/core/custom-tools/loader.d.ts.map +1 -1
  47. package/dist/core/custom-tools/loader.js +13 -80
  48. package/dist/core/custom-tools/loader.js.map +1 -1
  49. package/dist/core/custom-tools/types.d.ts +84 -59
  50. package/dist/core/custom-tools/types.d.ts.map +1 -1
  51. package/dist/core/custom-tools/types.js.map +1 -1
  52. package/dist/core/custom-tools/wrapper.d.ts +15 -0
  53. package/dist/core/custom-tools/wrapper.d.ts.map +1 -0
  54. package/dist/core/custom-tools/wrapper.js +23 -0
  55. package/dist/core/custom-tools/wrapper.js.map +1 -0
  56. package/dist/core/exec.d.ts +29 -0
  57. package/dist/core/exec.d.ts.map +1 -0
  58. package/dist/core/exec.js +71 -0
  59. package/dist/core/exec.js.map +1 -0
  60. package/dist/core/export-html/index.d.ts +17 -0
  61. package/dist/core/export-html/index.d.ts.map +1 -0
  62. package/dist/core/export-html/index.js +171 -0
  63. package/dist/core/export-html/index.js.map +1 -0
  64. package/dist/core/export-html/template.css +781 -0
  65. package/dist/core/export-html/template.html +54 -0
  66. package/dist/core/export-html/template.js +1185 -0
  67. package/dist/core/export-html/vendor/highlight.min.js +1213 -0
  68. package/dist/core/export-html/vendor/marked.min.js +6 -0
  69. package/dist/core/hooks/index.d.ts +4 -4
  70. package/dist/core/hooks/index.d.ts.map +1 -1
  71. package/dist/core/hooks/index.js +3 -3
  72. package/dist/core/hooks/index.js.map +1 -1
  73. package/dist/core/hooks/loader.d.ts +40 -5
  74. package/dist/core/hooks/loader.d.ts.map +1 -1
  75. package/dist/core/hooks/loader.js +43 -10
  76. package/dist/core/hooks/loader.js.map +1 -1
  77. package/dist/core/hooks/runner.d.ts +94 -18
  78. package/dist/core/hooks/runner.d.ts.map +1 -1
  79. package/dist/core/hooks/runner.js +199 -120
  80. package/dist/core/hooks/runner.js.map +1 -1
  81. package/dist/core/hooks/tool-wrapper.d.ts +1 -1
  82. package/dist/core/hooks/tool-wrapper.d.ts.map +1 -1
  83. package/dist/core/hooks/tool-wrapper.js +36 -19
  84. package/dist/core/hooks/tool-wrapper.js.map +1 -1
  85. package/dist/core/hooks/types.d.ts +407 -96
  86. package/dist/core/hooks/types.d.ts.map +1 -1
  87. package/dist/core/hooks/types.js.map +1 -1
  88. package/dist/core/index.d.ts +4 -3
  89. package/dist/core/index.d.ts.map +1 -1
  90. package/dist/core/index.js.map +1 -1
  91. package/dist/core/messages.d.ts +44 -12
  92. package/dist/core/messages.d.ts.map +1 -1
  93. package/dist/core/messages.js +82 -34
  94. package/dist/core/messages.js.map +1 -1
  95. package/dist/core/model-registry.d.ts +5 -5
  96. package/dist/core/model-registry.d.ts.map +1 -1
  97. package/dist/core/model-registry.js +7 -7
  98. package/dist/core/model-registry.js.map +1 -1
  99. package/dist/core/model-resolver.d.ts +7 -7
  100. package/dist/core/model-resolver.d.ts.map +1 -1
  101. package/dist/core/model-resolver.js +45 -14
  102. package/dist/core/model-resolver.js.map +1 -1
  103. package/dist/core/sdk.d.ts +7 -10
  104. package/dist/core/sdk.d.ts.map +1 -1
  105. package/dist/core/sdk.js +88 -32
  106. package/dist/core/sdk.js.map +1 -1
  107. package/dist/core/session-manager.d.ts +202 -36
  108. package/dist/core/session-manager.d.ts.map +1 -1
  109. package/dist/core/session-manager.js +565 -133
  110. package/dist/core/session-manager.js.map +1 -1
  111. package/dist/core/settings-manager.d.ts +9 -3
  112. package/dist/core/settings-manager.d.ts.map +1 -1
  113. package/dist/core/settings-manager.js +13 -12
  114. package/dist/core/settings-manager.js.map +1 -1
  115. package/dist/core/system-prompt.d.ts.map +1 -1
  116. package/dist/core/system-prompt.js +6 -3
  117. package/dist/core/system-prompt.js.map +1 -1
  118. package/dist/core/tools/bash.d.ts +1 -1
  119. package/dist/core/tools/bash.d.ts.map +1 -1
  120. package/dist/core/tools/bash.js.map +1 -1
  121. package/dist/core/tools/edit-diff.d.ts +33 -0
  122. package/dist/core/tools/edit-diff.d.ts.map +1 -0
  123. package/dist/core/tools/edit-diff.js +171 -0
  124. package/dist/core/tools/edit-diff.js.map +1 -0
  125. package/dist/core/tools/edit.d.ts +7 -1
  126. package/dist/core/tools/edit.d.ts.map +1 -1
  127. package/dist/core/tools/edit.js +20 -95
  128. package/dist/core/tools/edit.js.map +1 -1
  129. package/dist/core/tools/find.d.ts +1 -1
  130. package/dist/core/tools/find.d.ts.map +1 -1
  131. package/dist/core/tools/find.js.map +1 -1
  132. package/dist/core/tools/grep.d.ts +1 -1
  133. package/dist/core/tools/grep.d.ts.map +1 -1
  134. package/dist/core/tools/grep.js.map +1 -1
  135. package/dist/core/tools/index.d.ts +1 -1
  136. package/dist/core/tools/index.d.ts.map +1 -1
  137. package/dist/core/tools/index.js.map +1 -1
  138. package/dist/core/tools/ls.d.ts +1 -1
  139. package/dist/core/tools/ls.d.ts.map +1 -1
  140. package/dist/core/tools/ls.js.map +1 -1
  141. package/dist/core/tools/read.d.ts +1 -1
  142. package/dist/core/tools/read.d.ts.map +1 -1
  143. package/dist/core/tools/read.js.map +1 -1
  144. package/dist/core/tools/write.d.ts +1 -1
  145. package/dist/core/tools/write.d.ts.map +1 -1
  146. package/dist/core/tools/write.js.map +1 -1
  147. package/dist/index.d.ts +8 -7
  148. package/dist/index.d.ts.map +1 -1
  149. package/dist/index.js +5 -5
  150. package/dist/index.js.map +1 -1
  151. package/dist/main.d.ts.map +1 -1
  152. package/dist/main.js +25 -25
  153. package/dist/main.js.map +1 -1
  154. package/dist/migrations.d.ts +28 -0
  155. package/dist/migrations.d.ts.map +1 -0
  156. package/dist/migrations.js +125 -0
  157. package/dist/migrations.js.map +1 -0
  158. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  159. package/dist/modes/interactive/components/assistant-message.js +3 -4
  160. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  161. package/dist/modes/interactive/components/bash-execution.d.ts +1 -1
  162. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  163. package/dist/modes/interactive/components/bash-execution.js +6 -2
  164. package/dist/modes/interactive/components/bash-execution.js.map +1 -1
  165. package/dist/modes/interactive/components/bordered-loader.d.ts +12 -0
  166. package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -0
  167. package/dist/modes/interactive/components/bordered-loader.js +30 -0
  168. package/dist/modes/interactive/components/bordered-loader.js.map +1 -0
  169. package/dist/modes/interactive/components/branch-summary-message.d.ts +14 -0
  170. package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -0
  171. package/dist/modes/interactive/components/branch-summary-message.js +35 -0
  172. package/dist/modes/interactive/components/branch-summary-message.js.map +1 -0
  173. package/dist/modes/interactive/components/compaction-summary-message.d.ts +14 -0
  174. package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -0
  175. package/dist/modes/interactive/components/compaction-summary-message.js +36 -0
  176. package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -0
  177. package/dist/modes/interactive/components/dynamic-border.d.ts +5 -1
  178. package/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
  179. package/dist/modes/interactive/components/dynamic-border.js +5 -1
  180. package/dist/modes/interactive/components/dynamic-border.js.map +1 -1
  181. package/dist/modes/interactive/components/footer.d.ts +12 -6
  182. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  183. package/dist/modes/interactive/components/footer.js +57 -25
  184. package/dist/modes/interactive/components/footer.js.map +1 -1
  185. package/dist/modes/interactive/components/hook-editor.d.ts +15 -0
  186. package/dist/modes/interactive/components/hook-editor.d.ts.map +1 -0
  187. package/dist/modes/interactive/components/hook-editor.js +95 -0
  188. package/dist/modes/interactive/components/hook-editor.js.map +1 -0
  189. package/dist/modes/interactive/components/hook-message.d.ts +18 -0
  190. package/dist/modes/interactive/components/hook-message.d.ts.map +1 -0
  191. package/dist/modes/interactive/components/hook-message.js +80 -0
  192. package/dist/modes/interactive/components/hook-message.js.map +1 -0
  193. package/dist/modes/interactive/components/model-selector.d.ts +3 -3
  194. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  195. package/dist/modes/interactive/components/model-selector.js +1 -1
  196. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  197. package/dist/modes/interactive/components/tool-execution.d.ts +15 -2
  198. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  199. package/dist/modes/interactive/components/tool-execution.js +70 -21
  200. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  201. package/dist/modes/interactive/components/tree-selector.d.ts +52 -0
  202. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -0
  203. package/dist/modes/interactive/components/tree-selector.js +745 -0
  204. package/dist/modes/interactive/components/tree-selector.js.map +1 -0
  205. package/dist/modes/interactive/components/user-message-selector.d.ts +3 -3
  206. package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
  207. package/dist/modes/interactive/components/user-message-selector.js +1 -1
  208. package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
  209. package/dist/modes/interactive/components/user-message.d.ts +1 -1
  210. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  211. package/dist/modes/interactive/components/user-message.js +2 -5
  212. package/dist/modes/interactive/components/user-message.js.map +1 -1
  213. package/dist/modes/interactive/interactive-mode.d.ts +29 -12
  214. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  215. package/dist/modes/interactive/interactive-mode.js +589 -208
  216. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  217. package/dist/modes/interactive/theme/dark.json +13 -1
  218. package/dist/modes/interactive/theme/light.json +13 -1
  219. package/dist/modes/interactive/theme/theme-schema.json +34 -0
  220. package/dist/modes/interactive/theme/theme.d.ts +20 -2
  221. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  222. package/dist/modes/interactive/theme/theme.js +135 -2
  223. package/dist/modes/interactive/theme/theme.js.map +1 -1
  224. package/dist/modes/print-mode.d.ts +3 -3
  225. package/dist/modes/print-mode.d.ts.map +1 -1
  226. package/dist/modes/print-mode.js +26 -20
  227. package/dist/modes/print-mode.js.map +1 -1
  228. package/dist/modes/rpc/rpc-client.d.ts +13 -10
  229. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  230. package/dist/modes/rpc/rpc-client.js +11 -10
  231. package/dist/modes/rpc/rpc-client.js.map +1 -1
  232. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  233. package/dist/modes/rpc/rpc-mode.js +88 -35
  234. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  235. package/dist/modes/rpc/rpc-types.d.ts +30 -11
  236. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  237. package/dist/modes/rpc/rpc-types.js.map +1 -1
  238. package/dist/utils/shell.d.ts +4 -2
  239. package/dist/utils/shell.d.ts.map +1 -1
  240. package/dist/utils/shell.js +36 -7
  241. package/dist/utils/shell.js.map +1 -1
  242. package/dist/utils/tools-manager.d.ts +1 -1
  243. package/dist/utils/tools-manager.d.ts.map +1 -1
  244. package/dist/utils/tools-manager.js +2 -2
  245. package/dist/utils/tools-manager.js.map +1 -1
  246. package/docs/compaction.md +388 -0
  247. package/docs/custom-tools.md +146 -43
  248. package/docs/extension-loading.md +1004 -0
  249. package/docs/hooks.md +562 -596
  250. package/docs/rpc.md +33 -19
  251. package/docs/sdk.md +93 -21
  252. package/docs/session-tree-plan.md +441 -0
  253. package/docs/session.md +172 -21
  254. package/docs/skills.md +2 -0
  255. package/docs/theme.md +31 -2
  256. package/docs/tree.md +197 -0
  257. package/docs/tui.md +343 -0
  258. package/examples/README.md +1 -9
  259. package/examples/custom-tools/hello/index.ts +4 -3
  260. package/examples/custom-tools/question/index.ts +4 -4
  261. package/examples/custom-tools/subagent/index.ts +7 -6
  262. package/examples/custom-tools/todo/index.ts +11 -5
  263. package/examples/hooks/README.md +29 -71
  264. package/examples/hooks/auto-commit-on-exit.ts +8 -9
  265. package/examples/hooks/confirm-destructive.ts +29 -30
  266. package/examples/hooks/custom-compaction.ts +20 -21
  267. package/examples/hooks/dirty-repo-guard.ts +41 -40
  268. package/examples/hooks/file-trigger.ts +10 -5
  269. package/examples/hooks/git-checkpoint.ts +16 -12
  270. package/examples/hooks/handoff.ts +150 -0
  271. package/examples/hooks/permission-gate.ts +1 -1
  272. package/examples/hooks/protected-paths.ts +1 -1
  273. package/examples/hooks/qna.ts +119 -0
  274. package/examples/hooks/snake.ts +343 -0
  275. package/examples/hooks/status-line.ts +40 -0
  276. package/examples/sdk/01-minimal.ts +1 -1
  277. package/examples/sdk/02-custom-model.ts +1 -1
  278. package/examples/sdk/03-custom-prompt.ts +1 -1
  279. package/examples/sdk/04-skills.ts +1 -1
  280. package/examples/sdk/05-tools.ts +4 -4
  281. package/examples/sdk/06-hooks.ts +1 -1
  282. package/examples/sdk/07-context-files.ts +1 -1
  283. package/examples/sdk/08-slash-commands.ts +6 -1
  284. package/examples/sdk/09-api-keys-and-oauth.ts +1 -1
  285. package/examples/sdk/10-settings.ts +1 -1
  286. package/examples/sdk/11-sessions.ts +1 -1
  287. package/examples/sdk/12-full-control.ts +4 -7
  288. package/package.json +6 -6
  289. package/dist/core/compaction.d.ts.map +0 -1
  290. package/dist/core/compaction.js +0 -412
  291. package/dist/core/compaction.js.map +0 -1
  292. package/dist/core/export-html.d.ts +0 -23
  293. package/dist/core/export-html.d.ts.map +0 -1
  294. package/dist/core/export-html.js +0 -1185
  295. package/dist/core/export-html.js.map +0 -1
  296. package/dist/modes/interactive/components/compaction.d.ts +0 -15
  297. package/dist/modes/interactive/components/compaction.d.ts.map +0 -1
  298. package/dist/modes/interactive/components/compaction.js +0 -41
  299. package/dist/modes/interactive/components/compaction.js.map +0 -1
  300. package/docs/hooks-v2.md +0 -385
  301. package/docs/session-tree.md +0 -452
@@ -1,13 +1,21 @@
1
+ > pi can create custom tools. Ask it to build one for your use case.
2
+
1
3
  # Custom Tools
2
4
 
3
5
  Custom tools are additional tools that the LLM can call directly, just like the built-in `read`, `write`, `edit`, and `bash` tools. They are TypeScript modules that define callable functions with parameters, return values, and optional TUI rendering.
4
6
 
7
+ **Key capabilities:**
8
+ - **User interaction** - Prompt users via `pi.ui` (select, confirm, input dialogs)
9
+ - **Custom rendering** - Control how tool calls and results appear via `renderCall`/`renderResult`
10
+ - **TUI components** - Render custom components with `pi.ui.custom()` (see [tui.md](tui.md))
11
+ - **State management** - Persist state in tool result `details` for proper branching support
12
+ - **Streaming results** - Send partial updates via `onUpdate` callback
13
+
5
14
  **Example use cases:**
6
- - Ask the user questions with selectable options
7
- - Maintain state across calls (todo lists, connection pools)
8
- - Custom TUI rendering (progress indicators, structured output)
9
- - Integrate external services with proper error handling
10
- - Tools that need user confirmation before proceeding
15
+ - Interactive dialogs (questions with selectable options)
16
+ - Stateful tools (todo lists, connection pools)
17
+ - Rich output rendering (progress indicators, structured views)
18
+ - External service integrations with confirmation flows
11
19
 
12
20
  **When to use custom tools vs. alternatives:**
13
21
 
@@ -36,10 +44,11 @@ const factory: CustomToolFactory = (pi) => ({
36
44
  name: Type.String({ description: "Name to greet" }),
37
45
  }),
38
46
 
39
- async execute(toolCallId, params) {
47
+ async execute(toolCallId, params, onUpdate, ctx, signal) {
48
+ const { name } = params as { name: string };
40
49
  return {
41
- content: [{ type: "text", text: `Hello, ${params.name}!` }],
42
- details: { greeted: params.name },
50
+ content: [{ type: "text", text: `Hello, ${name}!` }],
51
+ details: { greeted: name },
43
52
  };
44
53
  },
45
54
  });
@@ -82,7 +91,7 @@ Custom tools can import from these packages (automatically resolved by pi):
82
91
  | Package | Purpose |
83
92
  |---------|---------|
84
93
  | `@sinclair/typebox` | Schema definitions (`Type.Object`, `Type.String`, etc.) |
85
- | `@mariozechner/pi-coding-agent` | Types (`CustomToolFactory`, `ToolSessionEvent`, etc.) |
94
+ | `@mariozechner/pi-coding-agent` | Types (`CustomToolFactory`, `CustomTool`, `CustomToolContext`, etc.) |
86
95
  | `@mariozechner/pi-ai` | AI utilities (`StringEnum` for Google-compatible enums) |
87
96
  | `@mariozechner/pi-tui` | TUI components (`Text`, `Box`, etc. for custom rendering) |
88
97
 
@@ -94,7 +103,12 @@ Node.js built-in modules (`node:fs`, `node:path`, etc.) are also available.
94
103
  import { Type } from "@sinclair/typebox";
95
104
  import { StringEnum } from "@mariozechner/pi-ai";
96
105
  import { Text } from "@mariozechner/pi-tui";
97
- import type { CustomToolFactory, ToolSessionEvent } from "@mariozechner/pi-coding-agent";
106
+ import type {
107
+ CustomTool,
108
+ CustomToolContext,
109
+ CustomToolFactory,
110
+ CustomToolSessionEvent,
111
+ } from "@mariozechner/pi-coding-agent";
98
112
 
99
113
  const factory: CustomToolFactory = (pi) => ({
100
114
  name: "my_tool",
@@ -106,9 +120,10 @@ const factory: CustomToolFactory = (pi) => ({
106
120
  text: Type.Optional(Type.String()),
107
121
  }),
108
122
 
109
- async execute(toolCallId, params, signal, onUpdate) {
123
+ async execute(toolCallId, params, onUpdate, ctx, signal) {
110
124
  // signal - AbortSignal for cancellation
111
125
  // onUpdate - Callback for streaming partial results
126
+ // ctx - CustomToolContext with sessionManager, modelRegistry, model
112
127
  return {
113
128
  content: [{ type: "text", text: "Result for LLM" }],
114
129
  details: { /* structured data for rendering */ },
@@ -116,14 +131,17 @@ const factory: CustomToolFactory = (pi) => ({
116
131
  },
117
132
 
118
133
  // Optional: Session lifecycle callback
119
- onSession(event) { /* reconstruct state from entries */ },
134
+ onSession(event, ctx) {
135
+ if (event.reason === "shutdown") {
136
+ // Cleanup resources (close connections, save state, etc.)
137
+ return;
138
+ }
139
+ // Reconstruct state from ctx.sessionManager.getBranch()
140
+ },
120
141
 
121
142
  // Optional: Custom rendering
122
143
  renderCall(args, theme) { /* return Component */ },
123
144
  renderResult(result, options, theme) { /* return Component */ },
124
-
125
- // Optional: Cleanup on session end
126
- dispose() { /* save state, close connections */ },
127
145
  });
128
146
 
129
147
  export default factory;
@@ -131,23 +149,26 @@ export default factory;
131
149
 
132
150
  **Important:** Use `StringEnum` from `@mariozechner/pi-ai` instead of `Type.Union`/`Type.Literal` for string enums. The latter doesn't work with Google's API.
133
151
 
134
- ## ToolAPI Object
152
+ ## CustomToolAPI Object
135
153
 
136
- The factory receives a `ToolAPI` object (named `pi` by convention):
154
+ The factory receives a `CustomToolAPI` object (named `pi` by convention):
137
155
 
138
156
  ```typescript
139
- interface ToolAPI {
157
+ interface CustomToolAPI {
140
158
  cwd: string; // Current working directory
141
159
  exec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;
142
- ui: {
143
- select(title: string, options: string[]): Promise<string | null>;
144
- confirm(title: string, message: string): Promise<boolean>;
145
- input(title: string, placeholder?: string): Promise<string | null>;
146
- notify(message: string, type?: "info" | "warning" | "error"): void;
147
- };
160
+ ui: ToolUIContext;
148
161
  hasUI: boolean; // false in --print or --mode rpc
149
162
  }
150
163
 
164
+ interface ToolUIContext {
165
+ select(title: string, options: string[]): Promise<string | undefined>;
166
+ confirm(title: string, message: string): Promise<boolean>;
167
+ input(title: string, placeholder?: string): Promise<string | undefined>;
168
+ notify(message: string, type?: "info" | "warning" | "error"): void;
169
+ custom(component: Component & { dispose?(): void }): { close: () => void; requestRender: () => void };
170
+ }
171
+
151
172
  interface ExecOptions {
152
173
  signal?: AbortSignal; // Cancel the process
153
174
  timeout?: number; // Timeout in milliseconds
@@ -168,7 +189,7 @@ Always check `pi.hasUI` before using UI methods.
168
189
  Pass the `signal` from `execute` to `pi.exec` to support cancellation:
169
190
 
170
191
  ```typescript
171
- async execute(toolCallId, params, signal) {
192
+ async execute(toolCallId, params, onUpdate, ctx, signal) {
172
193
  const result = await pi.exec("long-running-command", ["arg"], { signal });
173
194
  if (result.killed) {
174
195
  return { content: [{ type: "text", text: "Cancelled" }] };
@@ -177,24 +198,101 @@ async execute(toolCallId, params, signal) {
177
198
  }
178
199
  ```
179
200
 
201
+ ### Error Handling
202
+
203
+ **Throw an error** when the tool fails. Do not return an error message as content.
204
+
205
+ ```typescript
206
+ async execute(toolCallId, params, onUpdate, ctx, signal) {
207
+ const { path } = params as { path: string };
208
+
209
+ // Throw on error - pi will catch it and report to the LLM
210
+ if (!fs.existsSync(path)) {
211
+ throw new Error(`File not found: ${path}`);
212
+ }
213
+
214
+ // Return content only on success
215
+ return { content: [{ type: "text", text: "Success" }] };
216
+ }
217
+ ```
218
+
219
+ Thrown errors are:
220
+ - Reported to the LLM as tool errors (with `isError: true`)
221
+ - Emitted to hooks via `tool_result` event (hooks can inspect `event.isError`)
222
+ - Displayed in the TUI with error styling
223
+
224
+ ## CustomToolContext
225
+
226
+ The `execute` and `onSession` callbacks receive a `CustomToolContext`:
227
+
228
+ ```typescript
229
+ interface CustomToolContext {
230
+ sessionManager: ReadonlySessionManager; // Read-only access to session
231
+ modelRegistry: ModelRegistry; // For API key resolution
232
+ model: Model | undefined; // Current model (may be undefined)
233
+ isIdle(): boolean; // Whether agent is streaming
234
+ hasQueuedMessages(): boolean; // Whether user has queued messages
235
+ abort(): void; // Abort current operation (fire-and-forget)
236
+ }
237
+ ```
238
+
239
+ Use `ctx.sessionManager.getBranch()` to get entries on the current branch for state reconstruction.
240
+
241
+ ### Checking Queue State
242
+
243
+ Interactive tools can skip prompts when the user has already queued a message:
244
+
245
+ ```typescript
246
+ async execute(toolCallId, params, onUpdate, ctx, signal) {
247
+ // If user already queued a message, skip the interactive prompt
248
+ if (ctx.hasQueuedMessages()) {
249
+ return {
250
+ content: [{ type: "text", text: "Skipped - user has queued input" }],
251
+ };
252
+ }
253
+
254
+ // Otherwise, prompt for input
255
+ const answer = await pi.ui.input("What would you like to do?");
256
+ // ...
257
+ }
258
+ ```
259
+
260
+ ### Multi-line Editor
261
+
262
+ For longer text editing, use `pi.ui.editor()` which supports Ctrl+G for external editor:
263
+
264
+ ```typescript
265
+ async execute(toolCallId, params, onUpdate, ctx, signal) {
266
+ const text = await pi.ui.editor("Edit your response:", "prefilled text");
267
+ // Returns edited text or undefined if cancelled (Escape)
268
+ // Ctrl+Enter to submit, Ctrl+G to open $VISUAL or $EDITOR
269
+
270
+ if (!text) {
271
+ return { content: [{ type: "text", text: "Cancelled" }] };
272
+ }
273
+ // ...
274
+ }
275
+ ```
276
+
180
277
  ## Session Lifecycle
181
278
 
182
279
  Tools can implement `onSession` to react to session changes:
183
280
 
184
281
  ```typescript
185
- interface ToolSessionEvent {
186
- entries: SessionEntry[]; // All session entries
187
- sessionFile: string | null; // Current session file
188
- previousSessionFile: string | null; // Previous session file
189
- reason: "start" | "switch" | "branch" | "new";
282
+ interface CustomToolSessionEvent {
283
+ reason: "start" | "switch" | "branch" | "tree" | "shutdown";
284
+ previousSessionFile: string | undefined;
190
285
  }
191
286
  ```
192
287
 
193
288
  **Reasons:**
194
289
  - `start`: Initial session load on startup
195
- - `switch`: User switched to a different session (`/resume`)
290
+ - `switch`: User started a new session (`/new`) or switched to a different session (`/resume`)
196
291
  - `branch`: User branched from a previous message (`/branch`)
197
- - `new`: User started a new session (`/new`)
292
+ - `tree`: User navigated to a different point in the session tree (`/tree`)
293
+ - `shutdown`: Process is exiting (Ctrl+C, Ctrl+D, or SIGTERM) - use to cleanup resources
294
+
295
+ To check if a session is fresh (no messages), use `ctx.sessionManager.getEntries().length === 0`.
198
296
 
199
297
  ### State Management Pattern
200
298
 
@@ -210,9 +308,11 @@ const factory: CustomToolFactory = (pi) => {
210
308
  let items: string[] = [];
211
309
 
212
310
  // Reconstruct state from session entries
213
- const reconstructState = (event: ToolSessionEvent) => {
311
+ const reconstructState = (event: CustomToolSessionEvent, ctx: CustomToolContext) => {
312
+ if (event.reason === "shutdown") return;
313
+
214
314
  items = [];
215
- for (const entry of event.entries) {
315
+ for (const entry of ctx.sessionManager.getBranch()) {
216
316
  if (entry.type !== "message") continue;
217
317
  const msg = entry.message;
218
318
  if (msg.role !== "toolResult") continue;
@@ -233,7 +333,7 @@ const factory: CustomToolFactory = (pi) => {
233
333
 
234
334
  onSession: reconstructState,
235
335
 
236
- async execute(toolCallId, params) {
336
+ async execute(toolCallId, params, onUpdate, ctx, signal) {
237
337
  // Modify items...
238
338
  items.push("new item");
239
339
 
@@ -254,7 +354,7 @@ This pattern ensures:
254
354
 
255
355
  ## Custom Rendering
256
356
 
257
- Custom tools can provide `renderCall` and `renderResult` methods to control how they appear in the TUI. Both are optional.
357
+ Custom tools can provide `renderCall` and `renderResult` methods to control how they appear in the TUI. Both are optional. See [tui.md](tui.md) for the full component API.
258
358
 
259
359
  ### How It Works
260
360
 
@@ -355,7 +455,7 @@ If `renderCall` or `renderResult` is not defined or throws an error:
355
455
  ## Execute Function
356
456
 
357
457
  ```typescript
358
- async execute(toolCallId, args, signal, onUpdate) {
458
+ async execute(toolCallId, args, onUpdate, ctx, signal) {
359
459
  // Type assertion for params (TypeBox schema doesn't flow through)
360
460
  const params = args as { action: "list" | "add"; text?: string };
361
461
 
@@ -387,13 +487,16 @@ const factory: CustomToolFactory = (pi) => {
387
487
  // Shared state
388
488
  let connection = null;
389
489
 
490
+ const handleSession = (event: CustomToolSessionEvent, ctx: CustomToolContext) => {
491
+ if (event.reason === "shutdown") {
492
+ connection?.close();
493
+ }
494
+ };
495
+
390
496
  return [
391
- { name: "db_connect", ... },
392
- { name: "db_query", ... },
393
- {
394
- name: "db_close",
395
- dispose() { connection?.close(); }
396
- },
497
+ { name: "db_connect", onSession: handleSession, ... },
498
+ { name: "db_query", onSession: handleSession, ... },
499
+ { name: "db_close", onSession: handleSession, ... },
397
500
  ];
398
501
  };
399
502
  ```