@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/hooks-v2.md DELETED
@@ -1,385 +0,0 @@
1
- # Hooks v2: Context Control + Commands
2
-
3
- Issue: #289
4
-
5
- ## Motivation
6
-
7
- Enable features like session stacking (`/pop`) as hooks, not core code. Core provides primitives, hooks implement features.
8
-
9
- ## Primitives
10
-
11
- | Primitive | Purpose |
12
- |-----------|---------|
13
- | `ctx.saveEntry({type, ...})` | Persist custom entry to session |
14
- | `pi.on("context", handler)` | Transform messages before LLM |
15
- | `ctx.rebuildContext()` | Trigger context rebuild |
16
- | `pi.command(name, opts)` | Register slash command |
17
-
18
- ## Extended HookEventContext
19
-
20
- ```typescript
21
- interface HookEventContext {
22
- // Existing
23
- exec, ui, hasUI, cwd, sessionFile
24
-
25
- // State (read-only)
26
- model: Model<any> | null;
27
- thinkingLevel: ThinkingLevel;
28
- entries: readonly SessionEntry[];
29
-
30
- // Utilities
31
- findModel(provider: string, id: string): Model<any> | null;
32
- availableModels(): Promise<Model<any>[]>;
33
- resolveApiKey(model: Model<any>): Promise<string | undefined>;
34
-
35
- // Mutation
36
- saveEntry(entry: { type: string; [k: string]: unknown }): Promise<void>;
37
- rebuildContext(): Promise<void>;
38
- }
39
-
40
- interface ContextMessage {
41
- message: AppMessage;
42
- entryIndex: number | null; // null = synthetic
43
- }
44
-
45
- interface ContextEvent {
46
- type: "context";
47
- entries: readonly SessionEntry[];
48
- messages: ContextMessage[];
49
- }
50
- ```
51
-
52
- Commands also get: `args`, `argsRaw`, `signal`, `setModel()`, `setThinkingLevel()`.
53
-
54
- ## Stacking: Design
55
-
56
- ### Entry Format
57
-
58
- ```typescript
59
- interface StackPopEntry {
60
- type: "stack_pop";
61
- backToIndex: number;
62
- summary: string;
63
- prePopSummary?: string; // when crossing compaction
64
- timestamp: number;
65
- }
66
- ```
67
-
68
- ### Crossing Compaction
69
-
70
- Entries are never deleted. Raw data always available.
71
-
72
- When `backToIndex < compaction.firstKeptEntryIndex`:
73
- 1. Read raw entries `[0, backToIndex)` → summarize → `prePopSummary`
74
- 2. Read raw entries `[backToIndex, now)` → summarize → `summary`
75
-
76
- ### Context Algorithm: Later Wins
77
-
78
- Assign sequential IDs to ranges. On overlap, highest ID wins.
79
-
80
- ```
81
- Compaction at 40: range [0, 30) id=0
82
- StackPop at 50, backTo=20, prePopSummary: ranges [0, 20) id=1, [20, 50) id=2
83
-
84
- Index 0-19: id=0 and id=1 cover → id=1 wins (prePopSummary)
85
- Index 20-29: id=0 and id=2 cover → id=2 wins (popSummary)
86
- Index 30-49: id=2 covers → id=2 (already emitted at 20)
87
- Index 50+: no coverage → include as messages
88
- ```
89
-
90
- ## Complex Scenario Trace
91
-
92
- ```
93
- Initial: [msg1, msg2, msg3, msg4, msg5]
94
- idx: 1, 2, 3, 4, 5
95
-
96
- Compaction triggers:
97
- [msg1-5, compaction{firstKept:4, summary:C1}]
98
- idx: 1-5, 6
99
- Context: [C1, msg4, msg5]
100
-
101
- User continues:
102
- [..., compaction, msg4, msg5, msg6, msg7]
103
- idx: 6, 4*, 5*, 7, 8 (* kept from before)
104
-
105
- User does /pop to msg2 (index 2):
106
- - backTo=2 < firstKept=4 → crossing!
107
- - prePopSummary: summarize raw [0,2) → P1
108
- - summary: summarize raw [2,8) → S1
109
- - save: stack_pop{backTo:2, summary:S1, prePopSummary:P1} at index 9
110
-
111
- Ranges:
112
- compaction [0,4) id=0
113
- prePopSummary [0,2) id=1
114
- popSummary [2,9) id=2
115
-
116
- Context build:
117
- idx 0: covered by id=0,1 → id=1 wins, emit P1
118
- idx 1: covered by id=0,1 → id=1 (already emitted)
119
- idx 2: covered by id=0,2 → id=2 wins, emit S1
120
- idx 3-8: covered by id=0 or id=2 → id=2 (already emitted)
121
- idx 9: stack_pop entry, skip
122
- idx 10+: not covered, include as messages
123
-
124
- Result: [P1, S1, msg10+]
125
-
126
- User continues, another compaction:
127
- [..., stack_pop, msg10, msg11, msg12, compaction{firstKept:11, summary:C2}]
128
- idx: 9, 10, 11, 12, 13
129
-
130
- Ranges:
131
- compaction@6 [0,4) id=0
132
- prePopSummary [0,2) id=1
133
- popSummary [2,9) id=2
134
- compaction@13 [0,11) id=3 ← this now covers previous ranges!
135
-
136
- Context build:
137
- idx 0-10: covered by multiple, id=3 wins → emit C2 at idx 0
138
- idx 11+: include as messages
139
-
140
- Result: [C2, msg11, msg12]
141
-
142
- C2's summary text includes info from P1 and S1 (they were in context when C2 was generated).
143
- ```
144
-
145
- The "later wins" rule naturally handles all cases.
146
-
147
- ## Core Changes
148
-
149
- | File | Change |
150
- |------|--------|
151
- | `session-manager.ts` | `saveEntry()`, `buildSessionContext()` returns `ContextMessage[]` |
152
- | `hooks/types.ts` | `ContextEvent`, `ContextMessage`, extended context, command types |
153
- | `hooks/loader.ts` | Track commands |
154
- | `hooks/runner.ts` | `setStateCallbacks()`, `emitContext()`, command methods |
155
- | `agent-session.ts` | `saveEntry()`, `rebuildContext()`, state callbacks |
156
- | `interactive-mode.ts` | Command handling, autocomplete |
157
-
158
- ## Stacking Hook: Complete Implementation
159
-
160
- ```typescript
161
- import { complete } from "@mariozechner/pi-ai";
162
- import type { HookAPI, AppMessage, SessionEntry, ContextMessage } from "@mariozechner/pi-coding-agent/hooks";
163
-
164
- export default function(pi: HookAPI) {
165
- pi.command("pop", {
166
- description: "Pop to previous turn, summarizing work",
167
- handler: async (ctx) => {
168
- const entries = ctx.entries as SessionEntry[];
169
-
170
- // Get user turns
171
- const turns = entries
172
- .map((e, i) => ({ e, i }))
173
- .filter(({ e }) => e.type === "message" && (e as any).message.role === "user")
174
- .map(({ e, i }) => ({ idx: i, text: preview((e as any).message) }));
175
-
176
- if (turns.length < 2) return { status: "Need at least 2 turns" };
177
-
178
- // Select target (skip last turn - that's current)
179
- const options = turns.slice(0, -1).map(t => `[${t.idx}] ${t.text}`);
180
- const selected = ctx.args[0]
181
- ? options.find(o => o.startsWith(`[${ctx.args[0]}]`))
182
- : await ctx.ui.select("Pop to:", options);
183
-
184
- if (!selected) return;
185
- const backTo = parseInt(selected.match(/\[(\d+)\]/)![1]);
186
-
187
- // Check compaction crossing
188
- const compactions = entries.filter(e => e.type === "compaction") as any[];
189
- const latestCompaction = compactions[compactions.length - 1];
190
- const crossing = latestCompaction && backTo < latestCompaction.firstKeptEntryIndex;
191
-
192
- // Generate summaries
193
- let prePopSummary: string | undefined;
194
- if (crossing) {
195
- ctx.ui.notify("Crossing compaction, generating pre-pop summary...", "info");
196
- const preMsgs = getMessages(entries.slice(0, backTo));
197
- prePopSummary = await summarize(preMsgs, ctx, "context before this work");
198
- }
199
-
200
- const popMsgs = getMessages(entries.slice(backTo));
201
- const summary = await summarize(popMsgs, ctx, "completed work");
202
-
203
- // Save and rebuild
204
- await ctx.saveEntry({
205
- type: "stack_pop",
206
- backToIndex: backTo,
207
- summary,
208
- prePopSummary,
209
- });
210
-
211
- await ctx.rebuildContext();
212
- return { status: `Popped to turn ${backTo}` };
213
- }
214
- });
215
-
216
- pi.on("context", (event, ctx) => {
217
- const hasPops = event.entries.some(e => e.type === "stack_pop");
218
- if (!hasPops) return;
219
-
220
- // Collect ranges with IDs
221
- let rangeId = 0;
222
- const ranges: Array<{from: number; to: number; summary: string; id: number}> = [];
223
-
224
- for (let i = 0; i < event.entries.length; i++) {
225
- const e = event.entries[i] as any;
226
- if (e.type === "compaction") {
227
- ranges.push({ from: 0, to: e.firstKeptEntryIndex, summary: e.summary, id: rangeId++ });
228
- }
229
- if (e.type === "stack_pop") {
230
- if (e.prePopSummary) {
231
- ranges.push({ from: 0, to: e.backToIndex, summary: e.prePopSummary, id: rangeId++ });
232
- }
233
- ranges.push({ from: e.backToIndex, to: i, summary: e.summary, id: rangeId++ });
234
- }
235
- }
236
-
237
- // Build messages
238
- const messages: ContextMessage[] = [];
239
- const emitted = new Set<number>();
240
-
241
- for (let i = 0; i < event.entries.length; i++) {
242
- const covering = ranges.filter(r => r.from <= i && i < r.to);
243
-
244
- if (covering.length) {
245
- const winner = covering.reduce((a, b) => a.id > b.id ? a : b);
246
- if (i === winner.from && !emitted.has(winner.id)) {
247
- messages.push({
248
- message: { role: "user", content: `[Summary]\n\n${winner.summary}`, timestamp: Date.now() } as AppMessage,
249
- entryIndex: null
250
- });
251
- emitted.add(winner.id);
252
- }
253
- continue;
254
- }
255
-
256
- const e = event.entries[i];
257
- if (e.type === "message") {
258
- messages.push({ message: (e as any).message, entryIndex: i });
259
- }
260
- }
261
-
262
- return { messages };
263
- });
264
- }
265
-
266
- function getMessages(entries: SessionEntry[]): AppMessage[] {
267
- return entries.filter(e => e.type === "message").map(e => (e as any).message);
268
- }
269
-
270
- function preview(msg: AppMessage): string {
271
- const text = typeof msg.content === "string" ? msg.content
272
- : (msg.content as any[]).filter(c => c.type === "text").map(c => c.text).join(" ");
273
- return text.slice(0, 40) + (text.length > 40 ? "..." : "");
274
- }
275
-
276
- async function summarize(msgs: AppMessage[], ctx: any, purpose: string): Promise<string> {
277
- const apiKey = await ctx.resolveApiKey(ctx.model);
278
- const resp = await complete(ctx.model, {
279
- messages: [...msgs, { role: "user", content: `Summarize as "${purpose}". Be concise.`, timestamp: Date.now() }]
280
- }, { apiKey, maxTokens: 2000, signal: ctx.signal });
281
- return resp.content.filter((c: any) => c.type === "text").map((c: any) => c.text).join("\n");
282
- }
283
- ```
284
-
285
- ## Edge Cases
286
-
287
- ### Session Resumed Without Hook
288
-
289
- User has stacking hook, does `/pop`, saves `stack_pop` entry. Later removes hook and resumes session.
290
-
291
- **What happens:**
292
- 1. Core loads all entries (including `stack_pop`)
293
- 2. Core's `buildSessionContext()` ignores unknown types, returns compaction + message entries
294
- 3. `context` event fires, but no handler processes `stack_pop`
295
- 4. Core's messages pass through unchanged
296
-
297
- **Result:** Messages that were "popped" return to context. The pop is effectively undone.
298
-
299
- **Why this is OK:**
300
- - Session file is intact, no data lost
301
- - If compaction happened after pop, the compaction summary captured the popped state
302
- - User removed the hook, so hook's behavior (hiding messages) is gone
303
- - User can re-add hook to restore stacking behavior
304
-
305
- **Mitigation:** Could warn on session load if unknown entry types found:
306
- ```typescript
307
- // In session load
308
- const unknownTypes = entries
309
- .map(e => e.type)
310
- .filter(t => !knownTypes.has(t));
311
- if (unknownTypes.length) {
312
- console.warn(`Session has entries of unknown types: ${unknownTypes.join(", ")}`);
313
- }
314
- ```
315
-
316
- ### Hook Added to Existing Session
317
-
318
- User has old session without stacking. Adds stacking hook, does `/pop`.
319
-
320
- **What happens:**
321
- 1. Hook saves `stack_pop` entry
322
- 2. `context` event fires, hook processes it
323
- 3. Works normally
324
-
325
- No issue. Hook processes entries it recognizes, ignores others.
326
-
327
- ### Multiple Hooks with Different Entry Types
328
-
329
- Hook A handles `type_a` entries, Hook B handles `type_b` entries.
330
-
331
- **What happens:**
332
- 1. `context` event chains through both hooks
333
- 2. Each hook checks for its entry types, passes through if none found
334
- 3. Each hook's transforms are applied in order
335
-
336
- **Best practice:** Hooks should:
337
- - Only process their own entry types
338
- - Return `undefined` (pass through) if no relevant entries
339
- - Use prefixed type names: `myhook_pop`, `myhook_prune`
340
-
341
- ### Conflicting Hooks
342
-
343
- Two hooks both try to handle the same entry type (e.g., both handle `compaction`).
344
-
345
- **What happens:**
346
- - Later hook (project > global) wins in the chain
347
- - Earlier hook's transform is overwritten
348
-
349
- **Mitigation:**
350
- - Core entry types (`compaction`, `message`, etc.) should not be overridden by hooks
351
- - Hooks should use unique prefixed type names
352
- - Document which types are "reserved"
353
-
354
- ### Session with Future Entry Types
355
-
356
- User downgrades pi version, session has entry types from newer version.
357
-
358
- **What happens:**
359
- - Same as "hook removed" - unknown types ignored
360
- - Core handles what it knows, hooks handle what they know
361
-
362
- **Session file is forward-compatible:** Unknown entries are preserved in file, just not processed.
363
-
364
- ## Implementation Phases
365
-
366
- | Phase | Scope | LOC |
367
- |-------|-------|-----|
368
- | v2.0 | `saveEntry`, `context` event, `rebuildContext`, extended context | ~150 |
369
- | v2.1 | `pi.command()`, TUI integration, autocomplete | ~200 |
370
- | v2.2 | Example hooks, documentation | ~300 |
371
-
372
- ## Implementation Order
373
-
374
- 1. `ContextMessage` type, update `buildSessionContext()` return type
375
- 2. `saveEntry()` in session-manager
376
- 3. `context` event in runner with chaining
377
- 4. State callbacks interface and wiring
378
- 5. `rebuildContext()` in agent-session
379
- 6. Manual test with simple hook
380
- 7. Command registration in loader
381
- 8. Command invocation in runner
382
- 9. TUI command handling + autocomplete
383
- 10. Stacking example hook
384
- 11. Pruning example hook
385
- 12. Update hooks.md