@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.
- package/README.md +55 -33
- package/README.zh-CN.md +44 -27
- package/extensions/bytefly/README.md +193 -140
- package/extensions/bytefly/block/component.ts +7 -15
- package/extensions/bytefly/block/register.ts +6 -1
- package/extensions/bytefly/doctor/register.ts +24 -5
- package/extensions/bytefly/doctor/tools.ts +4 -0
- package/extensions/bytefly/footer/collect/snapshot.ts +6 -1
- package/extensions/bytefly/footer/config.ts +10 -6
- package/extensions/bytefly/footer/price/price.ts +5 -3
- package/extensions/bytefly/footer/price/rates.ts +63 -26
- package/extensions/bytefly/footer/render/status.ts +3 -3
- package/extensions/bytefly/header/component.ts +3 -2
- package/extensions/bytefly/index.ts +3 -3
- package/extensions/bytefly/notice/component.ts +8 -3
- package/extensions/bytefly/notify/config.ts +6 -11
- package/extensions/bytefly/notify/dispatch.ts +13 -5
- package/extensions/bytefly/panel/commands.ts +21 -1
- package/extensions/bytefly/panel/format.ts +5 -2
- package/extensions/bytefly/panel/settings-runtime.ts +26 -6
- package/extensions/bytefly/panel/settings.ts +41 -4
- package/extensions/bytefly/panel/themes.ts +32 -0
- package/extensions/bytefly/panel/tui/editing.ts +77 -0
- package/extensions/bytefly/panel/tui/entries.ts +24 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +13 -8
- package/extensions/bytefly/panel/tui/frame.ts +121 -4
- package/extensions/bytefly/panel/tui/settings-view.ts +54 -15
- package/extensions/bytefly/panel/tui.ts +94 -152
- package/extensions/bytefly/picker/discovery.ts +1 -0
- package/extensions/bytefly/picker/types.ts +2 -0
- package/extensions/bytefly/separator/component.ts +14 -4
- package/extensions/bytefly/update/cache.ts +58 -19
- package/extensions/bytefly/update/check.ts +35 -0
- package/extensions/bytefly/update/format.ts +76 -35
- package/extensions/bytefly/update/register.ts +144 -55
- package/extensions/bytefly/update/state.ts +4 -2
- package/extensions/bytefly/update/suppress.ts +66 -0
- package/extensions/bytefly/update/types.ts +15 -0
- package/extensions/check/README.md +253 -0
- package/extensions/check/commands.ts +128 -0
- package/extensions/check/config.ts +39 -0
- package/extensions/check/detect.ts +269 -0
- package/extensions/check/failures.ts +230 -0
- package/extensions/check/format.ts +135 -0
- package/extensions/check/index.ts +26 -0
- package/extensions/check/render.ts +185 -0
- package/extensions/check/runner.ts +176 -0
- package/extensions/check/runtime.ts +244 -0
- package/extensions/check/source.ts +80 -0
- package/extensions/check/tools.ts +173 -0
- package/extensions/check/tui.ts +195 -0
- package/extensions/check/types.ts +99 -0
- package/extensions/document/README.md +1 -1
- package/extensions/document/extract/pdf.ts +8 -16
- package/extensions/document/formats.ts +2 -2
- package/extensions/file-browse/README.md +66 -32
- package/extensions/file-browse/client.ts +95 -32
- package/extensions/file-browse/config.ts +9 -9
- package/extensions/file-browse/git.ts +37 -47
- package/extensions/file-browse/html.ts +39 -58
- package/extensions/file-browse/markdown.ts +110 -92
- package/extensions/file-browse/raw-html.ts +478 -0
- package/extensions/file-browse/router.ts +32 -70
- package/extensions/file-browse/server.ts +18 -56
- package/extensions/file-browse/urls.ts +80 -0
- package/extensions/goal/README.md +103 -47
- package/extensions/goal/clarify-parse.ts +4 -9
- package/extensions/goal/clarify.ts +27 -4
- package/extensions/goal/commands.ts +24 -11
- package/extensions/goal/format.ts +41 -10
- package/extensions/goal/index.ts +39 -16
- package/extensions/goal/plan-route.ts +51 -0
- package/extensions/goal/schema.ts +6 -23
- package/extensions/goal/state.ts +39 -57
- package/extensions/goal/tools.ts +26 -20
- package/extensions/goal/types.ts +11 -1
- package/extensions/job/README.md +20 -6
- package/extensions/job/format.ts +18 -68
- package/extensions/job/index.ts +26 -41
- package/extensions/job/log-writer.ts +208 -0
- package/extensions/job/logs.ts +3 -2
- package/extensions/job/process.ts +6 -99
- package/extensions/job/runtime.ts +42 -181
- package/extensions/job/schema.ts +1 -4
- package/extensions/job/tools.ts +1 -15
- package/extensions/job/tui.ts +117 -307
- package/extensions/job/ui.ts +7 -33
- package/extensions/memory/README.md +1 -1
- package/extensions/memory/errors.ts +5 -6
- package/extensions/memory/tui.ts +14 -24
- package/extensions/plan/README.md +94 -18
- package/extensions/plan/commands.ts +9 -5
- package/extensions/plan/format.ts +87 -0
- package/extensions/plan/index.ts +31 -15
- package/extensions/plan/plans.ts +2 -3
- package/extensions/plan/policy.ts +0 -6
- package/extensions/plan/runtime.ts +4 -2
- package/extensions/plan/steps.ts +37 -8
- package/extensions/plan/todos.ts +44 -3
- package/extensions/plan/tools/enter.ts +94 -0
- package/extensions/plan/tools/exit.ts +296 -0
- package/extensions/plan/tools/nested.ts +21 -0
- package/extensions/plan/tools/steps.ts +136 -0
- package/extensions/plan/tools/todo-seed.ts +76 -0
- package/extensions/plan/tools/write.ts +127 -0
- package/extensions/plan/tools.ts +18 -703
- package/extensions/plan/web-client.ts +6 -22
- package/extensions/plan/web-page.ts +19 -30
- package/extensions/plan/web-router.ts +34 -71
- package/extensions/plan/web.ts +13 -40
- package/extensions/question/answers.ts +3 -8
- package/extensions/question/tui.ts +6 -6
- package/extensions/rewind/README.md +95 -22
- package/extensions/rewind/checklist.ts +185 -0
- package/extensions/rewind/commands.ts +8 -0
- package/extensions/rewind/config.ts +3 -11
- package/extensions/rewind/format.ts +220 -2
- package/extensions/rewind/git.ts +74 -7
- package/extensions/rewind/hunk-picker.ts +114 -0
- package/extensions/rewind/patch.ts +88 -0
- package/extensions/rewind/path-picker.ts +67 -100
- package/extensions/rewind/paths.ts +93 -19
- package/extensions/rewind/restore.ts +53 -10
- package/extensions/rewind/review-tui.ts +320 -0
- package/extensions/rewind/review.ts +397 -0
- package/extensions/rewind/rewind-tui.ts +4 -3
- package/extensions/rewind/runtime.ts +94 -17
- package/extensions/subagent/README.md +61 -6
- package/extensions/subagent/background-tui.ts +208 -0
- package/extensions/subagent/background.ts +470 -0
- package/extensions/subagent/commands.ts +80 -0
- package/extensions/subagent/config.ts +4 -0
- package/extensions/subagent/format.ts +24 -1
- package/extensions/subagent/index.ts +91 -2
- package/extensions/subagent/render.ts +13 -0
- package/extensions/subagent/run.ts +17 -16
- package/extensions/subagent/schema.ts +12 -0
- package/extensions/subagent/types.ts +11 -0
- package/extensions/subagent/ui.ts +50 -0
- package/extensions/todo/README.md +18 -13
- package/extensions/todo/format.ts +3 -2
- package/extensions/todo/runtime.ts +3 -2
- package/extensions/todo/schema.ts +8 -84
- package/extensions/todo/state.ts +5 -46
- package/extensions/todo/tools.ts +2 -1
- package/extensions/todo/types.ts +6 -15
- package/extensions/watch/README.md +14 -6
- package/extensions/watch/child.ts +150 -0
- package/extensions/watch/format.ts +8 -48
- package/extensions/watch/index.ts +42 -58
- package/extensions/watch/runtime.ts +49 -128
- package/extensions/watch/schema.ts +1 -4
- package/extensions/watch/tools.ts +1 -11
- package/extensions/watch/tui.ts +103 -273
- package/extensions/watch/ui.ts +5 -32
- package/extensions/web-access/README.md +1 -1
- package/extensions/web-access/fetch/page.ts +1 -1
- package/extensions/web-access/fetch/tool.ts +6 -2
- package/extensions/web-access/search/search.ts +2 -5
- package/extensions/wechat/README.md +1 -1
- package/extensions/wechat/client.ts +23 -0
- package/extensions/wechat/commands.ts +57 -42
- package/extensions/wechat/deliver.ts +5 -9
- package/extensions/wechat/errors.ts +3 -6
- package/extensions/wechat/inbound.ts +2 -5
- package/extensions/wechat/index.ts +2 -8
- package/extensions/wechat/outbound.ts +2 -4
- package/extensions/wechat/peer-worker.ts +3 -8
- package/extensions/wechat/poll.ts +10 -9
- package/extensions/wechat/presence.ts +35 -62
- package/extensions/wechat/reply-pipeline.ts +3 -1
- package/extensions/wechat/status-screen.ts +1 -1
- package/extensions/wechat/timer.ts +57 -0
- package/extensions/worktree/config.ts +8 -20
- package/extensions/worktree/enter.ts +7 -12
- package/extensions/worktree/git.ts +38 -69
- package/extensions/worktree/index.ts +5 -4
- package/extensions/worktree/registry.ts +7 -7
- package/extensions/worktree/root-tools.ts +2 -2
- package/extensions/worktree/status.ts +3 -2
- package/lib/README.md +22 -8
- package/lib/config.ts +50 -3
- package/lib/errors.ts +12 -0
- package/lib/format.ts +50 -0
- package/lib/git/read.ts +8 -0
- package/lib/goal-state.ts +23 -8
- package/lib/http/headers.ts +54 -0
- package/lib/http/index.ts +13 -0
- package/lib/http/response.ts +46 -0
- package/lib/http/router.ts +52 -0
- package/lib/http/server.ts +91 -0
- package/lib/lines.ts +77 -0
- package/lib/pdf.ts +82 -0
- package/lib/plan-entry.ts +71 -0
- package/lib/plan-state.ts +34 -0
- package/lib/report.ts +99 -0
- package/lib/requirements.ts +25 -19
- package/lib/spawn.ts +170 -0
- package/lib/todo-state.ts +110 -23
- package/lib/tool-names.ts +7 -0
- package/lib/tool-result.ts +33 -0
- package/lib/tui/README.md +68 -3
- package/lib/tui/chrome.ts +28 -4
- package/lib/tui/columns.ts +6 -1
- package/lib/tui/confirm.ts +65 -0
- package/lib/tui/dock-screen.ts +410 -0
- package/lib/tui/glyphs.ts +2 -1
- package/lib/tui/indent.ts +1 -1
- package/lib/tui/index.ts +2 -0
- package/lib/tui/rows.ts +21 -0
- package/lib/tui/status-controller.ts +35 -1
- package/lib/tui/tool.ts +59 -1
- package/lib/web/components.ts +20 -1
- package/lib/web/markup.ts +32 -0
- package/lib/web/runtime.ts +50 -0
- package/package.json +22 -5
- package/skills/init/SKILL.md +165 -0
- package/skills/init/references/agents-md-quality.md +103 -0
- package/skills/init/references/interview-questions.md +81 -0
- package/skills/refactor/SKILL.md +141 -0
- package/extensions/watch/process.ts +0 -63
- 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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
35
|
-
whole-tree restore. `Space` toggles a file or a directory,
|
|
36
|
-
all/none, and the confirm that follows previews only the
|
|
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
|
|
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
|
|
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
|
|
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`
|
|
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
|
|
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
|
|
213
|
-
| `
|
|
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`
|
|
224
|
-
| `format.ts` |
|
|
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
|
|
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
|
|
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;
|