@astralyn/pi 0.84.2 → 0.84.4
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 +41 -0
- 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/project-trust.d.ts.map +1 -1
- package/dist/cli/project-trust.js +1 -4
- package/dist/cli/project-trust.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 +2 -27
- 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/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/agents-command.d.ts +4 -0
- package/dist/extensions/subagent/agents-command.d.ts.map +1 -0
- package/dist/extensions/subagent/agents-command.js +230 -0
- package/dist/extensions/subagent/agents-command.js.map +1 -0
- package/dist/extensions/subagent/agents.d.ts.map +1 -1
- package/dist/extensions/subagent/agents.js +2 -0
- package/dist/extensions/subagent/agents.js.map +1 -1
- package/dist/extensions/subagent/display-name.d.ts +6 -0
- package/dist/extensions/subagent/display-name.d.ts.map +1 -0
- package/dist/extensions/subagent/display-name.js +13 -0
- package/dist/extensions/subagent/display-name.js.map +1 -0
- package/dist/extensions/subagent/index.d.ts.map +1 -1
- package/dist/extensions/subagent/index.js +19 -126
- 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 +1 -10
- package/dist/extensions/subagent/render.js.map +1 -1
- package/dist/extensions/subagent/settings.d.ts +0 -1
- package/dist/extensions/subagent/settings.d.ts.map +1 -1
- package/dist/extensions/subagent/settings.js +3 -7
- package/dist/extensions/subagent/settings.js.map +1 -1
- package/dist/extensions/subagent/types.d.ts +2 -0
- package/dist/extensions/subagent/types.d.ts.map +1 -1
- package/dist/extensions/subagent/types.js.map +1 -1
- package/dist/extensions/subagent/ui/model-list.d.ts +67 -0
- package/dist/extensions/subagent/ui/model-list.d.ts.map +1 -0
- package/dist/extensions/subagent/ui/model-list.js +242 -0
- package/dist/extensions/subagent/ui/model-list.js.map +1 -0
- package/dist/extensions/subagent/ui/profile-editor.d.ts +54 -0
- package/dist/extensions/subagent/ui/profile-editor.d.ts.map +1 -0
- package/dist/extensions/subagent/ui/profile-editor.js +154 -0
- package/dist/extensions/subagent/ui/profile-editor.js.map +1 -0
- package/dist/extensions/subagent/ui/profile-picker.d.ts +32 -0
- package/dist/extensions/subagent/ui/profile-picker.d.ts.map +1 -0
- package/dist/extensions/subagent/ui/profile-picker.js +149 -0
- package/dist/extensions/subagent/ui/profile-picker.js.map +1 -0
- package/dist/extensions/todo/constants.d.ts +6 -17
- package/dist/extensions/todo/constants.d.ts.map +1 -1
- package/dist/extensions/todo/constants.js +20 -31
- package/dist/extensions/todo/constants.js.map +1 -1
- package/dist/extensions/todo/index.d.ts.map +1 -1
- package/dist/extensions/todo/index.js +36 -57
- package/dist/extensions/todo/index.js.map +1 -1
- package/dist/extensions/todo/schema.d.ts +20 -59
- package/dist/extensions/todo/schema.d.ts.map +1 -1
- package/dist/extensions/todo/schema.js +23 -69
- package/dist/extensions/todo/schema.js.map +1 -1
- package/dist/extensions/todo/state.d.ts +23 -65
- package/dist/extensions/todo/state.d.ts.map +1 -1
- package/dist/extensions/todo/state.js +238 -1262
- package/dist/extensions/todo/state.js.map +1 -1
- package/dist/extensions/todo/view.d.ts +19 -10
- package/dist/extensions/todo/view.d.ts.map +1 -1
- package/dist/extensions/todo/view.js +411 -463
- package/dist/extensions/todo/view.js.map +1 -1
- package/dist/extensions/todo/widget.d.ts +15 -0
- package/dist/extensions/todo/widget.d.ts.map +1 -0
- package/dist/extensions/todo/widget.js +50 -0
- package/dist/extensions/todo/widget.js.map +1 -0
- package/dist/modes/interactive/components/extension-selector.d.ts +1 -4
- package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-selector.js +6 -12
- 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/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +1 -8
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/docs/bundled/README.md +1 -1
- package/docs/bundled/extensions/subagent.md +20 -5
- package/docs/bundled/extensions/todo.md +84 -24
- package/docs/bundled/tool-presentation.md +3 -3
- package/docs/extensions.md +6 -23
- package/docs/settings.md +1 -1
- package/docs/usage.md +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/dist/extensions/subagent/picker.d.ts +0 -31
- package/dist/extensions/subagent/picker.d.ts.map +0 -1
- package/dist/extensions/subagent/picker.js +0 -109
- package/dist/extensions/subagent/picker.js.map +0 -1
- package/dist/extensions/todo/overlay.d.ts +0 -24
- package/dist/extensions/todo/overlay.d.ts.map +0 -1
- package/dist/extensions/todo/overlay.js +0 -135
- package/dist/extensions/todo/overlay.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
|
@@ -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"]}
|