@dyyz1993/pi-coding-agent 0.70.5 → 0.74.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (301) hide show
  1. package/CHANGELOG.md +266 -80
  2. package/README.md +48 -20
  3. package/dist/bun/cli.d.ts.map +1 -1
  4. package/dist/bun/cli.js +4 -2
  5. package/dist/bun/cli.js.map +1 -1
  6. package/dist/bun/restore-sandbox-env.d.ts +13 -0
  7. package/dist/bun/restore-sandbox-env.d.ts.map +1 -0
  8. package/dist/bun/restore-sandbox-env.js +32 -0
  9. package/dist/bun/restore-sandbox-env.js.map +1 -0
  10. package/dist/cli/args.d.ts +2 -1
  11. package/dist/cli/args.d.ts.map +1 -1
  12. package/dist/cli/args.js +34 -22
  13. package/dist/cli/args.js.map +1 -1
  14. package/dist/cli/list-models.d.ts.map +1 -1
  15. package/dist/cli/list-models.js +2 -1
  16. package/dist/cli/list-models.js.map +1 -1
  17. package/dist/cli.d.ts.map +1 -1
  18. package/dist/cli.js +9 -4
  19. package/dist/cli.js.map +1 -1
  20. package/dist/config.d.ts +16 -8
  21. package/dist/config.d.ts.map +1 -1
  22. package/dist/config.js +238 -66
  23. package/dist/config.js.map +1 -1
  24. package/dist/core/agent-session-runtime.d.ts +10 -0
  25. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  26. package/dist/core/agent-session-runtime.js +14 -0
  27. package/dist/core/agent-session-runtime.js.map +1 -1
  28. package/dist/core/agent-session-services.d.ts +2 -1
  29. package/dist/core/agent-session-services.d.ts.map +1 -1
  30. package/dist/core/agent-session-services.js +1 -0
  31. package/dist/core/agent-session-services.js.map +1 -1
  32. package/dist/core/agent-session.d.ts +25 -26
  33. package/dist/core/agent-session.d.ts.map +1 -1
  34. package/dist/core/agent-session.js +1042 -1116
  35. package/dist/core/agent-session.js.map +1 -1
  36. package/dist/core/agent-types.d.ts +58 -0
  37. package/dist/core/agent-types.d.ts.map +1 -0
  38. package/dist/core/agent-types.js +203 -0
  39. package/dist/core/agent-types.js.map +1 -0
  40. package/dist/core/auth-guidance.d.ts +5 -0
  41. package/dist/core/auth-guidance.d.ts.map +1 -0
  42. package/dist/core/auth-guidance.js +21 -0
  43. package/dist/core/auth-guidance.js.map +1 -0
  44. package/dist/core/auth-storage.d.ts +9 -0
  45. package/dist/core/auth-storage.d.ts.map +1 -1
  46. package/dist/core/auth-storage.js +20 -1
  47. package/dist/core/auth-storage.js.map +1 -1
  48. package/dist/core/bash-executor.d.ts.map +1 -1
  49. package/dist/core/bash-executor.js +9 -6
  50. package/dist/core/bash-executor.js.map +1 -1
  51. package/dist/core/compaction/compaction.d.ts +0 -1
  52. package/dist/core/compaction/compaction.d.ts.map +1 -1
  53. package/dist/core/compaction/compaction.js.map +1 -1
  54. package/dist/core/export-html/ansi-to-html.d.ts.map +1 -1
  55. package/dist/core/export-html/ansi-to-html.js +1 -1
  56. package/dist/core/export-html/ansi-to-html.js.map +1 -1
  57. package/dist/core/export-html/template.css +53 -4
  58. package/dist/core/export-html/template.js +84 -20
  59. package/dist/core/export-html/tool-renderer.d.ts +0 -6
  60. package/dist/core/export-html/tool-renderer.d.ts.map +1 -1
  61. package/dist/core/export-html/tool-renderer.js +15 -2
  62. package/dist/core/export-html/tool-renderer.js.map +1 -1
  63. package/dist/core/extensions/channel-factory.d.ts +13 -0
  64. package/dist/core/extensions/channel-factory.d.ts.map +1 -0
  65. package/dist/core/extensions/channel-factory.js +19 -0
  66. package/dist/core/extensions/channel-factory.js.map +1 -0
  67. package/dist/core/extensions/channel-registry.d.ts +28 -0
  68. package/dist/core/extensions/channel-registry.d.ts.map +1 -0
  69. package/dist/core/extensions/channel-registry.js +12 -0
  70. package/dist/core/extensions/channel-registry.js.map +1 -0
  71. package/dist/core/extensions/index.d.ts +4 -1
  72. package/dist/core/extensions/index.d.ts.map +1 -1
  73. package/dist/core/extensions/index.js +1 -0
  74. package/dist/core/extensions/index.js.map +1 -1
  75. package/dist/core/extensions/loader.d.ts +0 -1
  76. package/dist/core/extensions/loader.d.ts.map +1 -1
  77. package/dist/core/extensions/loader.js +49 -137
  78. package/dist/core/extensions/loader.js.map +1 -1
  79. package/dist/core/extensions/runner.d.ts +24 -20
  80. package/dist/core/extensions/runner.d.ts.map +1 -1
  81. package/dist/core/extensions/runner.js +128 -253
  82. package/dist/core/extensions/runner.js.map +1 -1
  83. package/dist/core/extensions/server-channel.d.ts +8 -8
  84. package/dist/core/extensions/server-channel.d.ts.map +1 -1
  85. package/dist/core/extensions/server-channel.js.map +1 -1
  86. package/dist/core/extensions/types.d.ts +88 -60
  87. package/dist/core/extensions/types.d.ts.map +1 -1
  88. package/dist/core/extensions/types.js +10 -0
  89. package/dist/core/extensions/types.js.map +1 -1
  90. package/dist/core/file-store/file-snapshot-manager.d.ts +95 -0
  91. package/dist/core/file-store/file-snapshot-manager.d.ts.map +1 -0
  92. package/dist/core/file-store/file-snapshot-manager.js +508 -0
  93. package/dist/core/file-store/file-snapshot-manager.js.map +1 -0
  94. package/dist/core/file-store/index.d.ts +5 -0
  95. package/dist/core/file-store/index.d.ts.map +1 -0
  96. package/dist/core/file-store/index.js +3 -0
  97. package/dist/core/file-store/index.js.map +1 -0
  98. package/dist/core/messages.d.ts +10 -2
  99. package/dist/core/messages.d.ts.map +1 -1
  100. package/dist/core/messages.js +23 -6
  101. package/dist/core/messages.js.map +1 -1
  102. package/dist/core/model-registry.d.ts +19 -1
  103. package/dist/core/model-registry.d.ts.map +1 -1
  104. package/dist/core/model-registry.js +97 -16
  105. package/dist/core/model-registry.js.map +1 -1
  106. package/dist/core/model-resolver.d.ts.map +1 -1
  107. package/dist/core/model-resolver.js +24 -15
  108. package/dist/core/model-resolver.js.map +1 -1
  109. package/dist/core/package-manager.d.ts +1 -0
  110. package/dist/core/package-manager.d.ts.map +1 -1
  111. package/dist/core/package-manager.js +61 -35
  112. package/dist/core/package-manager.js.map +1 -1
  113. package/dist/core/provider-display-names.d.ts +2 -0
  114. package/dist/core/provider-display-names.d.ts.map +1 -0
  115. package/dist/core/provider-display-names.js +32 -0
  116. package/dist/core/provider-display-names.js.map +1 -0
  117. package/dist/core/resource-loader.d.ts.map +1 -1
  118. package/dist/core/resource-loader.js +9 -21
  119. package/dist/core/resource-loader.js.map +1 -1
  120. package/dist/core/sdk.d.ts +9 -1
  121. package/dist/core/sdk.d.ts.map +1 -1
  122. package/dist/core/sdk.js +39 -18
  123. package/dist/core/sdk.js.map +1 -1
  124. package/dist/core/session-manager.d.ts +27 -17
  125. package/dist/core/session-manager.d.ts.map +1 -1
  126. package/dist/core/session-manager.js +133 -47
  127. package/dist/core/session-manager.js.map +1 -1
  128. package/dist/core/settings-manager.d.ts +21 -3
  129. package/dist/core/settings-manager.d.ts.map +1 -1
  130. package/dist/core/settings-manager.js +51 -6
  131. package/dist/core/settings-manager.js.map +1 -1
  132. package/dist/core/skills.d.ts.map +1 -1
  133. package/dist/core/skills.js +3 -8
  134. package/dist/core/skills.js.map +1 -1
  135. package/dist/core/slash-commands.d.ts.map +1 -1
  136. package/dist/core/slash-commands.js +4 -3
  137. package/dist/core/slash-commands.js.map +1 -1
  138. package/dist/core/tools/bash.d.ts +0 -2
  139. package/dist/core/tools/bash.d.ts.map +1 -1
  140. package/dist/core/tools/bash.js +108 -154
  141. package/dist/core/tools/bash.js.map +1 -1
  142. package/dist/core/tools/edit-diff.d.ts.map +1 -1
  143. package/dist/core/tools/edit-diff.js +3 -2
  144. package/dist/core/tools/edit-diff.js.map +1 -1
  145. package/dist/core/tools/edit.d.ts.map +1 -1
  146. package/dist/core/tools/edit.js +4 -3
  147. package/dist/core/tools/edit.js.map +1 -1
  148. package/dist/core/tools/find.d.ts.map +1 -1
  149. package/dist/core/tools/find.js +1 -1
  150. package/dist/core/tools/find.js.map +1 -1
  151. package/dist/core/tools/grep.d.ts.map +1 -1
  152. package/dist/core/tools/grep.js +1 -1
  153. package/dist/core/tools/grep.js.map +1 -1
  154. package/dist/core/tools/output-accumulator.d.ts +50 -0
  155. package/dist/core/tools/output-accumulator.d.ts.map +1 -0
  156. package/dist/core/tools/output-accumulator.js +178 -0
  157. package/dist/core/tools/output-accumulator.js.map +1 -0
  158. package/dist/core/tools/output-collector.d.ts +35 -0
  159. package/dist/core/tools/output-collector.d.ts.map +1 -0
  160. package/dist/core/tools/output-collector.js +79 -0
  161. package/dist/core/tools/output-collector.js.map +1 -0
  162. package/dist/core/tools/read.d.ts.map +1 -1
  163. package/dist/core/tools/read.js +70 -13
  164. package/dist/core/tools/read.js.map +1 -1
  165. package/dist/core/tools/spawn-managed.d.ts +18 -0
  166. package/dist/core/tools/spawn-managed.d.ts.map +1 -0
  167. package/dist/core/tools/spawn-managed.js +52 -0
  168. package/dist/core/tools/spawn-managed.js.map +1 -0
  169. package/dist/index.d.ts +7 -4
  170. package/dist/index.d.ts.map +1 -1
  171. package/dist/index.js +6 -2
  172. package/dist/index.js.map +1 -1
  173. package/dist/main.d.ts.map +1 -1
  174. package/dist/main.js +17 -39
  175. package/dist/main.js.map +1 -1
  176. package/dist/migrations.d.ts +1 -1
  177. package/dist/migrations.d.ts.map +1 -1
  178. package/dist/migrations.js +3 -3
  179. package/dist/migrations.js.map +1 -1
  180. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  181. package/dist/modes/interactive/components/config-selector.js +3 -1
  182. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  183. package/dist/modes/interactive/components/extension-selector.d.ts +1 -4
  184. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  185. package/dist/modes/interactive/components/extension-selector.js +14 -56
  186. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  187. package/dist/modes/interactive/components/login-dialog.d.ts +5 -1
  188. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  189. package/dist/modes/interactive/components/login-dialog.js +19 -4
  190. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  191. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  192. package/dist/modes/interactive/components/model-selector.js +1 -1
  193. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  194. package/dist/modes/interactive/components/oauth-selector.d.ts +18 -6
  195. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  196. package/dist/modes/interactive/components/oauth-selector.js +93 -25
  197. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  198. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  199. package/dist/modes/interactive/components/scoped-models-selector.js +1 -1
  200. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  201. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  202. package/dist/modes/interactive/components/session-selector.js +3 -7
  203. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  204. package/dist/modes/interactive/components/settings-selector.d.ts +5 -0
  205. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  206. package/dist/modes/interactive/components/settings-selector.js +53 -1
  207. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  208. package/dist/modes/interactive/interactive-mode.d.ts +20 -4
  209. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  210. package/dist/modes/interactive/interactive-mode.js +423 -186
  211. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  212. package/dist/modes/interactive/theme/dark.json +1 -1
  213. package/dist/modes/interactive/theme/light.json +1 -1
  214. package/dist/modes/print-mode.d.ts +3 -0
  215. package/dist/modes/print-mode.d.ts.map +1 -1
  216. package/dist/modes/print-mode.js +62 -19
  217. package/dist/modes/print-mode.js.map +1 -1
  218. package/dist/modes/rpc/rpc-client.d.ts +80 -60
  219. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  220. package/dist/modes/rpc/rpc-client.js +108 -93
  221. package/dist/modes/rpc/rpc-client.js.map +1 -1
  222. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  223. package/dist/modes/rpc/rpc-mode.js +106 -0
  224. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  225. package/dist/modes/rpc/rpc-types.d.ts +115 -0
  226. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  227. package/dist/modes/rpc/rpc-types.js.map +1 -1
  228. package/dist/package-manager-cli.d.ts.map +1 -1
  229. package/dist/package-manager-cli.js +238 -12
  230. package/dist/package-manager-cli.js.map +1 -1
  231. package/dist/utils/child-process.d.ts +1 -0
  232. package/dist/utils/child-process.d.ts.map +1 -1
  233. package/dist/utils/child-process.js +8 -0
  234. package/dist/utils/child-process.js.map +1 -1
  235. package/dist/utils/clipboard-image.d.ts.map +1 -1
  236. package/dist/utils/clipboard-image.js +2 -2
  237. package/dist/utils/clipboard-image.js.map +1 -1
  238. package/dist/utils/clipboard.d.ts.map +1 -1
  239. package/dist/utils/clipboard.js +84 -45
  240. package/dist/utils/clipboard.js.map +1 -1
  241. package/dist/utils/paths.d.ts +9 -0
  242. package/dist/utils/paths.d.ts.map +1 -1
  243. package/dist/utils/paths.js +31 -0
  244. package/dist/utils/paths.js.map +1 -1
  245. package/dist/utils/pi-user-agent.d.ts +2 -0
  246. package/dist/utils/pi-user-agent.d.ts.map +1 -0
  247. package/dist/utils/pi-user-agent.js +5 -0
  248. package/dist/utils/pi-user-agent.js.map +1 -0
  249. package/dist/utils/structured-output.d.ts +10 -0
  250. package/dist/utils/structured-output.d.ts.map +1 -0
  251. package/dist/utils/structured-output.js +57 -0
  252. package/dist/utils/structured-output.js.map +1 -0
  253. package/dist/utils/tools-manager.d.ts.map +1 -1
  254. package/dist/utils/tools-manager.js +6 -2
  255. package/dist/utils/tools-manager.js.map +1 -1
  256. package/dist/utils/version-check.d.ts +14 -0
  257. package/dist/utils/version-check.d.ts.map +1 -0
  258. package/dist/utils/version-check.js +77 -0
  259. package/dist/utils/version-check.js.map +1 -0
  260. package/docs/compaction.md +14 -14
  261. package/docs/custom-provider.md +40 -31
  262. package/docs/development.md +1 -1
  263. package/docs/docs.json +148 -0
  264. package/docs/extensions.md +116 -56
  265. package/docs/index.md +70 -0
  266. package/docs/json.md +4 -4
  267. package/docs/models.md +150 -3
  268. package/docs/packages.md +10 -5
  269. package/docs/providers.md +62 -17
  270. package/docs/quickstart.md +142 -0
  271. package/docs/rollback-architecture.md +693 -0
  272. package/docs/rollback-test-cases.md +412 -0
  273. package/docs/rpc.md +1 -1
  274. package/docs/sdk.md +26 -26
  275. package/docs/{session.md → session-format.md} +6 -6
  276. package/docs/sessions.md +137 -0
  277. package/docs/settings.md +52 -9
  278. package/docs/termux.md +1 -1
  279. package/docs/themes.md +2 -2
  280. package/docs/tui.md +20 -20
  281. package/docs/usage.md +277 -0
  282. package/examples/extensions/README.md +2 -4
  283. package/examples/extensions/border-status-editor.ts +150 -0
  284. package/examples/extensions/commands.ts +2 -2
  285. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  286. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  287. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  288. package/examples/extensions/custom-provider-qwen-cli/package.json +1 -1
  289. package/examples/extensions/dynamic-resources/dynamic.json +1 -1
  290. package/examples/extensions/git-checkpoint.ts +1 -1
  291. package/examples/extensions/handoff.ts +49 -11
  292. package/examples/extensions/plan-mode/index.ts +1 -1
  293. package/examples/extensions/sandbox/package-lock.json +5 -5
  294. package/examples/extensions/sandbox/package.json +1 -1
  295. package/examples/extensions/subagent/agents.ts +126 -0
  296. package/examples/extensions/with-deps/package-lock.json +2 -2
  297. package/examples/extensions/with-deps/package.json +1 -1
  298. package/examples/sdk/README.md +2 -2
  299. package/package.json +7 -15
  300. package/docs/tree.md +0 -233
  301. package/examples/extensions/antigravity-image-gen.ts +0 -418
@@ -0,0 +1,693 @@
1
+ # Rollback Architecture — Design Document
2
+
3
+ ## 1. Overview
4
+
5
+ The session tree supports branching and navigation via `navigateTree()`, but file system state does not always follow. The `file-snapshot` extension creates per-turn snapshots using content-addressable storage and restores files on `session_tree` events, but it has gaps: no selective rollback, no API for querying modified files, no per-message diff, and a preview-mode bug where results are discarded.
6
+
7
+ This document designs a unified rollback architecture that:
8
+
9
+ 1. Supports **selective rollback**: message-only or both (messages + files)
10
+ 2. Exposes a **get modified files** API for the entire session (or any range)
11
+ 3. Exposes a **per-message file diff** API (before/after snapshot diff)
12
+ 4. Fixes the preview-mode bug
13
+ 5. Integrates snapshot logic into core for better testability and API surface
14
+ 6. Maintains append-only semantics — `session.jsonl` is never mutated
15
+
16
+ ### Design Decisions
17
+
18
+ | Decision | Choice | Rationale |
19
+ |---|---|---|
20
+ | Extension vs core | Integrate into core | `InternalGit` already lives in `src/core/file-store/`. Snapshot logic is fundamental to session integrity. Bugs in extension code (preview void) would be caught by core tests. Selective rollback needs deep session access. |
21
+ | Extension after integration | Thin adapter | Extension stays as a hook-registration shim (registers `session_start`, `turn_end`, `session_tree` handlers that delegate to `FileSnapshotManager`). |
22
+ | Entry format | No new entry types | Continue using `step-snapshot` custom entries. Unrevert-point entries stay as-is. |
23
+ | Storage format | No change | FNV-1a content-addressable objects under `~/.pi/agent/file-store/<projectHash>/objects/`. |
24
+ | Rollback modes | 2 modes: "message" and "both" | "code"-only mode removed. Users can edit files directly if they only want file changes. |
25
+
26
+ ---
27
+
28
+ ## 2. Current Architecture
29
+
30
+ ### 2.1 Components
31
+
32
+ ```
33
+ InternalGit (src/core/file-store/internal-git.ts)
34
+ - Content-addressable object store (FNV-1a hashing)
35
+ - Tree snapshots: writeTree(), readTree()
36
+ - Diff computation: computeDiff(), diffTrees()
37
+ - Working directory scanning: scanWorkingDir()
38
+ - Ignores .git, node_modules, .pi, etc. via `ignore` package
39
+
40
+ file-snapshot extension (extensions/file-snapshot/index.ts)
41
+ - Duplicate ObjectStore class ( reimplements InternalGit with simpler ignore logic)
42
+ - Hooks: session_start, turn_end, session_tree
43
+ - Creates step-snapshot custom entries per turn
44
+ - Restores files on session_tree event
45
+ - Creates unrevert-point entries before restoration
46
+ - BUG: preview mode discards result via `void { ... }`
47
+
48
+ AgentSession.navigateTree() (src/core/agent-session.ts)
49
+ - Moves leaf pointer via sessionManager.branch()
50
+ - Emits session_before_tree → session_tree events
51
+ - skipFiles option passes through to session_tree event
52
+ - previewRollback() emits session_tree with preview: true
53
+ - BUG: previewRollback() doesn't get result back from extension
54
+
55
+ SessionTreeEvent (src/core/extensions/types.ts)
56
+ - { type, newLeafId, oldLeafId, summaryEntry?, skipFiles?, preview? }
57
+ - No rollbackMode field
58
+ ```
59
+
60
+ ### 2.2 Data Flow (current)
61
+
62
+ ```
63
+ session_start
64
+ → ObjectStore.scanWorkingDir() → sessionStartTreeHash
65
+
66
+ turn_end
67
+ → ObjectStore.scanWorkingDir() → snapshotTreeHash
68
+ → computeTreeDiff(lastCommittedTreeHash, snapshotTreeHash)
69
+ → if hasChanges: pi.appendEntry("step-snapshot", { baselineTreeHash, snapshotTreeHash, diff, turnIndex })
70
+
71
+ navigateTree (user triggers /tree)
72
+ → AgentSession.navigateTree(targetId, { skipFiles? })
73
+ → sessionManager.branch(targetId)
74
+ → emit session_tree event
75
+ → extension reads step-snapshot entries on path
76
+ → finds target tree hash vs current tree hash
77
+ → if !skipFiles: restoreFiles() + deleteFiles()
78
+ → appends unrevert-point entry
79
+
80
+ previewRollback
81
+ → emit session_tree with preview: true
82
+ → extension: void { restored, deleted } ← BUG: discards result
83
+ ```
84
+
85
+ ### 2.3 Known Issues
86
+
87
+ | Issue | Location | Description |
88
+ |---|---|---|
89
+ | Duplicate storage logic | `extensions/file-snapshot/index.ts:101-234` | `ObjectStore` duplicates `InternalGit` with simpler ignore patterns (no `ignore` package, custom `matchGlob`). |
90
+ | Preview bug | `extensions/file-snapshot/index.ts:356-359` | `void { restored, deleted }` discards result instead of returning it. |
91
+ | No selective restore | `extensions/file-snapshot/index.ts:309-372` | `session_tree` handler restores ALL files or none (via `skipFiles`). No partial restore. |
92
+ | skipFiles not user-facing | `src/core/agent-session.ts:3178` | `skipFiles` exists but is not exposed via RPC or TUI. |
93
+ | No modified-files query | — | No API to list files changed between any two points. |
94
+ | No per-file diff | — | No API to get unified diff for a specific file between snapshots. |
95
+ | 1MB file limit only in extension | `extensions/file-snapshot/index.ts:163` | Extension skips files > 1MB but `InternalGit` has no such limit. Inconsistent behavior. |
96
+
97
+ ---
98
+
99
+ ## 3. Target Architecture
100
+
101
+ ### 3.1 Component Diagram
102
+
103
+ ```
104
+ AgentSession
105
+
106
+ ├── SessionManager (session.jsonl tree, entry CRUD)
107
+
108
+ ├── FileSnapshotManager (NEW — src/core/file-store/file-snapshot-manager.ts)
109
+ │ │
110
+ │ ├── InternalGit (existing — content-addressable storage)
111
+ │ │
112
+ │ ├── snapshotWorkingDir() — scan + store tree, return hash
113
+ │ ├── getSnapshotAtTurn(n) — query snapshot by turn index
114
+ │ ├── getSnapshotAtEntry(id) — query snapshot by entry ID
115
+ │ ├── getModifiedFiles(opts) — list files changed between two points
116
+ │ ├── getFileDiff(opts) — unified diff for a specific file
117
+ │ ├── restoreFiles(opts) — selective file restoration
118
+ │ │ opts: { targetEntryId?, snapshotHash?, files?: string[], preview? }
119
+ │ └── buildSnapshotIndex() — rebuild in-memory index from entries
120
+
121
+ ├── navigateTree(targetId, opts) — ENHANCED
122
+ │ opts.skipFiles: boolean (default: false)
123
+
124
+ └── ExtensionRunner
125
+ └── file-snapshot extension (THIN adapter)
126
+ delegates to FileSnapshotManager
127
+ ```
128
+
129
+ ### 3.2 FileSnapshotManager
130
+
131
+ ```typescript
132
+ class FileSnapshotManager {
133
+ private git: InternalGit;
134
+ private sessionStartTreeHash: string | null;
135
+ private lastCommittedTreeHash: string | null;
136
+ private turnIndex: number;
137
+ private snapshotIndex: Map<string, StepSnapshotData>; // entryId → snapshot
138
+ private turnIndexMap: Map<number, string>; // turnIndex → entryId
139
+
140
+ constructor(git: InternalGit);
141
+
142
+ // Called on session_start
143
+ initialize(cwd: string): Promise<void>;
144
+
145
+ // Called on turn_end
146
+ onTurnEnd(cwd: string, appendEntry: (data: StepSnapshotData) => void): void;
147
+
148
+ // Called on session reload to rebuild index from custom entries
149
+ rebuildIndex(entries: SessionEntry[]): void;
150
+
151
+ // Query
152
+ getSnapshotAtTurn(turnIndex: number): StepSnapshotData | null;
153
+ getSnapshotAtEntry(entryId: string): StepSnapshotData | null;
154
+ getLatestSnapshotOnPath(entries: SessionEntry[], leafId: string | null): StepSnapshotData | null;
155
+
156
+ // Modified files API
157
+ getModifiedFiles(options?: {
158
+ fromEntryId?: string;
159
+ toEntryId?: string;
160
+ }): ModifiedFileInfo[];
161
+
162
+ // Per-file diff API
163
+ getFileDiff(options: {
164
+ filePath: string;
165
+ fromEntryId?: string;
166
+ toEntryId?: string;
167
+ }): FileDiffInfo | null;
168
+
169
+ // File restoration
170
+ restoreFiles(cwd: string, options: {
171
+ targetEntryId?: string;
172
+ snapshotHash?: string;
173
+ files?: string[];
174
+ preview?: boolean;
175
+ currentLeafId?: string | null;
176
+ entries: SessionEntry[];
177
+ appendEntry: (type: string, data: unknown) => void;
178
+ }): Promise<RestoreResult>;
179
+ }
180
+ ```
181
+
182
+ ### 3.3 Rollback Behavior
183
+
184
+ `navigateTree` uses a simple `skipFiles: boolean` option:
185
+
186
+ ```
187
+ ┌─────────────┬──────────────────────┬──────────────────────┐
188
+ │ skipFiles │ Move leaf pointer │ Restore files │
189
+ ├─────────────┼──────────────────────┼──────────────────────┤
190
+ │ false │ Yes │ Yes │
191
+ │ true │ Yes │ No │
192
+ └─────────────┴──────────────────────┴──────────────────────┘
193
+ ```
194
+
195
+ - `skipFiles: false` (default) — move leaf + restore file snapshot. This is the existing behavior.
196
+ - `skipFiles: true` — move leaf only, skip file restoration. Useful when the user wants to rewind the conversation but keep current files on disk.
197
+
198
+ There is no "code"-only mode. Users who want to restore files without moving the conversation can edit files directly.
199
+
200
+ ---
201
+
202
+ ## 4. API Design
203
+
204
+ ### 4.1 ModifiedFileInfo
205
+
206
+ ```typescript
207
+ interface ModifiedFileInfo {
208
+ path: string;
209
+ status: "added" | "modified" | "deleted";
210
+ turnIndex: number;
211
+ entryId: string; // step-snapshot entry that recorded this change
212
+ }
213
+ ```
214
+
215
+ ### 4.2 FileDiffInfo
216
+
217
+ ```typescript
218
+ interface FileDiffInfo {
219
+ path: string;
220
+ oldContent: string | null; // null if file didn't exist at fromSnapshot
221
+ newContent: string | null; // null if file was deleted at toSnapshot
222
+ oldHash: string | null;
223
+ newHash: string | null;
224
+ unifiedDiff: string; // standard unified diff format
225
+ }
226
+ ```
227
+
228
+ ### 4.3 RestoreResult
229
+
230
+ ```typescript
231
+ interface RestoreResult {
232
+ restored: string[]; // files written back
233
+ deleted: string[]; // files removed
234
+ skipped: string[]; // files that were dirty (externally modified) and skipped
235
+ dirty: string[]; // files that differed from expected current state
236
+ }
237
+ ```
238
+
239
+ ### 4.4 StepSnapshotData (entry data format, unchanged)
240
+
241
+ ```typescript
242
+ interface StepSnapshotData {
243
+ baselineTreeHash: string | null;
244
+ snapshotTreeHash: string;
245
+ diff: {
246
+ added: string[];
247
+ modified: string[];
248
+ deleted: string[];
249
+ } | null;
250
+ turnIndex: number;
251
+ }
252
+ ```
253
+
254
+ ### 4.5 FileSnapshotManager Methods
255
+
256
+ #### `getModifiedFiles(options?)`
257
+
258
+ Lists all files that changed between two snapshots. Defaults to full session range (session start → current leaf).
259
+
260
+ ```typescript
261
+ getModifiedFiles(options?: {
262
+ fromEntryId?: string; // default: session start (no snapshot)
263
+ toEntryId?: string; // default: latest snapshot on current leaf path
264
+ }): ModifiedFileInfo[]
265
+ ```
266
+
267
+ Algorithm:
268
+ 1. Resolve `fromEntryId` to a snapshot (null → session start tree).
269
+ 2. Resolve `toEntryId` to a snapshot (null → latest on leaf path).
270
+ 3. Use `git.diffTrees()` or walk step-snapshot entries on the path, accumulating changes.
271
+ 4. Return aggregated list with earliest turnIndex for each file.
272
+
273
+ #### `getFileDiff(options)`
274
+
275
+ Returns before/after content and unified diff for a single file.
276
+
277
+ ```typescript
278
+ getFileDiff(options: {
279
+ filePath: string;
280
+ fromEntryId?: string; // default: session start
281
+ toEntryId?: string; // default: current leaf
282
+ }): FileDiffInfo | null
283
+ ```
284
+
285
+ Algorithm:
286
+ 1. Resolve from/to snapshots.
287
+ 2. Read both trees via `git.readTree()`.
288
+ 3. Extract file content from each.
289
+ 4. Generate unified diff (use `diffLines` from `diff` package or simple line-based diff).
290
+ 5. Return `null` if file exists in neither snapshot.
291
+
292
+ #### `restoreFiles(cwd, options)`
293
+
294
+ Restores files to a target snapshot state. Supports selective file list and preview mode.
295
+
296
+ ```typescript
297
+ restoreFiles(cwd: string, options: {
298
+ targetEntryId?: string; // resolve to snapshot hash
299
+ snapshotHash?: string; // or provide hash directly
300
+ files?: string[]; // if provided, only restore these files
301
+ preview?: boolean; // if true, return what would happen without writing
302
+ currentLeafId?: string | null;
303
+ entries: SessionEntry[];
304
+ appendEntry: (type: string, data: unknown) => void;
305
+ }): Promise<RestoreResult>
306
+ ```
307
+
308
+ Algorithm:
309
+ 1. Resolve target snapshot hash from `targetEntryId` or use `snapshotHash` directly.
310
+ 2. Resolve current snapshot hash from latest snapshot on `currentLeafId` path.
311
+ 3. Read both trees.
312
+ 4. Compute diff: files to restore (content differs) and files to delete (in current but not in target).
313
+ 5. If `files` option provided, filter to only those paths.
314
+ 6. **Conflict detection**: for each file to restore, read disk content, hash it, compare to current tree hash. If different, mark as `dirty`.
315
+ 7. If `preview`: return result without writing.
316
+ 8. If not preview: scan working dir, write unrevert-point entry, then write/delete files.
317
+
318
+ ---
319
+
320
+ ## 5. RPC Commands
321
+
322
+ ### 5.1 New Commands
323
+
324
+ #### `get_modified_files`
325
+
326
+ ```json
327
+ {
328
+ "id": "req-1",
329
+ "type": "get_modified_files",
330
+ "fromEntryId": "abc123",
331
+ "toEntryId": "def456"
332
+ }
333
+ ```
334
+
335
+ Both fields optional. Defaults: `fromEntryId` = session start, `toEntryId` = current leaf.
336
+
337
+ Response:
338
+ ```json
339
+ {
340
+ "id": "req-1",
341
+ "type": "response",
342
+ "command": "get_modified_files",
343
+ "success": true,
344
+ "data": {
345
+ "files": [
346
+ { "path": "src/foo.ts", "status": "modified", "turnIndex": 2, "entryId": "snap123" },
347
+ { "path": "src/bar.ts", "status": "added", "turnIndex": 3, "entryId": "snap456" }
348
+ ]
349
+ }
350
+ }
351
+ ```
352
+
353
+ #### `get_file_diff`
354
+
355
+ ```json
356
+ {
357
+ "id": "req-2",
358
+ "type": "get_file_diff",
359
+ "filePath": "src/foo.ts",
360
+ "fromEntryId": "abc123",
361
+ "toEntryId": "def456"
362
+ }
363
+ ```
364
+
365
+ `fromEntryId` and `toEntryId` optional with same defaults as above.
366
+
367
+ Response:
368
+ ```json
369
+ {
370
+ "id": "req-2",
371
+ "type": "response",
372
+ "command": "get_file_diff",
373
+ "success": true,
374
+ "data": {
375
+ "path": "src/foo.ts",
376
+ "oldContent": "original content\n",
377
+ "newContent": "modified content\n",
378
+ "oldHash": "a1b2c3d4",
379
+ "newHash": "e5f6g7h8",
380
+ "unifiedDiff": "--- src/foo.ts\n+++ src/foo.ts\n@@ -1 +1 @@\n-original content\n+modified content\n"
381
+ }
382
+ }
383
+ ```
384
+
385
+ Returns `null` data if file doesn't exist in either snapshot.
386
+
387
+ ### 5.2 Modified Commands
388
+
389
+ #### `navigate_tree`
390
+
391
+ Current:
392
+ ```json
393
+ {
394
+ "type": "navigate_tree",
395
+ "targetId": "abc123",
396
+ "summarize": true,
397
+ "skipFiles": false
398
+ }
399
+ ```
400
+
401
+ `skipFiles: boolean` (default: `false`) remains the option — no `rollbackMode` field:
402
+ ```json
403
+ {
404
+ "type": "navigate_tree",
405
+ "targetId": "abc123",
406
+ "summarize": true,
407
+ "skipFiles": false
408
+ }
409
+ ```
410
+
411
+ Behavior:
412
+ - `skipFiles: false` (default) — move leaf + restore files (current behavior)
413
+ - `skipFiles: true` — move leaf only, skip file restoration
414
+
415
+ ---
416
+
417
+ ## 6. Data Model
418
+
419
+ ### 6.1 Entry Types (no changes)
420
+
421
+ No new entry types. Existing entries used by the file snapshot system:
422
+
423
+ | Entry Type | customType | Purpose |
424
+ |---|---|---|
425
+ | `custom` | `step-snapshot` | Per-turn tree snapshot + diff |
426
+ | `custom` | `unrevert-point` | Pre-rollback state for undo |
427
+
428
+ ### 6.2 step-snapshot Entry
429
+
430
+ Stored as a `custom` entry with `customType: "step-snapshot"`:
431
+
432
+ ```json
433
+ {
434
+ "type": "custom",
435
+ "id": "snap1234",
436
+ "parentId": "prev1234",
437
+ "timestamp": "2025-05-07T10:00:00.000Z",
438
+ "customType": "step-snapshot",
439
+ "data": {
440
+ "baselineTreeHash": "a1b2c3d4",
441
+ "snapshotTreeHash": "e5f6g7h8",
442
+ "diff": {
443
+ "added": ["src/new-file.ts"],
444
+ "modified": ["src/existing.ts"],
445
+ "deleted": ["src/old-file.ts"]
446
+ },
447
+ "turnIndex": 3
448
+ }
449
+ }
450
+ ```
451
+
452
+ - `baselineTreeHash`: the tree hash this snapshot was compared against (previous snapshot or session start). Null if this is the first snapshot.
453
+ - `snapshotTreeHash`: the tree hash of the working directory at this turn.
454
+ - `diff`: delta from baseline to this snapshot. Null if no changes detected.
455
+ - `turnIndex`: 0-based turn counter.
456
+
457
+ ### 6.3 unrevert-point Entry
458
+
459
+ Stored as a `custom` entry with `customType: "unrevert-point"`:
460
+
461
+ ```json
462
+ {
463
+ "type": "custom",
464
+ "id": "urv1234",
465
+ "parentId": "snap5678",
466
+ "timestamp": "2025-05-07T10:05:00.000Z",
467
+ "customType": "unrevert-point",
468
+ "data": {
469
+ "preRollbackTreeHash": "c3d4e5f6",
470
+ "rolledBackToLeaf": "abc123",
471
+ "restoredFiles": ["src/foo.ts", "src/bar.ts"]
472
+ }
473
+ }
474
+ ```
475
+
476
+ ### 6.4 BranchSummaryEntry Extension
477
+
478
+ The `BranchSummaryEntry` gains an optional `skipFiles` field to record whether file restoration was skipped at this position:
479
+
480
+ ```typescript
481
+ interface BranchSummaryEntry extends SessionEntryBase {
482
+ type: "summary";
483
+ summary: string;
484
+ skipFiles?: boolean; // true if file restoration was skipped during this navigation
485
+ }
486
+ ```
487
+
488
+ This field is set when `branchWithSummary()` is called with `skipFiles: true`. Future operations can inspect the entry to determine whether files were restored at that point.
489
+
490
+ For `branch()` calls without a summary, `skipFiles` is not persisted — it's an operational parameter for that invocation only. This is acceptable because the user decides each time they navigate.
491
+
492
+ ### 6.5 Object Store Layout
493
+
494
+ ```
495
+ ~/.pi/agent/file-store/<projectHash>/
496
+ ├── objects/
497
+ │ ├── a1/
498
+ │ │ └── b2c3d4e5 # file content or tree data, keyed by FNV-1a hash
499
+ │ ├── e5/
500
+ │ │ └── f6g7h8i9
501
+ │ └── ...
502
+ ```
503
+
504
+ - Blob objects: raw file content.
505
+ - Tree objects: `\n`-separated lines of `<path>\0<hash>`, sorted by path.
506
+
507
+ ---
508
+
509
+ ## 7. Migration Plan
510
+
511
+ ### 7.1 Phase 1: Create FileSnapshotManager (non-breaking)
512
+
513
+ 1. Create `src/core/file-store/file-snapshot-manager.ts`.
514
+ 2. Move snapshot logic from `extensions/file-snapshot/index.ts` into `FileSnapshotManager`:
515
+ - `scanWorkingDir` → use `InternalGit.scanWorkingDir()` (eliminate duplicate).
516
+ - `writeTree`, `readTree`, `computeTreeDiff` → delegate to `InternalGit`.
517
+ - `session_start` handler → `initialize()`.
518
+ - `turn_end` handler → `onTurnEnd()`.
519
+ - `session_tree` handler → `restoreFiles()`.
520
+ - `findLatestSnapshotOnPath` → `getLatestSnapshotOnPath()`.
521
+ 3. Fix preview bug: `restoreFiles()` returns `RestoreResult` in all code paths.
522
+ 4. Port the `findCanonicalGitRoot()` and `shouldIgnore()` logic into core.
523
+ 5. Harmonize file size limit: add 1MB cap to `InternalGit.scanWorkingDir()` (or make it configurable).
524
+
525
+ ### 7.2 Phase 2: Add new APIs to FileSnapshotManager
526
+
527
+ 1. Implement `getModifiedFiles()`.
528
+ 2. Implement `getFileDiff()`.
529
+ 3. Implement `buildSnapshotIndex()` for session reload.
530
+ 4. Wire `FileSnapshotManager` into `AgentSession` (lazy init on `session_start`).
531
+
532
+ ### 7.3 Phase 3: Enhance navigateTree
533
+
534
+ 1. Keep `skipFiles: boolean` as the option on `navigateTree()` (no `rollbackMode`).
535
+ 2. When `skipFiles=false`: current behavior — move leaf + restore files via file-snapshot.
536
+ 3. When `skipFiles=true`: move leaf only, don't restore files.
537
+ 4. Store `skipFiles` on the created `BranchSummaryEntry` if `summarize=true`.
538
+ 5. If `summarize=false` and `skipFiles=true`, the flag is not persisted (operational only).
539
+ 6. `SessionTreeEvent` passes `skipFiles` through (no `rollbackMode` field).
540
+
541
+ ### 7.4 Phase 4: Add RPC commands
542
+
543
+ 1. Add `get_modified_files` command to `rpc-types.ts` and `rpc-mode.ts`.
544
+ 2. Add `get_file_diff` command.
545
+ 3. No `restore_files` command — users edit files directly.
546
+ 4. Keep `navigate_tree` command with `skipFiles: boolean` option.
547
+
548
+ ### 7.5 Phase 5: Thin the extension
549
+
550
+ 1. Update `extensions/file-snapshot/index.ts` to delegate to `AgentSession.fileSnapshotManager` instead of its own `ObjectStore`.
551
+ 2. Extension becomes ~50 lines: register `session_start`/`turn_end`/`session_tree` hooks that call `ctx.sessionManager.fileSnapshotManager.*()`.
552
+ 3. Extension can be disabled without losing core functionality (FileSnapshotManager always present).
553
+
554
+ ---
555
+
556
+ ## 8. Testing Strategy
557
+
558
+ ### 8.1 Unit Tests — FileSnapshotManager
559
+
560
+ File: `test/file-store/file-snapshot-manager.test.ts`
561
+
562
+ | Test | Description |
563
+ |---|---|
564
+ | initializes with working dir snapshot | `initialize()` creates session-start tree hash |
565
+ | skips snapshot when no changes | `onTurnEnd()` with unchanged dir doesn't append entry |
566
+ | creates snapshot on file change | `onTurnEnd()` after file write produces step-snapshot data |
567
+ | creates snapshot on file add | New file detected in diff |
568
+ | creates snapshot on file delete | Removed file detected in diff |
569
+ | gets snapshot at turn index | `getSnapshotAtTurn(0)` returns first snapshot |
570
+ | gets snapshot at entry ID | `getSnapshotAtEntry(id)` returns correct snapshot |
571
+ | rebuilds index from entries | `rebuildIndex()` restores snapshot map from custom entries |
572
+ | handles empty session | No snapshots, no crashes |
573
+
574
+ ### 8.2 Unit Tests — getModifiedFiles
575
+
576
+ | Test | Description |
577
+ |---|---|
578
+ | returns all changes for session | Full range from start to current |
579
+ | returns changes between two entries | Scoped range |
580
+ | returns empty for no changes | Identical snapshots |
581
+ | aggregates across multiple turns | Multiple snapshots consolidated |
582
+ | tracks per-file earliest change | Status reflects first occurrence |
583
+
584
+ ### 8.3 Unit Tests — getFileDiff
585
+
586
+ | Test | Description |
587
+ |---|---|
588
+ | returns diff for modified file | oldContent vs newContent with unified diff |
589
+ | returns diff for added file | oldContent = null |
590
+ | returns diff for deleted file | newContent = null |
591
+ | returns null for non-existent file | File not in either snapshot |
592
+ | handles default range | Session start to current |
593
+
594
+ ### 8.4 Unit Tests — restoreFiles
595
+
596
+ | Test | Description |
597
+ |---|---|
598
+ | restores modified files | Content written to disk |
599
+ | deletes files not in target | Files removed from disk |
600
+ | preview mode returns plan without writing | Disk unchanged |
601
+ | selective restore with files filter | Only specified files restored |
602
+ | conflict detection marks dirty files | Externally modified files in `dirty` list |
603
+ | appends unrevert-point entry | Pre-rollback state recorded |
604
+ | no-op when snapshots identical | No writes, empty result |
605
+
606
+ ### 8.5 Integration Tests — navigateTree with skipFiles
607
+
608
+ File: `test/suite/navigate-tree-rollback.test.ts`
609
+
610
+ | Test | Description |
611
+ |---|---|
612
+ | skipFiles=false restores files and moves leaf | Default behavior preserved |
613
+ | skipFiles=true moves leaf without restoring files | Files unchanged on disk |
614
+ | skipFiles=true with summarize stores flag on BranchSummaryEntry | Entry has `skipFiles: true` |
615
+ | skipFiles=true without summarize does not persist flag | Operational only |
616
+ | summarize with skipFiles=false does not set flag | BranchSummaryEntry has no `skipFiles` field |
617
+
618
+ ### 8.6 Integration Tests — RPC Commands
619
+
620
+ File: `test/suite/rpc-rollback.test.ts`
621
+
622
+ | Test | Description |
623
+ |---|---|
624
+ | get_modified_files returns correct list | RPC round-trip |
625
+ | get_file_diff returns unified diff | RPC round-trip |
626
+ | navigate_tree with skipFiles | RPC round-trip |
627
+
628
+ ---
629
+
630
+ ## 9. Risks and Mitigations
631
+
632
+ | Risk | Impact | Likelihood | Mitigation |
633
+ |---|---|---|---|
634
+ | FNV-1a hash collisions | Data corruption (wrong file content) | Low (32-bit hash, ~4B values) | Hash is only used as disk key. Collision means overwriting an object with same-named content. Acceptable for snapshots, not for security. Can upgrade to SHA-256 later if needed. |
635
+ | Large project scan time | Slow `turn_end` handler, blocking agent | Medium | `InternalGit.scanWorkingDir()` reads all files. Mitigate with: (1) 1MB file cap, (2) `skipFiles` patterns, (3) consider incremental scanning in future. |
636
+ | Disk space for object store | Object accumulation over many sessions | Low | Objects are deduplicated by hash. Add periodic garbage collection of orphaned objects (objects not referenced by any session's snapshots). |
637
+ | Race condition: external file change during restore | Inconsistent restore | Low | Scan working dir immediately before restore (already done for unrevert-point). Dirty detection catches this. |
638
+ | Breaking change to extension API | Existing file-snapshot extensions break | Medium | Phase migration: Phase 1 is non-breaking (new core class, extension unchanged). Phase 5 thins extension but old extension still works if present. |
639
+ | Preview mode was broken, clients may not expect it to work | Wrong assumptions about preview reliability | Low | Preview is currently broken (void). Fixing it is a pure improvement. |
640
+ | skipFiles=true leaves conversation behind files | User rewinds messages but files stay at later state | Low | This is intentional behavior — the user explicitly chose to keep files. UI should clearly indicate when file restoration was skipped. |
641
+
642
+ ---
643
+
644
+ ## 10. Implementation Order
645
+
646
+ ### Phase 1: Core foundation (1-2 days)
647
+
648
+ 1. Create `FileSnapshotManager` class in `src/core/file-store/`.
649
+ 2. Port logic from extension. Use `InternalGit` directly (no duplicate `ObjectStore`).
650
+ 3. Fix preview bug.
651
+ 4. Add 1MB file size limit to `InternalGit.scanWorkingDir()`.
652
+ 5. Unit tests for `FileSnapshotManager` (initialize, onTurnEnd, restoreFiles basic).
653
+ 6. `npm run check` passes.
654
+
655
+ ### Phase 2: Query APIs (1 day)
656
+
657
+ 1. Implement `getModifiedFiles()`.
658
+ 2. Implement `getFileDiff()`.
659
+ 3. Add unified diff generation (use existing `diff` package or implement simple line diff).
660
+ 4. Unit tests for both APIs.
661
+ 5. `npm run check` passes.
662
+
663
+ ### Phase 3: skipFiles behavior (1 day)
664
+
665
+ 1. Keep `skipFiles: boolean` on `navigateTree()` options.
666
+ 2. `SessionTreeEvent` passes `skipFiles` through (no `rollbackMode`).
667
+ 3. When `skipFiles=true`: move leaf only, don't restore files.
668
+ 4. Add `skipFiles?: boolean` field to `BranchSummaryEntry`.
669
+ 5. Store `skipFiles` on `BranchSummaryEntry` when `summarize=true` and `skipFiles=true`.
670
+ 6. Integration tests for both modes (skipFiles=false, skipFiles=true).
671
+ 7. `npm run check` passes.
672
+
673
+ ### Phase 4: RPC surface (1 day)
674
+
675
+ 1. Add `get_modified_files` and `get_file_diff` to `rpc-types.ts`.
676
+ 2. Implement handlers in `rpc-mode.ts`.
677
+ 3. No `restore_files` command.
678
+ 4. RPC integration tests.
679
+ 5. `npm run check` passes.
680
+
681
+ ### Phase 5: Extension thinning (0.5 day)
682
+
683
+ 1. Update `file-snapshot` extension to delegate to `ctx.sessionManager.fileSnapshotManager`.
684
+ 2. Verify extension tests still pass.
685
+ 3. Update `docs/extensions.md` with new architecture note.
686
+ 4. `npm run check` passes.
687
+
688
+ ### Phase 6: Documentation and polish (0.5 day)
689
+
690
+ 1. Update `docs/rpc.md` with new commands.
691
+ 2. Update `docs/tree.md` with skipFiles behavior.
692
+ 3. Update `docs/session.md` with FileSnapshotManager section.
693
+ 4. Final `npm run check`.