@gavin-hu/my-pi-agent 0.14.0 → 0.15.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 (222) hide show
  1. package/README.md +55 -33
  2. package/README.zh-CN.md +44 -27
  3. package/extensions/bytefly/README.md +193 -140
  4. package/extensions/bytefly/block/component.ts +7 -15
  5. package/extensions/bytefly/block/register.ts +6 -1
  6. package/extensions/bytefly/doctor/register.ts +24 -5
  7. package/extensions/bytefly/doctor/tools.ts +4 -0
  8. package/extensions/bytefly/footer/collect/snapshot.ts +6 -1
  9. package/extensions/bytefly/footer/config.ts +10 -6
  10. package/extensions/bytefly/footer/price/price.ts +5 -3
  11. package/extensions/bytefly/footer/price/rates.ts +63 -26
  12. package/extensions/bytefly/footer/render/status.ts +3 -3
  13. package/extensions/bytefly/header/component.ts +3 -2
  14. package/extensions/bytefly/index.ts +3 -3
  15. package/extensions/bytefly/notice/component.ts +8 -3
  16. package/extensions/bytefly/notify/config.ts +6 -11
  17. package/extensions/bytefly/notify/dispatch.ts +13 -5
  18. package/extensions/bytefly/panel/commands.ts +21 -1
  19. package/extensions/bytefly/panel/format.ts +5 -2
  20. package/extensions/bytefly/panel/settings-runtime.ts +26 -6
  21. package/extensions/bytefly/panel/settings.ts +41 -4
  22. package/extensions/bytefly/panel/themes.ts +32 -0
  23. package/extensions/bytefly/panel/tui/editing.ts +77 -0
  24. package/extensions/bytefly/panel/tui/entries.ts +24 -0
  25. package/extensions/bytefly/panel/tui/extensions-view.ts +13 -8
  26. package/extensions/bytefly/panel/tui/frame.ts +121 -4
  27. package/extensions/bytefly/panel/tui/settings-view.ts +54 -15
  28. package/extensions/bytefly/panel/tui.ts +94 -152
  29. package/extensions/bytefly/picker/discovery.ts +1 -0
  30. package/extensions/bytefly/picker/types.ts +2 -0
  31. package/extensions/bytefly/separator/component.ts +14 -4
  32. package/extensions/bytefly/update/cache.ts +58 -19
  33. package/extensions/bytefly/update/check.ts +35 -0
  34. package/extensions/bytefly/update/format.ts +76 -35
  35. package/extensions/bytefly/update/register.ts +144 -55
  36. package/extensions/bytefly/update/state.ts +4 -2
  37. package/extensions/bytefly/update/suppress.ts +66 -0
  38. package/extensions/bytefly/update/types.ts +15 -0
  39. package/extensions/check/README.md +253 -0
  40. package/extensions/check/commands.ts +128 -0
  41. package/extensions/check/config.ts +39 -0
  42. package/extensions/check/detect.ts +269 -0
  43. package/extensions/check/failures.ts +230 -0
  44. package/extensions/check/format.ts +135 -0
  45. package/extensions/check/index.ts +26 -0
  46. package/extensions/check/render.ts +185 -0
  47. package/extensions/check/runner.ts +176 -0
  48. package/extensions/check/runtime.ts +244 -0
  49. package/extensions/check/source.ts +80 -0
  50. package/extensions/check/tools.ts +173 -0
  51. package/extensions/check/tui.ts +195 -0
  52. package/extensions/check/types.ts +99 -0
  53. package/extensions/document/README.md +1 -1
  54. package/extensions/document/extract/pdf.ts +8 -16
  55. package/extensions/document/formats.ts +2 -2
  56. package/extensions/file-browse/README.md +66 -32
  57. package/extensions/file-browse/client.ts +95 -32
  58. package/extensions/file-browse/config.ts +9 -9
  59. package/extensions/file-browse/git.ts +37 -47
  60. package/extensions/file-browse/html.ts +39 -58
  61. package/extensions/file-browse/markdown.ts +110 -92
  62. package/extensions/file-browse/raw-html.ts +478 -0
  63. package/extensions/file-browse/router.ts +32 -70
  64. package/extensions/file-browse/server.ts +18 -56
  65. package/extensions/file-browse/urls.ts +80 -0
  66. package/extensions/goal/README.md +103 -47
  67. package/extensions/goal/clarify-parse.ts +4 -9
  68. package/extensions/goal/clarify.ts +27 -4
  69. package/extensions/goal/commands.ts +24 -11
  70. package/extensions/goal/format.ts +41 -10
  71. package/extensions/goal/index.ts +39 -16
  72. package/extensions/goal/plan-route.ts +51 -0
  73. package/extensions/goal/schema.ts +6 -23
  74. package/extensions/goal/state.ts +39 -57
  75. package/extensions/goal/tools.ts +26 -20
  76. package/extensions/goal/types.ts +11 -1
  77. package/extensions/job/README.md +20 -6
  78. package/extensions/job/format.ts +18 -68
  79. package/extensions/job/index.ts +26 -41
  80. package/extensions/job/log-writer.ts +208 -0
  81. package/extensions/job/logs.ts +3 -2
  82. package/extensions/job/process.ts +6 -99
  83. package/extensions/job/runtime.ts +42 -181
  84. package/extensions/job/schema.ts +1 -4
  85. package/extensions/job/tools.ts +1 -15
  86. package/extensions/job/tui.ts +117 -307
  87. package/extensions/job/ui.ts +7 -33
  88. package/extensions/memory/README.md +1 -1
  89. package/extensions/memory/errors.ts +5 -6
  90. package/extensions/memory/tui.ts +14 -24
  91. package/extensions/plan/README.md +94 -18
  92. package/extensions/plan/commands.ts +9 -5
  93. package/extensions/plan/format.ts +87 -0
  94. package/extensions/plan/index.ts +31 -15
  95. package/extensions/plan/plans.ts +2 -3
  96. package/extensions/plan/policy.ts +0 -6
  97. package/extensions/plan/runtime.ts +4 -2
  98. package/extensions/plan/steps.ts +37 -8
  99. package/extensions/plan/todos.ts +44 -3
  100. package/extensions/plan/tools/enter.ts +94 -0
  101. package/extensions/plan/tools/exit.ts +296 -0
  102. package/extensions/plan/tools/nested.ts +21 -0
  103. package/extensions/plan/tools/steps.ts +136 -0
  104. package/extensions/plan/tools/todo-seed.ts +76 -0
  105. package/extensions/plan/tools/write.ts +127 -0
  106. package/extensions/plan/tools.ts +18 -703
  107. package/extensions/plan/web-client.ts +6 -22
  108. package/extensions/plan/web-page.ts +19 -30
  109. package/extensions/plan/web-router.ts +34 -71
  110. package/extensions/plan/web.ts +13 -40
  111. package/extensions/question/answers.ts +3 -8
  112. package/extensions/question/tui.ts +6 -6
  113. package/extensions/rewind/README.md +95 -22
  114. package/extensions/rewind/checklist.ts +185 -0
  115. package/extensions/rewind/commands.ts +8 -0
  116. package/extensions/rewind/config.ts +3 -11
  117. package/extensions/rewind/format.ts +220 -2
  118. package/extensions/rewind/git.ts +74 -7
  119. package/extensions/rewind/hunk-picker.ts +114 -0
  120. package/extensions/rewind/patch.ts +88 -0
  121. package/extensions/rewind/path-picker.ts +67 -100
  122. package/extensions/rewind/paths.ts +93 -19
  123. package/extensions/rewind/restore.ts +53 -10
  124. package/extensions/rewind/review-tui.ts +320 -0
  125. package/extensions/rewind/review.ts +397 -0
  126. package/extensions/rewind/rewind-tui.ts +4 -3
  127. package/extensions/rewind/runtime.ts +94 -17
  128. package/extensions/subagent/README.md +61 -6
  129. package/extensions/subagent/background-tui.ts +208 -0
  130. package/extensions/subagent/background.ts +470 -0
  131. package/extensions/subagent/commands.ts +80 -0
  132. package/extensions/subagent/config.ts +4 -0
  133. package/extensions/subagent/format.ts +24 -1
  134. package/extensions/subagent/index.ts +91 -2
  135. package/extensions/subagent/render.ts +13 -0
  136. package/extensions/subagent/run.ts +17 -16
  137. package/extensions/subagent/schema.ts +12 -0
  138. package/extensions/subagent/types.ts +11 -0
  139. package/extensions/subagent/ui.ts +50 -0
  140. package/extensions/todo/README.md +18 -13
  141. package/extensions/todo/format.ts +3 -2
  142. package/extensions/todo/runtime.ts +3 -2
  143. package/extensions/todo/schema.ts +8 -84
  144. package/extensions/todo/state.ts +5 -46
  145. package/extensions/todo/tools.ts +2 -1
  146. package/extensions/todo/types.ts +6 -15
  147. package/extensions/watch/README.md +14 -6
  148. package/extensions/watch/child.ts +150 -0
  149. package/extensions/watch/format.ts +8 -48
  150. package/extensions/watch/index.ts +42 -58
  151. package/extensions/watch/runtime.ts +49 -128
  152. package/extensions/watch/schema.ts +1 -4
  153. package/extensions/watch/tools.ts +1 -11
  154. package/extensions/watch/tui.ts +103 -273
  155. package/extensions/watch/ui.ts +5 -32
  156. package/extensions/web-access/README.md +1 -1
  157. package/extensions/web-access/fetch/page.ts +1 -1
  158. package/extensions/web-access/fetch/tool.ts +6 -2
  159. package/extensions/web-access/search/search.ts +2 -5
  160. package/extensions/wechat/README.md +1 -1
  161. package/extensions/wechat/client.ts +23 -0
  162. package/extensions/wechat/commands.ts +57 -42
  163. package/extensions/wechat/deliver.ts +5 -9
  164. package/extensions/wechat/errors.ts +3 -6
  165. package/extensions/wechat/inbound.ts +2 -5
  166. package/extensions/wechat/index.ts +2 -8
  167. package/extensions/wechat/outbound.ts +2 -4
  168. package/extensions/wechat/peer-worker.ts +3 -8
  169. package/extensions/wechat/poll.ts +10 -9
  170. package/extensions/wechat/presence.ts +35 -62
  171. package/extensions/wechat/reply-pipeline.ts +3 -1
  172. package/extensions/wechat/status-screen.ts +1 -1
  173. package/extensions/wechat/timer.ts +57 -0
  174. package/extensions/worktree/config.ts +8 -20
  175. package/extensions/worktree/enter.ts +7 -12
  176. package/extensions/worktree/git.ts +38 -69
  177. package/extensions/worktree/index.ts +5 -4
  178. package/extensions/worktree/registry.ts +7 -7
  179. package/extensions/worktree/root-tools.ts +2 -2
  180. package/extensions/worktree/status.ts +3 -2
  181. package/lib/README.md +22 -8
  182. package/lib/config.ts +50 -3
  183. package/lib/errors.ts +12 -0
  184. package/lib/format.ts +50 -0
  185. package/lib/git/read.ts +8 -0
  186. package/lib/goal-state.ts +23 -8
  187. package/lib/http/headers.ts +54 -0
  188. package/lib/http/index.ts +13 -0
  189. package/lib/http/response.ts +46 -0
  190. package/lib/http/router.ts +52 -0
  191. package/lib/http/server.ts +91 -0
  192. package/lib/lines.ts +77 -0
  193. package/lib/pdf.ts +82 -0
  194. package/lib/plan-entry.ts +71 -0
  195. package/lib/plan-state.ts +34 -0
  196. package/lib/report.ts +99 -0
  197. package/lib/requirements.ts +25 -19
  198. package/lib/spawn.ts +170 -0
  199. package/lib/todo-state.ts +110 -23
  200. package/lib/tool-names.ts +7 -0
  201. package/lib/tool-result.ts +33 -0
  202. package/lib/tui/README.md +68 -3
  203. package/lib/tui/chrome.ts +28 -4
  204. package/lib/tui/columns.ts +6 -1
  205. package/lib/tui/confirm.ts +65 -0
  206. package/lib/tui/dock-screen.ts +410 -0
  207. package/lib/tui/glyphs.ts +2 -1
  208. package/lib/tui/indent.ts +1 -1
  209. package/lib/tui/index.ts +2 -0
  210. package/lib/tui/rows.ts +21 -0
  211. package/lib/tui/status-controller.ts +35 -1
  212. package/lib/tui/tool.ts +59 -1
  213. package/lib/web/components.ts +20 -1
  214. package/lib/web/markup.ts +32 -0
  215. package/lib/web/runtime.ts +50 -0
  216. package/package.json +22 -5
  217. package/skills/init/SKILL.md +165 -0
  218. package/skills/init/references/agents-md-quality.md +103 -0
  219. package/skills/init/references/interview-questions.md +81 -0
  220. package/skills/refactor/SKILL.md +141 -0
  221. package/extensions/watch/process.ts +0 -63
  222. package/extensions/web-access/fetch/pdf.ts +0 -45
@@ -2,16 +2,20 @@
2
2
 
3
3
  Snapshot the working tree at the start of each task and return to an earlier
4
4
  prompt from a menu — restoring the **code**, the **conversation**, or **both**.
5
- A snapshot is a git commit kept under `refs/pi/rewind/<id>`; restoring one
6
- rewrites the working tree **without touching the real index or moving HEAD**, so
7
- commits, branches, staged state, and the reflog are untouched. Conversation
8
- rewind uses Pi's session tree, so the branch you leave is not erased.
5
+ `/review` uses the same snapshot to show the latest turn's changes and revert one
6
+ file or the whole turn. A snapshot is a git commit kept under
7
+ `refs/pi/rewind/<id>`; restoring one rewrites the working tree **without
8
+ touching the real index or moving HEAD**, so commits, branches, staged state, and
9
+ the reflog are untouched. Conversation rewind uses Pi's session tree, so the
10
+ branch you leave is not erased.
9
11
 
10
12
  This is the `rewind` extension entrypoint; it is one of two sibling git
11
13
  extensions in this package (`worktree` is the other).
12
14
 
13
15
  <img src="../../assets/rewind.png" alt="The /rewind screen: prompt rows newest first, a diamond marking prompts with a code snapshot, and the focused prompt's snapshot detail" width="100%">
14
16
 
17
+ <img src="../../assets/review.png" alt="The /review screen: changed files with M, A, and D marks, line counts, and the focused file's detail" width="100%">
18
+
15
19
  ```
16
20
  pi --extension ./extensions/rewind # load this extension alone
17
21
  pi -e . # load the whole @gavin-hu/my-pi-agent package
@@ -22,6 +26,17 @@ pi install ./ # install the package
22
26
 
23
27
  - Takes **one automatic snapshot per user prompt**, before the prompt's first
24
28
  mutating tool call. A read-only prompt snapshots nothing.
29
+ - `/review` compares the working tree with the snapshot taken before the latest
30
+ prompt on the active branch. The file list carries `M`/`A`/`D`/`R` and line
31
+ counts; `Enter` opens a scrollable patch, `Space` selects files, `r` reverts
32
+ the selection (or the focused file when nothing is selected), and `R` reverts
33
+ every changed path. A modified text file's diff offers `h`, which opens a hunk
34
+ checklist; the chosen hunks revert as one reverse patch with its own
35
+ pre-restore safety snapshot. A rename is one `R` row, so reverting it restores
36
+ the old path and removes the new one together. A selected batch is one restore
37
+ and one pre-restore safety snapshot, not one per file. When the working tree
38
+ matches HEAD, the screen says the changes are already committed and a revert
39
+ leaves the commits in place.
25
40
  - `/rewind` lists **every user prompt on the active branch** (newest first), each
26
41
  marked `⬥` when a code snapshot was captured for it.
27
42
  - Picking a point asks what to restore: **Code and conversation**,
@@ -31,9 +46,10 @@ pi install ./ # install the package
31
46
  - When code moves, a second step asks **Restore what?** — **Everything (N
32
47
  files)** or **Choose paths…**. The checklist lists every changed path
33
48
  grouped by directory (`M` modified, `D` deleted since the snapshot, `A`
34
- created after it), with all of them selected, so `Enter` twice reproduces a
35
- whole-tree restore. `Space` toggles a file or a directory, `a`/`n` select
36
- all/none, and the confirm that follows previews only the chosen subset.
49
+ created after it, `R` renamed), with all of them selected, so `Enter` twice
50
+ reproduces a whole-tree restore. `Space` toggles a file or a directory,
51
+ `a`/`n` select all/none, and the confirm that follows previews only the
52
+ chosen subset.
37
53
  - *Conversation* moves the session tree back to that prompt and puts the
38
54
  prompt text back in the editor; the abandoned branch stays in the session.
39
55
  - Shows a `↺ N` status chip counting the prompts on the active branch — the
@@ -52,6 +68,7 @@ the main checkout; `list`/matching filter to the current root by default.
52
68
  | Command | What it does |
53
69
  |---|---|
54
70
  | `/rewind` | Open the timeline; pick a prompt, then a restore scope, then (for code) the paths. |
71
+ | `/review` | Review the latest prompt's changes; open a diff, select files with `Space`, revert the selection or all files, or revert chosen hunks with `h`. |
55
72
 
56
73
  The path checklist is its own `ctx.ui.custom` screen: `Space` toggles the focused
57
74
  row, `a`/`n` select all or none, `Enter` returns the selection, and `Esc` cancels
@@ -63,13 +80,22 @@ The scope choice and the confirmation (with a diff preview) run after the screen
63
80
  closes. The path checklist is the same grammar: `Space` toggles, `a`/`n` select
64
81
  all or none, `Enter` restores the selection, `Esc` cancels the rewind.
65
82
 
83
+ The review list is keyboard-driven too: `↑`/`↓` or `j`/`k` move, `PgUp`/`PgDn` and
84
+ `Home`/`End` jump, `Space` toggles the focused file, `a`/`n` select all or none,
85
+ `Enter` opens the focused file's diff, `r` reverts the selection (or the focused
86
+ file when nothing is selected), `R` reverts every changed path, and `Esc` closes.
87
+ In the diff, `j`/`k`, the arrows, `PgUp`/`PgDn`, and `g`/`G` scroll, `r` reverts
88
+ the file, and `Esc` goes back. A modified text file's diff offers `h` for the
89
+ hunk checklist: `Space` toggles, `a`/`n` select all or none, `Enter` reverts the
90
+ chosen hunks, and `Esc` cancels back to the diff.
91
+
66
92
  ## Behaviour by mode
67
93
 
68
94
  | Mode | Behaviour |
69
95
  |---|---|
70
- | Interactive TUI | Full selectable timeline component, then the scope choice, the path checklist, and the confirmation. |
71
- | RPC (`hasUI`) | Timeline falls back to `ui.select`; scope choice and confirmation use forwarded dialogs. |
72
- | `print` / `json` | No picker: the prompt list is printed. |
96
+ | Interactive TUI | Full selectable timeline, then the scope choice, the path checklist, and the confirmation. `/review` mounts the changed-file list, the per-file diff, and the hunk checklist. |
97
+ | RPC (`hasUI`) | Timeline falls back to `ui.select`; scope choice and confirmation use forwarded dialogs. `/review` prints the changed-path list. |
98
+ | `print` / `json` | No picker: the prompt list is printed. `/review` prints the changed-path list. |
73
99
 
74
100
  ## Configuration
75
101
 
@@ -94,15 +120,19 @@ The `rewind` section of the shared `bytefly.json` (global document, then
94
120
  | Key | Meaning |
95
121
  |---|---|
96
122
  | `autoSnapshots` | Take one automatic snapshot per prompt. Default `true`. |
97
- | `max` | Snapshot refs kept per root before the oldest are pruned on save. |
123
+ | `max` | Snapshot refs kept per root; automatic and pre-restore safety snapshots both count. |
98
124
  | `includeUntracked` | Capture untracked, non-ignored files. |
99
125
  | `safetySnapshot` | Save the current state before a code restore. |
100
- | `autoPrune` | Delete the oldest refs beyond `max` after saving. |
126
+ | `autoPrune` | Delete the oldest refs beyond `max` after an automatic snapshot. |
101
127
  | `showStatus` | Draw the `↺ N` status chip. |
102
128
  | `watch` | Extra tool names to treat as mutating. |
103
129
  | `ignore` | Tool names never to snapshot; this package's own tools are always excluded. |
104
130
  | `refNamespace` | Ref namespace for stored snapshots. |
105
131
 
132
+ A restore's safety snapshot is not pruned immediately, so the snapshot it
133
+ restored from stays available for a follow-up restore; the next automatic
134
+ snapshot applies the cap.
135
+
106
136
  ## How snapshots are stored
107
137
 
108
138
  Each snapshot is a commit under `refs/pi/rewind/<id>` whose body carries a
@@ -126,6 +156,19 @@ commit. These refs are not branches and are not touched by `git push --all`.
126
156
  - **The path checklist is TUI-only.** RPC, `print`, and `json` modes restore the
127
157
  whole tree; there is no typed path syntax, because a list of exact paths is what
128
158
  the checklist is for and a glob would need its own validation and preview.
159
+ - **Hunk selection covers modified text files.** Added, deleted, renamed, and
160
+ binary files, and a change with no textual hunks (a mode-only change), revert
161
+ whole; the diff offers `h` only when it has hunks to show.
162
+ - **`/review` follows the latest prompt with a snapshot.** A read-only prompt
163
+ after it does not change the target, and a turn whose snapshot failed has no
164
+ review of its own. Actions are TUI-only; other modes print the changed list.
165
+ - **`/review` refuses while plan mode is on.** Plan mode is read-only, so approve
166
+ the plan before reverting its changes.
167
+ - **`/review` labels committed changes only when the working tree is clean.**
168
+ `currentTree === HEAD^{tree}` means every reviewed difference is in commits;
169
+ the confirm then says the commit stays. A dirty tree with some commits since
170
+ the snapshot is unlabelled and keeps the generic "HEAD and the index are
171
+ untouched" wording.
129
172
 
130
173
  ## Non-goals
131
174
 
@@ -140,8 +183,8 @@ commit. These refs are not branches and are not touched by `git push --all`.
140
183
 
141
184
  | Integration point | Value |
142
185
  |---|---|
143
- | Model-facing tools | None. `/rewind` is user-driven; automatic snapshots cover the start of every task. |
144
- | Commands | `/rewind` (`pi.registerCommand`). |
186
+ | Model-facing tools | None. `/rewind` and `/review` are user-driven; automatic snapshots cover the start of every task. |
187
+ | Commands | `/rewind` and `/review` (`pi.registerCommand`). |
145
188
  | Events | `session_start` (reset run state, status, stale-index sweep), `session_tree` (status refresh after tree navigation), `before_agent_start` (record the prompt — absent on a run a custom message starts — and resolve the run's snapshot decision), `tool_call` (take the run's snapshot before the first mutating call), `session_shutdown` (release indexes, clear status). |
146
189
  | State storage | Git refs `refs/pi/rewind/<id>` with metadata in the commit body. No tool-result `details` and no `pi.appendEntry`. |
147
190
  | Lifecycle | Index paths are created lazily and released in `session_shutdown`; status chip is set on start/tree/snapshot and cleared on shutdown. |
@@ -194,6 +237,31 @@ commit. These refs are not branches and are not touched by `git push --all`.
194
237
  records its `root`; listing and matching filter to the current root and
195
238
  restoring another worktree's snapshot is refused. The effective root follows
196
239
  `PI_WORKTREE_ROOT` when set.
240
+ - **Review is derived, not stored.** `/review` uses the same
241
+ `buildRewindPoints` pairing as `/rewind`, calls `planRestore`, and fetches a
242
+ patch only when a file is opened. A revert is the existing subset restore, so
243
+ the safety snapshot and the working-tree-only rewrite are the ones `/rewind`
244
+ already guarantees. A selected batch is one subset restore, which is why it
245
+ takes one safety snapshot; the list carries the selection across a diff/back
246
+ round without persisting it.
247
+ - **A rename is one entry, not two.** `parseNameStatus` keeps git's `R` line as
248
+ `{ path: destination, from: source }`, and `entryPaths` is the one helper that
249
+ names both sides. The restore filters the full diff in memory rather than
250
+ narrowing it with a pathspec, because git detects a rename only when both
251
+ paths are in scope; a pathspec would turn the row into an addition and lose
252
+ the side that has to be recreated. The row's patch and line counts stay a
253
+ delete plus a create, so the pane and the counts describe the same two
254
+ operations a revert performs. A rename whose two names resolve to one file (a
255
+ case-only rename on a case-insensitive filesystem) skips the destination
256
+ delete, because the checkout has already renamed that file.
257
+ - **A hunk revert is a reverse patch, not a checkout.** `patch.ts` splits one
258
+ file's diff into its header and hunks; `buildHunkPatch` rebuilds only the chosen
259
+ ones, and `git apply --reverse` writes them to the working tree without
260
+ `--index`/`--cached`, so the real index and HEAD stay untouched. The patch goes
261
+ through a private temp directory because `pi.exec` has no stdin, and git checks
262
+ every hunk before writing, so a file edited after its diff was read fails
263
+ atomically. The revert takes the same pre-restore safety snapshot as a file
264
+ revert.
197
265
  - **Identity is fixed for internal commits.** `commit-tree` runs with
198
266
  `-c user.name=pi -c user.email=pi@localhost`, so snapshots work without committer
199
267
  identity configured and never depend on the user's git config.
@@ -202,23 +270,28 @@ commit. These refs are not branches and are not touched by `git push --all`.
202
270
 
203
271
  | File | Responsibility |
204
272
  |---|---|
205
- | `index.ts` | Registrar: wire the runtime, the command, and the run-state module to Pi's events (`registerRewind`). |
273
+ | `index.ts` | Registrar: wire the runtime, the commands, and the run-state module to Pi's events (`registerRewind`). |
206
274
  | `types.ts` | `Snapshot`, `SnapshotReason`, `RestoreSummary`. |
207
275
  | `config.ts` | Config defaults and merge/normalize over `lib/config.ts`. |
208
- | `git.ts` | Single git entry point: rewind plumbing, the shared read helpers rewind consumes, and the production `RunGit`. |
276
+ | `git.ts` | Single git entry point: rewind plumbing, the shared read helpers rewind consumes (name status, stat, patch, and `--numstat`), the reverse apply a hunk revert uses, and the production `RunGit`. |
209
277
  | `snapshot.ts` | Create a snapshot (tree from a temporary index → commit → ref). |
210
278
  | `snapshot-run.ts` | Per-run automatic-snapshot state: one snapshot per prompt, with a synchronous guard. |
211
- | `restore.ts` | Plan and apply a working-tree rewind, whole or for a chosen subset of paths. |
212
- | `paths.ts` | Pure: the changed-path list from `git diff --name-status`, its directory grouping, the change marks, and the restore counts. |
213
- | `path-picker.ts` | The checklist screen a partial restore opens: grouped paths, toggles, and the selection it returns. |
279
+ | `restore.ts` | Plan and apply a working-tree rewind, whole or for a chosen subset of paths, recreating a rename's source and removing its destination. |
280
+ | `paths.ts` | Pure: the changed-path list from `git diff --name-status` (a rename is one paired entry), its directory grouping, the change marks, the `--numstat` counts, and the restore counts. |
281
+ | `checklist.ts` | The shared multi-select checklist screen: the cursor, `Space`/`a`/`n`/`Enter`, the height math, and the dock frame. The three checklists below supply only their rows and their words. |
282
+ | `path-picker.ts` | The partial restore's checklist, a `ChecklistScreen` over grouped paths: directory toggles, the change marks, and the selection it returns. |
283
+ | `patch.ts` | Pure: split a single-file unified diff into its header and hunks, and rebuild a patch from chosen hunks. |
284
+ | `hunk-picker.ts` | The hunk checklist a modified file's diff opens, a `ChecklistScreen` over the hunks: previews, the indices it returns, and its own row budget. |
214
285
  | `store.ts` | Versioned metadata encoding and ref-backed listing/pruning. |
215
286
  | `policy.ts` | Which tool calls are worth a snapshot. |
216
287
  | `temp-index.ts` | Per-process `GIT_INDEX_FILE` paths: create, release, and sweep stale files. |
217
288
  | `status.ts` | The `↺ N` status chip. |
218
289
  | `queue.ts` | The serialized git operation queue for staging calls (`GIT_INDEX_FILE`). |
219
- | `runtime.ts` | Root/config caches and the snapshot/list/plan/restore operations. |
290
+ | `runtime.ts` | Root/config caches and the snapshot/list/plan/patch/numstat/restore/hunk-revert operations. |
220
291
  | `timeline.ts` | Pure: prompts on the branch, paired with their snapshots. |
221
292
  | `rewind.ts` | Scope choice, the path step, confirmation, and applying code + conversation rewind. |
293
+ | `review.ts` | The `/review` orchestration: the latest snapshot, the list/diff loop, the hunk step, confirmations, and the reverts. |
294
+ | `review-tui.ts` | The changed-file list (a `ChecklistScreen`) and the scrollable per-file diff, which offers the hunk checklist. |
222
295
  | `rewind-tui.ts` | The selectable, width-safe timeline component, drawn from the `lib/tui/` kit (`titleRule`, `dockListFrame`, `listRow`, `emptyState`). |
223
- | `commands.ts` | The `/rewind` command. |
224
- | `format.ts` | Model-facing and transcript text (pure). |
296
+ | `commands.ts` | The `/rewind` and `/review` commands. |
297
+ | `format.ts` | Pure rewind and review text, including diff bounding and the hunk prompt. |
@@ -0,0 +1,185 @@
1
+ /**
2
+ * The shared multi-select checklist screen.
3
+ *
4
+ * `/rewind`'s partial restore, `/review`'s changed-file list, and `/review`'s
5
+ * hunk step are one screen: a docked list where Space toggles the focused row,
6
+ * `a` and `n` fill or clear the selection, Enter returns it, and the summary
7
+ * names how much is chosen. The frame, the cursor arithmetic, the wheel, and the
8
+ * height math come from the `lib/tui/` kit; this base owns the selection and the
9
+ * keys, and a subclass supplies the rows, the row line, the detail block, the
10
+ * summary text, and the hints.
11
+ *
12
+ * Value-only: the cursor and the selection live on the instance.
13
+ */
14
+
15
+ import type { Theme } from "@earendil-works/pi-coding-agent";
16
+ import { Key, matchesKey, type Component, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
17
+ import { detailBlockRows, dockListFrame, type ViewportRowsSource } from "../../lib/tui/chrome.ts";
18
+ import { clip } from "../../lib/tui/columns.ts";
19
+ import { fitRows, formatRange, isCloseKey, ListCursor, navigateList, scrollList } from "../../lib/tui/cursor.ts";
20
+ import { emptyState } from "../../lib/tui/rows.ts";
21
+ import { titleRule } from "../../lib/tui/rules.ts";
22
+ import { paint } from "../../lib/tui/tokens.ts";
23
+
24
+ /** Rows the checklist shows when the terminal height is unknown. */
25
+ const SCREEN_DEFAULT_ITEMS = 12;
26
+ /** Header, summary, blanks, and footer rows around the list. */
27
+ const SCREEN_CHROME_ROWS = 6;
28
+
29
+ export interface ChecklistOptions {
30
+ /** The header rule's label. */
31
+ title: string;
32
+ theme: Theme;
33
+ /** Ask the host to repaint. */
34
+ requestRender(): void;
35
+ /** The terminal height source, for the visible-row count. */
36
+ viewportRows?: ViewportRowsSource;
37
+ /** The empty-list line; omit when an empty list is never shown. */
38
+ emptyLabel?: string;
39
+ }
40
+
41
+ export abstract class ChecklistScreen<T> implements Component {
42
+ /** Focus by row index: a checklist's rows are fixed for the life of the screen. */
43
+ protected readonly cursor: ListCursor;
44
+ /** Chosen row keys, in the order they were selected. */
45
+ protected readonly chosen = new Set<string>();
46
+ private seeded = false;
47
+
48
+ constructor(protected readonly options: ChecklistOptions) {
49
+ this.cursor = new ListCursor(options.requestRender);
50
+ }
51
+
52
+ /** The rows to show, in screen order. */
53
+ protected abstract rows(): readonly T[];
54
+ /** A row's stable key, which is what the selection holds. */
55
+ protected abstract keyOf(row: T): string;
56
+ /** One list line for `row`. */
57
+ protected abstract rowLine(row: T, width: number, selected: boolean): string;
58
+ /** The summary line's text, without its indent or paint role. */
59
+ protected abstract summaryText(): string;
60
+ /** The footer hint row. */
61
+ protected abstract hint(width: number): string;
62
+ /** Enter, with the chosen keys in selection order. */
63
+ protected abstract onEnter(selection: string[]): void;
64
+ /** Esc or q. */
65
+ protected abstract onCancel(): void;
66
+ /** The focus block under the list, painted and clipped; empty for none. */
67
+ protected detailLines(_width: number): string[] {
68
+ return [];
69
+ }
70
+ /** The keys selected on the first render. */
71
+ protected initialSelection(): readonly string[] {
72
+ return this.rows().map((row) => this.keyOf(row));
73
+ }
74
+ /** The keys `a` selects; defaults to every row. */
75
+ protected selectableKeys(): readonly string[] {
76
+ return this.rows().map((row) => this.keyOf(row));
77
+ }
78
+ /** Rows the detail block costs, for the visible-row count. */
79
+ protected detailRows(detail: readonly string[]): number {
80
+ return detailBlockRows(detail);
81
+ }
82
+ /** A verb other than navigation, toggle, all, none, and enter. */
83
+ protected verb?(data: string): void;
84
+
85
+ /** The row under the cursor. */
86
+ protected get focused(): T | undefined {
87
+ return this.rows()[this.cursor.selected];
88
+ }
89
+
90
+ /** Paint through the theme. */
91
+ protected get theme(): Theme {
92
+ return this.options.theme;
93
+ }
94
+
95
+ /** Space on the focused row. */
96
+ protected toggle(row: T): void {
97
+ const key = this.keyOf(row);
98
+ if (this.chosen.has(key)) this.chosen.delete(key);
99
+ else this.chosen.add(key);
100
+ }
101
+
102
+ handleInput(data: string): void {
103
+ this.seed();
104
+ if (isCloseKey(data)) {
105
+ this.onCancel();
106
+ return;
107
+ }
108
+ if (navigateList(data, this.cursor, this.rows().length)) return;
109
+ if (data === " ") {
110
+ const row = this.focused;
111
+ if (row !== undefined) this.toggle(row);
112
+ this.options.requestRender();
113
+ return;
114
+ }
115
+ if (data === "a") {
116
+ for (const key of this.selectableKeys()) this.chosen.add(key);
117
+ this.options.requestRender();
118
+ return;
119
+ }
120
+ if (data === "n") {
121
+ this.chosen.clear();
122
+ this.options.requestRender();
123
+ return;
124
+ }
125
+ if (matchesKey(data, Key.enter)) {
126
+ this.onEnter([...this.chosen]);
127
+ return;
128
+ }
129
+ this.verb?.(data);
130
+ }
131
+
132
+ /** Wheel scrolling in fullscreen moves the cursor; regular mode leaves the wheel to the terminal. */
133
+ handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
134
+ return scrollList(event, this.cursor, this.rows().length);
135
+ }
136
+
137
+ dispose(): void {}
138
+
139
+ invalidate(): void {}
140
+
141
+ render(width: number): string[] {
142
+ this.seed();
143
+ const w = Math.max(1, width);
144
+ const header = titleRule(this.theme, w, this.options.title);
145
+ const rows = this.rows();
146
+ if (rows.length === 0 && this.options.emptyLabel !== undefined) {
147
+ return dockListFrame({
148
+ header,
149
+ body: emptyState(this.theme, w, { label: this.options.emptyLabel }),
150
+ hint: this.hint(w),
151
+ });
152
+ }
153
+
154
+ const detail = this.detailLines(w);
155
+ this.cursor.visible = fitRows(
156
+ this.options.viewportRows,
157
+ rows.length,
158
+ SCREEN_CHROME_ROWS + this.detailRows(detail),
159
+ SCREEN_DEFAULT_ITEMS,
160
+ );
161
+ const { start, end } = this.cursor.window(rows.length);
162
+ const body: string[] = [];
163
+ for (let index = start; index < end; index++) {
164
+ body.push(this.rowLine(rows[index], w, index === this.cursor.selected));
165
+ }
166
+
167
+ return dockListFrame({
168
+ header,
169
+ summary: clip(` ${paint(this.theme, "meta", this.summaryText())}`, w),
170
+ body,
171
+ range: this.cursor.clipped(rows.length)
172
+ ? formatRange(this.theme, w, { start, end, total: rows.length })
173
+ : undefined,
174
+ detail: detail.length > 0 ? detail : undefined,
175
+ hint: this.hint(w),
176
+ });
177
+ }
178
+
179
+ /** Fill the selection on the first interaction, after the subclass is built. */
180
+ private seed(): void {
181
+ if (this.seeded) return;
182
+ this.seeded = true;
183
+ for (const key of this.initialSelection()) this.chosen.add(key);
184
+ }
185
+ }
@@ -10,6 +10,7 @@
10
10
  import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
11
11
  import { withRailsSuppressed } from "../../lib/rails.ts";
12
12
  import { formatRewindListText, formatRewindRow } from "./format.ts";
13
+ import { openReview } from "./review.ts";
13
14
  import { rewindTo } from "./rewind.ts";
14
15
  import type { RewindRuntime } from "./runtime.ts";
15
16
  import { RewindListComponent } from "./rewind-tui.ts";
@@ -71,4 +72,11 @@ export function registerCommands(pi: ExtensionAPI, runtime: RewindRuntime): void
71
72
  await openRewind(pi, runtime, ctx);
72
73
  },
73
74
  });
75
+
76
+ pi.registerCommand("review", {
77
+ description: "Review the latest turn's changes and revert a file or the whole turn",
78
+ handler: async (_args, ctx) => {
79
+ await openReview(pi, runtime, ctx);
80
+ },
81
+ });
74
82
  }
@@ -6,18 +6,18 @@
6
6
  * malformed documents are ignored, and every value is clamped to a safe range.
7
7
  */
8
8
 
9
- import { clampInteger, cleanString, loadConfigSection } from "../../lib/config.ts";
9
+ import { clampInteger, cleanString, loadConfigSection, stringList } from "../../lib/config.ts";
10
10
 
11
11
  export interface RewindConfig {
12
12
  /** Take one automatic snapshot per user prompt. Default: true. */
13
13
  autoSnapshots: boolean;
14
- /** Ref count kept per root before the oldest are pruned on save. */
14
+ /** Ref count kept per root; automatic and pre-restore safety snapshots both count. */
15
15
  max: number;
16
16
  /** Include untracked, non-ignored files in a snapshot. Default: true. */
17
17
  includeUntracked: boolean;
18
18
  /** Snapshot the current state before a restore, so a rewind is undoable. */
19
19
  safetySnapshot: boolean;
20
- /** Prune oldest refs beyond `max` after saving. */
20
+ /** Prune the oldest refs beyond `max` after an automatic snapshot. */
21
21
  autoPrune: boolean;
22
22
  /** Show a `↺ N` status chip while the session runs. */
23
23
  showStatus: boolean;
@@ -41,14 +41,6 @@ export const DEFAULT_CONFIG: RewindConfig = {
41
41
  refNamespace: "refs/pi/rewind",
42
42
  };
43
43
 
44
- /** Keep only trimmed, non-empty strings from an untrusted array. */
45
- function stringList(value: unknown, fallback: string[]): string[] {
46
- if (!Array.isArray(value)) return fallback;
47
- return value
48
- .filter((item): item is string => typeof item === "string" && item.trim().length > 0)
49
- .map((item) => item.trim());
50
- }
51
-
52
44
  /** Normalize one merged config object over the running base. */
53
45
  export function normalizeConfig(raw: Record<string, unknown> | undefined, base: RewindConfig): RewindConfig {
54
46
  if (!raw) return base;