@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.
- package/CHANGELOG.md +25 -1
- package/dist/cli/args.js +1 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/config-selector.d.ts.map +1 -1
- package/dist/cli/config-selector.js +2 -3
- package/dist/cli/config-selector.js.map +1 -1
- package/dist/cli/startup-ui.d.ts.map +1 -1
- package/dist/cli/startup-ui.js +3 -5
- package/dist/cli/startup-ui.js.map +1 -1
- package/dist/config.d.ts +5 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -1
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +1 -25
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +29 -408
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction-controller.d.ts +89 -0
- package/dist/core/compaction-controller.d.ts.map +1 -0
- package/dist/core/compaction-controller.js +428 -0
- package/dist/core/compaction-controller.js.map +1 -0
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.js +3 -3
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/types.d.ts +1 -15
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/provider-attribution.js +2 -2
- package/dist/core/provider-attribution.js.map +1 -1
- package/dist/core/settings-manager.js +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/telemetry.d.ts +1 -1
- package/dist/core/telemetry.d.ts.map +1 -1
- package/dist/core/telemetry.js +1 -1
- package/dist/core/telemetry.js.map +1 -1
- package/dist/extensions/deepwiki/index.d.ts.map +1 -1
- package/dist/extensions/deepwiki/index.js +23 -1
- package/dist/extensions/deepwiki/index.js.map +1 -1
- package/dist/extensions/deepwiki/render.d.ts +1 -1
- package/dist/extensions/deepwiki/render.d.ts.map +1 -1
- package/dist/extensions/deepwiki/render.js +4 -2
- package/dist/extensions/deepwiki/render.js.map +1 -1
- package/dist/extensions/index.d.ts.map +1 -1
- package/dist/extensions/index.js +0 -4
- package/dist/extensions/index.js.map +1 -1
- package/dist/extensions/question/index.d.ts.map +1 -1
- package/dist/extensions/question/index.js +0 -1
- package/dist/extensions/question/index.js.map +1 -1
- package/dist/extensions/subagent/index.d.ts.map +1 -1
- package/dist/extensions/subagent/index.js +20 -2
- package/dist/extensions/subagent/index.js.map +1 -1
- package/dist/extensions/todo/schema.d.ts +1 -1
- package/dist/extensions/todo/state.js +5 -5
- package/dist/extensions/todo/state.js.map +1 -1
- package/dist/modes/interactive/components/extension-selector.d.ts +0 -2
- package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-selector.js +1 -1
- package/dist/modes/interactive/components/extension-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +1 -1
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-chrome.d.ts +28 -0
- package/dist/modes/interactive/components/tool-chrome.d.ts.map +1 -0
- package/dist/modes/interactive/components/tool-chrome.js +81 -0
- package/dist/modes/interactive/components/tool-chrome.js.map +1 -0
- package/dist/modes/interactive/components/tool-execution.d.ts +0 -3
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +11 -108
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +35 -52
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/docs/bundled/README.md +0 -2
- package/docs/bundled/extensions/subagent.md +1 -1
- package/docs/bundled/tool-presentation.md +2 -2
- package/docs/docs.json +0 -8
- package/docs/extensions.md +6 -23
- package/docs/settings.md +1 -1
- package/docs/usage.md +1 -2
- package/examples/extensions/README.md +2 -0
- package/examples/extensions/plan-mode/README.md +66 -0
- package/examples/extensions/plan-mode/index.ts +390 -0
- package/examples/extensions/plan-mode/utils.ts +168 -0
- package/examples/extensions/subagent/README.md +175 -0
- package/examples/extensions/subagent/agents/planner.md +37 -0
- package/examples/extensions/subagent/agents/reviewer.md +35 -0
- package/examples/extensions/subagent/agents/scout.md +50 -0
- package/examples/extensions/subagent/agents/worker.md +24 -0
- package/examples/extensions/subagent/agents.ts +126 -0
- package/examples/extensions/subagent/index.ts +1009 -0
- package/examples/extensions/subagent/prompts/implement-and-review.md +10 -0
- package/examples/extensions/subagent/prompts/implement.md +10 -0
- package/examples/extensions/subagent/prompts/scout-and-plan.md +9 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/dist/extensions/biu/index.d.ts +0 -19
- package/dist/extensions/biu/index.d.ts.map +0 -1
- package/dist/extensions/biu/index.js +0 -350
- package/dist/extensions/biu/index.js.map +0 -1
- package/dist/extensions/biu/prompts.d.ts +0 -26
- package/dist/extensions/biu/prompts.d.ts.map +0 -1
- package/dist/extensions/biu/prompts.js +0 -260
- package/dist/extensions/biu/prompts.js.map +0 -1
- package/dist/extensions/biu/workspace.d.ts +0 -122
- package/dist/extensions/biu/workspace.d.ts.map +0 -1
- package/dist/extensions/biu/workspace.js +0 -404
- package/dist/extensions/biu/workspace.js.map +0 -1
- package/dist/extensions/rewind/anchor.d.ts +0 -29
- package/dist/extensions/rewind/anchor.d.ts.map +0 -1
- package/dist/extensions/rewind/anchor.js +0 -35
- package/dist/extensions/rewind/anchor.js.map +0 -1
- package/dist/extensions/rewind/config.d.ts +0 -16
- package/dist/extensions/rewind/config.d.ts.map +0 -1
- package/dist/extensions/rewind/config.js +0 -69
- package/dist/extensions/rewind/config.js.map +0 -1
- package/dist/extensions/rewind/engine.d.ts +0 -93
- package/dist/extensions/rewind/engine.d.ts.map +0 -1
- package/dist/extensions/rewind/engine.js +0 -778
- package/dist/extensions/rewind/engine.js.map +0 -1
- package/dist/extensions/rewind/gc.d.ts +0 -35
- package/dist/extensions/rewind/gc.d.ts.map +0 -1
- package/dist/extensions/rewind/gc.js +0 -169
- package/dist/extensions/rewind/gc.js.map +0 -1
- package/dist/extensions/rewind/index.d.ts +0 -3
- package/dist/extensions/rewind/index.d.ts.map +0 -1
- package/dist/extensions/rewind/index.js +0 -287
- package/dist/extensions/rewind/index.js.map +0 -1
- package/dist/extensions/rewind/menu.d.ts +0 -13
- package/dist/extensions/rewind/menu.d.ts.map +0 -1
- package/dist/extensions/rewind/menu.js +0 -123
- package/dist/extensions/rewind/menu.js.map +0 -1
- package/dist/extensions/rewind/paths.d.ts +0 -4
- package/dist/extensions/rewind/paths.d.ts.map +0 -1
- package/dist/extensions/rewind/paths.js +0 -15
- package/dist/extensions/rewind/paths.js.map +0 -1
- package/dist/extensions/rewind/restore.d.ts +0 -54
- package/dist/extensions/rewind/restore.d.ts.map +0 -1
- package/dist/extensions/rewind/restore.js +0 -94
- package/dist/extensions/rewind/restore.js.map +0 -1
- package/dist/extensions/rewind/snapshot.d.ts +0 -45
- package/dist/extensions/rewind/snapshot.d.ts.map +0 -1
- package/dist/extensions/rewind/snapshot.js +0 -21
- package/dist/extensions/rewind/snapshot.js.map +0 -1
- package/dist/extensions/rewind/storage.d.ts +0 -12
- package/dist/extensions/rewind/storage.d.ts.map +0 -1
- package/dist/extensions/rewind/storage.js +0 -33
- package/dist/extensions/rewind/storage.js.map +0 -1
- package/dist/extensions/rewind/text.d.ts +0 -7
- package/dist/extensions/rewind/text.d.ts.map +0 -1
- package/dist/extensions/rewind/text.js +0 -19
- package/dist/extensions/rewind/text.js.map +0 -1
- package/dist/extensions/rewind/tool-path.d.ts +0 -3
- package/dist/extensions/rewind/tool-path.d.ts.map +0 -1
- package/dist/extensions/rewind/tool-path.js +0 -9
- package/dist/extensions/rewind/tool-path.js.map +0 -1
- package/dist/utils/coalescing-terminal.d.ts +0 -132
- package/dist/utils/coalescing-terminal.d.ts.map +0 -1
- package/dist/utils/coalescing-terminal.js +0 -292
- package/dist/utils/coalescing-terminal.js.map +0 -1
- package/docs/bundled/extensions/biu.md +0 -63
- 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.
|