@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,441 @@
1
+ # Session Tree Implementation Plan
2
+
3
+ Reference: [session-tree.md](./session-tree.md)
4
+
5
+ ## Phase 1: SessionManager Core ✅
6
+
7
+ - [x] Update entry types with `id`, `parentId` fields (using SessionEntryBase)
8
+ - [x] Add `version` field to `SessionHeader`
9
+ - [x] Change `CompactionEntry.firstKeptEntryIndex` → `firstKeptEntryId`
10
+ - [x] Add `BranchSummaryEntry` type
11
+ - [x] Add `CustomEntry` type for hooks
12
+ - [x] Add `byId: Map<string, SessionEntry>` index
13
+ - [x] Add `leafId: string` tracking
14
+ - [x] Implement `getPath(fromId?)` tree traversal
15
+ - [x] Implement `getTree()` returning `SessionTreeNode[]`
16
+ - [x] Implement `getEntry(id)` lookup
17
+ - [x] Implement `getLeafUuid()` and `getLeafEntry()` helpers
18
+ - [x] Update `_buildIndex()` to populate `byId` map
19
+ - [x] Rename `saveXXX()` to `appendXXX()` (returns id, advances leaf)
20
+ - [x] Add `appendCustomEntry(customType, data)` for hooks
21
+ - [x] Update `buildSessionContext()` to use `getPath()` traversal
22
+
23
+ ## Phase 2: Migration ✅
24
+
25
+ - [x] Add `CURRENT_SESSION_VERSION = 2` constant
26
+ - [x] Implement `migrateV1ToV2()` with extensible migration chain
27
+ - [x] Update `setSessionFile()` to detect version and migrate
28
+ - [x] Implement `_rewriteFile()` for post-migration persistence
29
+ - [x] Handle `firstKeptEntryIndex` → `firstKeptEntryId` conversion in migration
30
+
31
+ ## Phase 3: Branching ✅
32
+
33
+ - [x] Implement `branch(id)` - switch leaf pointer
34
+ - [x] Implement `branchWithSummary(id, summary)` - create summary entry
35
+ - [x] Implement `createBranchedSession(leafId)` - extract path to new file
36
+ - [x] Update `AgentSession.branch()` to use new API
37
+
38
+ ## Phase 4: Compaction Integration ✅
39
+
40
+ - [x] Update `compaction.ts` to work with IDs
41
+ - [x] Update `prepareCompaction()` to return `firstKeptEntryId`
42
+ - [x] Update `compact()` to return `CompactionResult` with `firstKeptEntryId`
43
+ - [x] Update `AgentSession` compaction methods
44
+ - [x] Add `firstKeptEntryId` to `before_compact` hook event
45
+
46
+ ## Phase 5: Testing ✅
47
+
48
+ - [x] `migration.test.ts` - v1 to v2 migration, idempotency
49
+ - [x] `build-context.test.ts` - context building with tree structure, compaction, branches
50
+ - [x] `tree-traversal.test.ts` - append operations, getPath, getTree, branching
51
+ - [x] `file-operations.test.ts` - loadEntriesFromFile, findMostRecentSession
52
+ - [x] `save-entry.test.ts` - custom entry integration
53
+ - [x] Update existing compaction tests for new types
54
+
55
+ ---
56
+
57
+ ## Remaining Work
58
+
59
+ ### Compaction Refactor
60
+
61
+ - [x] Use `CompactionResult` type for hook return value
62
+ - [x] Make `CompactionEntry<T>` generic with optional `details?: T` field for hook-specific data
63
+ - [x] Make `CompactionResult<T>` generic to match
64
+ - [x] Update `SessionEventBase` to pass `sessionManager` and `modelRegistry` instead of derived fields
65
+ - [x] Update `before_compact` event:
66
+ - Pass `preparation: CompactionPreparation` instead of individual fields
67
+ - Pass `previousCompactions: CompactionEntry[]` (newest first) instead of `previousSummary?: string`
68
+ - Keep: `customInstructions`, `model`, `signal`
69
+ - Drop: `resolveApiKey` (use `modelRegistry.getApiKey()`), `cutPoint`, `entries`
70
+ - [x] Update hook example `custom-compaction.ts` to use new API
71
+ - [x] Update `getSessionFile()` to return `string | undefined` for in-memory sessions
72
+ - [x] Update `before_switch` to have `targetSessionFile`, `switch` to have `previousSessionFile`
73
+
74
+ Reference: [#314](https://github.com/badlogic/pi-mono/pull/314) - Structured compaction with anchored iterative summarization needs `details` field to store `ArtifactIndex` and version markers.
75
+
76
+ ### Branch Summary Design ✅
77
+
78
+ Current type:
79
+ ```typescript
80
+ export interface BranchSummaryEntry extends SessionEntryBase {
81
+ type: "branch_summary";
82
+ summary: string;
83
+ fromId: string; // References the abandoned leaf
84
+ fromHook?: boolean; // Whether summary was generated by a hook
85
+ details?: unknown; // File tracking: { readFiles, modifiedFiles }
86
+ }
87
+ ```
88
+
89
+ - [x] `fromId` field references the abandoned leaf
90
+ - [x] `fromHook` field distinguishes pi-generated vs hook-generated summaries
91
+ - [x] `details` field for file tracking
92
+ - [x] Branch summarizer implemented with structured output format
93
+ - [x] Uses serialization approach (same as compaction) to prevent model confusion
94
+ - [x] Tests for `branchWithSummary()` flow
95
+
96
+ ### Entry Labels ✅
97
+
98
+ - [x] Add `LabelEntry` type with `targetId` and `label` fields
99
+ - [x] Add `labelsById: Map<string, string>` private field
100
+ - [x] Build labels map in `_buildIndex()` via linear scan
101
+ - [x] Add `getLabel(id)` method
102
+ - [x] Add `appendLabelChange(targetId, label)` method (undefined clears)
103
+ - [x] Update `createBranchedSession()` to filter out LabelEntry and recreate from resolved map
104
+ - [x] `buildSessionContext()` already ignores LabelEntry (only handles message types)
105
+ - [x] Add `label?: string` to `SessionTreeNode`, populated by `getTree()`
106
+ - [x] Display labels in UI (tree-selector shows labels)
107
+ - [x] `/label` command (implemented in tree-selector)
108
+
109
+ ### CustomMessageEntry<T>
110
+
111
+ Hook-injected messages that participate in LLM context. Unlike `CustomEntry<T>` (for hook state only), these are sent to the model.
112
+
113
+ ```typescript
114
+ export interface CustomMessageEntry<T = unknown> extends SessionEntryBase {
115
+ type: "custom_message";
116
+ customType: string; // Hook identifier
117
+ content: string | (TextContent | ImageContent)[]; // Message content (same as UserMessage)
118
+ details?: T; // Hook-specific data for state reconstruction on reload
119
+ display: boolean; // Whether to display in TUI
120
+ }
121
+ ```
122
+
123
+ Behavior:
124
+ - [x] Type definition matching plan
125
+ - [x] `appendCustomMessageEntry(customType, content, display, details?)` in SessionManager
126
+ - [x] `buildSessionContext()` includes custom_message entries as user messages
127
+ - [x] Exported from main index
128
+ - [x] TUI rendering:
129
+ - `display: false` - hidden entirely
130
+ - `display: true` - rendered with purple styling (customMessageBg, customMessageText, customMessageLabel theme colors)
131
+ - [x] `registerCustomMessageRenderer(customType, renderer)` in HookAPI for custom renderers
132
+ - [x] Renderer returns inner Component, TUI wraps in styled Box
133
+
134
+ ### Hook API Changes ✅
135
+
136
+ **Renamed:**
137
+ - `renderCustomMessage()` → `registerCustomMessageRenderer()`
138
+
139
+ **New: `sendMessage()` ✅**
140
+
141
+ Replaces `send()`. Always creates CustomMessageEntry, never user messages.
142
+
143
+ ```typescript
144
+ type HookMessage<T = unknown> = Pick<CustomMessageEntry<T>, 'customType' | 'content' | 'display' | 'details'>;
145
+
146
+ sendMessage(message: HookMessage, triggerTurn?: boolean): void;
147
+ ```
148
+
149
+ Implementation:
150
+ - Uses agent's queue mechanism with `_hookData` marker on AppMessage
151
+ - `message_end` handler routes based on marker presence
152
+ - `AgentSession.sendHookMessage()` handles three cases:
153
+ - Streaming: queues via `agent.queueMessage()`, loop processes and emits `message_end`
154
+ - Not streaming + triggerTurn: direct append + `agent.continue()`
155
+ - Not streaming + no trigger: direct append only
156
+ - TUI updates via event (streaming) or explicit rebuild (non-streaming)
157
+
158
+ **New: `appendEntry()` ✅**
159
+
160
+ For hook state persistence (NOT in LLM context):
161
+
162
+ ```typescript
163
+ appendEntry(customType: string, data?: unknown): void;
164
+ ```
165
+
166
+ Calls `sessionManager.appendCustomEntry()` directly.
167
+
168
+ **New: `registerCommand()` (types ✅, wiring TODO)**
169
+
170
+ ```typescript
171
+ // HookAPI (the `pi` object) - utilities available to all hooks:
172
+ interface HookAPI {
173
+ sendMessage(message: HookMessage, triggerTurn?: boolean): void;
174
+ appendEntry(customType: string, data?: unknown): void;
175
+ registerCommand(name: string, options: RegisteredCommand): void;
176
+ registerCustomMessageRenderer(customType: string, renderer: CustomMessageRenderer): void;
177
+ exec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;
178
+ }
179
+
180
+ // HookEventContext - passed to event handlers, has stable context:
181
+ interface HookEventContext {
182
+ ui: HookUIContext;
183
+ hasUI: boolean;
184
+ cwd: string;
185
+ sessionManager: SessionManager;
186
+ modelRegistry: ModelRegistry;
187
+ }
188
+ // Note: exec moved to HookAPI, sessionManager/modelRegistry moved from SessionEventBase
189
+
190
+ // HookCommandContext - passed to command handlers:
191
+ interface HookCommandContext {
192
+ args: string; // Everything after /commandname
193
+ ui: HookUIContext;
194
+ hasUI: boolean;
195
+ cwd: string;
196
+ sessionManager: SessionManager;
197
+ modelRegistry: ModelRegistry;
198
+ }
199
+ // Note: exec and sendMessage accessed via `pi` closure
200
+
201
+ registerCommand(name: string, options: {
202
+ description?: string;
203
+ handler: (ctx: HookCommandContext) => Promise<void>;
204
+ }): void;
205
+ ```
206
+
207
+ Handler return:
208
+ - `void` - command completed (use `sendMessage()` with `triggerTurn: true` to prompt LLM)
209
+
210
+ Wiring (all in AgentSession.prompt()):
211
+ - [x] Add hook commands to autocomplete in interactive-mode
212
+ - [x] `_tryExecuteHookCommand()` in AgentSession handles command execution
213
+ - [x] Build HookCommandContext with ui (from hookRunner), exec, sessionManager, etc.
214
+ - [x] If handler returns string, use as prompt text
215
+ - [x] If handler returns undefined, return early (no LLM call)
216
+ - [x] Works for all modes (interactive, RPC, print) via shared AgentSession
217
+
218
+ **New: `ui.custom()` ✅**
219
+
220
+ For arbitrary hook UI with keyboard focus:
221
+
222
+ ```typescript
223
+ interface HookUIContext {
224
+ // ... existing: select, confirm, input, notify
225
+
226
+ /** Show custom component with keyboard focus. Call done() when finished. */
227
+ custom(component: Component, done: () => void): void;
228
+ }
229
+ ```
230
+
231
+ See also: `CustomEntry<T>` for storing hook state that does NOT participate in context.
232
+
233
+ **New: `context` event ✅**
234
+
235
+ Fires before messages are sent to the LLM, allowing hooks to modify context non-destructively.
236
+
237
+ ```typescript
238
+ interface ContextEvent {
239
+ type: "context";
240
+ /** Messages that will be sent to the LLM */
241
+ messages: Message[];
242
+ }
243
+
244
+ interface ContextEventResult {
245
+ /** Modified messages to send instead */
246
+ messages?: Message[];
247
+ }
248
+
249
+ // In HookAPI:
250
+ on(event: "context", handler: HookHandler<ContextEvent, ContextEventResult | void>): void;
251
+ ```
252
+
253
+ Example use case: **Dynamic Context Pruning** ([discussion #330](https://github.com/badlogic/pi-mono/discussions/330))
254
+
255
+ Non-destructive pruning of tool results to reduce context size:
256
+
257
+ ```typescript
258
+ export default function(pi: HookAPI) {
259
+ // Register /prune command
260
+ pi.registerCommand("prune", {
261
+ description: "Mark tool results for pruning",
262
+ handler: async (ctx) => {
263
+ // Show UI to select which tool results to prune
264
+ // Append custom entry recording pruning decisions:
265
+ // { toolResultId, strategy: "summary" | "truncate" | "remove" }
266
+ pi.appendEntry("tool-result-pruning", { ... });
267
+ }
268
+ });
269
+
270
+ // Intercept context before LLM call
271
+ pi.on("context", async (event, ctx) => {
272
+ // Find all pruning entries in session
273
+ const entries = ctx.sessionManager.getEntries();
274
+ const pruningRules = entries
275
+ .filter(e => e.type === "custom" && e.customType === "tool-result-pruning")
276
+ .map(e => e.data);
277
+
278
+ // Apply pruning rules to messages
279
+ const prunedMessages = applyPruning(event.messages, pruningRules);
280
+ return { messages: prunedMessages };
281
+ });
282
+ }
283
+ ```
284
+
285
+ Benefits:
286
+ - Original tool results stay intact in session
287
+ - Pruning is stored as custom entries, survives session reload
288
+ - Works with branching (pruning entries are part of the tree)
289
+ - Trade-off: cache busting on first submission after pruning
290
+
291
+ ### Investigate: `context` event vs `before_agent_start` ✅
292
+
293
+ References:
294
+ - [#324](https://github.com/badlogic/pi-mono/issues/324) - `before_agent_start` proposal
295
+ - [#330](https://github.com/badlogic/pi-mono/discussions/330) - Dynamic Context Pruning (why `context` was added)
296
+
297
+ **Current `context` event:**
298
+ - Fires before each LLM call within the agent loop
299
+ - Receives `AgentMessage[]` (deep copy, safe to modify)
300
+ - Returns `Message[]` (inconsistent with input type)
301
+ - Modifications are transient (not persisted to session)
302
+ - No TUI visibility of what was changed
303
+ - Use case: non-destructive pruning, dynamic context manipulation
304
+
305
+ **Type inconsistency:** Event receives `AgentMessage[]` but result returns `Message[]`:
306
+ ```typescript
307
+ interface ContextEvent {
308
+ messages: AgentMessage[]; // Input
309
+ }
310
+ interface ContextEventResult {
311
+ messages?: Message[]; // Output - different type!
312
+ }
313
+ ```
314
+
315
+ Questions:
316
+ - [ ] Should input/output both be `Message[]` (LLM format)?
317
+ - [ ] Or both be `AgentMessage[]` with conversion happening after?
318
+ - [ ] Where does `AgentMessage[]` → `Message[]` conversion currently happen?
319
+
320
+ **Proposed `before_agent_start` event:**
321
+ - Fires once when user submits a prompt, before `agent_start`
322
+ - Allows hooks to inject additional content that gets **persisted** to session
323
+ - Injected content is visible in TUI (observability)
324
+ - Does not bust prompt cache (appended after user message, not modifying system prompt)
325
+
326
+ **Key difference:**
327
+ | Aspect | `context` | `before_agent_start` |
328
+ |--------|-----------|---------------------|
329
+ | When | Before each LLM call | Once per user prompt |
330
+ | Persisted | No | Yes (as SystemMessage) |
331
+ | TUI visible | No | Yes (collapsible) |
332
+ | Cache impact | Can bust cache | Append-only, cache-safe |
333
+ | Use case | Transient manipulation | Persistent context injection |
334
+
335
+ **Implementation (completed):**
336
+ - Reuses `HookMessage` type (no new message type needed)
337
+ - Handler returns `{ message: Pick<HookMessage, "customType" | "content" | "display" | "details"> }`
338
+ - Message is appended to agent state AND persisted to session before `agent.prompt()` is called
339
+ - Renders using existing `HookMessageComponent` (or custom renderer if registered)
340
+ - [ ] How does it interact with compaction? (treated like user messages?)
341
+ - [ ] Can hook return multiple messages or just one?
342
+
343
+ **Implementation sketch:**
344
+ ```typescript
345
+ interface BeforeAgentStartEvent {
346
+ type: "before_agent_start";
347
+ userMessage: UserMessage; // The prompt user just submitted
348
+ }
349
+
350
+ interface BeforeAgentStartResult {
351
+ /** Additional context to inject (persisted as SystemMessage) */
352
+ inject?: {
353
+ label: string; // Shown in collapsed TUI state
354
+ content: string | (TextContent | ImageContent)[];
355
+ };
356
+ }
357
+ ```
358
+
359
+ ### HTML Export
360
+
361
+ - [ ] Add collapsible sidebar showing full tree structure
362
+ - [ ] Allow selecting any node in tree to view that path
363
+ - [ ] Add "reset to session leaf" button
364
+ - [ ] Render full path (no compaction resolution needed)
365
+ - [ ] Responsive: collapse sidebar on mobile
366
+
367
+ ### UI Commands ✅
368
+
369
+ - [x] `/branch` - Creates new session file from current path (uses `createBranchedSession()`)
370
+ - [x] `/tree` - In-session tree navigation via tree-selector component
371
+ - Shows full tree structure with labels
372
+ - Navigate between branches (moves leaf pointer)
373
+ - Shows current position
374
+ - Generates branch summaries when switching branches
375
+
376
+ ### Tree Selector Improvements ✅
377
+
378
+ - [x] Active line highlight using `selectedBg` theme color
379
+ - [x] Filter modes via `^O` (forward) / `Shift+^O` (backward):
380
+ - `default`: hides label/custom entries
381
+ - `no-tools`: default minus tool results
382
+ - `user-only`: just user messages
383
+ - `labeled-only`: just labeled entries
384
+ - `all`: everything
385
+
386
+ ### Documentation
387
+
388
+ Review and update all docs:
389
+
390
+ - [ ] `docs/hooks.md` - Major update for hook API:
391
+ - `pi.send()` → `pi.sendMessage()` with new signature
392
+ - New `pi.appendEntry()` for state persistence
393
+ - New `pi.registerCommand()` for custom slash commands
394
+ - New `pi.registerCustomMessageRenderer()` for custom TUI rendering
395
+ - `HookCommandContext` interface and handler patterns
396
+ - `HookMessage<T>` type
397
+ - Updated event signatures (`SessionEventBase`, `before_compact`, etc.)
398
+ - [ ] `docs/hooks-v2.md` - Review/merge or remove if obsolete
399
+ - [ ] `docs/sdk.md` - Update for:
400
+ - `HookMessage` and `isHookMessage()`
401
+ - `Agent.prompt(AppMessage)` overload
402
+ - Session v2 tree structure
403
+ - SessionManager API changes
404
+ - [ ] `docs/session.md` - Update for v2 tree structure, new entry types
405
+ - [ ] `docs/custom-tools.md` - Check if hook changes affect custom tools
406
+ - [ ] `docs/rpc.md` - Check if hook commands work in RPC mode
407
+ - [ ] `docs/skills.md` - Review for any hook-related updates
408
+ - [ ] `docs/extension-loading.md` - Review
409
+ - [x] `docs/theme.md` - Added selectedBg, customMessageBg/Text/Label color tokens (50 total)
410
+ - [ ] `README.md` - Update hook examples if any
411
+
412
+ ### Examples
413
+
414
+ Review and update examples:
415
+
416
+ - [ ] `examples/hooks/` - Update existing, add new examples:
417
+ - [ ] Review `custom-compaction.ts` for new API
418
+ - [ ] Add `registerCommand()` example
419
+ - [ ] Add `sendMessage()` example
420
+ - [ ] Add `registerCustomMessageRenderer()` example
421
+ - [ ] `examples/sdk/` - Update for new session/hook APIs
422
+ - [ ] `examples/custom-tools/` - Review for compatibility
423
+
424
+ ---
425
+
426
+ ## Before Release
427
+
428
+ - [ ] Run full automated test suite: `npm test`
429
+ - [ ] Manual testing of tree navigation and branch summarization
430
+ - [ ] Verify compaction with file tracking works correctly
431
+
432
+ ---
433
+
434
+ ## Notes
435
+
436
+ - All append methods return the new entry's ID
437
+ - Migration rewrites file on first load if version < CURRENT_VERSION
438
+ - Existing sessions become linear chains after migration (parentId = previous entry)
439
+ - Tree features available immediately after migration
440
+ - SessionHeader does NOT have id/parentId (it's metadata, not part of tree)
441
+ - Session is append-only: entries cannot be modified or deleted, only branching changes the leaf pointer
@@ -0,0 +1,240 @@
1
+ # Session File Format
2
+
3
+ Sessions are stored as JSONL (JSON Lines) files. Each line is a JSON object with a `type` field. Session entries form a tree structure via `id`/`parentId` fields, enabling in-place branching without creating new files.
4
+
5
+ ## File Location
6
+
7
+ ```
8
+ ~/.pi/agent/sessions/--<path>--/<timestamp>_<uuid>.jsonl
9
+ ```
10
+
11
+ Where `<path>` is the working directory with `/` replaced by `-`.
12
+
13
+ ## Session Version
14
+
15
+ Sessions have a version field in the header:
16
+
17
+ - **Version 1**: Linear entry sequence (legacy, auto-migrated on load)
18
+ - **Version 2**: Tree structure with `id`/`parentId` linking
19
+
20
+ Existing v1 sessions are automatically migrated to v2 when loaded.
21
+
22
+ ## Type Definitions
23
+
24
+ - [`src/core/session-manager.ts`](../src/core/session-manager.ts) - Session entry types
25
+ - [`packages/agent/src/types.ts`](../../agent/src/types.ts) - `AgentMessage`, `Attachment`, `ThinkingLevel`
26
+ - [`packages/ai/src/types.ts`](../../ai/src/types.ts) - `UserMessage`, `AssistantMessage`, `ToolResultMessage`, `Usage`, `ToolCall`
27
+
28
+ ## Entry Base
29
+
30
+ All entries (except `SessionHeader`) extend `SessionEntryBase`:
31
+
32
+ ```typescript
33
+ interface SessionEntryBase {
34
+ type: string;
35
+ id: string; // 8-char hex ID
36
+ parentId: string | null; // Parent entry ID (null for first entry)
37
+ timestamp: string; // ISO timestamp
38
+ }
39
+ ```
40
+
41
+ ## Entry Types
42
+
43
+ ### SessionHeader
44
+
45
+ First line of the file. Metadata only, not part of the tree (no `id`/`parentId`).
46
+
47
+ ```json
48
+ {"type":"session","version":2,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project"}
49
+ ```
50
+
51
+ For sessions with a parent (created via `/branch` or `newSession({ parentSession })`):
52
+
53
+ ```json
54
+ {"type":"session","version":2,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project","parentSession":"/path/to/original/session.jsonl"}
55
+ ```
56
+
57
+ ### SessionMessageEntry
58
+
59
+ A message in the conversation. The `message` field contains an `AgentMessage`.
60
+
61
+ ```json
62
+ {"type":"message","id":"a1b2c3d4","parentId":"prev1234","timestamp":"2024-12-03T14:00:01.000Z","message":{"role":"user","content":"Hello"}}
63
+ {"type":"message","id":"b2c3d4e5","parentId":"a1b2c3d4","timestamp":"2024-12-03T14:00:02.000Z","message":{"role":"assistant","content":[{"type":"text","text":"Hi!"}],"provider":"anthropic","model":"claude-sonnet-4-5","usage":{...},"stopReason":"stop"}}
64
+ {"type":"message","id":"c3d4e5f6","parentId":"b2c3d4e5","timestamp":"2024-12-03T14:00:03.000Z","message":{"role":"toolResult","toolCallId":"call_123","toolName":"bash","content":[{"type":"text","text":"output"}],"isError":false}}
65
+ ```
66
+
67
+ ### ModelChangeEntry
68
+
69
+ Emitted when the user switches models mid-session.
70
+
71
+ ```json
72
+ {"type":"model_change","id":"d4e5f6g7","parentId":"c3d4e5f6","timestamp":"2024-12-03T14:05:00.000Z","provider":"openai","modelId":"gpt-4o"}
73
+ ```
74
+
75
+ ### ThinkingLevelChangeEntry
76
+
77
+ Emitted when the user changes the thinking/reasoning level.
78
+
79
+ ```json
80
+ {"type":"thinking_level_change","id":"e5f6g7h8","parentId":"d4e5f6g7","timestamp":"2024-12-03T14:06:00.000Z","thinkingLevel":"high"}
81
+ ```
82
+
83
+ ### CompactionEntry
84
+
85
+ Created when context is compacted. Stores a summary of earlier messages.
86
+
87
+ ```json
88
+ {"type":"compaction","id":"f6g7h8i9","parentId":"e5f6g7h8","timestamp":"2024-12-03T14:10:00.000Z","summary":"User discussed X, Y, Z...","firstKeptEntryId":"c3d4e5f6","tokensBefore":50000}
89
+ ```
90
+
91
+ Optional fields:
92
+ - `details`: Compaction-implementation specific data (e.g., file operations for default implementation, or custom data for custom hook implementations)
93
+ - `fromHook`: `true` if generated by a hook, `false`/`undefined` if pi-generated
94
+
95
+ ### BranchSummaryEntry
96
+
97
+ Created when switching branches via `/tree` with an LLM generated summary of the left branch up to the common ancestor. Captures context from the abandoned path.
98
+
99
+ ```json
100
+ {"type":"branch_summary","id":"g7h8i9j0","parentId":"a1b2c3d4","timestamp":"2024-12-03T14:15:00.000Z","fromId":"f6g7h8i9","summary":"Branch explored approach A..."}
101
+ ```
102
+
103
+ Optional fields:
104
+ - `details`: File tracking data (`{ readFiles: string[], modifiedFiles: string[] }`) for default implementation, arbitrary for custom implementation
105
+ - `fromHook`: `true` if generated by a hook
106
+
107
+ ### CustomEntry
108
+
109
+ Hook state persistence. Does NOT participate in LLM context.
110
+
111
+ ```json
112
+ {"type":"custom","id":"h8i9j0k1","parentId":"g7h8i9j0","timestamp":"2024-12-03T14:20:00.000Z","customType":"my-hook","data":{"count":42}}
113
+ ```
114
+
115
+ Use `customType` to identify your hook's entries on reload.
116
+
117
+ ### CustomMessageEntry
118
+
119
+ Hook-injected messages that DO participate in LLM context.
120
+
121
+ ```json
122
+ {"type":"custom_message","id":"i9j0k1l2","parentId":"h8i9j0k1","timestamp":"2024-12-03T14:25:00.000Z","customType":"my-hook","content":"Injected context...","display":true}
123
+ ```
124
+
125
+ Fields:
126
+ - `content`: String or `(TextContent | ImageContent)[]` (same as UserMessage)
127
+ - `display`: `true` = show in TUI with purple styling, `false` = hidden
128
+ - `details`: Optional hook-specific metadata (not sent to LLM)
129
+
130
+ ### LabelEntry
131
+
132
+ User-defined bookmark/marker on an entry.
133
+
134
+ ```json
135
+ {"type":"label","id":"j0k1l2m3","parentId":"i9j0k1l2","timestamp":"2024-12-03T14:30:00.000Z","targetId":"a1b2c3d4","label":"checkpoint-1"}
136
+ ```
137
+
138
+ Set `label` to `undefined` to clear a label.
139
+
140
+ ## Tree Structure
141
+
142
+ Entries form a tree:
143
+ - First entry has `parentId: null`
144
+ - Each subsequent entry points to its parent via `parentId`
145
+ - Branching creates new children from an earlier entry
146
+ - The "leaf" is the current position in the tree
147
+
148
+ ```
149
+ [user msg] ─── [assistant] ─── [user msg] ─── [assistant] ─┬─ [user msg] ← current leaf
150
+
151
+ └─ [branch_summary] ─── [user msg] ← alternate branch
152
+ ```
153
+
154
+ ## Context Building
155
+
156
+ `buildSessionContext()` walks from the current leaf to the root, producing the message list for the LLM:
157
+
158
+ 1. Collects all entries on the path
159
+ 2. Extracts current model and thinking level settings
160
+ 3. If a `CompactionEntry` is on the path:
161
+ - Emits the summary first
162
+ - Then messages from `firstKeptEntryId` to compaction
163
+ - Then messages after compaction
164
+ 4. Converts `BranchSummaryEntry` and `CustomMessageEntry` to appropriate message formats
165
+
166
+ ## Parsing Example
167
+
168
+ ```typescript
169
+ import { readFileSync } from "fs";
170
+
171
+ const lines = readFileSync("session.jsonl", "utf8").trim().split("\n");
172
+
173
+ for (const line of lines) {
174
+ const entry = JSON.parse(line);
175
+
176
+ switch (entry.type) {
177
+ case "session":
178
+ console.log(`Session v${entry.version ?? 1}: ${entry.id}`);
179
+ break;
180
+ case "message":
181
+ console.log(`[${entry.id}] ${entry.message.role}: ${JSON.stringify(entry.message.content)}`);
182
+ break;
183
+ case "compaction":
184
+ console.log(`[${entry.id}] Compaction: ${entry.tokensBefore} tokens summarized`);
185
+ break;
186
+ case "branch_summary":
187
+ console.log(`[${entry.id}] Branch from ${entry.fromId}`);
188
+ break;
189
+ case "custom":
190
+ console.log(`[${entry.id}] Custom (${entry.customType}): ${JSON.stringify(entry.data)}`);
191
+ break;
192
+ case "custom_message":
193
+ console.log(`[${entry.id}] Hook message (${entry.customType}): ${entry.content}`);
194
+ break;
195
+ case "label":
196
+ console.log(`[${entry.id}] Label "${entry.label}" on ${entry.targetId}`);
197
+ break;
198
+ case "model_change":
199
+ console.log(`[${entry.id}] Model: ${entry.provider}/${entry.modelId}`);
200
+ break;
201
+ case "thinking_level_change":
202
+ console.log(`[${entry.id}] Thinking: ${entry.thinkingLevel}`);
203
+ break;
204
+ }
205
+ }
206
+ ```
207
+
208
+ ## SessionManager API
209
+
210
+ Key methods for working with sessions programmatically:
211
+
212
+ ### Creation
213
+ - `SessionManager.create(cwd, sessionDir?)` - New session
214
+ - `SessionManager.open(path, sessionDir?)` - Open existing
215
+ - `SessionManager.continueRecent(cwd, sessionDir?)` - Continue most recent or create new
216
+ - `SessionManager.inMemory(cwd?)` - No file persistence
217
+
218
+ ### Appending (all return entry ID)
219
+ - `appendMessage(message)` - Add message
220
+ - `appendThinkingLevelChange(level)` - Record thinking change
221
+ - `appendModelChange(provider, modelId)` - Record model change
222
+ - `appendCompaction(summary, firstKeptEntryId, tokensBefore, details?, fromHook?)` - Add compaction
223
+ - `appendCustomEntry(customType, data?)` - Hook state (not in context)
224
+ - `appendCustomMessageEntry(customType, content, display, details?)` - Hook message (in context)
225
+ - `appendLabelChange(targetId, label)` - Set/clear label
226
+
227
+ ### Tree Navigation
228
+ - `getLeafId()` - Current position
229
+ - `getEntry(id)` - Get entry by ID
230
+ - `getPath(fromId?)` - Walk from entry to root
231
+ - `getTree()` - Get full tree structure
232
+ - `getChildren(parentId)` - Get direct children
233
+ - `getLabel(id)` - Get label for entry
234
+ - `branch(entryId)` - Move leaf to earlier entry
235
+ - `branchWithSummary(entryId, summary, details?, fromHook?)` - Branch with context summary
236
+
237
+ ### Context
238
+ - `buildSessionContext()` - Get messages for LLM
239
+ - `getEntries()` - All entries (excluding header)
240
+ - `getHeader()` - Session metadata