@mariozechner/pi-coding-agent 0.30.2 → 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 (297) hide show
  1. package/CHANGELOG.md +244 -1
  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 -2
  19. package/dist/core/auth-storage.d.ts.map +1 -1
  20. package/dist/core/auth-storage.js +2 -2
  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 +22 -21
  153. package/dist/main.js.map +1 -1
  154. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  155. package/dist/modes/interactive/components/assistant-message.js +3 -4
  156. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  157. package/dist/modes/interactive/components/bash-execution.d.ts +1 -1
  158. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  159. package/dist/modes/interactive/components/bash-execution.js +6 -2
  160. package/dist/modes/interactive/components/bash-execution.js.map +1 -1
  161. package/dist/modes/interactive/components/bordered-loader.d.ts +12 -0
  162. package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -0
  163. package/dist/modes/interactive/components/bordered-loader.js +30 -0
  164. package/dist/modes/interactive/components/bordered-loader.js.map +1 -0
  165. package/dist/modes/interactive/components/branch-summary-message.d.ts +14 -0
  166. package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -0
  167. package/dist/modes/interactive/components/branch-summary-message.js +35 -0
  168. package/dist/modes/interactive/components/branch-summary-message.js.map +1 -0
  169. package/dist/modes/interactive/components/compaction-summary-message.d.ts +14 -0
  170. package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -0
  171. package/dist/modes/interactive/components/compaction-summary-message.js +36 -0
  172. package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -0
  173. package/dist/modes/interactive/components/dynamic-border.d.ts +5 -1
  174. package/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
  175. package/dist/modes/interactive/components/dynamic-border.js +5 -1
  176. package/dist/modes/interactive/components/dynamic-border.js.map +1 -1
  177. package/dist/modes/interactive/components/footer.d.ts +12 -6
  178. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  179. package/dist/modes/interactive/components/footer.js +57 -25
  180. package/dist/modes/interactive/components/footer.js.map +1 -1
  181. package/dist/modes/interactive/components/hook-editor.d.ts +15 -0
  182. package/dist/modes/interactive/components/hook-editor.d.ts.map +1 -0
  183. package/dist/modes/interactive/components/hook-editor.js +95 -0
  184. package/dist/modes/interactive/components/hook-editor.js.map +1 -0
  185. package/dist/modes/interactive/components/hook-message.d.ts +18 -0
  186. package/dist/modes/interactive/components/hook-message.d.ts.map +1 -0
  187. package/dist/modes/interactive/components/hook-message.js +80 -0
  188. package/dist/modes/interactive/components/hook-message.js.map +1 -0
  189. package/dist/modes/interactive/components/model-selector.d.ts +3 -3
  190. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  191. package/dist/modes/interactive/components/model-selector.js +1 -1
  192. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  193. package/dist/modes/interactive/components/tool-execution.d.ts +15 -2
  194. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  195. package/dist/modes/interactive/components/tool-execution.js +70 -21
  196. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  197. package/dist/modes/interactive/components/tree-selector.d.ts +52 -0
  198. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -0
  199. package/dist/modes/interactive/components/tree-selector.js +745 -0
  200. package/dist/modes/interactive/components/tree-selector.js.map +1 -0
  201. package/dist/modes/interactive/components/user-message-selector.d.ts +3 -3
  202. package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
  203. package/dist/modes/interactive/components/user-message-selector.js +1 -1
  204. package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
  205. package/dist/modes/interactive/components/user-message.d.ts +1 -1
  206. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  207. package/dist/modes/interactive/components/user-message.js +2 -5
  208. package/dist/modes/interactive/components/user-message.js.map +1 -1
  209. package/dist/modes/interactive/interactive-mode.d.ts +29 -12
  210. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  211. package/dist/modes/interactive/interactive-mode.js +589 -208
  212. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  213. package/dist/modes/interactive/theme/dark.json +13 -1
  214. package/dist/modes/interactive/theme/light.json +13 -1
  215. package/dist/modes/interactive/theme/theme-schema.json +34 -0
  216. package/dist/modes/interactive/theme/theme.d.ts +20 -2
  217. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  218. package/dist/modes/interactive/theme/theme.js +135 -2
  219. package/dist/modes/interactive/theme/theme.js.map +1 -1
  220. package/dist/modes/print-mode.d.ts +3 -3
  221. package/dist/modes/print-mode.d.ts.map +1 -1
  222. package/dist/modes/print-mode.js +26 -20
  223. package/dist/modes/print-mode.js.map +1 -1
  224. package/dist/modes/rpc/rpc-client.d.ts +13 -10
  225. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  226. package/dist/modes/rpc/rpc-client.js +11 -10
  227. package/dist/modes/rpc/rpc-client.js.map +1 -1
  228. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  229. package/dist/modes/rpc/rpc-mode.js +88 -35
  230. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  231. package/dist/modes/rpc/rpc-types.d.ts +30 -11
  232. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  233. package/dist/modes/rpc/rpc-types.js.map +1 -1
  234. package/dist/utils/shell.d.ts +4 -2
  235. package/dist/utils/shell.d.ts.map +1 -1
  236. package/dist/utils/shell.js +36 -7
  237. package/dist/utils/shell.js.map +1 -1
  238. package/dist/utils/tools-manager.d.ts +1 -1
  239. package/dist/utils/tools-manager.d.ts.map +1 -1
  240. package/dist/utils/tools-manager.js +2 -2
  241. package/dist/utils/tools-manager.js.map +1 -1
  242. package/docs/compaction.md +388 -0
  243. package/docs/custom-tools.md +146 -43
  244. package/docs/extension-loading.md +1004 -0
  245. package/docs/hooks.md +562 -596
  246. package/docs/rpc.md +33 -19
  247. package/docs/sdk.md +93 -21
  248. package/docs/session-tree-plan.md +441 -0
  249. package/docs/session.md +172 -21
  250. package/docs/skills.md +2 -0
  251. package/docs/theme.md +31 -2
  252. package/docs/tree.md +197 -0
  253. package/docs/tui.md +343 -0
  254. package/examples/README.md +1 -9
  255. package/examples/custom-tools/hello/index.ts +4 -3
  256. package/examples/custom-tools/question/index.ts +4 -4
  257. package/examples/custom-tools/subagent/index.ts +7 -6
  258. package/examples/custom-tools/todo/index.ts +11 -5
  259. package/examples/hooks/README.md +29 -71
  260. package/examples/hooks/auto-commit-on-exit.ts +8 -9
  261. package/examples/hooks/confirm-destructive.ts +29 -30
  262. package/examples/hooks/custom-compaction.ts +20 -21
  263. package/examples/hooks/dirty-repo-guard.ts +41 -40
  264. package/examples/hooks/file-trigger.ts +10 -5
  265. package/examples/hooks/git-checkpoint.ts +16 -12
  266. package/examples/hooks/handoff.ts +150 -0
  267. package/examples/hooks/permission-gate.ts +1 -1
  268. package/examples/hooks/protected-paths.ts +1 -1
  269. package/examples/hooks/qna.ts +119 -0
  270. package/examples/hooks/snake.ts +343 -0
  271. package/examples/hooks/status-line.ts +40 -0
  272. package/examples/sdk/01-minimal.ts +1 -1
  273. package/examples/sdk/02-custom-model.ts +1 -1
  274. package/examples/sdk/03-custom-prompt.ts +1 -1
  275. package/examples/sdk/04-skills.ts +1 -1
  276. package/examples/sdk/05-tools.ts +4 -4
  277. package/examples/sdk/06-hooks.ts +1 -1
  278. package/examples/sdk/07-context-files.ts +1 -1
  279. package/examples/sdk/08-slash-commands.ts +6 -1
  280. package/examples/sdk/09-api-keys-and-oauth.ts +1 -1
  281. package/examples/sdk/10-settings.ts +1 -1
  282. package/examples/sdk/11-sessions.ts +1 -1
  283. package/examples/sdk/12-full-control.ts +4 -7
  284. package/package.json +6 -6
  285. package/dist/core/compaction.d.ts.map +0 -1
  286. package/dist/core/compaction.js +0 -412
  287. package/dist/core/compaction.js.map +0 -1
  288. package/dist/core/export-html.d.ts +0 -23
  289. package/dist/core/export-html.d.ts.map +0 -1
  290. package/dist/core/export-html.js +0 -1185
  291. package/dist/core/export-html.js.map +0 -1
  292. package/dist/modes/interactive/components/compaction.d.ts +0 -15
  293. package/dist/modes/interactive/components/compaction.d.ts.map +0 -1
  294. package/dist/modes/interactive/components/compaction.js +0 -41
  295. package/dist/modes/interactive/components/compaction.js.map +0 -1
  296. package/docs/hooks-v2.md +0 -385
  297. package/docs/session-tree.md +0 -452
package/docs/tui.md ADDED
@@ -0,0 +1,343 @@
1
+ > pi can create TUI components. Ask it to build one for your use case.
2
+
3
+ # TUI Components
4
+
5
+ Hooks and custom tools can render custom TUI components for interactive user interfaces. This page covers the component system and available building blocks.
6
+
7
+ **Source:** [`@mariozechner/pi-tui`](https://github.com/badlogic/pi-mono/tree/main/packages/tui)
8
+
9
+ ## Component Interface
10
+
11
+ All components implement:
12
+
13
+ ```typescript
14
+ interface Component {
15
+ render(width: number): string[];
16
+ handleInput?(data: string): void;
17
+ invalidate?(): void;
18
+ }
19
+ ```
20
+
21
+ | Method | Description |
22
+ |--------|-------------|
23
+ | `render(width)` | Return array of strings (one per line). Each line **must not exceed `width`**. |
24
+ | `handleInput?(data)` | Receive keyboard input when component has focus. |
25
+ | `invalidate?()` | Clear cached render state. |
26
+
27
+ ## Using Components
28
+
29
+ **In hooks** via `ctx.ui.custom()`:
30
+
31
+ ```typescript
32
+ pi.on("session_start", async (_event, ctx) => {
33
+ const handle = ctx.ui.custom(myComponent);
34
+ // handle.requestRender() - trigger re-render
35
+ // handle.close() - restore normal UI
36
+ });
37
+ ```
38
+
39
+ **In custom tools** via `pi.ui.custom()`:
40
+
41
+ ```typescript
42
+ async execute(toolCallId, params, onUpdate, ctx, signal) {
43
+ const handle = pi.ui.custom(myComponent);
44
+ // ...
45
+ handle.close();
46
+ }
47
+ ```
48
+
49
+ ## Built-in Components
50
+
51
+ Import from `@mariozechner/pi-tui`:
52
+
53
+ ```typescript
54
+ import { Text, Box, Container, Spacer, Markdown } from "@mariozechner/pi-tui";
55
+ ```
56
+
57
+ ### Text
58
+
59
+ Multi-line text with word wrapping.
60
+
61
+ ```typescript
62
+ const text = new Text(
63
+ "Hello World", // content
64
+ 1, // paddingX (default: 1)
65
+ 1, // paddingY (default: 1)
66
+ (s) => bgGray(s) // optional background function
67
+ );
68
+ text.setText("Updated");
69
+ ```
70
+
71
+ ### Box
72
+
73
+ Container with padding and background color.
74
+
75
+ ```typescript
76
+ const box = new Box(
77
+ 1, // paddingX
78
+ 1, // paddingY
79
+ (s) => bgGray(s) // background function
80
+ );
81
+ box.addChild(new Text("Content", 0, 0));
82
+ box.setBgFn((s) => bgBlue(s));
83
+ ```
84
+
85
+ ### Container
86
+
87
+ Groups child components vertically.
88
+
89
+ ```typescript
90
+ const container = new Container();
91
+ container.addChild(component1);
92
+ container.addChild(component2);
93
+ container.removeChild(component1);
94
+ ```
95
+
96
+ ### Spacer
97
+
98
+ Empty vertical space.
99
+
100
+ ```typescript
101
+ const spacer = new Spacer(2); // 2 empty lines
102
+ ```
103
+
104
+ ### Markdown
105
+
106
+ Renders markdown with syntax highlighting.
107
+
108
+ ```typescript
109
+ const md = new Markdown(
110
+ "# Title\n\nSome **bold** text",
111
+ 1, // paddingX
112
+ 1, // paddingY
113
+ theme // MarkdownTheme (see below)
114
+ );
115
+ md.setText("Updated markdown");
116
+ ```
117
+
118
+ ### Image
119
+
120
+ Renders images in supported terminals (Kitty, iTerm2, Ghostty, WezTerm).
121
+
122
+ ```typescript
123
+ const image = new Image(
124
+ base64Data, // base64-encoded image
125
+ "image/png", // MIME type
126
+ theme, // ImageTheme
127
+ { maxWidthCells: 80, maxHeightCells: 24 }
128
+ );
129
+ ```
130
+
131
+ ## Keyboard Input
132
+
133
+ Use key detection helpers:
134
+
135
+ ```typescript
136
+ import {
137
+ isEnter, isEscape, isTab,
138
+ isArrowUp, isArrowDown, isArrowLeft, isArrowRight,
139
+ isCtrlC, isCtrlO, isBackspace, isDelete,
140
+ // ... and more
141
+ } from "@mariozechner/pi-tui";
142
+
143
+ handleInput(data: string) {
144
+ if (isArrowUp(data)) {
145
+ this.selectedIndex--;
146
+ } else if (isEnter(data)) {
147
+ this.onSelect?.(this.selectedIndex);
148
+ } else if (isEscape(data)) {
149
+ this.onCancel?.();
150
+ }
151
+ }
152
+ ```
153
+
154
+ ## Line Width
155
+
156
+ **Critical:** Each line from `render()` must not exceed the `width` parameter.
157
+
158
+ ```typescript
159
+ import { visibleWidth, truncateToWidth } from "@mariozechner/pi-tui";
160
+
161
+ render(width: number): string[] {
162
+ // Truncate long lines
163
+ return [truncateToWidth(this.text, width)];
164
+ }
165
+ ```
166
+
167
+ Utilities:
168
+ - `visibleWidth(str)` - Get display width (ignores ANSI codes)
169
+ - `truncateToWidth(str, width, ellipsis?)` - Truncate with optional ellipsis
170
+ - `wrapTextWithAnsi(str, width)` - Word wrap preserving ANSI codes
171
+
172
+ ## Creating Custom Components
173
+
174
+ Example: Interactive selector
175
+
176
+ ```typescript
177
+ import {
178
+ isEnter, isEscape, isArrowUp, isArrowDown,
179
+ truncateToWidth, visibleWidth
180
+ } from "@mariozechner/pi-tui";
181
+
182
+ class MySelector {
183
+ private items: string[];
184
+ private selected = 0;
185
+ private cachedWidth?: number;
186
+ private cachedLines?: string[];
187
+
188
+ public onSelect?: (item: string) => void;
189
+ public onCancel?: () => void;
190
+
191
+ constructor(items: string[]) {
192
+ this.items = items;
193
+ }
194
+
195
+ handleInput(data: string): void {
196
+ if (isArrowUp(data) && this.selected > 0) {
197
+ this.selected--;
198
+ this.invalidate();
199
+ } else if (isArrowDown(data) && this.selected < this.items.length - 1) {
200
+ this.selected++;
201
+ this.invalidate();
202
+ } else if (isEnter(data)) {
203
+ this.onSelect?.(this.items[this.selected]);
204
+ } else if (isEscape(data)) {
205
+ this.onCancel?.();
206
+ }
207
+ }
208
+
209
+ render(width: number): string[] {
210
+ if (this.cachedLines && this.cachedWidth === width) {
211
+ return this.cachedLines;
212
+ }
213
+
214
+ this.cachedLines = this.items.map((item, i) => {
215
+ const prefix = i === this.selected ? "> " : " ";
216
+ return truncateToWidth(prefix + item, width);
217
+ });
218
+ this.cachedWidth = width;
219
+ return this.cachedLines;
220
+ }
221
+
222
+ invalidate(): void {
223
+ this.cachedWidth = undefined;
224
+ this.cachedLines = undefined;
225
+ }
226
+ }
227
+ ```
228
+
229
+ Usage in a hook:
230
+
231
+ ```typescript
232
+ pi.registerCommand("pick", {
233
+ description: "Pick an item",
234
+ handler: async (args, ctx) => {
235
+ const items = ["Option A", "Option B", "Option C"];
236
+ const selector = new MySelector(items);
237
+
238
+ let handle: { close: () => void; requestRender: () => void };
239
+
240
+ await new Promise<void>((resolve) => {
241
+ selector.onSelect = (item) => {
242
+ ctx.ui.notify(`Selected: ${item}`, "info");
243
+ handle.close();
244
+ resolve();
245
+ };
246
+ selector.onCancel = () => {
247
+ handle.close();
248
+ resolve();
249
+ };
250
+ handle = ctx.ui.custom(selector);
251
+ });
252
+ }
253
+ });
254
+ ```
255
+
256
+ ## Theming
257
+
258
+ Components accept theme objects for styling.
259
+
260
+ **In `renderCall`/`renderResult`**, use the `theme` parameter:
261
+
262
+ ```typescript
263
+ renderResult(result, options, theme) {
264
+ // Use theme.fg() for foreground colors
265
+ return new Text(theme.fg("success", "Done!"), 0, 0);
266
+
267
+ // Use theme.bg() for background colors
268
+ const styled = theme.bg("toolPendingBg", theme.fg("accent", "text"));
269
+ }
270
+ ```
271
+
272
+ **Foreground colors** (`theme.fg(color, text)`):
273
+
274
+ | Category | Colors |
275
+ |----------|--------|
276
+ | General | `text`, `accent`, `muted`, `dim` |
277
+ | Status | `success`, `error`, `warning` |
278
+ | Borders | `border`, `borderAccent`, `borderMuted` |
279
+ | Messages | `userMessageText`, `customMessageText`, `customMessageLabel` |
280
+ | Tools | `toolTitle`, `toolOutput` |
281
+ | Diffs | `toolDiffAdded`, `toolDiffRemoved`, `toolDiffContext` |
282
+ | Markdown | `mdHeading`, `mdLink`, `mdLinkUrl`, `mdCode`, `mdCodeBlock`, `mdCodeBlockBorder`, `mdQuote`, `mdQuoteBorder`, `mdHr`, `mdListBullet` |
283
+ | Syntax | `syntaxComment`, `syntaxKeyword`, `syntaxFunction`, `syntaxVariable`, `syntaxString`, `syntaxNumber`, `syntaxType`, `syntaxOperator`, `syntaxPunctuation` |
284
+ | Thinking | `thinkingOff`, `thinkingMinimal`, `thinkingLow`, `thinkingMedium`, `thinkingHigh`, `thinkingXhigh` |
285
+ | Modes | `bashMode` |
286
+
287
+ **Background colors** (`theme.bg(color, text)`):
288
+
289
+ `selectedBg`, `userMessageBg`, `customMessageBg`, `toolPendingBg`, `toolSuccessBg`, `toolErrorBg`
290
+
291
+ **For Markdown**, use `getMarkdownTheme()`:
292
+
293
+ ```typescript
294
+ import { getMarkdownTheme } from "@mariozechner/pi-coding-agent";
295
+ import { Markdown } from "@mariozechner/pi-tui";
296
+
297
+ renderResult(result, options, theme) {
298
+ const mdTheme = getMarkdownTheme();
299
+ return new Markdown(result.details.markdown, 0, 0, mdTheme);
300
+ }
301
+ ```
302
+
303
+ **For custom components**, define your own theme interface:
304
+
305
+ ```typescript
306
+ interface MyTheme {
307
+ selected: (s: string) => string;
308
+ normal: (s: string) => string;
309
+ }
310
+ ```
311
+
312
+ ## Performance
313
+
314
+ Cache rendered output when possible:
315
+
316
+ ```typescript
317
+ class CachedComponent {
318
+ private cachedWidth?: number;
319
+ private cachedLines?: string[];
320
+
321
+ render(width: number): string[] {
322
+ if (this.cachedLines && this.cachedWidth === width) {
323
+ return this.cachedLines;
324
+ }
325
+ // ... compute lines ...
326
+ this.cachedWidth = width;
327
+ this.cachedLines = lines;
328
+ return lines;
329
+ }
330
+
331
+ invalidate(): void {
332
+ this.cachedWidth = undefined;
333
+ this.cachedLines = undefined;
334
+ }
335
+ }
336
+ ```
337
+
338
+ Call `invalidate()` when state changes, then `handle.requestRender()` to trigger re-render.
339
+
340
+ ## Examples
341
+
342
+ - **Snake game**: [examples/hooks/snake.ts](../examples/hooks/snake.ts) - Full game with keyboard input, game loop, state persistence
343
+ - **Custom tool rendering**: [examples/custom-tools/todo/](../examples/custom-tools/todo/) - Custom `renderCall` and `renderResult`
@@ -1,6 +1,6 @@
1
1
  # Examples
2
2
 
3
- Example code for pi-coding-agent.
3
+ Example code for pi-coding-agent SDK, hooks, and custom tools.
4
4
 
5
5
  ## Directories
6
6
 
@@ -13,14 +13,6 @@ Example hooks for intercepting tool calls, adding safety gates, and integrating
13
13
  ### [custom-tools/](custom-tools/)
14
14
  Example custom tools that extend the agent's capabilities.
15
15
 
16
- ## Running Examples
17
-
18
- ```bash
19
- cd packages/coding-agent
20
- npx tsx examples/sdk/01-minimal.ts
21
- npx tsx examples/hooks/permission-gate.ts
22
- ```
23
-
24
16
  ## Documentation
25
17
 
26
18
  - [SDK Reference](sdk/README.md)
@@ -9,10 +9,11 @@ const factory: CustomToolFactory = (_pi) => ({
9
9
  name: Type.String({ description: "Name to greet" }),
10
10
  }),
11
11
 
12
- async execute(_toolCallId, params) {
12
+ async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
13
+ const { name } = params as { name: string };
13
14
  return {
14
- content: [{ type: "text", text: `Hello, ${params.name}!` }],
15
- details: { greeted: params.name },
15
+ content: [{ type: "text", text: `Hello, ${name}!` }],
16
+ details: { greeted: name },
16
17
  };
17
18
  },
18
19
  });
@@ -2,7 +2,7 @@
2
2
  * Question Tool - Let the LLM ask the user a question with options
3
3
  */
4
4
 
5
- import type { CustomAgentTool, CustomToolFactory } from "@mariozechner/pi-coding-agent";
5
+ import type { CustomTool, CustomToolFactory } from "@mariozechner/pi-coding-agent";
6
6
  import { Text } from "@mariozechner/pi-tui";
7
7
  import { Type } from "@sinclair/typebox";
8
8
 
@@ -18,13 +18,13 @@ const QuestionParams = Type.Object({
18
18
  });
19
19
 
20
20
  const factory: CustomToolFactory = (pi) => {
21
- const tool: CustomAgentTool<typeof QuestionParams, QuestionDetails> = {
21
+ const tool: CustomTool<typeof QuestionParams, QuestionDetails> = {
22
22
  name: "question",
23
23
  label: "Question",
24
24
  description: "Ask the user a question and let them pick from options. Use when you need user input to proceed.",
25
25
  parameters: QuestionParams,
26
26
 
27
- async execute(_toolCallId, params) {
27
+ async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
28
28
  if (!pi.hasUI) {
29
29
  return {
30
30
  content: [{ type: "text", text: "Error: UI not available (running in non-interactive mode)" }],
@@ -41,7 +41,7 @@ const factory: CustomToolFactory = (pi) => {
41
41
 
42
42
  const answer = await pi.ui.select(params.question, params.options);
43
43
 
44
- if (answer === null) {
44
+ if (answer === undefined) {
45
45
  return {
46
46
  content: [{ type: "text", text: "User cancelled the selection" }],
47
47
  details: { question: params.question, options: params.options, answer: null },
@@ -16,13 +16,14 @@ import { spawn } from "node:child_process";
16
16
  import * as fs from "node:fs";
17
17
  import * as os from "node:os";
18
18
  import * as path from "node:path";
19
- import type { AgentToolResult, Message } from "@mariozechner/pi-ai";
19
+ import type { AgentToolResult } from "@mariozechner/pi-agent-core";
20
+ import type { Message } from "@mariozechner/pi-ai";
20
21
  import { StringEnum } from "@mariozechner/pi-ai";
21
22
  import {
22
- type CustomAgentTool,
23
+ type CustomTool,
24
+ type CustomToolAPI,
23
25
  type CustomToolFactory,
24
26
  getMarkdownTheme,
25
- type ToolAPI,
26
27
  } from "@mariozechner/pi-coding-agent";
27
28
  import { Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
28
29
  import { Type } from "@sinclair/typebox";
@@ -223,7 +224,7 @@ function writePromptToTempFile(agentName: string, prompt: string): { dir: string
223
224
  type OnUpdateCallback = (partial: AgentToolResult<SubagentDetails>) => void;
224
225
 
225
226
  async function runSingleAgent(
226
- pi: ToolAPI,
227
+ pi: CustomToolAPI,
227
228
  agents: AgentConfig[],
228
229
  agentName: string,
229
230
  task: string,
@@ -410,7 +411,7 @@ const SubagentParams = Type.Object({
410
411
  });
411
412
 
412
413
  const factory: CustomToolFactory = (pi) => {
413
- const tool: CustomAgentTool<typeof SubagentParams, SubagentDetails> = {
414
+ const tool: CustomTool<typeof SubagentParams, SubagentDetails> = {
414
415
  name: "subagent",
415
416
  label: "Subagent",
416
417
  get description() {
@@ -432,7 +433,7 @@ const factory: CustomToolFactory = (pi) => {
432
433
  },
433
434
  parameters: SubagentParams,
434
435
 
435
- async execute(_toolCallId, params, signal, onUpdate) {
436
+ async execute(_toolCallId, params, onUpdate, _ctx, signal) {
436
437
  const agentScope: AgentScope = params.agentScope ?? "user";
437
438
  const discovery = discoverAgents(pi.cwd, agentScope);
438
439
  const agents = discovery.agents;
@@ -9,7 +9,12 @@
9
9
  */
10
10
 
11
11
  import { StringEnum } from "@mariozechner/pi-ai";
12
- import type { CustomAgentTool, CustomToolFactory, ToolSessionEvent } from "@mariozechner/pi-coding-agent";
12
+ import type {
13
+ CustomTool,
14
+ CustomToolContext,
15
+ CustomToolFactory,
16
+ CustomToolSessionEvent,
17
+ } from "@mariozechner/pi-coding-agent";
13
18
  import { Text } from "@mariozechner/pi-tui";
14
19
  import { Type } from "@sinclair/typebox";
15
20
 
@@ -43,11 +48,12 @@ const factory: CustomToolFactory = (_pi) => {
43
48
  * Reconstruct state from session entries.
44
49
  * Scans tool results for this tool and applies them in order.
45
50
  */
46
- const reconstructState = (event: ToolSessionEvent) => {
51
+ const reconstructState = (_event: CustomToolSessionEvent, ctx: CustomToolContext) => {
47
52
  todos = [];
48
53
  nextId = 1;
49
54
 
50
- for (const entry of event.entries) {
55
+ // Use getBranch() to get entries on the current branch
56
+ for (const entry of ctx.sessionManager.getBranch()) {
51
57
  if (entry.type !== "message") continue;
52
58
  const msg = entry.message;
53
59
 
@@ -63,7 +69,7 @@ const factory: CustomToolFactory = (_pi) => {
63
69
  }
64
70
  };
65
71
 
66
- const tool: CustomAgentTool<typeof TodoParams, TodoDetails> = {
72
+ const tool: CustomTool<typeof TodoParams, TodoDetails> = {
67
73
  name: "todo",
68
74
  label: "Todo",
69
75
  description: "Manage a todo list. Actions: list, add (text), toggle (id), clear",
@@ -72,7 +78,7 @@ const factory: CustomToolFactory = (_pi) => {
72
78
  // Called on session start/switch/branch/clear
73
79
  onSession: reconstructState,
74
80
 
75
- async execute(_toolCallId, params) {
81
+ async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
76
82
  switch (params.action) {
77
83
  case "list":
78
84
  return {
@@ -2,97 +2,55 @@
2
2
 
3
3
  Example hooks for pi-coding-agent.
4
4
 
5
- ## Examples
6
-
7
- ### permission-gate.ts
8
- Prompts for confirmation before running dangerous bash commands (rm -rf, sudo, chmod 777, etc.).
9
-
10
- ### git-checkpoint.ts
11
- Creates git stash checkpoints at each turn, allowing code restoration when branching.
12
-
13
- ### protected-paths.ts
14
- Blocks writes to protected paths (.env, .git/, node_modules/).
15
-
16
- ### file-trigger.ts
17
- Watches a trigger file and injects its contents into the conversation. Useful for external systems (CI, file watchers, webhooks) to send messages to the agent.
18
-
19
- ### confirm-destructive.ts
20
- Prompts for confirmation before destructive session actions (clear, switch, branch). Demonstrates how to cancel `before_*` session events.
21
-
22
- ### dirty-repo-guard.ts
23
- Prevents session changes when there are uncommitted git changes. Blocks clear/switch/branch until you commit.
24
-
25
- ### auto-commit-on-exit.ts
26
- Automatically commits changes when the agent exits (shutdown event). Uses the last assistant message to generate a commit message.
27
-
28
- ### custom-compaction.ts
29
- Custom context compaction that summarizes the entire conversation instead of keeping recent turns. Uses the `before_compact` hook event to intercept compaction and generate a comprehensive summary using `complete()` from the AI package. Useful when you want maximum context window space at the cost of losing exact conversation history.
30
-
31
5
  ## Usage
32
6
 
33
7
  ```bash
34
- # Test directly
8
+ # Load a hook with --hook flag
35
9
  pi --hook examples/hooks/permission-gate.ts
36
10
 
37
- # Or copy to hooks directory for persistent use
11
+ # Or copy to hooks directory for auto-discovery
38
12
  cp permission-gate.ts ~/.pi/agent/hooks/
39
13
  ```
40
14
 
15
+ ## Examples
16
+
17
+ | Hook | Description |
18
+ |------|-------------|
19
+ | `permission-gate.ts` | Prompts for confirmation before dangerous bash commands (rm -rf, sudo, etc.) |
20
+ | `git-checkpoint.ts` | Creates git stash checkpoints at each turn for code restoration on branch |
21
+ | `protected-paths.ts` | Blocks writes to protected paths (.env, .git/, node_modules/) |
22
+ | `file-trigger.ts` | Watches a trigger file and injects contents into conversation |
23
+ | `confirm-destructive.ts` | Confirms before destructive session actions (clear, switch, branch) |
24
+ | `dirty-repo-guard.ts` | Prevents session changes with uncommitted git changes |
25
+ | `auto-commit-on-exit.ts` | Auto-commits on exit using last assistant message for commit message |
26
+ | `custom-compaction.ts` | Custom compaction that summarizes entire conversation |
27
+ | `qna.ts` | Extracts questions from last response into editor via `ctx.ui.setEditorText()` |
28
+ | `snake.ts` | Snake game with custom UI, keyboard handling, and session persistence |
29
+ | `status-line.ts` | Shows turn progress in footer via `ctx.ui.setStatus()` with themed colors |
30
+ | `handoff.ts` | Transfer context to a new focused session via `/handoff <goal>` |
31
+
41
32
  ## Writing Hooks
42
33
 
43
34
  See [docs/hooks.md](../../docs/hooks.md) for full documentation.
44
35
 
45
- ### Key Points
46
-
47
- **Hook structure:**
48
36
  ```typescript
49
37
  import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";
50
38
 
51
39
  export default function (pi: HookAPI) {
52
- pi.on("session", async (event, ctx) => {
53
- // event.reason: "start" | "before_switch" | "switch" | "before_clear" | "clear" |
54
- // "before_branch" | "branch" | "shutdown"
55
- // event.targetTurnIndex: number (only for before_branch/branch)
56
- // ctx.ui, ctx.exec, ctx.cwd, ctx.sessionFile, ctx.hasUI
57
-
58
- // Cancel before_* actions:
59
- if (event.reason === "before_clear") {
60
- return { cancel: true };
61
- }
62
- return undefined;
63
- });
64
-
40
+ // Subscribe to events
65
41
  pi.on("tool_call", async (event, ctx) => {
66
- // Can block tool execution
67
- if (dangerous) {
68
- return { block: true, reason: "Blocked" };
42
+ if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) {
43
+ const ok = await ctx.ui.confirm("Dangerous!", "Allow rm -rf?");
44
+ if (!ok) return { block: true, reason: "Blocked by user" };
69
45
  }
70
- return undefined;
71
46
  });
72
47
 
73
- pi.on("tool_result", async (event, ctx) => {
74
- // Can modify result
75
- return { result: "modified result" };
48
+ // Register custom commands
49
+ pi.registerCommand("hello", {
50
+ description: "Say hello",
51
+ handler: async (args, ctx) => {
52
+ ctx.ui.notify("Hello!", "info");
53
+ },
76
54
  });
77
55
  }
78
56
  ```
79
-
80
- **Available events:**
81
- - `session` - lifecycle events with before/after variants (can cancel before_* actions)
82
- - `agent_start` / `agent_end` - per user prompt
83
- - `turn_start` / `turn_end` - per LLM turn
84
- - `tool_call` - before tool execution (can block)
85
- - `tool_result` - after tool execution (can modify)
86
-
87
- **UI methods:**
88
- ```typescript
89
- const choice = await ctx.ui.select("Title", ["Option A", "Option B"]);
90
- const confirmed = await ctx.ui.confirm("Title", "Are you sure?");
91
- const input = await ctx.ui.input("Title", "placeholder");
92
- ctx.ui.notify("Message", "info"); // or "warning", "error"
93
- ```
94
-
95
- **Sending messages:**
96
- ```typescript
97
- pi.send("Message to inject into conversation");
98
- ```
@@ -5,14 +5,12 @@
5
5
  * Uses the last assistant message to generate a commit message.
6
6
  */
7
7
 
8
- import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";
8
+ import type { HookAPI } from "@mariozechner/pi-coding-agent";
9
9
 
10
10
  export default function (pi: HookAPI) {
11
- pi.on("session", async (event, ctx) => {
12
- if (event.reason !== "shutdown") return;
13
-
11
+ pi.on("session_shutdown", async (_event, ctx) => {
14
12
  // Check for uncommitted changes
15
- const { stdout: status, code } = await ctx.exec("git", ["status", "--porcelain"]);
13
+ const { stdout: status, code } = await pi.exec("git", ["status", "--porcelain"]);
16
14
 
17
15
  if (code !== 0 || status.trim().length === 0) {
18
16
  // Not a git repo or no changes
@@ -20,9 +18,10 @@ export default function (pi: HookAPI) {
20
18
  }
21
19
 
22
20
  // Find the last assistant message for commit context
21
+ const entries = ctx.sessionManager.getEntries();
23
22
  let lastAssistantText = "";
24
- for (let i = event.entries.length - 1; i >= 0; i--) {
25
- const entry = event.entries[i];
23
+ for (let i = entries.length - 1; i >= 0; i--) {
24
+ const entry = entries[i];
26
25
  if (entry.type === "message" && entry.message.role === "assistant") {
27
26
  const content = entry.message.content;
28
27
  if (Array.isArray(content)) {
@@ -40,8 +39,8 @@ export default function (pi: HookAPI) {
40
39
  const commitMessage = `[pi] ${firstLine.slice(0, 50)}${firstLine.length > 50 ? "..." : ""}`;
41
40
 
42
41
  // Stage and commit
43
- await ctx.exec("git", ["add", "-A"]);
44
- const { code: commitCode } = await ctx.exec("git", ["commit", "-m", commitMessage]);
42
+ await pi.exec("git", ["add", "-A"]);
43
+ const { code: commitCode } = await pi.exec("git", ["commit", "-m", commitMessage]);
45
44
 
46
45
  if (commitCode === 0 && ctx.hasUI) {
47
46
  ctx.ui.notify(`Auto-committed: ${commitMessage}`, "info");