@astralyn/pi 0.82.4 → 0.82.6
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 +52 -0
- package/dist/cli/project-trust.d.ts.map +1 -1
- package/dist/cli/project-trust.js +4 -1
- package/dist/cli/project-trust.js.map +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +2 -2
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +4 -22
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/extensions/index.d.ts +2 -2
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +2 -3
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/types.d.ts +37 -5
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js +11 -0
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/tools/output-accumulator.d.ts +3 -13
- package/dist/core/tools/output-accumulator.d.ts.map +1 -1
- package/dist/core/tools/output-accumulator.js +6 -36
- package/dist/core/tools/output-accumulator.js.map +1 -1
- package/dist/extensions/plan/constants.d.ts +11 -2
- package/dist/extensions/plan/constants.d.ts.map +1 -1
- package/dist/extensions/plan/constants.js +14 -2
- package/dist/extensions/plan/constants.js.map +1 -1
- package/dist/extensions/plan/index.d.ts +0 -18
- package/dist/extensions/plan/index.d.ts.map +1 -1
- package/dist/extensions/plan/index.js +35 -3
- package/dist/extensions/plan/index.js.map +1 -1
- package/dist/extensions/plan/view.d.ts +36 -0
- package/dist/extensions/plan/view.d.ts.map +1 -0
- package/dist/extensions/plan/view.js +74 -0
- package/dist/extensions/plan/view.js.map +1 -0
- package/dist/extensions/question/dialog.d.ts +3 -1
- package/dist/extensions/question/dialog.d.ts.map +1 -1
- package/dist/extensions/question/dialog.js +106 -52
- package/dist/extensions/question/dialog.js.map +1 -1
- package/dist/extensions/question/index.d.ts.map +1 -1
- package/dist/extensions/question/index.js +2 -2
- package/dist/extensions/question/index.js.map +1 -1
- package/dist/extensions/question/render-cache.d.ts +9 -0
- package/dist/extensions/question/render-cache.d.ts.map +1 -1
- package/dist/extensions/question/render-cache.js +22 -0
- package/dist/extensions/question/render-cache.js.map +1 -1
- package/dist/extensions/question/results.d.ts.map +1 -1
- package/dist/extensions/question/results.js +6 -1
- package/dist/extensions/question/results.js.map +1 -1
- package/dist/extensions/question/schema.d.ts +4 -0
- package/dist/extensions/question/schema.d.ts.map +1 -1
- package/dist/extensions/question/schema.js +19 -28
- package/dist/extensions/question/schema.js.map +1 -1
- package/dist/extensions/question/types.d.ts +1 -1
- package/dist/extensions/question/types.d.ts.map +1 -1
- package/dist/extensions/question/types.js.map +1 -1
- package/dist/extensions/router/stream.d.ts.map +1 -1
- package/dist/extensions/router/stream.js +44 -0
- package/dist/extensions/router/stream.js.map +1 -1
- package/dist/extensions/subagent/activity.d.ts.map +1 -1
- package/dist/extensions/subagent/activity.js +25 -4
- package/dist/extensions/subagent/activity.js.map +1 -1
- package/dist/extensions/subagent/agents.d.ts.map +1 -1
- package/dist/extensions/subagent/agents.js +12 -6
- package/dist/extensions/subagent/agents.js.map +1 -1
- package/dist/extensions/subagent/constants.d.ts +1 -1
- package/dist/extensions/subagent/constants.d.ts.map +1 -1
- package/dist/extensions/subagent/constants.js +3 -2
- package/dist/extensions/subagent/constants.js.map +1 -1
- package/dist/extensions/subagent/index.d.ts.map +1 -1
- package/dist/extensions/subagent/index.js +14 -0
- package/dist/extensions/subagent/index.js.map +1 -1
- package/dist/extensions/subagent/render.d.ts.map +1 -1
- package/dist/extensions/subagent/render.js +243 -116
- package/dist/extensions/subagent/render.js.map +1 -1
- package/dist/extensions/subagent/resolve.d.ts.map +1 -1
- package/dist/extensions/subagent/resolve.js +13 -9
- package/dist/extensions/subagent/resolve.js.map +1 -1
- package/dist/extensions/subagent/runner.d.ts +1 -1
- package/dist/extensions/subagent/runner.d.ts.map +1 -1
- package/dist/extensions/subagent/runner.js +9 -3
- package/dist/extensions/subagent/runner.js.map +1 -1
- package/dist/extensions/subagent/schema.d.ts.map +1 -1
- package/dist/extensions/subagent/schema.js +4 -4
- package/dist/extensions/subagent/schema.js.map +1 -1
- package/dist/extensions/subagent/sdk-runner.d.ts.map +1 -1
- package/dist/extensions/subagent/sdk-runner.js +5 -0
- package/dist/extensions/subagent/sdk-runner.js.map +1 -1
- package/dist/extensions/subagent/types.d.ts +7 -0
- package/dist/extensions/subagent/types.d.ts.map +1 -1
- package/dist/extensions/subagent/types.js.map +1 -1
- package/dist/extensions/todo/constants.d.ts +17 -6
- package/dist/extensions/todo/constants.d.ts.map +1 -1
- package/dist/extensions/todo/constants.js +47 -9
- package/dist/extensions/todo/constants.js.map +1 -1
- package/dist/extensions/todo/index.d.ts +1 -15
- package/dist/extensions/todo/index.d.ts.map +1 -1
- package/dist/extensions/todo/index.js +35 -4
- package/dist/extensions/todo/index.js.map +1 -1
- package/dist/extensions/todo/schema.d.ts +59 -7
- package/dist/extensions/todo/schema.d.ts.map +1 -1
- package/dist/extensions/todo/schema.js +72 -12
- package/dist/extensions/todo/schema.js.map +1 -1
- package/dist/extensions/todo/state.d.ts +20 -5
- package/dist/extensions/todo/state.d.ts.map +1 -1
- package/dist/extensions/todo/state.js +1016 -134
- package/dist/extensions/todo/state.js.map +1 -1
- package/dist/extensions/todo/view.d.ts +16 -1
- package/dist/extensions/todo/view.d.ts.map +1 -1
- package/dist/extensions/todo/view.js +293 -7
- package/dist/extensions/todo/view.js.map +1 -1
- package/dist/modes/interactive/components/extension-selector.d.ts +6 -1
- package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-selector.js +14 -6
- package/dist/modes/interactive/components/extension-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +6 -0
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +2 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +41 -2
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +8 -1
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/utils/coalescing-terminal.d.ts +71 -17
- package/dist/utils/coalescing-terminal.d.ts.map +1 -1
- package/dist/utils/coalescing-terminal.js +136 -31
- package/dist/utils/coalescing-terminal.js.map +1 -1
- package/docs/bundled/extensions/plan.md +6 -4
- package/docs/bundled/extensions/question.md +9 -7
- package/docs/bundled/extensions/subagent.md +4 -4
- package/docs/bundled/extensions/todo.md +9 -5
- package/docs/bundled/tool-presentation.md +1 -1
- package/docs/extensions.md +14 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/dist/utils/output-decoder.d.ts +0 -52
- package/dist/utils/output-decoder.d.ts.map +0 -1
- package/dist/utils/output-decoder.js +0 -134
- package/dist/utils/output-decoder.js.map +0 -1
|
@@ -24,29 +24,88 @@ const KITTY_APC_PREFIX = "\x1b_G";
|
|
|
24
24
|
*
|
|
25
25
|
* Scrollback preservation: pi-tui's fullRender(clear) opens with
|
|
26
26
|
* `2J H 3J`, wiping the terminal's scrollback, and then replays the entire
|
|
27
|
-
* transcript.
|
|
28
|
-
* during
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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 enables it via
|
|
34
|
+
* setScrollbackPreservation while an agent run is mutating the transcript
|
|
35
|
+
* (and keeps it on until the user's next input after the run settles, since
|
|
36
|
+
* they may still be scrolled up reading), and leaves it off otherwise. An
|
|
37
|
+
* idle-session full redraw — Ctrl+O toggles, mode switches — passes through
|
|
38
|
+
* with upstream's wipe-and-replay, whose result is byte-perfect: scrollback
|
|
39
|
+
* becomes the clean new transcript. Mid-run user toggles can arm
|
|
40
|
+
* passNextFullRedraw for the same clean replay on their own frame.
|
|
41
|
+
*
|
|
42
|
+
* While preservation is active, a matching frame is rewritten to repaint
|
|
43
|
+
* without the wipe. With setViewportTopProvider wired, the rewrite knows
|
|
44
|
+
* which transcript line the visible screen starts at, so a frame that grew
|
|
45
|
+
* the transcript repaints the on-screen rows with the new content at those
|
|
46
|
+
* same positions and then scrolls the extra lines in at the bottom row.
|
|
47
|
+
* Each bottom-row newline pushes the top row — already repainted with the
|
|
48
|
+
* correct new content — into scrollback, exactly like ordinary streaming
|
|
49
|
+
* output, so scrollback ends up byte-equivalent to the upstream full replay
|
|
50
|
+
* without ever clearing it. Without the provider, or when the transcript
|
|
51
|
+
* shrank (lines cannot be pulled back out of scrollback), the rewrite falls
|
|
52
|
+
* back to overwriting the visible screen in place with the bottom
|
|
53
|
+
* viewport-height rows, at the cost of a stale seam above the screen. ED 2
|
|
54
|
+
* is avoided entirely in rewritten frames because conhost/Windows Terminal
|
|
55
|
+
* implement it by scrolling the screen contents into scrollback rather than
|
|
56
|
+
* erasing in place.
|
|
57
|
+
*
|
|
58
|
+
* Width- and height-change redraws keep the upstream wipe because re-wrapped
|
|
59
|
+
* content genuinely invalidates old scrollback, and frames containing kitty
|
|
60
|
+
* graphics pass through untouched. A frame that does not match the expected
|
|
61
|
+
* shape is passed through unchanged, so if upstream reshapes its render
|
|
62
|
+
* output the wrapper degrades back to stock behavior.
|
|
44
63
|
*/
|
|
45
64
|
export class CoalescingTerminal extends ProcessTerminal {
|
|
46
65
|
pending = "";
|
|
47
66
|
flushQueued = false;
|
|
48
67
|
lastWriteColumns;
|
|
68
|
+
lastWriteRows;
|
|
69
|
+
viewportTopProvider;
|
|
70
|
+
preserve = false;
|
|
71
|
+
passArmed = false;
|
|
49
72
|
flushOnExit = () => this.flush();
|
|
73
|
+
/**
|
|
74
|
+
* Enable or disable the scrollback-preserving rewrite. Preservation is
|
|
75
|
+
* only worth its imperfections (stale renderings above the screen, seams
|
|
76
|
+
* on shrink) while the transcript is mutating on its own and the reader
|
|
77
|
+
* may be scrolled up — i.e. during an agent run and until the user's next
|
|
78
|
+
* input after it settles. Outside that window every full redraw passes
|
|
79
|
+
* through with upstream's wipe-and-replay, whose result is byte-perfect:
|
|
80
|
+
* scrollback becomes the clean new transcript. Default: disabled.
|
|
81
|
+
*/
|
|
82
|
+
setScrollbackPreservation(enabled) {
|
|
83
|
+
this.preserve = enabled;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Arm a one-shot exemption for an explicit user action (Ctrl+O tool
|
|
87
|
+
* expansion, thinking visibility) so its own full redraw keeps the
|
|
88
|
+
* upstream wipe-and-replay even while preservation is active mid-run.
|
|
89
|
+
* The user just pressed a key, so they are interacting at the bottom;
|
|
90
|
+
* a clean replay beats preservation's stale-seam trade-offs there. The
|
|
91
|
+
* flag is cleared by the next write so it cannot leak onto a later
|
|
92
|
+
* streaming redraw.
|
|
93
|
+
*/
|
|
94
|
+
passNextFullRedraw() {
|
|
95
|
+
this.passArmed = true;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Wire the renderer's notion of which transcript line the visible screen
|
|
99
|
+
* currently starts at (pi-tui's `previousViewportTop`). Full-redraw frames
|
|
100
|
+
* that grew the transcript then scroll the new above-screen lines into
|
|
101
|
+
* scrollback instead of dropping them. The provider is read while the
|
|
102
|
+
* frame is being written, i.e. before the renderer updates its own
|
|
103
|
+
* bookkeeping for that frame; returning undefined falls back to the
|
|
104
|
+
* bottom-anchored in-place repaint.
|
|
105
|
+
*/
|
|
106
|
+
setViewportTopProvider(provider) {
|
|
107
|
+
this.viewportTopProvider = provider;
|
|
108
|
+
}
|
|
50
109
|
constructor() {
|
|
51
110
|
super();
|
|
52
111
|
// A synchronous process.exit() before the queued microtask runs would
|
|
@@ -102,14 +161,29 @@ export class CoalescingTerminal extends ProcessTerminal {
|
|
|
102
161
|
*/
|
|
103
162
|
preserveScrollback(data) {
|
|
104
163
|
const columns = this.columns;
|
|
164
|
+
const rows = this.rows;
|
|
105
165
|
const widthChanged = this.lastWriteColumns !== undefined && this.lastWriteColumns !== columns;
|
|
166
|
+
const heightChanged = this.lastWriteRows !== undefined && this.lastWriteRows !== rows;
|
|
106
167
|
this.lastWriteColumns = columns;
|
|
168
|
+
this.lastWriteRows = rows;
|
|
169
|
+
// One-shot: the very next write is the armed toggle's own frame (or a
|
|
170
|
+
// differential frame when the toggle stayed inside the viewport, in
|
|
171
|
+
// which case no exemption is needed and the flag must not linger).
|
|
172
|
+
const passAllowed = this.passArmed;
|
|
173
|
+
this.passArmed = false;
|
|
107
174
|
const prefix = SYNC_START + CLEAR_WITH_SCROLLBACK;
|
|
108
175
|
if (!data.startsWith(prefix) || !data.endsWith(SYNC_END)) {
|
|
109
176
|
return data;
|
|
110
177
|
}
|
|
111
|
-
//
|
|
112
|
-
|
|
178
|
+
// Outside the preservation window (idle sessions, pre-session UI) and
|
|
179
|
+
// for armed user toggles, upstream's wipe-and-replay is byte-perfect;
|
|
180
|
+
// let it through.
|
|
181
|
+
if (!this.preserve || passAllowed) {
|
|
182
|
+
return data;
|
|
183
|
+
}
|
|
184
|
+
// Re-wrapped/re-flowed content invalidates what scrollback holds and
|
|
185
|
+
// where the screen sits in it; keep the wipe.
|
|
186
|
+
if (widthChanged || heightChanged) {
|
|
113
187
|
return data;
|
|
114
188
|
}
|
|
115
189
|
if (data.includes(KITTY_APC_PREFIX)) {
|
|
@@ -121,23 +195,54 @@ export class CoalescingTerminal extends ProcessTerminal {
|
|
|
121
195
|
if (body.includes(CLEAR_WITH_SCROLLBACK)) {
|
|
122
196
|
return data;
|
|
123
197
|
}
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
// truncated overwrite produces the exact same final screen and cursor
|
|
129
|
-
// position as the full replay (rows beyond the screen would have
|
|
130
|
-
// scrolled out anyway). ED 2 (`2J`) is deliberately avoided even for
|
|
131
|
-
// the visible screen: conhost/Windows Terminal implement it by
|
|
132
|
-
// scrolling the current screen into scrollback (cls compatibility), so
|
|
133
|
-
// without the `3J` wipe each redraw would stack another screenful of
|
|
134
|
-
// duplicates into history.
|
|
198
|
+
// ED 2 (`2J`) is deliberately avoided in both rewrites below: conhost/
|
|
199
|
+
// Windows Terminal implement it by scrolling the current screen into
|
|
200
|
+
// scrollback (cls compatibility), so without the `3J` wipe each redraw
|
|
201
|
+
// would stack another screenful of duplicates into history.
|
|
135
202
|
const lines = body.split("\r\n");
|
|
136
|
-
const
|
|
203
|
+
const newViewportTop = Math.max(0, lines.length - rows);
|
|
204
|
+
const viewportTop = this.readViewportTop();
|
|
205
|
+
if (viewportTop !== undefined && viewportTop <= newViewportTop) {
|
|
206
|
+
// The transcript grew (or held steady): repaint the rows currently
|
|
207
|
+
// on screen with the new content at those same transcript
|
|
208
|
+
// positions, then scroll the remaining lines in at the bottom row.
|
|
209
|
+
// Each bottom-row newline pushes the already-repainted top row into
|
|
210
|
+
// scrollback, so scrollback receives exactly the new lines the
|
|
211
|
+
// upstream replay would have put there — no wipe, no loss.
|
|
212
|
+
const onScreen = lines.slice(viewportTop, viewportTop + rows);
|
|
213
|
+
const scrolled = lines.slice(viewportTop + rows);
|
|
214
|
+
let repaint = onScreen.map((line) => `\x1b[2K${line}`).join("\r\n");
|
|
215
|
+
for (const line of scrolled) {
|
|
216
|
+
repaint += `\r\n\x1b[2K${line}`;
|
|
217
|
+
}
|
|
218
|
+
return `${SYNC_START}\x1b[H${repaint}\x1b[J${SYNC_END}`;
|
|
219
|
+
}
|
|
220
|
+
// Fallback (no provider, or the transcript shrank — lines cannot be
|
|
221
|
+
// pulled back out of scrollback): repaint only the bottom
|
|
222
|
+
// viewport-height rows, overwriting the screen in place: home,
|
|
223
|
+
// erase-and-rewrite each row, erase whatever remains below. Writing the
|
|
224
|
+
// full transcript after a screen clear would push a duplicate copy of
|
|
225
|
+
// everything above the viewport into scrollback; the truncated
|
|
226
|
+
// overwrite produces the exact same final screen and cursor position as
|
|
227
|
+
// the full replay (rows beyond the screen would have scrolled out
|
|
228
|
+
// anyway).
|
|
137
229
|
const kept = lines.length > rows ? lines.slice(lines.length - rows) : lines;
|
|
138
230
|
const repaint = kept.map((line) => `\x1b[2K${line}`).join("\r\n");
|
|
139
231
|
return `${SYNC_START}\x1b[H${repaint}\x1b[J${SYNC_END}`;
|
|
140
232
|
}
|
|
233
|
+
// The provider reaches into the renderer's bookkeeping; treat anything
|
|
234
|
+
// unexpected as "unknown" and fall back rather than corrupt the screen.
|
|
235
|
+
readViewportTop() {
|
|
236
|
+
if (!this.viewportTopProvider)
|
|
237
|
+
return undefined;
|
|
238
|
+
try {
|
|
239
|
+
const value = this.viewportTopProvider();
|
|
240
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : undefined;
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
141
246
|
flush() {
|
|
142
247
|
this.flushQueued = false;
|
|
143
248
|
if (this.pending.length === 0) {
|
|
@@ -1 +1 @@
|
|
|
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. Content-driven triggers (a line changing above the viewport\n * during streaming markdown reflow, large content shrinks) fire this\n * routinely, which erases pre-session shell history and — in terminals that\n * clamp the scroll offset when scrollback vanishes, notably Windows\n * Terminal — yanks a scrolled-up viewport to the top of the buffer\n * (upstream #6502, #5576, #6050; rejected upstream fix #4204). Frames\n * matching that shape are rewritten to overwrite the visible screen in place\n * (home, erase-and-rewrite each row, erase below) with only the bottom\n * viewport-height rows, which leaves scrollback (and the reader's scroll\n * position) untouched while producing a byte-identical final screen state.\n * ED 2 is avoided entirely because conhost/Windows Terminal implement it by\n * scrolling the screen contents into scrollback rather than erasing in\n * place. Width-change redraws keep the upstream wipe because\n * re-wrapped content genuinely invalidates old scrollback, and frames\n * containing kitty graphics pass through untouched. A frame that does not\n * match the expected shape is passed through unchanged, so if upstream\n * reshapes its render 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 flushOnExit = () => this.flush();\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 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 widthChanged = this.lastWriteColumns !== undefined && this.lastWriteColumns !== columns;\n\t\tthis.lastWriteColumns = columns;\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// Re-wrapped content invalidates what scrollback holds; keep the wipe.\n\t\tif (widthChanged) {\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// Repaint only the bottom viewport-height rows, overwriting the screen\n\t\t// in place: home, erase-and-rewrite each row, erase whatever remains\n\t\t// below. Writing the full transcript after a screen clear would push a\n\t\t// duplicate copy of everything above the viewport into scrollback; the\n\t\t// truncated overwrite produces the exact same final screen and cursor\n\t\t// position as the full replay (rows beyond the screen would have\n\t\t// scrolled out anyway). ED 2 (`2J`) is deliberately avoided even for\n\t\t// the visible screen: conhost/Windows Terminal implement it by\n\t\t// scrolling the current screen into scrollback (cls compatibility), so\n\t\t// without the `3J` wipe each redraw would stack another screenful of\n\t\t// duplicates into history.\n\t\tconst lines = body.split(\"\\r\\n\");\n\t\tconst rows = this.rows;\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\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
|
+
{"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 enables it via\n * setScrollbackPreservation while an agent run is mutating the transcript\n * (and keeps it on until the user's next input after the run settles, since\n * they may still be scrolled up reading), and leaves it off otherwise. 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 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 */\n\tsetScrollbackPreservation(enabled: boolean): void {\n\t\tthis.preserve = enabled;\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 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"]}
|
|
@@ -6,10 +6,10 @@ Adds `/plan` for a Claude-Code-style planning phase: the model explores read-onl
|
|
|
6
6
|
|
|
7
7
|
- `/plan` (or the `--plan` CLI flag) toggles plan mode. On entry the built-in `edit`, `write`, and `bash` tools and the bundled `subagent` tool are deactivated — `subagent` because child sessions do not inherit the restricted tool set. Read-only built-ins and other extension tools (`question`, `todo`, `deepwiki`, …) stay available. A `tool_call` guard also blocks the four tools for requests already in flight when the mode toggles.
|
|
8
8
|
- While planning, the system prompt gains a plan-mode block: explore before asking, interview one decision at a time with the `question` tool (with a recommendation and trade-offs), skip the interview for small clear tasks, then present the full plan as response text before calling `exit_plan`.
|
|
9
|
-
- `exit_plan(title, plan, revises?)` opens the approval menu:
|
|
10
|
-
- **Start executing (keep full context)
|
|
11
|
-
- **Compact context, then execute** — saves the plan, ends the run (`terminate`), compacts once the run has settled, and only then restores tools; execution restarts via a kickoff message that embeds the plan text, so continuation never depends on the compaction summary. If compaction fails, tools are restored and execution proceeds with full context.
|
|
12
|
-
- **Keep planning** — optionally collects feedback (multi-line editor in the TUI, input dialog in RPC) and returns it to the model; plan mode stays active. `Esc` and dialog timeouts map here, never to an executing option.
|
|
9
|
+
- `exit_plan(title, plan, revises?)` opens the approval menu. Each option carries a muted description, and the dialog subtitle reports current context usage — the fact the compact-or-not decision turns on:
|
|
10
|
+
- **Start executing** *(keep full context)* — saves the plan, restores tools, and execution continues in the same run.
|
|
11
|
+
- **Compact context, then execute** *(best for long tasks)* — saves the plan, ends the run (`terminate`), compacts once the run has settled, and only then restores tools; execution restarts via a kickoff message that embeds the plan text, so continuation never depends on the compaction summary. If compaction fails, tools are restored and execution proceeds with full context.
|
|
12
|
+
- **Keep planning** *(refine before running)* — optionally collects feedback (multi-line editor in the TUI, input dialog in RPC) and returns it to the model; plan mode stays active. `Esc` and dialog timeouts map here, never to an executing option, so the dismiss hint reads "keep planning" rather than "cancel".
|
|
13
13
|
- Both executing options save the plan file. If the session has no name yet, the plan title becomes the session name.
|
|
14
14
|
- `/plan` while planning exits directly without a menu or a file — the plan text is still in context. During a pending compaction the toggle is refused until the compaction settles.
|
|
15
15
|
- Plan files live under `<agentDir>/plans/<sessionId>/NN-<slug>.md` (`~/.pi/agent/plans/…` by default, `PI_CODING_AGENT_DIR` respected) with `title`, `created`, `cwd`, `session`, and optional `revises` frontmatter. Numbering is append-only; revisions are new files that point back via `revises`.
|
|
@@ -20,6 +20,7 @@ Adds `/plan` for a Claude-Code-style planning phase: the model explores read-onl
|
|
|
20
20
|
- The kickoff message embeds at most 24 000 characters of plan markdown; longer plans are truncated with a pointer to the full file.
|
|
21
21
|
- In `json`/`print` modes `exit_plan` cannot ask, so it saves the plan and exits with context kept (write access is restored without a menu).
|
|
22
22
|
- Approving with **Compact context, then execute** and then interrupting before compaction completes (quit, resume, branch switch) degrades to the keep-context exit on the next session start.
|
|
23
|
+
- The dialog subtitle is omitted when context usage is not yet known (no assistant turn has reported usage).
|
|
23
24
|
|
|
24
25
|
## Implementation notes
|
|
25
26
|
|
|
@@ -29,3 +30,4 @@ Adds `/plan` for a Claude-Code-style planning phase: the model explores read-onl
|
|
|
29
30
|
- **No write access is ever needed for the plan itself** — the plan text travels as an `exit_plan` parameter and the extension writes the file.
|
|
30
31
|
- **The compact-then-execute window is closed by construction**: plan mode (and its tool restrictions) persists until compaction completes; the run is terminated by the tool result and the compaction starts on `agent_settled`, which fires once per fully settled run.
|
|
31
32
|
- The `exit_plan` result text authoritatively announces mode changes because the per-run system prompt override is not rebuilt mid-run.
|
|
33
|
+
- **The tool row renders from the title and `details`, not from the parameters.** `exit_plan` carries the whole plan as a parameter, and its result text is written for the model, so the default renderer would echo the plan markdown escaped onto one line and then restate tool-state rules at the user. `renderCall` shows the title (plan body on expand); `renderResult` shows where the plan landed and what happens next.
|
|
@@ -7,13 +7,15 @@ Adds a `question` tool for asking one to four multiple-choice questions when the
|
|
|
7
7
|
- Each option requires a concise description of its consequence or trade-off. Questions, labels, descriptions, and previews have input-length limits so the dialog stays usable.
|
|
8
8
|
- A single single-select question submits as soon as the user selects an option.
|
|
9
9
|
- A multi-question flow advances after each single-select answer; multi-select uses `Space` to toggle choices and Enter to continue. Once every question is answered, a **Review answers** view requires explicit submission.
|
|
10
|
+
- Options are numbered; pressing `1`–`9` jumps to the matching option — selecting it in single-select, toggling it in multi-select. The custom-answer row's number opens its input.
|
|
10
11
|
- `←` / `→` switch between adjacent questions without wrapping. The tab bar marks answered questions and the Review state.
|
|
11
12
|
- `Chat about this` is available after the choices. It returns a `needs_clarification` outcome so the model explains or reformulates instead of treating the user as having declined.
|
|
12
|
-
- `Type something.` is appended automatically for custom answers. Authored options may not use reserved labels (`Other`, `Type something.`, `Chat about this
|
|
13
|
-
- `Tab` opens a note editor for the focused option; in multi-select, the option must be selected first. On the custom-answer row, it opens custom-answer input.
|
|
14
|
-
- Multi-select custom answers stay on the `Type something.` row, are selected when saved, and can be toggled with `Space` without losing text.
|
|
15
|
-
- `preview` shows focused single-select content beside the choices on wide terminals and beneath them on narrow terminals. Preview height is capped and reports hidden lines.
|
|
13
|
+
- `Type something.` is appended automatically for custom answers. Authored options may not use reserved labels (`Other`, `Type something.`, `Chat about this`); the comparison is case-insensitive and ignores surrounding whitespace.
|
|
14
|
+
- `Tab` opens a note editor for the focused option; in multi-select, the option must be selected first. In single-select it also selects the option, and cancelling the note editor with `Esc` restores the previous selection. On the custom-answer row, it opens custom-answer input.
|
|
15
|
+
- Multi-select custom answers stay on the `Type something.` row, are selected when saved, and can be toggled with `Space` without losing text. Enter on that row opens the input while no custom answer exists yet.
|
|
16
|
+
- `preview` shows focused single-select content beside the choices on wide terminals and beneath them on narrow terminals. Preview height is capped and reports hidden lines. Previews on multi-select questions are rejected with a `preview_multiselect` error.
|
|
16
17
|
- The dialog is available only in TUI mode. RPC, JSON, and print calls return a structured `no_ui` error rather than attempting a custom component.
|
|
18
|
+
- Aborting the turn closes the dialog and resolves the call as cancelled with the answers given so far.
|
|
17
19
|
|
|
18
20
|
## Result contract
|
|
19
21
|
|
|
@@ -28,7 +30,7 @@ Adds a `question` tool for asking one to four multiple-choice questions when the
|
|
|
28
30
|
}
|
|
29
31
|
```
|
|
30
32
|
|
|
31
|
-
Only `content` reaches the model. Successful results are numbered, clearly identify single/custom/multi answers, retain notes, and state when a preview was selected without echoing its full source. Model-facing output is capped at 12,000 characters; if it is truncated, the result instructs the model to ask a focused follow-up question. Notes and custom answers are capped at 4,000 characters in the dialog.
|
|
33
|
+
Only `content` reaches the model. Successful results are numbered, clearly identify single/custom/multi answers, retain notes, and state when a preview was selected without echoing its full source. Cancelling — via `Esc` or a turn abort — lists any answers already given as partial answers alongside the decline message. Model-facing output is capped at 12,000 characters; if it is truncated, the result instructs the model to ask a focused follow-up question. Notes and custom answers are capped at 4,000 characters in the dialog.
|
|
32
34
|
|
|
33
35
|
The transcript uses a private `renderCall` / `renderResult` only to replace raw question JSON and model-oriented result text with concise user summaries. Pi retains its native tool shell, pending/error state, and collapsed/expanded behavior.
|
|
34
36
|
|
|
@@ -42,7 +44,7 @@ The transcript uses a private `renderCall` / `renderResult` only to replace raw
|
|
|
42
44
|
## Implementation notes
|
|
43
45
|
|
|
44
46
|
- Uses Pi's native `ctx.ui.custom()` lifecycle; no state is shared with another extension.
|
|
45
|
-
- The render cache is keyed by terminal width **and height**, because preview height depends on available rows and Pi resize only requests a render.
|
|
46
|
-
- `validateQuestions` enforces uniqueness, reserved-label rejection, and
|
|
47
|
+
- The render cache is keyed by terminal width **and height**, because preview height depends on available rows and Pi resize only requests a render. Preview markdown rendering is additionally memoized per text and dimensions so editor keystrokes don't re-parse previews.
|
|
48
|
+
- `validateQuestions` enforces only what the JSON schema cannot express: case-insensitive uniqueness, reserved-label rejection, and the preview/multi-select conflict. Length and count limits are enforced against the schema before the tool executes.
|
|
47
49
|
- Dialog navigation follows Pi's injected select/input keybindings where applicable; custom actions such as Space-to-toggle remain explicit in the footer.
|
|
48
50
|
|
|
@@ -17,7 +17,7 @@ Every task object has:
|
|
|
17
17
|
agent? Agent profile name; defaults to general
|
|
18
18
|
description Short UI label
|
|
19
19
|
prompt Self-contained worker briefing
|
|
20
|
-
cwd? Relative directory inside the parent working directory
|
|
20
|
+
cwd? Relative or absolute directory inside the parent working directory
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
Parallel batches allow at most eight tasks. A session-scoped gate runs at most five workers concurrently, including sibling `subagent` calls from the same parent session.
|
|
@@ -50,9 +50,9 @@ Project definitions are loaded only after Pi trusts the project. Invalid Markdow
|
|
|
50
50
|
Built-in profiles:
|
|
51
51
|
|
|
52
52
|
- `general`: read, bash, edit, and write.
|
|
53
|
-
- `explorer`:
|
|
53
|
+
- `explorer`: `read`, `grep`, `find`, and `ls`, plus `bash` constrained by its system prompt to read-only inspection (git history, counting, and similar).
|
|
54
54
|
|
|
55
|
-
A profile may only allow Pi built-in tools. Child sessions load no extensions, skills, or prompt templates, so a worker cannot recursively call `subagent` or inherit unrelated extension capabilities. Project context files such as `AGENTS.md` remain available.
|
|
55
|
+
A profile may only allow Pi built-in tools. Child sessions load no extensions, skills, or prompt templates, so a worker cannot recursively call `subagent` or inherit unrelated extension capabilities. Project context files such as `AGENTS.md` remain available, except for the built-in `explorer`, which skips them: read-only exploration needs no commit/PR rules, and the parent interprets its results.
|
|
56
56
|
|
|
57
57
|
## Model and thinking selection
|
|
58
58
|
|
|
@@ -85,7 +85,7 @@ They apply only to future subagent runs and never change the parent session's `/
|
|
|
85
85
|
- Parent abort, `/reload`, `/new`, `/resume`, `/fork`, and session shutdown abort active child sessions. Queued workers do not survive the call.
|
|
86
86
|
- Parallel worker failures do not cancel other independent workers.
|
|
87
87
|
- Transient provider errors auto-retry: the worker session retries retryable stream errors with backoff (visible as `Retrying (n/m) in Xs…`), and a failure that produced no work (for example a preflight auth throw) is rerun at the task level up to two more times. Runs with partial work behind them are never rerun.
|
|
88
|
-
- Progress is rendered in Pi's native tool transcript. The collapsed view
|
|
88
|
+
- Progress is rendered in Pi's native tool transcript. The collapsed view stays lean in both phases: while running, one intent line (for example `Verifying changes`) carries tool uses, the `ctx:` context watermark, cost, and elapsed time above the current tool marked with `›` and the live output tail; once settled, the excerpted result is followed by a single metrics line (`tok · $ · duration`, plus tool uses and `ctx:` for single runs) and the configured expand hint. Live lines never quote token totals — mid-run totals are cache-inflated. The call-level status dot belongs to the tool shell; run and activity rows only use `›`, `○`, `✓`, `×`, and `■`, and parallel run rows carry stable dim ordinals that match the expanded section numbers. `Ctrl+O` expands each run into a report cover sheet: a status line, a metrics line, a fixed two-line `Prompt` preview (`… continues, N more lines`, honest about the 1KB bound), an `Activity · last n of N` digest whose successful rows are quiet one-liners, a `Working` tail while streaming, and the full `Report` (or `Error`, with `Report · partial` for salvaged output) rendered as Markdown. Expanded batches start with a numbered contents list whose numbers match the `──` section headers.
|
|
89
89
|
- Full child transcripts are not stored separately. Bounded run details remain in the parent tool result, so completed calls restore naturally in the parent session tree.
|
|
90
90
|
- Nested usage is returned with the tool result and included in parent session accounting.
|
|
91
91
|
|
|
@@ -4,23 +4,27 @@ Adds a `todo` tool plus `/todos` for multi-step work, with a live overlay above
|
|
|
4
4
|
|
|
5
5
|
## Behavior
|
|
6
6
|
|
|
7
|
-
- Actions: `create`, `update`, `list`, `get`, `delete`, `clear`. Statuses: `pending`, `in_progress`, `completed`, `deleted`. `list` accepts an optional `status` filter and `includeDeleted`; `status: "deleted"` includes tombstones directly.
|
|
7
|
+
- Actions: `create`, `create_many`, `update`, `list`, `get`, `delete`, `clear`. Statuses: `pending`, `in_progress`, `completed`, `deleted`. `create_many` atomically creates up to 20 pending tasks in input order (all-or-nothing); every item needs a `subject` and `description`, and unique batch `key` values let `blockedByKeys` name prerequisites in the same batch. `list` accepts an optional `status` filter and `includeDeleted`; `status: "deleted"` includes tombstones directly. It also supports `limit`/`afterId` pagination, a case-insensitive subject/description `query`, and `unblockedOnly` for tasks with no unresolved dependencies. Parameters that do not apply to the chosen action are rejected with guidance (for example `blockedBy` on `update` points at `addBlockedBy`/`removeBlockedBy`) instead of being silently ignored.
|
|
8
8
|
- The live overlay renders above the editor and hides itself when there are no visible tasks.
|
|
9
|
-
- Creation always starts a task as `pending
|
|
10
|
-
-
|
|
9
|
+
- Creation always starts a task as `pending` and requires both `subject` and `description` (what done means); `blockedBy` dependencies are supported for sequencing, and missing dependencies, deleted dependencies, self-dependencies, and cycles are rejected. A task cannot move to `in_progress` or `completed` until every `blockedBy` dependency is `completed`. If a new unresolved dependency blocks an active task, it is automatically moved to `pending` and named in the tool result.
|
|
10
|
+
- `update` manages both edge directions: `addBlockedBy`/`removeBlockedBy` change what the task waits on, `addBlocks`/`removeBlocks` change what waits on it (stored as the targets' `blockedBy`, with the same validation and cycle checks). An empty string removes `description`, `activeForm`, or `owner`; `metadata` merges with `null` deleting keys.
|
|
11
|
+
- `list` lines show only unresolved blockers plus the task's `@owner`; full `blockedBy`/`blocks` detail lives in `get`. `/todos` uses the same 50-task display bound and directs longer lists to paged `todo list` calls.
|
|
12
|
+
- Deletion — via `delete` or `update` with `status: "deleted"` — leaves an immutable tombstone for branch history, removes that task from every dependent `blockedBy` list, and therefore releases dependents; the tool result names pending dependents that ended up fully unblocked. A deletion cannot be combined with other field edits in the same call. `clear` requires `confirm: true` and an `expectedCount` equal to the current task count, then empties the list without reusing ids.
|
|
11
13
|
- Newly completed tasks remain visible for 30 seconds, then drop from the overlay; historical completions loaded by `/reload` or `/tree` stay hidden so live work remains prominent.
|
|
12
14
|
- **Exactly one `in_progress`.** Moving a task to `in_progress` auto-demotes any other `in_progress` tasks to `pending`; the tool result lists demoted ids so the model sees the side effect.
|
|
13
15
|
- **Verification soft nudge.** When a completion leaves the list fully done with 3+ completed tasks and no subject/description matching `verif|test|check|review`, the tool result appends a short NOTE (text only — `details` schema unchanged).
|
|
14
16
|
|
|
15
17
|
## Limits
|
|
16
18
|
|
|
17
|
-
- `list` output is bounded at 50 items by default,
|
|
19
|
+
- `list` output is bounded at 50 items by default; `limit` accepts 1–50, and a paged result includes its next `afterId` when truncated.
|
|
20
|
+
- Task input is bounded: subjects and active forms are limited to 240 characters, descriptions to 4,000, owners to 160, batch keys to 120, and dependency lists to 50 entries. List queries are limited to 200 characters.
|
|
21
|
+
- `metadata` is a bounded JSON-like object: 8 levels deep, 100 entries, 160-character keys, and 16 KiB total UTF-8 payload.
|
|
18
22
|
- The overlay body is capped at 10 rows; its truncation summary includes hidden status counts.
|
|
19
23
|
- Newly completed tasks remain visible for 30 seconds then drop from the overlay.
|
|
20
24
|
|
|
21
25
|
## Implementation notes
|
|
22
26
|
|
|
23
|
-
- **Conversation-backed state.** Every tool result stores a full snapshot in `details`; lifecycle handlers replay the current branch on `/reload`, compaction, and session-tree navigation. There is no separate disk database. Compaction-safe by design: `sessionManager.getBranch()` returns the full branch history — only `buildSessionContext` summarizes for the LLM. Replay walks the branch **tail → head**
|
|
27
|
+
- **Conversation-backed state.** Every tool result stores a schema-versioned full snapshot in `details`; lifecycle handlers replay the current branch on `/reload`, compaction, and session-tree navigation. There is no separate disk database. Compaction-safe by design: `sessionManager.getBranch()` returns the full branch history — only `buildSessionContext` summarizes for the LLM. Replay walks the branch **tail → head** to find the latest valid snapshot, accepting compatible unversioned history while rejecting malformed or unsupported versioned details.
|
|
24
28
|
- **State is keyed per session id.** Resume and `/tree` can switch sessions within one process; `execute` and the lifecycle handlers re-point the active bucket before touching state.
|
|
25
29
|
- **Status transitions are gated.** `completed` can be reopened to `in_progress` or `pending`, but a `deleted` tombstone is terminal. Single-active is enforced on demote (not hard-reject) so the model does not need a retry loop.
|
|
26
30
|
- **Tool execution is sequential** (`executionMode: "sequential"`) so parallel tool calls cannot race on in-memory state. Validation failures **throw** so Pi marks `isError: true` and the branch replays the last good snapshot.
|
|
@@ -31,7 +31,7 @@ The native tool presentation owns:
|
|
|
31
31
|
|
|
32
32
|
Built-in renderers remain responsible for semantic content such as file paths, syntax highlighting, search results, Diff previews, and command output. The outer shell is native so built-in tools, bundled extensions, and compatible third-party tools share the same presentation.
|
|
33
33
|
|
|
34
|
-
Consecutive tools may opt into a shared collapsed group through `toolGroup`. Built-in `read` and `find` calls use the `explore` group: their call rows render as one compact run with a single leading gap, while `Ctrl+O` restores each tool's complete call and result.
|
|
34
|
+
Consecutive tools may opt into a shared collapsed group through `toolGroup`. Built-in `read` and `find` calls use the `explore` group, and the bundled `todo` tool uses the `todo` group: their call rows render as one compact run with a single leading gap, while `Ctrl+O` restores each tool's complete call and result. Collapsed Todo rows are result-aware: successful current snapshots summarize the completed operation (created ids, update/get status, list counts, deletion, or clear) rather than only its arguments. Failures include a sanitized reason capped at 120 characters; missing or unrecognized result details fall back safely to the call summary.
|
|
35
35
|
|
|
36
36
|
## Renderer inheritance
|
|
37
37
|
|
package/docs/extensions.md
CHANGED
|
@@ -2462,6 +2462,20 @@ Extensions can interact with users via `ctx.ui` methods and customize how messag
|
|
|
2462
2462
|
// Select from options
|
|
2463
2463
|
const choice = await ctx.ui.select("Pick one:", ["A", "B", "C"]);
|
|
2464
2464
|
|
|
2465
|
+
// Options can carry a muted description line, so the action and its
|
|
2466
|
+
// trade-off can be scanned separately. select() still resolves to the label.
|
|
2467
|
+
const action = await ctx.ui.select(
|
|
2468
|
+
"Approve?",
|
|
2469
|
+
[
|
|
2470
|
+
{ label: "Start executing", description: "keep full context" },
|
|
2471
|
+
{ label: "Compact, then execute", description: "best for long tasks" },
|
|
2472
|
+
],
|
|
2473
|
+
{
|
|
2474
|
+
subtitle: "Context now 29% full", // muted line under the title
|
|
2475
|
+
cancelHint: "keep planning", // Esc is not destructive here
|
|
2476
|
+
},
|
|
2477
|
+
);
|
|
2478
|
+
|
|
2465
2479
|
// Confirm dialog
|
|
2466
2480
|
const ok = await ctx.ui.confirm("Delete?", "This cannot be undone");
|
|
2467
2481
|
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astralyn/pi",
|
|
3
|
-
"version": "0.82.
|
|
3
|
+
"version": "0.82.6",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@astralyn/pi",
|
|
9
|
-
"version": "0.82.
|
|
9
|
+
"version": "0.82.6",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@earendil-works/pi-agent-core": "0.82.1",
|
package/package.json
CHANGED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Resolve the fallback encoding for console output on this system.
|
|
3
|
-
*
|
|
4
|
-
* On Windows, cmd.exe builtins, Windows PowerShell, and classic console tools
|
|
5
|
-
* such as ipconfig emit piped output in the legacy OEM code page (GBK on
|
|
6
|
-
* Simplified Chinese systems, Shift_JIS on Japanese systems, and so on)
|
|
7
|
-
* rather than UTF-8. Returns the TextDecoder label for that code page, or
|
|
8
|
-
* null when output should stay UTF-8: non-Windows platforms, UTF-8 systems
|
|
9
|
-
* (code page 65001), and code pages TextDecoder cannot represent.
|
|
10
|
-
*/
|
|
11
|
-
export declare function getConsoleFallbackEncoding(): string | null;
|
|
12
|
-
export interface DecodedChunk {
|
|
13
|
-
/** Decoded text for this push, or the full re-decoded transcript when rewound. */
|
|
14
|
-
text: string;
|
|
15
|
-
/**
|
|
16
|
-
* True when the decoder switched to the fallback encoding. All previously
|
|
17
|
-
* returned text is invalid; `text` replaces the entire transcript.
|
|
18
|
-
*/
|
|
19
|
-
rewound: boolean;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Streaming decoder that assumes UTF-8 and falls back to the system console
|
|
23
|
-
* encoding when the stream turns out not to be UTF-8.
|
|
24
|
-
*
|
|
25
|
-
* Decoding starts as plain streaming UTF-8 — the fast path for virtually all
|
|
26
|
-
* output. When a replacement character appears, the bytes seen so far are
|
|
27
|
-
* checked with isUtf8: if they are genuinely invalid UTF-8, everything is
|
|
28
|
-
* re-decoded with the fallback encoding and the result is reported with
|
|
29
|
-
* `rewound: true` so the caller can rebuild its derived state. If the bytes
|
|
30
|
-
* are valid UTF-8 (the source itself contained replacement characters),
|
|
31
|
-
* detection stops and UTF-8 decoding continues.
|
|
32
|
-
*
|
|
33
|
-
* Raw bytes are retained only while detection is active and only up to
|
|
34
|
-
* `detectionLimitBytes`; larger streams lock to UTF-8 and memory stays flat.
|
|
35
|
-
*/
|
|
36
|
-
export declare class OutputDecoder {
|
|
37
|
-
private decoder;
|
|
38
|
-
private readonly fallbackEncoding;
|
|
39
|
-
private readonly detectionLimitBytes;
|
|
40
|
-
private pending;
|
|
41
|
-
private pendingBytes;
|
|
42
|
-
private detecting;
|
|
43
|
-
constructor(options?: {
|
|
44
|
-
fallbackEncoding?: string | null;
|
|
45
|
-
detectionLimitBytes?: number;
|
|
46
|
-
});
|
|
47
|
-
push(data: Buffer): DecodedChunk;
|
|
48
|
-
flush(): DecodedChunk;
|
|
49
|
-
private tryRewind;
|
|
50
|
-
private stopDetecting;
|
|
51
|
-
}
|
|
52
|
-
//# sourceMappingURL=output-decoder.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"output-decoder.d.ts","sourceRoot":"","sources":["../../src/utils/output-decoder.ts"],"names":[],"mappings":"AAqBA;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,IAAI,MAAM,GAAG,IAAI,CAK1D;AA4BD,MAAM,WAAW,YAAY;IAC5B,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,OAAO,EAAE,OAAO,CAAC;CACjB;AAID;;;;;;;;;;;;;;GAcG;AACH,qBAAa,aAAa;IACzB,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAgB;IACjD,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAS;IAC7C,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,SAAS,CAAU;IAE3B,YAAY,OAAO,GAAE;QAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,mBAAmB,CAAC,EAAE,MAAM,CAAA;KAAO,EAK3F;IAED,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAc/B;IAED,KAAK,IAAI,YAAY,CAOpB;IAED,OAAO,CAAC,SAAS;IAmBjB,OAAO,CAAC,aAAa;CAKrB","sourcesContent":["import { isUtf8 } from \"node:buffer\";\nimport { spawnSync } from \"child_process\";\n\n/**\n * Windows OEM code pages mapped to WHATWG TextDecoder labels. Console\n * programs emit piped output in the OEM code page, so only code pages that\n * TextDecoder can represent are listed; others keep UTF-8 decoding.\n */\nconst OEM_CODE_PAGE_ENCODINGS: Record<string, string> = {\n\t\"866\": \"ibm866\",\n\t\"874\": \"windows-874\",\n\t\"932\": \"shift_jis\",\n\t\"936\": \"gbk\",\n\t\"949\": \"euc-kr\",\n\t\"950\": \"big5\",\n\t\"1251\": \"windows-1251\",\n\t\"1252\": \"windows-1252\",\n};\n\nlet cachedConsoleFallbackEncoding: string | null | undefined;\n\n/**\n * Resolve the fallback encoding for console output on this system.\n *\n * On Windows, cmd.exe builtins, Windows PowerShell, and classic console tools\n * such as ipconfig emit piped output in the legacy OEM code page (GBK on\n * Simplified Chinese systems, Shift_JIS on Japanese systems, and so on)\n * rather than UTF-8. Returns the TextDecoder label for that code page, or\n * null when output should stay UTF-8: non-Windows platforms, UTF-8 systems\n * (code page 65001), and code pages TextDecoder cannot represent.\n */\nexport function getConsoleFallbackEncoding(): string | null {\n\tif (cachedConsoleFallbackEncoding === undefined) {\n\t\tcachedConsoleFallbackEncoding = detectConsoleFallbackEncoding();\n\t}\n\treturn cachedConsoleFallbackEncoding;\n}\n\nfunction detectConsoleFallbackEncoding(): string | null {\n\tif (process.platform !== \"win32\") {\n\t\treturn null;\n\t}\n\ttry {\n\t\tconst result = spawnSync(\n\t\t\t\"reg\",\n\t\t\t[\"query\", \"HKLM\\\\SYSTEM\\\\CurrentControlSet\\\\Control\\\\Nls\\\\CodePage\", \"/v\", \"OEMCP\"],\n\t\t\t{ encoding: \"utf-8\", timeout: 5000, windowsHide: true },\n\t\t);\n\t\tif (result.status !== 0 || !result.stdout) {\n\t\t\treturn null;\n\t\t}\n\t\tconst match = result.stdout.match(/OEMCP\\s+REG_SZ\\s+(\\d+)/);\n\t\tconst encoding = match ? OEM_CODE_PAGE_ENCODINGS[match[1]] : undefined;\n\t\tif (!encoding) {\n\t\t\treturn null;\n\t\t}\n\t\t// Node builds with small-icu lack these decoders; verify availability.\n\t\tnew TextDecoder(encoding);\n\t\treturn encoding;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nexport interface DecodedChunk {\n\t/** Decoded text for this push, or the full re-decoded transcript when rewound. */\n\ttext: string;\n\t/**\n\t * True when the decoder switched to the fallback encoding. All previously\n\t * returned text is invalid; `text` replaces the entire transcript.\n\t */\n\trewound: boolean;\n}\n\nconst DEFAULT_DETECTION_LIMIT_BYTES = 256 * 1024;\n\n/**\n * Streaming decoder that assumes UTF-8 and falls back to the system console\n * encoding when the stream turns out not to be UTF-8.\n *\n * Decoding starts as plain streaming UTF-8 — the fast path for virtually all\n * output. When a replacement character appears, the bytes seen so far are\n * checked with isUtf8: if they are genuinely invalid UTF-8, everything is\n * re-decoded with the fallback encoding and the result is reported with\n * `rewound: true` so the caller can rebuild its derived state. If the bytes\n * are valid UTF-8 (the source itself contained replacement characters),\n * detection stops and UTF-8 decoding continues.\n *\n * Raw bytes are retained only while detection is active and only up to\n * `detectionLimitBytes`; larger streams lock to UTF-8 and memory stays flat.\n */\nexport class OutputDecoder {\n\tprivate decoder = new TextDecoder();\n\tprivate readonly fallbackEncoding: string | null;\n\tprivate readonly detectionLimitBytes: number;\n\tprivate pending: Buffer[] = [];\n\tprivate pendingBytes = 0;\n\tprivate detecting: boolean;\n\n\tconstructor(options: { fallbackEncoding?: string | null; detectionLimitBytes?: number } = {}) {\n\t\tthis.fallbackEncoding =\n\t\t\toptions.fallbackEncoding === undefined ? getConsoleFallbackEncoding() : options.fallbackEncoding;\n\t\tthis.detectionLimitBytes = options.detectionLimitBytes ?? DEFAULT_DETECTION_LIMIT_BYTES;\n\t\tthis.detecting = this.fallbackEncoding !== null;\n\t}\n\n\tpush(data: Buffer): DecodedChunk {\n\t\tconst text = this.decoder.decode(data, { stream: true });\n\t\tif (!this.detecting) {\n\t\t\treturn { text, rewound: false };\n\t\t}\n\t\tthis.pending.push(data);\n\t\tthis.pendingBytes += data.length;\n\t\tif (text.includes(\"�\")) {\n\t\t\treturn this.tryRewind(text);\n\t\t}\n\t\tif (this.pendingBytes > this.detectionLimitBytes) {\n\t\t\tthis.stopDetecting();\n\t\t}\n\t\treturn { text, rewound: false };\n\t}\n\n\tflush(): DecodedChunk {\n\t\tconst text = this.decoder.decode();\n\t\tif (this.detecting && text.includes(\"�\")) {\n\t\t\treturn this.tryRewind(text);\n\t\t}\n\t\tthis.stopDetecting();\n\t\treturn { text, rewound: false };\n\t}\n\n\tprivate tryRewind(utf8Text: string): DecodedChunk {\n\t\tconst raw = Buffer.concat(this.pending);\n\t\tif (isUtf8(raw)) {\n\t\t\t// The replacement characters exist in the source bytes themselves;\n\t\t\t// the stream is valid UTF-8, so stop watching and keep it.\n\t\t\tthis.stopDetecting();\n\t\t\treturn { text: utf8Text, rewound: false };\n\t\t}\n\t\ttry {\n\t\t\tthis.decoder = new TextDecoder(this.fallbackEncoding as string);\n\t\t} catch {\n\t\t\tthis.stopDetecting();\n\t\t\treturn { text: utf8Text, rewound: false };\n\t\t}\n\t\tconst text = this.decoder.decode(raw, { stream: true });\n\t\tthis.stopDetecting();\n\t\treturn { text, rewound: true };\n\t}\n\n\tprivate stopDetecting(): void {\n\t\tthis.detecting = false;\n\t\tthis.pending = [];\n\t\tthis.pendingBytes = 0;\n\t}\n}\n"]}
|