@astralyn/pi 0.84.1 → 0.84.3

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 (163) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/dist/cli/args.js +1 -1
  3. package/dist/cli/args.js.map +1 -1
  4. package/dist/cli/config-selector.d.ts.map +1 -1
  5. package/dist/cli/config-selector.js +2 -3
  6. package/dist/cli/config-selector.js.map +1 -1
  7. package/dist/cli/startup-ui.d.ts.map +1 -1
  8. package/dist/cli/startup-ui.js +3 -5
  9. package/dist/cli/startup-ui.js.map +1 -1
  10. package/dist/config.d.ts +5 -0
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +8 -1
  13. package/dist/config.js.map +1 -1
  14. package/dist/core/agent-session.d.ts +1 -25
  15. package/dist/core/agent-session.d.ts.map +1 -1
  16. package/dist/core/agent-session.js +29 -408
  17. package/dist/core/agent-session.js.map +1 -1
  18. package/dist/core/compaction-controller.d.ts +89 -0
  19. package/dist/core/compaction-controller.d.ts.map +1 -0
  20. package/dist/core/compaction-controller.js +428 -0
  21. package/dist/core/compaction-controller.js.map +1 -0
  22. package/dist/core/extensions/index.d.ts +1 -1
  23. package/dist/core/extensions/index.d.ts.map +1 -1
  24. package/dist/core/extensions/index.js.map +1 -1
  25. package/dist/core/extensions/loader.js +3 -3
  26. package/dist/core/extensions/loader.js.map +1 -1
  27. package/dist/core/extensions/types.d.ts +1 -15
  28. package/dist/core/extensions/types.d.ts.map +1 -1
  29. package/dist/core/extensions/types.js.map +1 -1
  30. package/dist/core/provider-attribution.js +2 -2
  31. package/dist/core/provider-attribution.js.map +1 -1
  32. package/dist/core/settings-manager.js +1 -1
  33. package/dist/core/settings-manager.js.map +1 -1
  34. package/dist/core/telemetry.d.ts +1 -1
  35. package/dist/core/telemetry.d.ts.map +1 -1
  36. package/dist/core/telemetry.js +1 -1
  37. package/dist/core/telemetry.js.map +1 -1
  38. package/dist/extensions/deepwiki/index.d.ts.map +1 -1
  39. package/dist/extensions/deepwiki/index.js +23 -1
  40. package/dist/extensions/deepwiki/index.js.map +1 -1
  41. package/dist/extensions/deepwiki/render.d.ts +1 -1
  42. package/dist/extensions/deepwiki/render.d.ts.map +1 -1
  43. package/dist/extensions/deepwiki/render.js +4 -2
  44. package/dist/extensions/deepwiki/render.js.map +1 -1
  45. package/dist/extensions/index.d.ts.map +1 -1
  46. package/dist/extensions/index.js +0 -4
  47. package/dist/extensions/index.js.map +1 -1
  48. package/dist/extensions/question/index.d.ts.map +1 -1
  49. package/dist/extensions/question/index.js +0 -1
  50. package/dist/extensions/question/index.js.map +1 -1
  51. package/dist/extensions/subagent/index.d.ts.map +1 -1
  52. package/dist/extensions/subagent/index.js +20 -2
  53. package/dist/extensions/subagent/index.js.map +1 -1
  54. package/dist/extensions/todo/schema.d.ts +1 -1
  55. package/dist/extensions/todo/state.js +5 -5
  56. package/dist/extensions/todo/state.js.map +1 -1
  57. package/dist/modes/interactive/components/extension-selector.d.ts +0 -2
  58. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  59. package/dist/modes/interactive/components/extension-selector.js +1 -1
  60. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  61. package/dist/modes/interactive/components/settings-selector.js +1 -1
  62. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  63. package/dist/modes/interactive/components/tool-chrome.d.ts +28 -0
  64. package/dist/modes/interactive/components/tool-chrome.d.ts.map +1 -0
  65. package/dist/modes/interactive/components/tool-chrome.js +81 -0
  66. package/dist/modes/interactive/components/tool-chrome.js.map +1 -0
  67. package/dist/modes/interactive/components/tool-execution.d.ts +0 -3
  68. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  69. package/dist/modes/interactive/components/tool-execution.js +11 -108
  70. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  71. package/dist/modes/interactive/interactive-mode.d.ts +1 -1
  72. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  73. package/dist/modes/interactive/interactive-mode.js +35 -52
  74. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  75. package/docs/bundled/README.md +0 -2
  76. package/docs/bundled/extensions/subagent.md +1 -1
  77. package/docs/bundled/tool-presentation.md +2 -2
  78. package/docs/docs.json +0 -8
  79. package/docs/extensions.md +6 -23
  80. package/docs/settings.md +1 -1
  81. package/docs/usage.md +1 -2
  82. package/examples/extensions/README.md +2 -0
  83. package/examples/extensions/plan-mode/README.md +66 -0
  84. package/examples/extensions/plan-mode/index.ts +390 -0
  85. package/examples/extensions/plan-mode/utils.ts +168 -0
  86. package/examples/extensions/subagent/README.md +175 -0
  87. package/examples/extensions/subagent/agents/planner.md +37 -0
  88. package/examples/extensions/subagent/agents/reviewer.md +35 -0
  89. package/examples/extensions/subagent/agents/scout.md +50 -0
  90. package/examples/extensions/subagent/agents/worker.md +24 -0
  91. package/examples/extensions/subagent/agents.ts +126 -0
  92. package/examples/extensions/subagent/index.ts +1009 -0
  93. package/examples/extensions/subagent/prompts/implement-and-review.md +10 -0
  94. package/examples/extensions/subagent/prompts/implement.md +10 -0
  95. package/examples/extensions/subagent/prompts/scout-and-plan.md +9 -0
  96. package/npm-shrinkwrap.json +2 -2
  97. package/package.json +1 -1
  98. package/dist/extensions/biu/index.d.ts +0 -19
  99. package/dist/extensions/biu/index.d.ts.map +0 -1
  100. package/dist/extensions/biu/index.js +0 -350
  101. package/dist/extensions/biu/index.js.map +0 -1
  102. package/dist/extensions/biu/prompts.d.ts +0 -26
  103. package/dist/extensions/biu/prompts.d.ts.map +0 -1
  104. package/dist/extensions/biu/prompts.js +0 -260
  105. package/dist/extensions/biu/prompts.js.map +0 -1
  106. package/dist/extensions/biu/workspace.d.ts +0 -122
  107. package/dist/extensions/biu/workspace.d.ts.map +0 -1
  108. package/dist/extensions/biu/workspace.js +0 -404
  109. package/dist/extensions/biu/workspace.js.map +0 -1
  110. package/dist/extensions/rewind/anchor.d.ts +0 -29
  111. package/dist/extensions/rewind/anchor.d.ts.map +0 -1
  112. package/dist/extensions/rewind/anchor.js +0 -35
  113. package/dist/extensions/rewind/anchor.js.map +0 -1
  114. package/dist/extensions/rewind/config.d.ts +0 -16
  115. package/dist/extensions/rewind/config.d.ts.map +0 -1
  116. package/dist/extensions/rewind/config.js +0 -69
  117. package/dist/extensions/rewind/config.js.map +0 -1
  118. package/dist/extensions/rewind/engine.d.ts +0 -93
  119. package/dist/extensions/rewind/engine.d.ts.map +0 -1
  120. package/dist/extensions/rewind/engine.js +0 -778
  121. package/dist/extensions/rewind/engine.js.map +0 -1
  122. package/dist/extensions/rewind/gc.d.ts +0 -35
  123. package/dist/extensions/rewind/gc.d.ts.map +0 -1
  124. package/dist/extensions/rewind/gc.js +0 -169
  125. package/dist/extensions/rewind/gc.js.map +0 -1
  126. package/dist/extensions/rewind/index.d.ts +0 -3
  127. package/dist/extensions/rewind/index.d.ts.map +0 -1
  128. package/dist/extensions/rewind/index.js +0 -287
  129. package/dist/extensions/rewind/index.js.map +0 -1
  130. package/dist/extensions/rewind/menu.d.ts +0 -13
  131. package/dist/extensions/rewind/menu.d.ts.map +0 -1
  132. package/dist/extensions/rewind/menu.js +0 -123
  133. package/dist/extensions/rewind/menu.js.map +0 -1
  134. package/dist/extensions/rewind/paths.d.ts +0 -4
  135. package/dist/extensions/rewind/paths.d.ts.map +0 -1
  136. package/dist/extensions/rewind/paths.js +0 -15
  137. package/dist/extensions/rewind/paths.js.map +0 -1
  138. package/dist/extensions/rewind/restore.d.ts +0 -54
  139. package/dist/extensions/rewind/restore.d.ts.map +0 -1
  140. package/dist/extensions/rewind/restore.js +0 -94
  141. package/dist/extensions/rewind/restore.js.map +0 -1
  142. package/dist/extensions/rewind/snapshot.d.ts +0 -45
  143. package/dist/extensions/rewind/snapshot.d.ts.map +0 -1
  144. package/dist/extensions/rewind/snapshot.js +0 -21
  145. package/dist/extensions/rewind/snapshot.js.map +0 -1
  146. package/dist/extensions/rewind/storage.d.ts +0 -12
  147. package/dist/extensions/rewind/storage.d.ts.map +0 -1
  148. package/dist/extensions/rewind/storage.js +0 -33
  149. package/dist/extensions/rewind/storage.js.map +0 -1
  150. package/dist/extensions/rewind/text.d.ts +0 -7
  151. package/dist/extensions/rewind/text.d.ts.map +0 -1
  152. package/dist/extensions/rewind/text.js +0 -19
  153. package/dist/extensions/rewind/text.js.map +0 -1
  154. package/dist/extensions/rewind/tool-path.d.ts +0 -3
  155. package/dist/extensions/rewind/tool-path.d.ts.map +0 -1
  156. package/dist/extensions/rewind/tool-path.js +0 -9
  157. package/dist/extensions/rewind/tool-path.js.map +0 -1
  158. package/dist/utils/coalescing-terminal.d.ts +0 -132
  159. package/dist/utils/coalescing-terminal.d.ts.map +0 -1
  160. package/dist/utils/coalescing-terminal.js +0 -292
  161. package/dist/utils/coalescing-terminal.js.map +0 -1
  162. package/docs/bundled/extensions/biu.md +0 -63
  163. package/docs/bundled/extensions/rewind.md +0 -86
@@ -1,292 +0,0 @@
1
- import { ProcessTerminal } from "@earendil-works/pi-tui";
2
- const SYNC_START = "\x1b[?2026h";
3
- const SYNC_END = "\x1b[?2026l";
4
- /** Clear screen, home, clear scrollback — pi-tui's fullRender(clear) preamble. */
5
- const CLEAR_WITH_SCROLLBACK = "\x1b[2J\x1b[H\x1b[3J";
6
- /** Kitty graphics APC prefix; frames touching images interleave cursor moves. */
7
- const KITTY_APC_PREFIX = "\x1b_G";
8
- /**
9
- * ProcessTerminal that (1) coalesces every write issued within one synchronous
10
- * task into a single stdout write and (2) keeps content-driven full redraws
11
- * from destroying the terminal's scrollback.
12
- *
13
- * Coalescing: pi-tui emits each render pass as several separate stdout
14
- * writes: the frame wrapped in synchronized output (CSI ?2026h/l), then a
15
- * relative cursor move that parks the hardware cursor at the focused
16
- * component's caret for IME composition, then a cursor visibility toggle.
17
- * Terminals that anchor the IME candidate window to the hardware cursor
18
- * (Windows Terminal and other ConPTY hosts, WezTerm) can process the frame
19
- * write before the reposition write arrives, briefly leaving the cursor at
20
- * the end of the last repainted line. With an IME active this makes the
21
- * candidate window jump between the caret and the right edge of the input
22
- * line on every keystroke. Merging the writes makes each frame atomic, so the
23
- * only cursor position a terminal can observe is the final one.
24
- *
25
- * Scrollback preservation: pi-tui's fullRender(clear) opens with
26
- * `2J H 3J`, wiping the terminal's scrollback, and then replays the entire
27
- * transcript. During streaming, content-driven triggers (a line changing
28
- * above the viewport during markdown reflow) fire this routinely, which — in
29
- * terminals that clamp the scroll offset when scrollback vanishes, notably
30
- * Windows Terminal — yanks a scrolled-up reader to the top of the buffer on
31
- * every reflow (upstream #6502, #5576, #6050; rejected upstream fix #4204).
32
- *
33
- * Preservation is therefore windowed, not permanent: the host marks agent
34
- * runs via setAgentRunActive, which turns preservation on at run start and
35
- * keeps it on until the user's next input after the run settles, since they
36
- * may still be scrolled up reading. The input half of the window is observed
37
- * directly on this terminal's own input path, so it needs no TUI listener
38
- * and survives renderer switches (which reuse this terminal) untouched. An
39
- * idle-session full redraw — Ctrl+O toggles, mode switches — passes through
40
- * with upstream's wipe-and-replay, whose result is byte-perfect: scrollback
41
- * becomes the clean new transcript. Mid-run user toggles can arm
42
- * passNextFullRedraw for the same clean replay on their own frame.
43
- *
44
- * While preservation is active, a matching frame is rewritten to repaint
45
- * without the wipe. With setViewportTopProvider wired, the rewrite knows
46
- * which transcript line the visible screen starts at, so a frame that grew
47
- * the transcript repaints the on-screen rows with the new content at those
48
- * same positions and then scrolls the extra lines in at the bottom row.
49
- * Each bottom-row newline pushes the top row — already repainted with the
50
- * correct new content — into scrollback, exactly like ordinary streaming
51
- * output, so scrollback ends up byte-equivalent to the upstream full replay
52
- * without ever clearing it. Without the provider, or when the transcript
53
- * shrank (lines cannot be pulled back out of scrollback), the rewrite falls
54
- * back to overwriting the visible screen in place with the bottom
55
- * viewport-height rows, at the cost of a stale seam above the screen. ED 2
56
- * is avoided entirely in rewritten frames because conhost/Windows Terminal
57
- * implement it by scrolling the screen contents into scrollback rather than
58
- * erasing in place.
59
- *
60
- * Width- and height-change redraws keep the upstream wipe because re-wrapped
61
- * content genuinely invalidates old scrollback, and frames containing kitty
62
- * graphics pass through untouched. A frame that does not match the expected
63
- * shape is passed through unchanged, so if upstream reshapes its render
64
- * output the wrapper degrades back to stock behavior.
65
- */
66
- export class CoalescingTerminal extends ProcessTerminal {
67
- /**
68
- * Enable or disable the scrollback-preserving rewrite. Preservation is
69
- * only worth its imperfections (stale renderings above the screen, seams
70
- * on shrink) while the transcript is mutating on its own and the reader
71
- * may be scrolled up — i.e. during an agent run and until the user's next
72
- * input after it settles. Outside that window every full redraw passes
73
- * through with upstream's wipe-and-replay, whose result is byte-perfect:
74
- * scrollback becomes the clean new transcript. Default: disabled.
75
- * setAgentRunActive drives this for the normal agent-run window; this
76
- * primitive remains for direct control in tests.
77
- */
78
- setScrollbackPreservation(enabled) {
79
- this.preserve = enabled;
80
- }
81
- /**
82
- * Open or close the agent-run half of the preservation window. Opening
83
- * (agent_start) turns preservation on. Closing (agent_settled) leaves
84
- * preservation on: trailing reflow frames may land after the run while
85
- * the reader is still scrolled up. The window then closes on the user's
86
- * next input, observed in start()'s input wrapper — once the user acts
87
- * they are back at the bottom, and upstream's clean wipe-and-replay wins
88
- * again until the next run.
89
- */
90
- setAgentRunActive(active) {
91
- this.agentRunActive = active;
92
- if (active)
93
- this.preserve = true;
94
- }
95
- /**
96
- * Arm a one-shot exemption for an explicit user action (Ctrl+O tool
97
- * expansion, thinking visibility) so its own full redraw keeps the
98
- * upstream wipe-and-replay even while preservation is active mid-run.
99
- * The user just pressed a key, so they are interacting at the bottom;
100
- * a clean replay beats preservation's stale-seam trade-offs there. The
101
- * flag is cleared by the next write so it cannot leak onto a later
102
- * streaming redraw.
103
- */
104
- passNextFullRedraw() {
105
- this.passArmed = true;
106
- }
107
- /**
108
- * Wire the renderer's notion of which transcript line the visible screen
109
- * currently starts at (pi-tui's `previousViewportTop`). Full-redraw frames
110
- * that grew the transcript then scroll the new above-screen lines into
111
- * scrollback instead of dropping them. The provider is read while the
112
- * frame is being written, i.e. before the renderer updates its own
113
- * bookkeeping for that frame; returning undefined falls back to the
114
- * bottom-anchored in-place repaint.
115
- */
116
- setViewportTopProvider(provider) {
117
- this.viewportTopProvider = provider;
118
- }
119
- constructor() {
120
- super();
121
- this.pending = "";
122
- this.flushQueued = false;
123
- this.preserve = false;
124
- this.passArmed = false;
125
- this.agentRunActive = false;
126
- this.flushOnExit = () => this.flush();
127
- // A synchronous process.exit() before the queued microtask runs would
128
- // otherwise drop the final frame.
129
- process.on("exit", this.flushOnExit);
130
- }
131
- start(onInput, onResize) {
132
- // stop() removes the exit flush, and renderer switches stop and start
133
- // the shared terminal each time; restore it so a synchronous
134
- // process.exit() during this lifecycle still flushes pending frames.
135
- // remove-then-add keeps exactly one registration even when start()
136
- // runs right after construction (constructor already registered) or
137
- // across repeated start/stop cycles.
138
- process.removeListener("exit", this.flushOnExit);
139
- process.on("exit", this.flushOnExit);
140
- // User input outside an active run closes the preservation window
141
- // (see setAgentRunActive). Observing input here instead of on a TUI
142
- // input listener keeps the window logic in one place and renderer
143
- // switches — which stop and restart this shared terminal — rewrap
144
- // the new handler automatically.
145
- super.start((data) => {
146
- if (!this.agentRunActive)
147
- this.preserve = false;
148
- onInput(data);
149
- }, onResize);
150
- }
151
- write(data) {
152
- if (data.length === 0)
153
- return;
154
- this.pending += this.preserveScrollback(data);
155
- if (this.flushQueued)
156
- return;
157
- this.flushQueued = true;
158
- queueMicrotask(() => this.flush());
159
- }
160
- // Route every escape-sequence helper through the coalescing buffer so a
161
- // caller mixing helpers with write() keeps its output ordered.
162
- moveBy(lines) {
163
- if (lines > 0) {
164
- this.write(`\x1b[${lines}B`);
165
- }
166
- else if (lines < 0) {
167
- this.write(`\x1b[${-lines}A`);
168
- }
169
- }
170
- hideCursor() {
171
- this.write("\x1b[?25l");
172
- }
173
- showCursor() {
174
- this.write("\x1b[?25h");
175
- }
176
- clearLine() {
177
- this.write("\x1b[K");
178
- }
179
- clearFromCursor() {
180
- this.write("\x1b[J");
181
- }
182
- clearScreen() {
183
- this.write("\x1b[2J\x1b[H");
184
- }
185
- stop() {
186
- // ProcessTerminal.stop() writes its teardown sequences directly to
187
- // stdout, so pending frame data must go out first.
188
- this.flush();
189
- process.removeListener("exit", this.flushOnExit);
190
- super.stop();
191
- }
192
- /**
193
- * Rewrite a full-redraw frame (`?2026h 2J H 3J <lines> ?2026l`) so it
194
- * repaints the screen without wiping scrollback. Anything that does not
195
- * match this exact shape — including width-change redraws and frames
196
- * touching kitty images — passes through unchanged.
197
- */
198
- preserveScrollback(data) {
199
- const columns = this.columns;
200
- const rows = this.rows;
201
- const widthChanged = this.lastWriteColumns !== undefined && this.lastWriteColumns !== columns;
202
- const heightChanged = this.lastWriteRows !== undefined && this.lastWriteRows !== rows;
203
- this.lastWriteColumns = columns;
204
- this.lastWriteRows = rows;
205
- // One-shot: the very next write is the armed toggle's own frame (or a
206
- // differential frame when the toggle stayed inside the viewport, in
207
- // which case no exemption is needed and the flag must not linger).
208
- const passAllowed = this.passArmed;
209
- this.passArmed = false;
210
- const prefix = SYNC_START + CLEAR_WITH_SCROLLBACK;
211
- if (!data.startsWith(prefix) || !data.endsWith(SYNC_END)) {
212
- return data;
213
- }
214
- // Outside the preservation window (idle sessions, pre-session UI) and
215
- // for armed user toggles, upstream's wipe-and-replay is byte-perfect;
216
- // let it through.
217
- if (!this.preserve || passAllowed) {
218
- return data;
219
- }
220
- // Re-wrapped/re-flowed content invalidates what scrollback holds and
221
- // where the screen sits in it; keep the wipe.
222
- if (widthChanged || heightChanged) {
223
- return data;
224
- }
225
- if (data.includes(KITTY_APC_PREFIX)) {
226
- return data;
227
- }
228
- const body = data.slice(prefix.length, data.length - SYNC_END.length);
229
- // A second clear inside one write means the frame shape changed
230
- // upstream; bail out rather than guess.
231
- if (body.includes(CLEAR_WITH_SCROLLBACK)) {
232
- return data;
233
- }
234
- // ED 2 (`2J`) is deliberately avoided in both rewrites below: conhost/
235
- // Windows Terminal implement it by scrolling the current screen into
236
- // scrollback (cls compatibility), so without the `3J` wipe each redraw
237
- // would stack another screenful of duplicates into history.
238
- const lines = body.split("\r\n");
239
- const newViewportTop = Math.max(0, lines.length - rows);
240
- const viewportTop = this.readViewportTop();
241
- if (viewportTop !== undefined && viewportTop <= newViewportTop) {
242
- // The transcript grew (or held steady): repaint the rows currently
243
- // on screen with the new content at those same transcript
244
- // positions, then scroll the remaining lines in at the bottom row.
245
- // Each bottom-row newline pushes the already-repainted top row into
246
- // scrollback, so scrollback receives exactly the new lines the
247
- // upstream replay would have put there — no wipe, no loss.
248
- const onScreen = lines.slice(viewportTop, viewportTop + rows);
249
- const scrolled = lines.slice(viewportTop + rows);
250
- let repaint = onScreen.map((line) => `\x1b[2K${line}`).join("\r\n");
251
- for (const line of scrolled) {
252
- repaint += `\r\n\x1b[2K${line}`;
253
- }
254
- return `${SYNC_START}\x1b[H${repaint}\x1b[J${SYNC_END}`;
255
- }
256
- // Fallback (no provider, or the transcript shrank — lines cannot be
257
- // pulled back out of scrollback): repaint only the bottom
258
- // viewport-height rows, overwriting the screen in place: home,
259
- // erase-and-rewrite each row, erase whatever remains below. Writing the
260
- // full transcript after a screen clear would push a duplicate copy of
261
- // everything above the viewport into scrollback; the truncated
262
- // overwrite produces the exact same final screen and cursor position as
263
- // the full replay (rows beyond the screen would have scrolled out
264
- // anyway).
265
- const kept = lines.length > rows ? lines.slice(lines.length - rows) : lines;
266
- const repaint = kept.map((line) => `\x1b[2K${line}`).join("\r\n");
267
- return `${SYNC_START}\x1b[H${repaint}\x1b[J${SYNC_END}`;
268
- }
269
- // The provider reaches into the renderer's bookkeeping; treat anything
270
- // unexpected as "unknown" and fall back rather than corrupt the screen.
271
- readViewportTop() {
272
- if (!this.viewportTopProvider)
273
- return undefined;
274
- try {
275
- const value = this.viewportTopProvider();
276
- return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : undefined;
277
- }
278
- catch {
279
- return undefined;
280
- }
281
- }
282
- flush() {
283
- this.flushQueued = false;
284
- if (this.pending.length === 0) {
285
- return;
286
- }
287
- const data = this.pending;
288
- this.pending = "";
289
- super.write(data);
290
- }
291
- }
292
- //# sourceMappingURL=coalescing-terminal.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"coalescing-terminal.js","sourceRoot":"","sources":["../../src/utils/coalescing-terminal.ts"],"names":[],"mappings":"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","sourcesContent":["import { ProcessTerminal } from \"@earendil-works/pi-tui\";\n\nconst SYNC_START = \"\\x1b[?2026h\";\nconst SYNC_END = \"\\x1b[?2026l\";\n/** Clear screen, home, clear scrollback — pi-tui's fullRender(clear) preamble. */\nconst CLEAR_WITH_SCROLLBACK = \"\\x1b[2J\\x1b[H\\x1b[3J\";\n/** Kitty graphics APC prefix; frames touching images interleave cursor moves. */\nconst KITTY_APC_PREFIX = \"\\x1b_G\";\n\n/**\n * ProcessTerminal that (1) coalesces every write issued within one synchronous\n * task into a single stdout write and (2) keeps content-driven full redraws\n * from destroying the terminal's scrollback.\n *\n * Coalescing: pi-tui emits each render pass as several separate stdout\n * writes: the frame wrapped in synchronized output (CSI ?2026h/l), then a\n * relative cursor move that parks the hardware cursor at the focused\n * component's caret for IME composition, then a cursor visibility toggle.\n * Terminals that anchor the IME candidate window to the hardware cursor\n * (Windows Terminal and other ConPTY hosts, WezTerm) can process the frame\n * write before the reposition write arrives, briefly leaving the cursor at\n * the end of the last repainted line. With an IME active this makes the\n * candidate window jump between the caret and the right edge of the input\n * line on every keystroke. Merging the writes makes each frame atomic, so the\n * only cursor position a terminal can observe is the final one.\n *\n * Scrollback preservation: pi-tui's fullRender(clear) opens with\n * `2J H 3J`, wiping the terminal's scrollback, and then replays the entire\n * transcript. During streaming, content-driven triggers (a line changing\n * above the viewport during markdown reflow) fire this routinely, which — in\n * terminals that clamp the scroll offset when scrollback vanishes, notably\n * Windows Terminal — yanks a scrolled-up reader to the top of the buffer on\n * every reflow (upstream #6502, #5576, #6050; rejected upstream fix #4204).\n *\n * Preservation is therefore windowed, not permanent: the host marks agent\n * runs via setAgentRunActive, which turns preservation on at run start and\n * keeps it on until the user's next input after the run settles, since they\n * may still be scrolled up reading. The input half of the window is observed\n * directly on this terminal's own input path, so it needs no TUI listener\n * and survives renderer switches (which reuse this terminal) untouched. An\n * idle-session full redraw — Ctrl+O toggles, mode switches — passes through\n * with upstream's wipe-and-replay, whose result is byte-perfect: scrollback\n * becomes the clean new transcript. Mid-run user toggles can arm\n * passNextFullRedraw for the same clean replay on their own frame.\n *\n * While preservation is active, a matching frame is rewritten to repaint\n * without the wipe. With setViewportTopProvider wired, the rewrite knows\n * which transcript line the visible screen starts at, so a frame that grew\n * the transcript repaints the on-screen rows with the new content at those\n * same positions and then scrolls the extra lines in at the bottom row.\n * Each bottom-row newline pushes the top row — already repainted with the\n * correct new content — into scrollback, exactly like ordinary streaming\n * output, so scrollback ends up byte-equivalent to the upstream full replay\n * without ever clearing it. Without the provider, or when the transcript\n * shrank (lines cannot be pulled back out of scrollback), the rewrite falls\n * back to overwriting the visible screen in place with the bottom\n * viewport-height rows, at the cost of a stale seam above the screen. ED 2\n * is avoided entirely in rewritten frames because conhost/Windows Terminal\n * implement it by scrolling the screen contents into scrollback rather than\n * erasing in place.\n *\n * Width- and height-change redraws keep the upstream wipe because re-wrapped\n * content genuinely invalidates old scrollback, and frames containing kitty\n * graphics pass through untouched. A frame that does not match the expected\n * shape is passed through unchanged, so if upstream reshapes its render\n * output the wrapper degrades back to stock behavior.\n */\nexport class CoalescingTerminal extends ProcessTerminal {\n\tprivate pending = \"\";\n\tprivate flushQueued = false;\n\tprivate lastWriteColumns: number | undefined;\n\tprivate lastWriteRows: number | undefined;\n\tprivate viewportTopProvider: (() => number | undefined) | undefined;\n\tprivate preserve = false;\n\tprivate passArmed = false;\n\tprivate agentRunActive = false;\n\tprivate flushOnExit = () => this.flush();\n\n\t/**\n\t * Enable or disable the scrollback-preserving rewrite. Preservation is\n\t * only worth its imperfections (stale renderings above the screen, seams\n\t * on shrink) while the transcript is mutating on its own and the reader\n\t * may be scrolled up — i.e. during an agent run and until the user's next\n\t * input after it settles. Outside that window every full redraw passes\n\t * through with upstream's wipe-and-replay, whose result is byte-perfect:\n\t * scrollback becomes the clean new transcript. Default: disabled.\n\t * setAgentRunActive drives this for the normal agent-run window; this\n\t * primitive remains for direct control in tests.\n\t */\n\tsetScrollbackPreservation(enabled: boolean): void {\n\t\tthis.preserve = enabled;\n\t}\n\n\t/**\n\t * Open or close the agent-run half of the preservation window. Opening\n\t * (agent_start) turns preservation on. Closing (agent_settled) leaves\n\t * preservation on: trailing reflow frames may land after the run while\n\t * the reader is still scrolled up. The window then closes on the user's\n\t * next input, observed in start()'s input wrapper — once the user acts\n\t * they are back at the bottom, and upstream's clean wipe-and-replay wins\n\t * again until the next run.\n\t */\n\tsetAgentRunActive(active: boolean): void {\n\t\tthis.agentRunActive = active;\n\t\tif (active) this.preserve = true;\n\t}\n\n\t/**\n\t * Arm a one-shot exemption for an explicit user action (Ctrl+O tool\n\t * expansion, thinking visibility) so its own full redraw keeps the\n\t * upstream wipe-and-replay even while preservation is active mid-run.\n\t * The user just pressed a key, so they are interacting at the bottom;\n\t * a clean replay beats preservation's stale-seam trade-offs there. The\n\t * flag is cleared by the next write so it cannot leak onto a later\n\t * streaming redraw.\n\t */\n\tpassNextFullRedraw(): void {\n\t\tthis.passArmed = true;\n\t}\n\n\t/**\n\t * Wire the renderer's notion of which transcript line the visible screen\n\t * currently starts at (pi-tui's `previousViewportTop`). Full-redraw frames\n\t * that grew the transcript then scroll the new above-screen lines into\n\t * scrollback instead of dropping them. The provider is read while the\n\t * frame is being written, i.e. before the renderer updates its own\n\t * bookkeeping for that frame; returning undefined falls back to the\n\t * bottom-anchored in-place repaint.\n\t */\n\tsetViewportTopProvider(provider: () => number | undefined): void {\n\t\tthis.viewportTopProvider = provider;\n\t}\n\n\tconstructor() {\n\t\tsuper();\n\t\t// A synchronous process.exit() before the queued microtask runs would\n\t\t// otherwise drop the final frame.\n\t\tprocess.on(\"exit\", this.flushOnExit);\n\t}\n\n\toverride start(onInput: (data: string) => void, onResize: () => void): void {\n\t\t// stop() removes the exit flush, and renderer switches stop and start\n\t\t// the shared terminal each time; restore it so a synchronous\n\t\t// process.exit() during this lifecycle still flushes pending frames.\n\t\t// remove-then-add keeps exactly one registration even when start()\n\t\t// runs right after construction (constructor already registered) or\n\t\t// across repeated start/stop cycles.\n\t\tprocess.removeListener(\"exit\", this.flushOnExit);\n\t\tprocess.on(\"exit\", this.flushOnExit);\n\t\t// User input outside an active run closes the preservation window\n\t\t// (see setAgentRunActive). Observing input here instead of on a TUI\n\t\t// input listener keeps the window logic in one place and renderer\n\t\t// switches — which stop and restart this shared terminal — rewrap\n\t\t// the new handler automatically.\n\t\tsuper.start((data) => {\n\t\t\tif (!this.agentRunActive) this.preserve = false;\n\t\t\tonInput(data);\n\t\t}, onResize);\n\t}\n\n\toverride write(data: string): void {\n\t\tif (data.length === 0) return;\n\t\tthis.pending += this.preserveScrollback(data);\n\t\tif (this.flushQueued) return;\n\t\tthis.flushQueued = true;\n\t\tqueueMicrotask(() => this.flush());\n\t}\n\n\t// Route every escape-sequence helper through the coalescing buffer so a\n\t// caller mixing helpers with write() keeps its output ordered.\n\toverride moveBy(lines: number): void {\n\t\tif (lines > 0) {\n\t\t\tthis.write(`\\x1b[${lines}B`);\n\t\t} else if (lines < 0) {\n\t\t\tthis.write(`\\x1b[${-lines}A`);\n\t\t}\n\t}\n\n\toverride hideCursor(): void {\n\t\tthis.write(\"\\x1b[?25l\");\n\t}\n\n\toverride showCursor(): void {\n\t\tthis.write(\"\\x1b[?25h\");\n\t}\n\n\toverride clearLine(): void {\n\t\tthis.write(\"\\x1b[K\");\n\t}\n\n\toverride clearFromCursor(): void {\n\t\tthis.write(\"\\x1b[J\");\n\t}\n\n\toverride clearScreen(): void {\n\t\tthis.write(\"\\x1b[2J\\x1b[H\");\n\t}\n\n\toverride stop(): void {\n\t\t// ProcessTerminal.stop() writes its teardown sequences directly to\n\t\t// stdout, so pending frame data must go out first.\n\t\tthis.flush();\n\t\tprocess.removeListener(\"exit\", this.flushOnExit);\n\t\tsuper.stop();\n\t}\n\n\t/**\n\t * Rewrite a full-redraw frame (`?2026h 2J H 3J <lines> ?2026l`) so it\n\t * repaints the screen without wiping scrollback. Anything that does not\n\t * match this exact shape — including width-change redraws and frames\n\t * touching kitty images — passes through unchanged.\n\t */\n\tprivate preserveScrollback(data: string): string {\n\t\tconst columns = this.columns;\n\t\tconst rows = this.rows;\n\t\tconst widthChanged = this.lastWriteColumns !== undefined && this.lastWriteColumns !== columns;\n\t\tconst heightChanged = this.lastWriteRows !== undefined && this.lastWriteRows !== rows;\n\t\tthis.lastWriteColumns = columns;\n\t\tthis.lastWriteRows = rows;\n\t\t// One-shot: the very next write is the armed toggle's own frame (or a\n\t\t// differential frame when the toggle stayed inside the viewport, in\n\t\t// which case no exemption is needed and the flag must not linger).\n\t\tconst passAllowed = this.passArmed;\n\t\tthis.passArmed = false;\n\n\t\tconst prefix = SYNC_START + CLEAR_WITH_SCROLLBACK;\n\t\tif (!data.startsWith(prefix) || !data.endsWith(SYNC_END)) {\n\t\t\treturn data;\n\t\t}\n\t\t// Outside the preservation window (idle sessions, pre-session UI) and\n\t\t// for armed user toggles, upstream's wipe-and-replay is byte-perfect;\n\t\t// let it through.\n\t\tif (!this.preserve || passAllowed) {\n\t\t\treturn data;\n\t\t}\n\t\t// Re-wrapped/re-flowed content invalidates what scrollback holds and\n\t\t// where the screen sits in it; keep the wipe.\n\t\tif (widthChanged || heightChanged) {\n\t\t\treturn data;\n\t\t}\n\t\tif (data.includes(KITTY_APC_PREFIX)) {\n\t\t\treturn data;\n\t\t}\n\t\tconst body = data.slice(prefix.length, data.length - SYNC_END.length);\n\t\t// A second clear inside one write means the frame shape changed\n\t\t// upstream; bail out rather than guess.\n\t\tif (body.includes(CLEAR_WITH_SCROLLBACK)) {\n\t\t\treturn data;\n\t\t}\n\t\t// ED 2 (`2J`) is deliberately avoided in both rewrites below: conhost/\n\t\t// Windows Terminal implement it by scrolling the current screen into\n\t\t// scrollback (cls compatibility), so without the `3J` wipe each redraw\n\t\t// would stack another screenful of duplicates into history.\n\t\tconst lines = body.split(\"\\r\\n\");\n\t\tconst newViewportTop = Math.max(0, lines.length - rows);\n\t\tconst viewportTop = this.readViewportTop();\n\t\tif (viewportTop !== undefined && viewportTop <= newViewportTop) {\n\t\t\t// The transcript grew (or held steady): repaint the rows currently\n\t\t\t// on screen with the new content at those same transcript\n\t\t\t// positions, then scroll the remaining lines in at the bottom row.\n\t\t\t// Each bottom-row newline pushes the already-repainted top row into\n\t\t\t// scrollback, so scrollback receives exactly the new lines the\n\t\t\t// upstream replay would have put there — no wipe, no loss.\n\t\t\tconst onScreen = lines.slice(viewportTop, viewportTop + rows);\n\t\t\tconst scrolled = lines.slice(viewportTop + rows);\n\t\t\tlet repaint = onScreen.map((line) => `\\x1b[2K${line}`).join(\"\\r\\n\");\n\t\t\tfor (const line of scrolled) {\n\t\t\t\trepaint += `\\r\\n\\x1b[2K${line}`;\n\t\t\t}\n\t\t\treturn `${SYNC_START}\\x1b[H${repaint}\\x1b[J${SYNC_END}`;\n\t\t}\n\t\t// Fallback (no provider, or the transcript shrank — lines cannot be\n\t\t// pulled back out of scrollback): repaint only the bottom\n\t\t// viewport-height rows, overwriting the screen in place: home,\n\t\t// erase-and-rewrite each row, erase whatever remains below. Writing the\n\t\t// full transcript after a screen clear would push a duplicate copy of\n\t\t// everything above the viewport into scrollback; the truncated\n\t\t// overwrite produces the exact same final screen and cursor position as\n\t\t// the full replay (rows beyond the screen would have scrolled out\n\t\t// anyway).\n\t\tconst kept = lines.length > rows ? lines.slice(lines.length - rows) : lines;\n\t\tconst repaint = kept.map((line) => `\\x1b[2K${line}`).join(\"\\r\\n\");\n\t\treturn `${SYNC_START}\\x1b[H${repaint}\\x1b[J${SYNC_END}`;\n\t}\n\n\t// The provider reaches into the renderer's bookkeeping; treat anything\n\t// unexpected as \"unknown\" and fall back rather than corrupt the screen.\n\tprivate readViewportTop(): number | undefined {\n\t\tif (!this.viewportTopProvider) return undefined;\n\t\ttry {\n\t\t\tconst value = this.viewportTopProvider();\n\t\t\treturn typeof value === \"number\" && Number.isInteger(value) && value >= 0 ? value : undefined;\n\t\t} catch {\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tprivate flush(): void {\n\t\tthis.flushQueued = false;\n\t\tif (this.pending.length === 0) {\n\t\t\treturn;\n\t\t}\n\t\tconst data = this.pending;\n\t\tthis.pending = \"\";\n\t\tsuper.write(data);\n\t}\n}\n"]}
@@ -1,63 +0,0 @@
1
- # Biu — project development workflow
2
-
3
- Biu is a simple, structured development workflow for the interactive TUI: `/biu` switches Biu Mode on, and inside the mode `/biu` opens its menu. One project cycle moves through soft stages — **plan** (clarify requirements into an approved `SPEC.md`), an **optional decompose** step (break a complex SPEC into task handoffs; simple SPECs are implemented directly), **execute** (implement, one task at a time when tasks exist), and **archive** (summarize and close the cycle).
4
-
5
- There is no state machine and no workflow tool. Workflow state lives in the frontmatter of the workspace Markdown files — the SPEC's `status: draft|ready`, each task's `status: ready|in_progress|completed` and `depends_on` — and the extension derives a read-only snapshot from them each turn. The model edits these files with the normal file tools through the `biu://` scheme.
6
-
7
- ## Command
8
-
9
- | Command | Behavior |
10
- |---|---|
11
- | `/biu` | When the mode is off, turn it on (no agent turn is triggered); when it is on, open the Biu menu |
12
-
13
- `/biu` accepts no arguments. Turning the mode on with no prior cycle leaves the workflow at the plan stage — describe what you want to build in your next message and the interview starts from there. With an existing cycle, the mode resumes at the stage derived from the workspace files.
14
-
15
- The menu keeps only the actions needed to operate the workflow:
16
-
17
- - **Continue · stage** — send a kickoff message (collapsed to one line; expandable with the standard expand key) that starts a turn continuing the current stage.
18
- - **Archive cycle** — deterministically move `SPEC.md`, `Summary.md`, and `tasks/` into `archived/YYYY-MM-DD-<shortname>/`. Requires an existing, model-drafted and user-confirmed `Summary.md`; archiving with unfinished tasks asks for confirmation first. The shortname is derived from the SPEC's `title` frontmatter (a dialog asks for one when it is missing). On failure, already-moved files are rolled back.
19
- - **Exit Biu Mode** — turn the mode off. Workflow files are kept.
20
-
21
- A subtitle summarizes the current stage; while the mode is on, the footer keeps the marker `Biu · execute 2/5` (warning-colored when workspace files have problems, such as malformed frontmatter).
22
-
23
- Biu Mode is TUI-only. RPC, JSON, and print sessions do not enable it, inject its resident prompt, or open its menu. Opening the same branch later in the TUI restores its recorded enabled flag.
24
-
25
- ## Storage and the `biu://` scheme
26
-
27
- Biu artifacts live under the normal Pi agent directory, grouped by working directory with the same path encoding used for sessions:
28
-
29
- ```text
30
- ~/.pi/agent/biu/
31
- └── --encoded-working-directory--/
32
- ├── SPEC.md # frontmatter: title, status, execution, baseline_commit
33
- ├── Summary.md # frontmatter: title, head_commit; temporary while archiving
34
- ├── tasks/
35
- │ └── TASK-<short-name>.md # frontmatter: title, status, depends_on
36
- └── archived/
37
- └── YYYY-MM-DD-shortname/
38
- ├── SPEC.md
39
- ├── Summary.md
40
- └── tasks/
41
- ```
42
-
43
- `PI_CODING_AGENT_DIR` is respected.
44
-
45
- The model never sees or uses these real paths. A `tool_call` hook rewrites `biu://` paths — `biu://SPEC.md`, `biu://tasks/TASK-api.md`, `biu://Summary.md` — to the workspace of the current working directory before `read`, `write`, `edit`, `grep`, `find`, and `ls` execute. The session records keep the original `biu://` arguments, so the transcript and TUI show the short stable paths while execution uses the resolved ones. Paths that escape the workspace (`..` segments, rooted paths) are blocked. The scheme resolves regardless of whether Biu Mode is on; the mode only controls prompting, the statusline, and the menu.
46
-
47
- ## Prompting
48
-
49
- While the mode is on, the full Biu block is injected into the system prompt each turn: the `biu://` conventions, a JSON snapshot of the workspace (derived stage, SPEC metadata, task statuses and focus, detected problems), and the playbook for the derived stage, including the relevant Markdown templates. At the execute stage the playbook follows the SPEC's `execution` field — `direct` implements straight against the SPEC, `tasks` decomposes into task files first, and a SPEC without the field falls back to a combined playbook that asks to record the choice. There is nothing to fetch on demand — the workspace files are the state, and the snapshot always reflects them.
50
-
51
- Stage transitions are file edits, with one guarded exception: flipping the SPEC's `status` to `ready` opens an approval dialog. Approving lets the write land and the cycle move on to execute; declining blocks the write and hands your feedback to the model as the tool result, so it revises the SPEC and asks again. During planning the model also records the agreed execution path in the frontmatter (`execution: direct` by default; `tasks` when the work splits into independently verifiable chunks). Writing task files opts into decomposition, task frontmatter tracks execution, and writing `Summary.md` (or completing every task) enters archive. Moving backward — for example reopening the SPEC as `draft` when execution reveals a gap, or switching `execution` when the size estimate was wrong — is just another frontmatter edit.
52
-
53
- ## Lifecycle
54
-
55
- The enabled flag is stored as a branch-aware custom session entry. In the TUI, `session_start` and `session_tree` replay the latest flag, so `/reload`, resume, fork, and tree navigation restore the mode. Non-TUI sessions leave that flag untouched but keep Biu inactive. The statusline refreshes whenever a `write` or `edit` lands inside the workspace.
56
-
57
- Biu artifacts are project-global rather than conversation-branch snapshots: navigating `/tree` changes the enabled flag with the branch, but workflow files do not roll back.
58
-
59
- ## Limits
60
-
61
- - The snapshot lists at most 100 task files; additional files are reported as a problem instead of being silently ignored.
62
- - Malformed frontmatter never blocks the workflow: the affected file degrades to an `unknown` status and the problem is surfaced to the model and in the statusline.
63
- - Concurrent writers in multiple Pi processes are not coordinated. Use one active Biu writer per project.
@@ -1,86 +0,0 @@
1
- # rewind — checkpoints & rewind
2
-
3
- Per-edit file backups plus a `/rewind` settings menu. Instead of snapshotting the
4
- whole working tree, rewind backs up **only the files Pi's `edit`/`write` tools are
5
- about to change** — one `copyFile` before each edit. Cost is proportional to how
6
- many files Pi changed, never to project size, so it never blocks the session
7
- lifecycle (the old shadow-git design froze the UI in large multi-project
8
- directories) and storage stays tiny.
9
-
10
- ## Behavior
11
-
12
- Per turn: `before_agent_start` opens a snapshot frame (re-recording every tracked
13
- file at its turn-start state, reusing the latest backup when unchanged);
14
- `tool_call(edit|write)` backs up each newly edited file *before* it lands;
15
- `agent_settled` persists the frame to the session JSONL as a `pi-rewind-snapshot`
16
- custom entry **only when files changed**. The frame anchors to the **first** user
17
- entry the run appended — the message whose start the frame recorded; steering and
18
- follow-up messages consumed inside the same run append later user entries and do
19
- not steal the anchor (a run that appends no user entry, e.g. custom-triggered,
20
- records no frame). Using `agent_settled` (not `agent_end`)
21
- keeps auto-retry, overflow compaction-retry, and queued follow-ups in one logical
22
- turn — `agent_end` can fire while Pi still continues. Requires **Pi ≥ 0.80.4**
23
- (when `agent_settled` was added); older hosts never fire that event, so frames
24
- would not finalize. Entries survive `/reload` and compaction; the index is
25
- rebuilt from them on `session_start`.
26
-
27
- - **Scope (deliberate trade-off):** rewind covers edits made through Pi's built-in
28
- `edit` and `write` tools. Files written by `bash` (redirects, codegen, `mv`) or
29
- edited by hand outside Pi are **not** tracked — same boundary as Claude Code's
30
- file-history. Rewind undoes *Pi's edits*, not arbitrary filesystem state.
31
- - **Time-travel is via `/tree`.** Navigating to a node whose recorded state
32
- differs from the work tree prompts to restore, listing the affected files
33
- (cwd-relative, up to 8 then *"+N more"*) under *"Restore 3 files to this
34
- point? (+120 / −40)"* when coarse line stats are available. The restore
35
- target mirrors `/tree`'s own semantics: a **user message** target (whose turn
36
- is removed and returned to the editor) restores to the state *before* that
37
- turn ran; any **other** target (whose turn stays in the conversation)
38
- restores to the state *after* that turn — the state recorded when the next
39
- run began. When nothing was recorded after the target (e.g. the last turn's
40
- assistant message), navigation is silent rather than rolling back edits the
41
- conversation still contains. Nodes with no file changes navigate silently.
42
- Only files that actually differ are rewritten. Line totals are a bag-of-lines
43
- estimate (capped at 1 MB per file), not a full Myers diff.
44
- - **`/rewind` is a settings + storage menu**, not a restore picker:
45
- - toggle rewind on/off,
46
- - set the auto-clean retention window,
47
- - inspect storage and prune (clean aged + orphaned / remove orphaned / remove
48
- all except current — the latter also keeps backups written to in the last
49
- 10 minutes, which may belong to sessions running in another Pi process).
50
- - **New files** created by `write` are tracked with a "did not exist" marker, so
51
- rewinding deletes them.
52
- - **Resume/fork** hard-links the prior session's backup blobs into the new
53
- session (falling back to copy), so rewind keeps working without duplicating
54
- storage.
55
-
56
- ## Storage & cleanup
57
-
58
- - **Layout:** `~/.pi/agent/rewind/`
59
- - `config.json` — `{ enabled, retentionDays, maxSnapshots }`
60
- - `backups/<sessionId>/<sha256(relpath)[:16]>@v<n>` — backup blobs (flat, hashed
61
- names keep paths short on Windows; mode preserved)
62
- - **Auto-clean:** at `session_start`, backup directories older than
63
- `retentionDays` (default **30**, set in `/rewind`) are reclaimed, plus an
64
- **orphan sweep** of directories whose session id has no session JSONL (crashed
65
- sessions). The GC is time-boxed and deletion-capped so it never slows startup.
66
- - The former `pi-config` storage layout is **not** read or migrated by this
67
- engine. Remove any old `~/.pi/agent/pi-config/` data manually if it is no
68
- longer needed.
69
-
70
- ## Limits
71
-
72
- - Covers edits made through Pi's built-in `edit` and `write` tools only. Files written by `bash` or edited outside Pi are not tracked.
73
- - A backup failure is swallowed — the edit proceeds without checkpointing rather than blocking the tool.
74
- - `/rewind` on its own does not restore files. Restoration uses `/tree` — navigate to a turn and confirm the prompt.
75
- - Restore is coarse: only files that differ are rewritten, but line totals are a bag-of-lines estimate (capped at 1 MB per file), not a full Myers diff.
76
-
77
- ## Implementation notes
78
-
79
- - **Never blocks the edit.** Backup runs before the write lands; a backup failure is swallowed so the session never stalls.
80
- - **Restore safety.** `applySnapshot` only rewrites files that differ and degrades gracefully on unreadable backups.
81
- - **Change detection.** A file is re-backed-up only when its stat or content differs from the latest backup. Content comparison streams in 64 KiB chunks instead of buffering whole files up to the 25 MB cap.
82
- - **Config is memory-cached.** `config.json` is re-read on `session_start` and updated in memory when `/rewind` saves.
83
- - **Restore path is single-scan.** The `/tree` confirm pass caches changed absolute paths; IO is concurrency-capped (16).
84
-
85
-
86
- Architecture informed by oh-my-pi (GPL-3.0) and Claude Code's file-history; independent implementation.