@oh-my-pi/pi-coding-agent 1.337.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 (224) hide show
  1. package/CHANGELOG.md +1228 -0
  2. package/README.md +1041 -0
  3. package/docs/compaction.md +403 -0
  4. package/docs/custom-tools.md +541 -0
  5. package/docs/extension-loading.md +1004 -0
  6. package/docs/hooks.md +867 -0
  7. package/docs/rpc.md +1040 -0
  8. package/docs/sdk.md +994 -0
  9. package/docs/session-tree-plan.md +441 -0
  10. package/docs/session.md +240 -0
  11. package/docs/skills.md +290 -0
  12. package/docs/theme.md +637 -0
  13. package/docs/tree.md +197 -0
  14. package/docs/tui.md +341 -0
  15. package/examples/README.md +21 -0
  16. package/examples/custom-tools/README.md +124 -0
  17. package/examples/custom-tools/hello/index.ts +20 -0
  18. package/examples/custom-tools/question/index.ts +84 -0
  19. package/examples/custom-tools/subagent/README.md +172 -0
  20. package/examples/custom-tools/subagent/agents/planner.md +37 -0
  21. package/examples/custom-tools/subagent/agents/reviewer.md +35 -0
  22. package/examples/custom-tools/subagent/agents/scout.md +50 -0
  23. package/examples/custom-tools/subagent/agents/worker.md +24 -0
  24. package/examples/custom-tools/subagent/agents.ts +156 -0
  25. package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
  26. package/examples/custom-tools/subagent/commands/implement.md +10 -0
  27. package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
  28. package/examples/custom-tools/subagent/index.ts +1002 -0
  29. package/examples/custom-tools/todo/index.ts +212 -0
  30. package/examples/hooks/README.md +56 -0
  31. package/examples/hooks/auto-commit-on-exit.ts +49 -0
  32. package/examples/hooks/confirm-destructive.ts +59 -0
  33. package/examples/hooks/custom-compaction.ts +116 -0
  34. package/examples/hooks/dirty-repo-guard.ts +52 -0
  35. package/examples/hooks/file-trigger.ts +41 -0
  36. package/examples/hooks/git-checkpoint.ts +53 -0
  37. package/examples/hooks/handoff.ts +150 -0
  38. package/examples/hooks/permission-gate.ts +34 -0
  39. package/examples/hooks/protected-paths.ts +30 -0
  40. package/examples/hooks/qna.ts +119 -0
  41. package/examples/hooks/snake.ts +343 -0
  42. package/examples/hooks/status-line.ts +40 -0
  43. package/examples/sdk/01-minimal.ts +22 -0
  44. package/examples/sdk/02-custom-model.ts +49 -0
  45. package/examples/sdk/03-custom-prompt.ts +44 -0
  46. package/examples/sdk/04-skills.ts +44 -0
  47. package/examples/sdk/05-tools.ts +90 -0
  48. package/examples/sdk/06-hooks.ts +61 -0
  49. package/examples/sdk/07-context-files.ts +36 -0
  50. package/examples/sdk/08-slash-commands.ts +42 -0
  51. package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
  52. package/examples/sdk/10-settings.ts +38 -0
  53. package/examples/sdk/11-sessions.ts +48 -0
  54. package/examples/sdk/12-full-control.ts +95 -0
  55. package/examples/sdk/README.md +154 -0
  56. package/package.json +81 -0
  57. package/src/cli/args.ts +246 -0
  58. package/src/cli/file-processor.ts +72 -0
  59. package/src/cli/list-models.ts +104 -0
  60. package/src/cli/plugin-cli.ts +650 -0
  61. package/src/cli/session-picker.ts +41 -0
  62. package/src/cli.ts +10 -0
  63. package/src/commands/init.md +20 -0
  64. package/src/config.ts +159 -0
  65. package/src/core/agent-session.ts +1900 -0
  66. package/src/core/auth-storage.ts +236 -0
  67. package/src/core/bash-executor.ts +196 -0
  68. package/src/core/compaction/branch-summarization.ts +343 -0
  69. package/src/core/compaction/compaction.ts +742 -0
  70. package/src/core/compaction/index.ts +7 -0
  71. package/src/core/compaction/utils.ts +154 -0
  72. package/src/core/custom-tools/index.ts +21 -0
  73. package/src/core/custom-tools/loader.ts +248 -0
  74. package/src/core/custom-tools/types.ts +169 -0
  75. package/src/core/custom-tools/wrapper.ts +28 -0
  76. package/src/core/exec.ts +129 -0
  77. package/src/core/export-html/index.ts +211 -0
  78. package/src/core/export-html/template.css +781 -0
  79. package/src/core/export-html/template.html +54 -0
  80. package/src/core/export-html/template.js +1185 -0
  81. package/src/core/export-html/vendor/highlight.min.js +1213 -0
  82. package/src/core/export-html/vendor/marked.min.js +6 -0
  83. package/src/core/hooks/index.ts +16 -0
  84. package/src/core/hooks/loader.ts +312 -0
  85. package/src/core/hooks/runner.ts +434 -0
  86. package/src/core/hooks/tool-wrapper.ts +99 -0
  87. package/src/core/hooks/types.ts +773 -0
  88. package/src/core/index.ts +52 -0
  89. package/src/core/mcp/client.ts +158 -0
  90. package/src/core/mcp/config.ts +154 -0
  91. package/src/core/mcp/index.ts +45 -0
  92. package/src/core/mcp/loader.ts +68 -0
  93. package/src/core/mcp/manager.ts +181 -0
  94. package/src/core/mcp/tool-bridge.ts +148 -0
  95. package/src/core/mcp/transports/http.ts +316 -0
  96. package/src/core/mcp/transports/index.ts +6 -0
  97. package/src/core/mcp/transports/stdio.ts +252 -0
  98. package/src/core/mcp/types.ts +220 -0
  99. package/src/core/messages.ts +189 -0
  100. package/src/core/model-registry.ts +317 -0
  101. package/src/core/model-resolver.ts +393 -0
  102. package/src/core/plugins/doctor.ts +59 -0
  103. package/src/core/plugins/index.ts +38 -0
  104. package/src/core/plugins/installer.ts +189 -0
  105. package/src/core/plugins/loader.ts +338 -0
  106. package/src/core/plugins/manager.ts +672 -0
  107. package/src/core/plugins/parser.ts +105 -0
  108. package/src/core/plugins/paths.ts +32 -0
  109. package/src/core/plugins/types.ts +190 -0
  110. package/src/core/sdk.ts +760 -0
  111. package/src/core/session-manager.ts +1128 -0
  112. package/src/core/settings-manager.ts +443 -0
  113. package/src/core/skills.ts +437 -0
  114. package/src/core/slash-commands.ts +248 -0
  115. package/src/core/system-prompt.ts +439 -0
  116. package/src/core/timings.ts +25 -0
  117. package/src/core/tools/ask.ts +211 -0
  118. package/src/core/tools/bash-interceptor.ts +120 -0
  119. package/src/core/tools/bash.ts +250 -0
  120. package/src/core/tools/context.ts +32 -0
  121. package/src/core/tools/edit-diff.ts +475 -0
  122. package/src/core/tools/edit.ts +208 -0
  123. package/src/core/tools/exa/company.ts +59 -0
  124. package/src/core/tools/exa/index.ts +64 -0
  125. package/src/core/tools/exa/linkedin.ts +59 -0
  126. package/src/core/tools/exa/logger.ts +56 -0
  127. package/src/core/tools/exa/mcp-client.ts +368 -0
  128. package/src/core/tools/exa/render.ts +196 -0
  129. package/src/core/tools/exa/researcher.ts +90 -0
  130. package/src/core/tools/exa/search.ts +337 -0
  131. package/src/core/tools/exa/types.ts +168 -0
  132. package/src/core/tools/exa/websets.ts +248 -0
  133. package/src/core/tools/find.ts +261 -0
  134. package/src/core/tools/grep.ts +555 -0
  135. package/src/core/tools/index.ts +202 -0
  136. package/src/core/tools/ls.ts +140 -0
  137. package/src/core/tools/lsp/client.ts +605 -0
  138. package/src/core/tools/lsp/config.ts +147 -0
  139. package/src/core/tools/lsp/edits.ts +101 -0
  140. package/src/core/tools/lsp/index.ts +804 -0
  141. package/src/core/tools/lsp/render.ts +447 -0
  142. package/src/core/tools/lsp/rust-analyzer.ts +145 -0
  143. package/src/core/tools/lsp/types.ts +463 -0
  144. package/src/core/tools/lsp/utils.ts +486 -0
  145. package/src/core/tools/notebook.ts +229 -0
  146. package/src/core/tools/path-utils.ts +61 -0
  147. package/src/core/tools/read.ts +240 -0
  148. package/src/core/tools/renderers.ts +540 -0
  149. package/src/core/tools/task/agents.ts +153 -0
  150. package/src/core/tools/task/artifacts.ts +114 -0
  151. package/src/core/tools/task/bundled-agents/browser.md +71 -0
  152. package/src/core/tools/task/bundled-agents/explore.md +82 -0
  153. package/src/core/tools/task/bundled-agents/plan.md +54 -0
  154. package/src/core/tools/task/bundled-agents/reviewer.md +59 -0
  155. package/src/core/tools/task/bundled-agents/task.md +53 -0
  156. package/src/core/tools/task/bundled-commands/architect-plan.md +10 -0
  157. package/src/core/tools/task/bundled-commands/implement-with-critic.md +11 -0
  158. package/src/core/tools/task/bundled-commands/implement.md +11 -0
  159. package/src/core/tools/task/commands.ts +213 -0
  160. package/src/core/tools/task/discovery.ts +208 -0
  161. package/src/core/tools/task/executor.ts +367 -0
  162. package/src/core/tools/task/index.ts +388 -0
  163. package/src/core/tools/task/model-resolver.ts +115 -0
  164. package/src/core/tools/task/parallel.ts +38 -0
  165. package/src/core/tools/task/render.ts +232 -0
  166. package/src/core/tools/task/types.ts +99 -0
  167. package/src/core/tools/truncate.ts +265 -0
  168. package/src/core/tools/web-fetch.ts +2370 -0
  169. package/src/core/tools/web-search/auth.ts +193 -0
  170. package/src/core/tools/web-search/index.ts +537 -0
  171. package/src/core/tools/web-search/providers/anthropic.ts +198 -0
  172. package/src/core/tools/web-search/providers/exa.ts +302 -0
  173. package/src/core/tools/web-search/providers/perplexity.ts +195 -0
  174. package/src/core/tools/web-search/render.ts +182 -0
  175. package/src/core/tools/web-search/types.ts +180 -0
  176. package/src/core/tools/write.ts +99 -0
  177. package/src/index.ts +176 -0
  178. package/src/main.ts +464 -0
  179. package/src/migrations.ts +135 -0
  180. package/src/modes/index.ts +43 -0
  181. package/src/modes/interactive/components/armin.ts +382 -0
  182. package/src/modes/interactive/components/assistant-message.ts +86 -0
  183. package/src/modes/interactive/components/bash-execution.ts +196 -0
  184. package/src/modes/interactive/components/bordered-loader.ts +41 -0
  185. package/src/modes/interactive/components/branch-summary-message.ts +42 -0
  186. package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
  187. package/src/modes/interactive/components/custom-editor.ts +122 -0
  188. package/src/modes/interactive/components/diff.ts +147 -0
  189. package/src/modes/interactive/components/dynamic-border.ts +25 -0
  190. package/src/modes/interactive/components/footer.ts +381 -0
  191. package/src/modes/interactive/components/hook-editor.ts +117 -0
  192. package/src/modes/interactive/components/hook-input.ts +64 -0
  193. package/src/modes/interactive/components/hook-message.ts +96 -0
  194. package/src/modes/interactive/components/hook-selector.ts +91 -0
  195. package/src/modes/interactive/components/model-selector.ts +247 -0
  196. package/src/modes/interactive/components/oauth-selector.ts +120 -0
  197. package/src/modes/interactive/components/plugin-settings.ts +479 -0
  198. package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
  199. package/src/modes/interactive/components/session-selector.ts +204 -0
  200. package/src/modes/interactive/components/settings-selector.ts +453 -0
  201. package/src/modes/interactive/components/show-images-selector.ts +45 -0
  202. package/src/modes/interactive/components/theme-selector.ts +62 -0
  203. package/src/modes/interactive/components/thinking-selector.ts +64 -0
  204. package/src/modes/interactive/components/tool-execution.ts +675 -0
  205. package/src/modes/interactive/components/tree-selector.ts +866 -0
  206. package/src/modes/interactive/components/user-message-selector.ts +159 -0
  207. package/src/modes/interactive/components/user-message.ts +18 -0
  208. package/src/modes/interactive/components/visual-truncate.ts +50 -0
  209. package/src/modes/interactive/components/welcome.ts +183 -0
  210. package/src/modes/interactive/interactive-mode.ts +2516 -0
  211. package/src/modes/interactive/theme/dark.json +101 -0
  212. package/src/modes/interactive/theme/light.json +98 -0
  213. package/src/modes/interactive/theme/theme-schema.json +308 -0
  214. package/src/modes/interactive/theme/theme.ts +998 -0
  215. package/src/modes/print-mode.ts +128 -0
  216. package/src/modes/rpc/rpc-client.ts +527 -0
  217. package/src/modes/rpc/rpc-mode.ts +483 -0
  218. package/src/modes/rpc/rpc-types.ts +203 -0
  219. package/src/utils/changelog.ts +99 -0
  220. package/src/utils/clipboard.ts +265 -0
  221. package/src/utils/fuzzy.ts +108 -0
  222. package/src/utils/mime.ts +30 -0
  223. package/src/utils/shell.ts +276 -0
  224. package/src/utils/tools-manager.ts +274 -0
@@ -0,0 +1,541 @@
1
+ > pi can create custom tools. Ask it to build one for your use case.
2
+
3
+ # Custom Tools
4
+
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.
6
+
7
+ **Key capabilities:**
8
+
9
+ - **User interaction** - Prompt users via `pi.ui` (select, confirm, input dialogs)
10
+ - **Custom rendering** - Control how tool calls and results appear via `renderCall`/`renderResult`
11
+ - **TUI components** - Render custom components with `pi.ui.custom()` (see [tui.md](tui.md))
12
+ - **State management** - Persist state in tool result `details` for proper branching support
13
+ - **Streaming results** - Send partial updates via `onUpdate` callback
14
+
15
+ **Example use cases:**
16
+
17
+ - Interactive dialogs (questions with selectable options)
18
+ - Stateful tools (todo lists, connection pools)
19
+ - Rich output rendering (progress indicators, structured views)
20
+ - External service integrations with confirmation flows
21
+
22
+ **When to use custom tools vs. alternatives:**
23
+
24
+ | Need | Solution |
25
+ | -------------------------------------------------------- | --------------- |
26
+ | Always-needed context (conventions, commands) | AGENTS.md |
27
+ | User triggers a specific prompt template | Slash command |
28
+ | On-demand capability package (workflows, scripts, setup) | Skill |
29
+ | Additional tool directly callable by the LLM | **Custom tool** |
30
+
31
+ See [examples/custom-tools/](../examples/custom-tools/) for working examples.
32
+
33
+ ## Quick Start
34
+
35
+ Create a file `~/.pi/agent/tools/hello/index.ts`:
36
+
37
+ ```typescript
38
+ import type { CustomToolFactory } from "@oh-my-pi/pi-coding-agent";
39
+
40
+ const factory: CustomToolFactory = (pi) => ({
41
+ name: "hello",
42
+ label: "Hello",
43
+ description: "A simple greeting tool",
44
+ parameters: pi.typebox.Type.Object({
45
+ name: pi.typebox.Type.String({ description: "Name to greet" }),
46
+ }),
47
+
48
+ async execute(toolCallId, params, onUpdate, ctx, signal) {
49
+ const { name } = params as { name: string };
50
+ return {
51
+ content: [{ type: "text", text: `Hello, ${name}!` }],
52
+ details: { greeted: name },
53
+ };
54
+ },
55
+ });
56
+
57
+ export default factory;
58
+ ```
59
+
60
+ The tool is automatically discovered and available in your next pi session.
61
+
62
+ ## Tool Locations
63
+
64
+ Tools must be in a subdirectory with an `index.ts` entry point:
65
+
66
+ | Location | Scope | Auto-discovered |
67
+ | ----------------------------------- | --------------------- | --------------- |
68
+ | `~/.pi/agent/tools/*/index.ts` | Global (all projects) | Yes |
69
+ | `.pi/tools/*/index.ts` | Project-local | Yes |
70
+ | `settings.json` `customTools` array | Configured paths | Yes |
71
+ | `--tool <path>` CLI flag | One-off/debugging | No |
72
+
73
+ **Example structure:**
74
+
75
+ ```
76
+ ~/.pi/agent/tools/
77
+ ├── hello/
78
+ │ └── index.ts # Entry point (auto-discovered)
79
+ └── complex-tool/
80
+ ├── index.ts # Entry point (auto-discovered)
81
+ ├── helpers.ts # Helper module (not loaded directly)
82
+ └── types.ts # Type definitions (not loaded directly)
83
+ ```
84
+
85
+ **Priority:** Later sources win on name conflicts. CLI `--tool` takes highest priority.
86
+
87
+ **Reserved names:** Custom tools cannot use built-in tool names (`read`, `write`, `edit`, `bash`, `grep`, `find`, `ls`).
88
+
89
+ ## Available Imports
90
+
91
+ Custom tools can import from these packages:
92
+
93
+ | Package | Purpose | Import Method |
94
+ | --------------------------- | --------------------------------------------------------- | --------------------------------------------------- |
95
+ | `@sinclair/typebox` | Schema definitions (`Type.Object`, `Type.String`, etc.) | Via `pi.typebox.*` (injected) |
96
+ | `@oh-my-pi/pi-coding-agent` | Types and utilities | Via `pi.pi.*` (injected) or direct import for types |
97
+ | `@oh-my-pi/pi-ai` | AI utilities (`StringEnum` for Google-compatible enums) | Via `pi.pi.*` (re-exported through coding-agent) |
98
+ | `@oh-my-pi/pi-tui` | TUI components (`Text`, `Box`, etc. for custom rendering) | Via `pi.pi.*` (re-exported through coding-agent) |
99
+
100
+ Node.js built-in modules (`node:fs`, `node:path`, etc.) are also available.
101
+
102
+ **Important:** Use `pi.typebox.Type.*` instead of importing from `@sinclair/typebox` directly. Dependencies are injected via the `CustomToolAPI` to avoid import resolution issues.
103
+
104
+ ## Tool Definition
105
+
106
+ ```typescript
107
+ import type {
108
+ CustomTool,
109
+ CustomToolContext,
110
+ CustomToolFactory,
111
+ CustomToolSessionEvent,
112
+ } from "@oh-my-pi/pi-coding-agent";
113
+
114
+ const factory: CustomToolFactory = (pi) => {
115
+ // Destructure injected dependencies
116
+ const { Type } = pi.typebox;
117
+ const { StringEnum } = pi.pi;
118
+ const { Text } = pi.pi;
119
+
120
+ return {
121
+ name: "my_tool",
122
+ label: "My Tool",
123
+ description: "What this tool does (be specific for LLM)",
124
+ parameters: Type.Object({
125
+ // Use StringEnum for string enums (Google API compatible)
126
+ action: StringEnum(["list", "add", "remove"] as const),
127
+ text: Type.Optional(Type.String()),
128
+ }),
129
+
130
+ async execute(toolCallId, params, onUpdate, ctx, signal) {
131
+ // signal - AbortSignal for cancellation
132
+ // onUpdate - Callback for streaming partial results
133
+ // ctx - CustomToolContext with sessionManager, modelRegistry, model
134
+ return {
135
+ content: [{ type: "text", text: "Result for LLM" }],
136
+ details: {
137
+ /* structured data for rendering */
138
+ },
139
+ };
140
+ },
141
+
142
+ // Optional: Session lifecycle callback
143
+ onSession(event, ctx) {
144
+ if (event.reason === "shutdown") {
145
+ // Cleanup resources (close connections, save state, etc.)
146
+ return;
147
+ }
148
+ // Reconstruct state from ctx.sessionManager.getBranch()
149
+ },
150
+
151
+ // Optional: Custom rendering
152
+ renderCall(args, theme) {
153
+ /* return Component */
154
+ },
155
+ renderResult(result, options, theme) {
156
+ /* return Component */
157
+ },
158
+ };
159
+ };
160
+
161
+ export default factory;
162
+ ```
163
+
164
+ **Important:** Use `StringEnum` from `pi.pi` instead of `Type.Union`/`Type.Literal` for string enums. The latter doesn't work with Google's API.
165
+
166
+ ## CustomToolAPI Object
167
+
168
+ The factory receives a `CustomToolAPI` object (named `pi` by convention):
169
+
170
+ ```typescript
171
+ interface CustomToolAPI {
172
+ cwd: string; // Current working directory
173
+ exec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;
174
+ ui: ToolUIContext;
175
+ hasUI: boolean; // false in --print or --mode rpc
176
+ typebox: typeof import("@sinclair/typebox"); // Injected @sinclair/typebox
177
+ pi: typeof import("@oh-my-pi/pi-coding-agent"); // Injected pi-coding-agent exports
178
+ }
179
+
180
+ interface ToolUIContext {
181
+ select(title: string, options: string[]): Promise<string | undefined>;
182
+ confirm(title: string, message: string): Promise<boolean>;
183
+ input(title: string, placeholder?: string): Promise<string | undefined>;
184
+ notify(message: string, type?: "info" | "warning" | "error"): void;
185
+ custom(component: Component & { dispose?(): void }): { close: () => void; requestRender: () => void };
186
+ }
187
+
188
+ interface ExecOptions {
189
+ signal?: AbortSignal; // Cancel the process
190
+ timeout?: number; // Timeout in milliseconds
191
+ }
192
+
193
+ interface ExecResult {
194
+ stdout: string;
195
+ stderr: string;
196
+ code: number;
197
+ killed?: boolean; // True if process was killed by signal/timeout
198
+ }
199
+ ```
200
+
201
+ Always check `pi.hasUI` before using UI methods.
202
+
203
+ ### Cancellation Example
204
+
205
+ Pass the `signal` from `execute` to `pi.exec` to support cancellation:
206
+
207
+ ```typescript
208
+ async execute(toolCallId, params, onUpdate, ctx, signal) {
209
+ const result = await pi.exec("long-running-command", ["arg"], { signal });
210
+ if (result.killed) {
211
+ return { content: [{ type: "text", text: "Cancelled" }] };
212
+ }
213
+ return { content: [{ type: "text", text: result.stdout }] };
214
+ }
215
+ ```
216
+
217
+ ### Error Handling
218
+
219
+ **Throw an error** when the tool fails. Do not return an error message as content.
220
+
221
+ ```typescript
222
+ async execute(toolCallId, params, onUpdate, ctx, signal) {
223
+ const { path } = params as { path: string };
224
+
225
+ // Throw on error - pi will catch it and report to the LLM
226
+ if (!fs.existsSync(path)) {
227
+ throw new Error(`File not found: ${path}`);
228
+ }
229
+
230
+ // Return content only on success
231
+ return { content: [{ type: "text", text: "Success" }] };
232
+ }
233
+ ```
234
+
235
+ Thrown errors are:
236
+
237
+ - Reported to the LLM as tool errors (with `isError: true`)
238
+ - Emitted to hooks via `tool_result` event (hooks can inspect `event.isError`)
239
+ - Displayed in the TUI with error styling
240
+
241
+ ## CustomToolContext
242
+
243
+ The `execute` and `onSession` callbacks receive a `CustomToolContext`:
244
+
245
+ ```typescript
246
+ interface CustomToolContext {
247
+ sessionManager: ReadonlySessionManager; // Read-only access to session
248
+ modelRegistry: ModelRegistry; // For API key resolution
249
+ model: Model | undefined; // Current model (may be undefined)
250
+ isIdle(): boolean; // Whether agent is streaming
251
+ hasQueuedMessages(): boolean; // Whether user has queued messages
252
+ abort(): void; // Abort current operation (fire-and-forget)
253
+ }
254
+ ```
255
+
256
+ Use `ctx.sessionManager.getBranch()` to get entries on the current branch for state reconstruction.
257
+
258
+ ### Checking Queue State
259
+
260
+ Interactive tools can skip prompts when the user has already queued a message:
261
+
262
+ ```typescript
263
+ async execute(toolCallId, params, onUpdate, ctx, signal) {
264
+ // If user already queued a message, skip the interactive prompt
265
+ if (ctx.hasQueuedMessages()) {
266
+ return {
267
+ content: [{ type: "text", text: "Skipped - user has queued input" }],
268
+ };
269
+ }
270
+
271
+ // Otherwise, prompt for input
272
+ const answer = await pi.ui.input("What would you like to do?");
273
+ // ...
274
+ }
275
+ ```
276
+
277
+ ### Multi-line Editor
278
+
279
+ For longer text editing, use `pi.ui.editor()` which supports Ctrl+G for external editor:
280
+
281
+ ```typescript
282
+ async execute(toolCallId, params, onUpdate, ctx, signal) {
283
+ const text = await pi.ui.editor("Edit your response:", "prefilled text");
284
+ // Returns edited text or undefined if cancelled (Escape)
285
+ // Ctrl+Enter to submit, Ctrl+G to open $VISUAL or $EDITOR
286
+
287
+ if (!text) {
288
+ return { content: [{ type: "text", text: "Cancelled" }] };
289
+ }
290
+ // ...
291
+ }
292
+ ```
293
+
294
+ ## Session Lifecycle
295
+
296
+ Tools can implement `onSession` to react to session changes:
297
+
298
+ ```typescript
299
+ interface CustomToolSessionEvent {
300
+ reason: "start" | "switch" | "branch" | "tree" | "shutdown";
301
+ previousSessionFile: string | undefined;
302
+ }
303
+ ```
304
+
305
+ **Reasons:**
306
+
307
+ - `start`: Initial session load on startup
308
+ - `switch`: User started a new session (`/new`) or switched to a different session (`/resume`)
309
+ - `branch`: User branched from a previous message (`/branch`)
310
+ - `tree`: User navigated to a different point in the session tree (`/tree`)
311
+ - `shutdown`: Process is exiting (Ctrl+C, Ctrl+D, or SIGTERM) - use to cleanup resources
312
+
313
+ To check if a session is fresh (no messages), use `ctx.sessionManager.getEntries().length === 0`.
314
+
315
+ ### State Management Pattern
316
+
317
+ Tools that maintain state should store it in `details` of their results, not external files. This allows branching to work correctly, as the state is reconstructed from the session history.
318
+
319
+ ```typescript
320
+ interface MyToolDetails {
321
+ items: string[];
322
+ }
323
+
324
+ const factory: CustomToolFactory = (pi) => {
325
+ const { Type } = pi.typebox;
326
+
327
+ // In-memory state
328
+ let items: string[] = [];
329
+
330
+ // Reconstruct state from session entries
331
+ const reconstructState = (event: CustomToolSessionEvent, ctx: CustomToolContext) => {
332
+ if (event.reason === "shutdown") return;
333
+
334
+ items = [];
335
+ for (const entry of ctx.sessionManager.getBranch()) {
336
+ if (entry.type !== "message") continue;
337
+ const msg = entry.message;
338
+ if (msg.role !== "toolResult") continue;
339
+ if (msg.toolName !== "my_tool") continue;
340
+
341
+ const details = msg.details as MyToolDetails | undefined;
342
+ if (details) {
343
+ items = details.items;
344
+ }
345
+ }
346
+ };
347
+
348
+ return {
349
+ name: "my_tool",
350
+ label: "My Tool",
351
+ description: "...",
352
+ parameters: Type.Object({ ... }),
353
+
354
+ onSession: reconstructState,
355
+
356
+ async execute(toolCallId, params, onUpdate, ctx, signal) {
357
+ // Modify items...
358
+ items.push("new item");
359
+
360
+ return {
361
+ content: [{ type: "text", text: "Added item" }],
362
+ // Store current state in details for reconstruction
363
+ details: { items: [...items] },
364
+ };
365
+ },
366
+ };
367
+ };
368
+ ```
369
+
370
+ This pattern ensures:
371
+
372
+ - When user branches, state is correct for that point in history
373
+ - When user switches sessions, state matches that session
374
+ - When user starts a new session, state resets
375
+
376
+ ## Custom Rendering
377
+
378
+ 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.
379
+
380
+ ### How It Works
381
+
382
+ Tool output is wrapped in a `Box` component that handles:
383
+
384
+ - Padding (1 character horizontal, 1 line vertical)
385
+ - Background color based on state (pending/success/error)
386
+
387
+ Your render methods return `Component` instances (typically `Text`) that go inside this box. Use `Text(content, 0, 0)` since the Box handles padding.
388
+
389
+ ### renderCall
390
+
391
+ Renders the tool call (before/during execution):
392
+
393
+ ```typescript
394
+ renderCall(args, theme) {
395
+ let text = theme.fg("toolTitle", theme.bold("my_tool "));
396
+ text += theme.fg("muted", args.action);
397
+ if (args.text) {
398
+ text += " " + theme.fg("dim", `"${args.text}"`);
399
+ }
400
+ return new Text(text, 0, 0);
401
+ }
402
+ ```
403
+
404
+ Called when:
405
+
406
+ - Tool call starts (may have partial args during streaming)
407
+ - Args are updated during streaming
408
+
409
+ ### renderResult
410
+
411
+ Renders the tool result:
412
+
413
+ ```typescript
414
+ renderResult(result, { expanded, isPartial }, theme) {
415
+ const { details } = result;
416
+
417
+ // Handle streaming/partial results
418
+ if (isPartial) {
419
+ return new Text(theme.fg("warning", "Processing..."), 0, 0);
420
+ }
421
+
422
+ // Handle errors
423
+ if (details?.error) {
424
+ return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
425
+ }
426
+
427
+ // Normal result
428
+ let text = theme.fg("success", "✓ ") + theme.fg("muted", "Done");
429
+
430
+ // Support expanded view (Ctrl+O)
431
+ if (expanded && details?.items) {
432
+ for (const item of details.items) {
433
+ text += "\n" + theme.fg("dim", ` ${item}`);
434
+ }
435
+ }
436
+
437
+ return new Text(text, 0, 0);
438
+ }
439
+ ```
440
+
441
+ **Options:**
442
+
443
+ - `expanded`: User pressed Ctrl+O to expand
444
+ - `isPartial`: Result is from `onUpdate` (streaming), not final
445
+
446
+ ### Best Practices
447
+
448
+ 1. **Use `Text` with padding `(0, 0)`** - The Box handles padding
449
+ 2. **Use `\n` for multi-line content** - Not multiple Text components
450
+ 3. **Handle `isPartial`** - Show progress during streaming
451
+ 4. **Support `expanded`** - Show more detail when user requests
452
+ 5. **Use theme colors** - For consistent appearance
453
+ 6. **Keep it compact** - Show summary by default, details when expanded
454
+
455
+ ### Theme Colors
456
+
457
+ ```typescript
458
+ // Foreground
459
+ theme.fg("toolTitle", text); // Tool names
460
+ theme.fg("accent", text); // Highlights
461
+ theme.fg("success", text); // Success
462
+ theme.fg("error", text); // Errors
463
+ theme.fg("warning", text); // Warnings
464
+ theme.fg("muted", text); // Secondary text
465
+ theme.fg("dim", text); // Tertiary text
466
+ theme.fg("toolOutput", text); // Output content
467
+
468
+ // Styles
469
+ theme.bold(text);
470
+ theme.italic(text);
471
+ ```
472
+
473
+ ### Fallback Behavior
474
+
475
+ If `renderCall` or `renderResult` is not defined or throws an error:
476
+
477
+ - `renderCall`: Shows tool name
478
+ - `renderResult`: Shows raw text output from `content`
479
+
480
+ ## Execute Function
481
+
482
+ ```typescript
483
+ async execute(toolCallId, args, onUpdate, ctx, signal) {
484
+ // Type assertion for params (TypeBox schema doesn't flow through)
485
+ const params = args as { action: "list" | "add"; text?: string };
486
+
487
+ // Check for abort
488
+ if (signal?.aborted) {
489
+ return { content: [...], details: { status: "aborted" } };
490
+ }
491
+
492
+ // Stream progress
493
+ onUpdate?.({
494
+ content: [{ type: "text", text: "Working..." }],
495
+ details: { progress: 50 },
496
+ });
497
+
498
+ // Return final result
499
+ return {
500
+ content: [{ type: "text", text: "Done" }], // Sent to LLM
501
+ details: { data: result }, // For rendering only
502
+ };
503
+ }
504
+ ```
505
+
506
+ ## Multiple Tools from One File
507
+
508
+ Return an array to share state between related tools:
509
+
510
+ ```typescript
511
+ const factory: CustomToolFactory = (pi) => {
512
+ // Shared state
513
+ let connection = null;
514
+
515
+ const handleSession = (event: CustomToolSessionEvent, ctx: CustomToolContext) => {
516
+ if (event.reason === "shutdown") {
517
+ connection?.close();
518
+ }
519
+ };
520
+
521
+ return [
522
+ { name: "db_connect", onSession: handleSession, ... },
523
+ { name: "db_query", onSession: handleSession, ... },
524
+ { name: "db_close", onSession: handleSession, ... },
525
+ ];
526
+ };
527
+ ```
528
+
529
+ ## Examples
530
+
531
+ See [`examples/custom-tools/todo/index.ts`](../examples/custom-tools/todo/index.ts) for a complete example with:
532
+
533
+ - `onSession` for state reconstruction
534
+ - Custom `renderCall` and `renderResult`
535
+ - Proper branching support via details storage
536
+
537
+ Test with:
538
+
539
+ ```bash
540
+ pi --tool packages/coding-agent/examples/custom-tools/todo/index.ts
541
+ ```