@astralyn/pi 0.84.2 → 0.84.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -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/startup-ui.d.ts.map +1 -1
- package/dist/cli/startup-ui.js +3 -5
- package/dist/cli/startup-ui.js.map +1 -1
- package/dist/config.d.ts +5 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -1
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +1 -25
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +29 -408
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction-controller.d.ts +89 -0
- package/dist/core/compaction-controller.d.ts.map +1 -0
- package/dist/core/compaction-controller.js +428 -0
- package/dist/core/compaction-controller.js.map +1 -0
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.js +3 -3
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/types.d.ts +1 -15
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/provider-attribution.js +2 -2
- package/dist/core/provider-attribution.js.map +1 -1
- package/dist/core/settings-manager.js +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/telemetry.d.ts +1 -1
- package/dist/core/telemetry.d.ts.map +1 -1
- package/dist/core/telemetry.js +1 -1
- package/dist/core/telemetry.js.map +1 -1
- package/dist/extensions/deepwiki/index.d.ts.map +1 -1
- package/dist/extensions/deepwiki/index.js +23 -1
- package/dist/extensions/deepwiki/index.js.map +1 -1
- package/dist/extensions/deepwiki/render.d.ts +1 -1
- package/dist/extensions/deepwiki/render.d.ts.map +1 -1
- package/dist/extensions/deepwiki/render.js +4 -2
- package/dist/extensions/deepwiki/render.js.map +1 -1
- package/dist/extensions/question/index.d.ts.map +1 -1
- package/dist/extensions/question/index.js +0 -1
- package/dist/extensions/question/index.js.map +1 -1
- package/dist/extensions/subagent/index.d.ts.map +1 -1
- package/dist/extensions/subagent/index.js +20 -2
- package/dist/extensions/subagent/index.js.map +1 -1
- package/dist/modes/interactive/components/extension-selector.d.ts +0 -2
- package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-selector.js +1 -1
- package/dist/modes/interactive/components/extension-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +1 -1
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-chrome.d.ts +28 -0
- package/dist/modes/interactive/components/tool-chrome.d.ts.map +1 -0
- package/dist/modes/interactive/components/tool-chrome.js +81 -0
- package/dist/modes/interactive/components/tool-chrome.js.map +1 -0
- package/dist/modes/interactive/components/tool-execution.d.ts +0 -3
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +11 -108
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +35 -52
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/docs/bundled/extensions/subagent.md +1 -1
- package/docs/bundled/tool-presentation.md +2 -2
- 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/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
|
@@ -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 a live `Retrying (n/m) in Xs…` countdown), and a failure that produced no work (for example a preflight auth throw) is rerun at the task level up to two more times. Provider and task retries share one absolute-deadline state; at zero it reads `Retrying now…`, and parent abort/session shutdown interrupts backoff immediately. Runs with partial work behind them are never rerun.
|
|
88
|
-
- Progress is rendered in Pi's native tool transcript. While a call is running, the
|
|
88
|
+
- Progress is rendered in Pi's native tool transcript. While a call is running, the Subagent renderer re-schedules its own repaint once per second, independently of worker output and the main Working spinner. Silent single and parallel runs therefore keep advancing their own elapsed clocks in collapsed and expanded views, and queued retries keep counting down. Once settled, the final duration is fixed and refresh stops.
|
|
89
89
|
- The collapsed view stays lean in both phases: while running, one bounded 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 sentence-aware result excerpt 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 aligned 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 `── N · ✓ Agent` section headers.
|
|
90
90
|
- Failed and aborted batch runs place their bounded reason on a separate error-colored line below the run headline instead of cutting it into the status row. Reasons are capped at 200 characters and keep their hanging indent when wrapped; successful settled excerpts prefer complete leading sentences within their 96-character budget. Rounded durations switch to minute form at 60 seconds without producing `1m 60s`.
|
|
91
91
|
- 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.
|
|
@@ -12,9 +12,9 @@ The `@astralyn/pi` package uses Pi's native tool transcript presentation rather
|
|
|
12
12
|
│ final result line
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
The dim result rail continues through every visual line; an empty output line renders as a bare `│`. Calls keep their status in the leading dot: warning while pending or running, green after success, and red after failure. For
|
|
15
|
+
The dim result rail continues through every visual line; an empty output line renders as a bare `│`. Calls keep their status in the leading dot: warning while pending or running, green after success, and red after failure. For bash commands that are still running after two seconds, the shell adds a live result row such as `Running… (2.1s)` and removes it when the call settles. Other tools with live progress render it themselves; tools using `renderShell: "self"` continue to own their entire presentation.
|
|
16
16
|
|
|
17
|
-
Time-driven renderers
|
|
17
|
+
Time-driven renderers schedule their own repaints: while a result is partial they arm a timer in renderer state and call the render context's `invalidate()`, clearing the timer on the first settled render. Renderers compute elapsed/countdown values from absolute timestamps or deadlines, so a delayed repaint never changes the underlying wall-clock meaning.
|
|
18
18
|
|
|
19
19
|
## Implementation boundary
|
|
20
20
|
|
package/docs/extensions.md
CHANGED
|
@@ -1996,10 +1996,6 @@ pi.registerTool({
|
|
|
1996
1996
|
},
|
|
1997
1997
|
|
|
1998
1998
|
// Optional: Custom rendering
|
|
1999
|
-
// rendersOwnProgress suppresses only the shell's generic Running… row.
|
|
2000
|
-
rendersOwnProgress: true,
|
|
2001
|
-
// While the result is partial, rebuild renderCall/renderResult every second.
|
|
2002
|
-
renderRefreshIntervalMs: 1000,
|
|
2003
1999
|
renderCall(args, theme, context) { ... },
|
|
2004
2000
|
renderResult(result, options, theme, context) { ... },
|
|
2005
2001
|
});
|
|
@@ -2253,12 +2249,7 @@ By default, tool output is wrapped in Pi's native call/result shell, which owns
|
|
|
2253
2249
|
|
|
2254
2250
|
Set `renderShell: "self"` when the tool should render its own shell instead of using the native shell. This is useful for tools that need complete control over framing or background behavior, for example large previews that must stay visually stable after the tool settles.
|
|
2255
2251
|
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
- `rendersOwnProgress: true` suppresses only the native shell's generic `Running… (Ns)` row. It does not schedule renderer updates.
|
|
2259
|
-
- `renderRefreshIntervalMs` periodically rebuilds `renderCall` and `renderResult` while the tool result is partial. It does not suppress generic progress.
|
|
2260
|
-
|
|
2261
|
-
Use both when a tool renders its own elapsed time, retry countdown, or similar progress UI. Refresh intervals are rounded and constrained to 250–60,000ms. The shell may rebuild sooner when another native concern needs it, so the configured interval is the maximum refresh gap rather than an exclusive cadence. Refresh stops when the result settles or the row is disposed.
|
|
2252
|
+
For live progress UI (elapsed time, retry countdowns), schedule repaints from the renderer itself: store a timer handle in `context.state`, arm a `setTimeout` that calls `context.invalidate()` while `options.isPartial` is true, and clear it on the first settled render. `context.invalidate()` is a no-op after the row is disposed, so a trailing timeout is harmless.
|
|
2262
2253
|
|
|
2263
2254
|
Derive displayed time from an absolute timestamp or deadline (`Date.now()`), not by decrementing a counter on every refresh. A refresh is only a repaint opportunity; event-loop stalls and machine sleep must not extend a wall-clock timeout.
|
|
2264
2255
|
|
|
@@ -2368,8 +2359,7 @@ Custom editors and `ctx.ui.custom()` components receive `keybindings: Keybinding
|
|
|
2368
2359
|
- Use `Text` with padding `(0, 0)`. The native shell handles padding.
|
|
2369
2360
|
- Use `\n` for multi-line content.
|
|
2370
2361
|
- Handle `isPartial` for streaming progress.
|
|
2371
|
-
-
|
|
2372
|
-
- Set `rendersOwnProgress` separately when the renderer replaces the native `Running…` row.
|
|
2362
|
+
- For time-driven renderers, self-schedule repaints with `context.state` timers and `context.invalidate()`, and compute displayed values from absolute time.
|
|
2373
2363
|
- Support `expanded` for detail on demand.
|
|
2374
2364
|
- Keep default view compact.
|
|
2375
2365
|
- Read `context.args` in `renderResult` instead of copying args into `context.state`.
|
|
@@ -2542,17 +2532,10 @@ const choice = await ctx.ui.select("Pick one:", ["A", "B", "C"]);
|
|
|
2542
2532
|
|
|
2543
2533
|
// Options can carry a muted description line, so the action and its
|
|
2544
2534
|
// trade-off can be scanned separately. select() still resolves to the label.
|
|
2545
|
-
const action = await ctx.ui.select(
|
|
2546
|
-
"
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
{ label: "Compact, then execute", description: "best for long tasks" },
|
|
2550
|
-
],
|
|
2551
|
-
{
|
|
2552
|
-
subtitle: "Context now 29% full", // muted line under the title
|
|
2553
|
-
cancelHint: "go back", // Describe what Esc actually does
|
|
2554
|
-
},
|
|
2555
|
-
);
|
|
2535
|
+
const action = await ctx.ui.select("Approve?", [
|
|
2536
|
+
{ label: "Start executing", description: "keep full context" },
|
|
2537
|
+
{ label: "Compact, then execute", description: "best for long tasks" },
|
|
2538
|
+
]);
|
|
2556
2539
|
|
|
2557
2540
|
// Confirm dialog
|
|
2558
2541
|
const ok = await ctx.ui.confirm("Delete?", "This cannot be undone");
|
package/docs/settings.md
CHANGED
|
@@ -65,7 +65,7 @@ Use `/trust` in interactive mode to save a project trust decision for future ses
|
|
|
65
65
|
| `outputPad` | number | `1` | Horizontal padding for user messages, assistant messages, and thinking (0 or 1) |
|
|
66
66
|
| `autocompleteMaxVisible` | number | `5` | Max visible items in autocomplete dropdown (3-20) |
|
|
67
67
|
| `showHardwareCursor` | boolean | `false` | Show the terminal cursor while TUI positions it for IME support |
|
|
68
|
-
| `tuiMode` | string | `"
|
|
68
|
+
| `tuiMode` | string | `"fullscreen"` | Interactive TUI mode: `"fullscreen"` or `"regular"`. Changes from `/settings` apply immediately; `--tui-mode` overrides this setting at startup |
|
|
69
69
|
| `fullscreenScrollbar` | string | `"auto"` | Fullscreen transcript scrollbar: `"auto"` shows it temporarily while scrolling, `"always"` reserves the rightmost column and keeps it visible, and `"hidden"` hides it. Has no effect in regular TUI mode |
|
|
70
70
|
|
|
71
71
|
For VS Code, include `--wait` so pi resumes after the editor exits:
|
package/docs/usage.md
CHANGED
|
@@ -233,7 +233,7 @@ pi --no-extensions -e ./my-extension.ts
|
|
|
233
233
|
|--------|-------------|
|
|
234
234
|
| `--system-prompt <text>` | Replace default prompt; context files and skills are still appended |
|
|
235
235
|
| `--append-system-prompt <text>` | Append to system prompt |
|
|
236
|
-
| `--tui-mode <mode>` | TUI mode: `
|
|
236
|
+
| `--tui-mode <mode>` | TUI mode: `fullscreen` (default) or `regular` |
|
|
237
237
|
| `--verbose` | Force verbose startup |
|
|
238
238
|
| `-a`, `--approve` | Trust project-local files for this run |
|
|
239
239
|
| `-na`, `--no-approve` | Ignore project-local files for this run |
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astralyn/pi",
|
|
3
|
-
"version": "0.84.
|
|
3
|
+
"version": "0.84.3",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@astralyn/pi",
|
|
9
|
-
"version": "0.84.
|
|
9
|
+
"version": "0.84.3",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@earendil-works/pi-agent-core": "0.84.1",
|
package/package.json
CHANGED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import { ProcessTerminal } from "@earendil-works/pi-tui";
|
|
2
|
-
/**
|
|
3
|
-
* ProcessTerminal that (1) coalesces every write issued within one synchronous
|
|
4
|
-
* task into a single stdout write and (2) keeps content-driven full redraws
|
|
5
|
-
* from destroying the terminal's scrollback.
|
|
6
|
-
*
|
|
7
|
-
* Coalescing: pi-tui emits each render pass as several separate stdout
|
|
8
|
-
* writes: the frame wrapped in synchronized output (CSI ?2026h/l), then a
|
|
9
|
-
* relative cursor move that parks the hardware cursor at the focused
|
|
10
|
-
* component's caret for IME composition, then a cursor visibility toggle.
|
|
11
|
-
* Terminals that anchor the IME candidate window to the hardware cursor
|
|
12
|
-
* (Windows Terminal and other ConPTY hosts, WezTerm) can process the frame
|
|
13
|
-
* write before the reposition write arrives, briefly leaving the cursor at
|
|
14
|
-
* the end of the last repainted line. With an IME active this makes the
|
|
15
|
-
* candidate window jump between the caret and the right edge of the input
|
|
16
|
-
* line on every keystroke. Merging the writes makes each frame atomic, so the
|
|
17
|
-
* only cursor position a terminal can observe is the final one.
|
|
18
|
-
*
|
|
19
|
-
* Scrollback preservation: pi-tui's fullRender(clear) opens with
|
|
20
|
-
* `2J H 3J`, wiping the terminal's scrollback, and then replays the entire
|
|
21
|
-
* transcript. During streaming, content-driven triggers (a line changing
|
|
22
|
-
* above the viewport during markdown reflow) fire this routinely, which — in
|
|
23
|
-
* terminals that clamp the scroll offset when scrollback vanishes, notably
|
|
24
|
-
* Windows Terminal — yanks a scrolled-up reader to the top of the buffer on
|
|
25
|
-
* every reflow (upstream #6502, #5576, #6050; rejected upstream fix #4204).
|
|
26
|
-
*
|
|
27
|
-
* Preservation is therefore windowed, not permanent: the host marks agent
|
|
28
|
-
* runs via setAgentRunActive, which turns preservation on at run start and
|
|
29
|
-
* keeps it on until the user's next input after the run settles, since they
|
|
30
|
-
* may still be scrolled up reading. The input half of the window is observed
|
|
31
|
-
* directly on this terminal's own input path, so it needs no TUI listener
|
|
32
|
-
* and survives renderer switches (which reuse this terminal) untouched. An
|
|
33
|
-
* idle-session full redraw — Ctrl+O toggles, mode switches — passes through
|
|
34
|
-
* with upstream's wipe-and-replay, whose result is byte-perfect: scrollback
|
|
35
|
-
* becomes the clean new transcript. Mid-run user toggles can arm
|
|
36
|
-
* passNextFullRedraw for the same clean replay on their own frame.
|
|
37
|
-
*
|
|
38
|
-
* While preservation is active, a matching frame is rewritten to repaint
|
|
39
|
-
* without the wipe. With setViewportTopProvider wired, the rewrite knows
|
|
40
|
-
* which transcript line the visible screen starts at, so a frame that grew
|
|
41
|
-
* the transcript repaints the on-screen rows with the new content at those
|
|
42
|
-
* same positions and then scrolls the extra lines in at the bottom row.
|
|
43
|
-
* Each bottom-row newline pushes the top row — already repainted with the
|
|
44
|
-
* correct new content — into scrollback, exactly like ordinary streaming
|
|
45
|
-
* output, so scrollback ends up byte-equivalent to the upstream full replay
|
|
46
|
-
* without ever clearing it. Without the provider, or when the transcript
|
|
47
|
-
* shrank (lines cannot be pulled back out of scrollback), the rewrite falls
|
|
48
|
-
* back to overwriting the visible screen in place with the bottom
|
|
49
|
-
* viewport-height rows, at the cost of a stale seam above the screen. ED 2
|
|
50
|
-
* is avoided entirely in rewritten frames because conhost/Windows Terminal
|
|
51
|
-
* implement it by scrolling the screen contents into scrollback rather than
|
|
52
|
-
* erasing in place.
|
|
53
|
-
*
|
|
54
|
-
* Width- and height-change redraws keep the upstream wipe because re-wrapped
|
|
55
|
-
* content genuinely invalidates old scrollback, and frames containing kitty
|
|
56
|
-
* graphics pass through untouched. A frame that does not match the expected
|
|
57
|
-
* shape is passed through unchanged, so if upstream reshapes its render
|
|
58
|
-
* output the wrapper degrades back to stock behavior.
|
|
59
|
-
*/
|
|
60
|
-
export declare class CoalescingTerminal extends ProcessTerminal {
|
|
61
|
-
private pending;
|
|
62
|
-
private flushQueued;
|
|
63
|
-
private lastWriteColumns;
|
|
64
|
-
private lastWriteRows;
|
|
65
|
-
private viewportTopProvider;
|
|
66
|
-
private preserve;
|
|
67
|
-
private passArmed;
|
|
68
|
-
private agentRunActive;
|
|
69
|
-
private flushOnExit;
|
|
70
|
-
/**
|
|
71
|
-
* Enable or disable the scrollback-preserving rewrite. Preservation is
|
|
72
|
-
* only worth its imperfections (stale renderings above the screen, seams
|
|
73
|
-
* on shrink) while the transcript is mutating on its own and the reader
|
|
74
|
-
* may be scrolled up — i.e. during an agent run and until the user's next
|
|
75
|
-
* input after it settles. Outside that window every full redraw passes
|
|
76
|
-
* through with upstream's wipe-and-replay, whose result is byte-perfect:
|
|
77
|
-
* scrollback becomes the clean new transcript. Default: disabled.
|
|
78
|
-
* setAgentRunActive drives this for the normal agent-run window; this
|
|
79
|
-
* primitive remains for direct control in tests.
|
|
80
|
-
*/
|
|
81
|
-
setScrollbackPreservation(enabled: boolean): void;
|
|
82
|
-
/**
|
|
83
|
-
* Open or close the agent-run half of the preservation window. Opening
|
|
84
|
-
* (agent_start) turns preservation on. Closing (agent_settled) leaves
|
|
85
|
-
* preservation on: trailing reflow frames may land after the run while
|
|
86
|
-
* the reader is still scrolled up. The window then closes on the user's
|
|
87
|
-
* next input, observed in start()'s input wrapper — once the user acts
|
|
88
|
-
* they are back at the bottom, and upstream's clean wipe-and-replay wins
|
|
89
|
-
* again until the next run.
|
|
90
|
-
*/
|
|
91
|
-
setAgentRunActive(active: boolean): void;
|
|
92
|
-
/**
|
|
93
|
-
* Arm a one-shot exemption for an explicit user action (Ctrl+O tool
|
|
94
|
-
* expansion, thinking visibility) so its own full redraw keeps the
|
|
95
|
-
* upstream wipe-and-replay even while preservation is active mid-run.
|
|
96
|
-
* The user just pressed a key, so they are interacting at the bottom;
|
|
97
|
-
* a clean replay beats preservation's stale-seam trade-offs there. The
|
|
98
|
-
* flag is cleared by the next write so it cannot leak onto a later
|
|
99
|
-
* streaming redraw.
|
|
100
|
-
*/
|
|
101
|
-
passNextFullRedraw(): void;
|
|
102
|
-
/**
|
|
103
|
-
* Wire the renderer's notion of which transcript line the visible screen
|
|
104
|
-
* currently starts at (pi-tui's `previousViewportTop`). Full-redraw frames
|
|
105
|
-
* that grew the transcript then scroll the new above-screen lines into
|
|
106
|
-
* scrollback instead of dropping them. The provider is read while the
|
|
107
|
-
* frame is being written, i.e. before the renderer updates its own
|
|
108
|
-
* bookkeeping for that frame; returning undefined falls back to the
|
|
109
|
-
* bottom-anchored in-place repaint.
|
|
110
|
-
*/
|
|
111
|
-
setViewportTopProvider(provider: () => number | undefined): void;
|
|
112
|
-
constructor();
|
|
113
|
-
start(onInput: (data: string) => void, onResize: () => void): void;
|
|
114
|
-
write(data: string): void;
|
|
115
|
-
moveBy(lines: number): void;
|
|
116
|
-
hideCursor(): void;
|
|
117
|
-
showCursor(): void;
|
|
118
|
-
clearLine(): void;
|
|
119
|
-
clearFromCursor(): void;
|
|
120
|
-
clearScreen(): void;
|
|
121
|
-
stop(): void;
|
|
122
|
-
/**
|
|
123
|
-
* Rewrite a full-redraw frame (`?2026h 2J H 3J <lines> ?2026l`) so it
|
|
124
|
-
* repaints the screen without wiping scrollback. Anything that does not
|
|
125
|
-
* match this exact shape — including width-change redraws and frames
|
|
126
|
-
* touching kitty images — passes through unchanged.
|
|
127
|
-
*/
|
|
128
|
-
private preserveScrollback;
|
|
129
|
-
private readViewportTop;
|
|
130
|
-
private flush;
|
|
131
|
-
}
|
|
132
|
-
//# sourceMappingURL=coalescing-terminal.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"coalescing-terminal.d.ts","sourceRoot":"","sources":["../../src/utils/coalescing-terminal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AASzD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AACH,qBAAa,kBAAmB,SAAQ,eAAe;IACtD,OAAO,CAAC,OAAO,CAAM;IACrB,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,gBAAgB,CAAqB;IAC7C,OAAO,CAAC,aAAa,CAAqB;IAC1C,OAAO,CAAC,mBAAmB,CAAyC;IACpE,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,WAAW,CAAsB;IAEzC;;;;;;;;;;OAUG;IACH,yBAAyB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAIjD;;;;;;;;OAQG;IACH,iBAAiB,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI;IAKxC;;;;;;;;OAQG;IACH,kBAAkB,IAAI,IAAI;IAI1B;;;;;;;;OAQG;IACH,sBAAsB,CAAC,QAAQ,EAAE,MAAM,MAAM,GAAG,SAAS,GAAG,IAAI;;IAWvD,KAAK,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI;IAoBlE,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAUzB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAQ3B,UAAU,IAAI,IAAI;IAIlB,UAAU,IAAI,IAAI;IAIlB,SAAS,IAAI,IAAI;IAIjB,eAAe,IAAI,IAAI;IAIvB,WAAW,IAAI,IAAI;IAInB,IAAI,IAAI,IAAI;IAQrB;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;IA2E1B,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,KAAK;CASb"}
|
|
@@ -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"]}
|