@aragon-agent/cli 0.5.10 → 0.5.12
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 +24 -0
- package/README.md +5 -3
- package/dist/agent/controller.d.ts +5 -1
- package/dist/agent/controller.js +9 -4
- package/dist/agent/reducer.js +14 -4
- package/dist/cli.js +93 -0
- package/dist/commands/builtins.js +21 -2
- package/dist/commands/perf.d.ts +5 -1
- package/dist/commands/perf.js +43 -1
- package/dist/config/env.d.ts +1 -0
- package/dist/config/env.js +17 -0
- package/dist/config/load.d.ts +13 -0
- package/dist/config/load.js +31 -0
- package/dist/config/schema.d.ts +35 -0
- package/dist/config/schema.js +26 -0
- package/dist/todo/store.d.ts +12 -6
- package/dist/todo/store.js +12 -6
- package/dist/todo/types.d.ts +2 -1
- package/dist/ui/ActivityLine.d.ts +16 -0
- package/dist/ui/App.js +32 -3
- package/dist/ui/PromptInput.d.ts +10 -0
- package/dist/ui/PromptInput.js +41 -48
- package/dist/ui/Transcript.js +15 -0
- package/dist/ui/editor-reducer.d.ts +62 -0
- package/dist/ui/editor-reducer.js +90 -0
- package/dist/ui/entries/FastCard.js +20 -2
- package/dist/ui/frame-differ.d.ts +95 -0
- package/dist/ui/frame-differ.js +230 -0
- package/dist/ui/overlays/HelpOverlay.js +1 -1
- package/dist/ui/stdout-frame-writer.d.ts +41 -0
- package/dist/ui/stdout-frame-writer.js +102 -0
- package/package.json +2 -2
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frame differ — F1 of tui-input-flicker-fix (§3.2).
|
|
3
|
+
*
|
|
4
|
+
* Ink 5.2.1 has exactly one repaint strategy: on every commit it serialises the
|
|
5
|
+
* WHOLE frame and hands it to `log-update`, which writes
|
|
6
|
+
* `eraseLines(previousLineCount) + frame` in a single `stdout.write`
|
|
7
|
+
* (`ink/build/log-update.js:17`). `ansiEscapes.eraseLines` walks BOTTOM-UP, so
|
|
8
|
+
* the composer — the last row of the frame — is erased near the start of every
|
|
9
|
+
* repaint and rewritten at the very end of it. During a run that happens 15–30
|
|
10
|
+
* times a second with the composer's bytes unchanged, and the user sees the
|
|
11
|
+
* bottom of their screen flicker while they type (§2 R1/R2).
|
|
12
|
+
*
|
|
13
|
+
* This module recognises that write shape, diffs the new frame against the
|
|
14
|
+
* previous one line by line, and emits only the rows that actually changed, each
|
|
15
|
+
* addressed absolutely. It is PURE: no I/O, no Node globals, no event emitter —
|
|
16
|
+
* `stdout-frame-writer.ts` is what binds it to a real stream. That is what keeps
|
|
17
|
+
* it unit-testable with plain string literals and no TTY.
|
|
18
|
+
*
|
|
19
|
+
* THREE INVARIANTS LIVE HERE AND EACH ONE FAILS SILENTLY IF DROPPED:
|
|
20
|
+
*
|
|
21
|
+
* - I-4 · An emitted line is `CUP · SGR0 · text · SGR0 · CSI K`, IN THAT ORDER,
|
|
22
|
+
* and it never contains `CSI 2K`. Erasing first would blank the row and refill
|
|
23
|
+
* it, which is R1's defect at 1/H the scale but on the one row the user is
|
|
24
|
+
* watching. Overwriting in place means no cell is ever presentably empty; the
|
|
25
|
+
* trailing `CSI K` removes only the residue a longer previous line left, and
|
|
26
|
+
* the trailing SGR reset before it stops a BCE terminal painting that residue
|
|
27
|
+
* in whatever background colour the line ended inside.
|
|
28
|
+
* - I-5 · Every emitted batch ends parked on row `H+1`. Ink's own
|
|
29
|
+
* `previousLineCount` is computed from the string IT produced and is
|
|
30
|
+
* unaffected by what we actually wrote, so a later pass-through's
|
|
31
|
+
* `eraseLines(H+1)` must find the cursor exactly where log-update left it last
|
|
32
|
+
* time. Drop this and one fallback shifts the frame up a row for the rest of
|
|
33
|
+
* the session.
|
|
34
|
+
* - I-6 · Any unrecognised write invalidates the cache. Absolute addressing is
|
|
35
|
+
* valid only while nothing else writes to stdout. Full-screen mode already
|
|
36
|
+
* forbids that, but "forbidden" is not "impossible", and the failure mode of
|
|
37
|
+
* the guard being absent is a permanently misaligned screen while the failure
|
|
38
|
+
* mode of it firing spuriously is one extra full repaint.
|
|
39
|
+
*/
|
|
40
|
+
import { MIN_FULLSCREEN_ROWS } from './layout/frame.js';
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Constants — exact, so no call site ever re-derives an escape sequence.
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
/** Control Sequence Introducer. Spelled out once; nothing below re-escapes it. */
|
|
45
|
+
const CSI = '\x1b[';
|
|
46
|
+
/** Erase the WHOLE line. Ink uses it on every repaint; this module never emits it (I-4). */
|
|
47
|
+
const ERASE_LINE = `${CSI}2K`;
|
|
48
|
+
/** Erase cursor → end of line. This is what an emitted line ends with. */
|
|
49
|
+
const ERASE_TO_EOL = `${CSI}K`;
|
|
50
|
+
const CURSOR_UP_1 = `${CSI}1A`;
|
|
51
|
+
const CURSOR_LEFT = `${CSI}G`;
|
|
52
|
+
const SGR_RESET = `${CSI}0m`;
|
|
53
|
+
/** Erase cursor → end of screen. One sequence instead of H+1 line erases. */
|
|
54
|
+
const ERASE_DOWN = `${CSI}J`;
|
|
55
|
+
/** DEC private mode 2026 — begin/end synchronized update (F2, §3.3). */
|
|
56
|
+
const SYNC_BEGIN = `${CSI}?2026h`;
|
|
57
|
+
const SYNC_END = `${CSI}?2026l`;
|
|
58
|
+
/**
|
|
59
|
+
* `eraseLines(n)` for any n ≥ 1, anchored at the start of the chunk.
|
|
60
|
+
*
|
|
61
|
+
* FALLBACK ONLY (P2-3). Whenever `prev !== null` the differ already knows the
|
|
62
|
+
* exact prefix it expects — `eraseLinesPrefix(prev.length + 1)` is a
|
|
63
|
+
* deterministic string — so the hot path is `chunk.startsWith(expected)`:
|
|
64
|
+
* linear, unambiguous, and it collapses step 7's `erased !== prev.length + 1`
|
|
65
|
+
* test into the same comparison. This regex is reached only on the first
|
|
66
|
+
* diffable chunk after a session start or an `invalidate()`, where no
|
|
67
|
+
* expectation exists yet.
|
|
68
|
+
*/
|
|
69
|
+
const ERASE_PREFIX_RE = /^(?:\x1b\[2K(?:\x1b\[1A)?)+\x1b\[G/;
|
|
70
|
+
function cursorTo(row) {
|
|
71
|
+
return `${CSI}${row};1H`;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Rebuild `ansiEscapes.eraseLines(count)` byte for byte.
|
|
75
|
+
*
|
|
76
|
+
* Exported for the upgrade tripwire (`frame-differ-ink-shape.test.ts`), which
|
|
77
|
+
* drives the real `ink/build/log-update.js` and asserts this reconstruction is
|
|
78
|
+
* byte-equal to what it produced. If a future Ink changes the shape, that test
|
|
79
|
+
* fails loudly instead of this module silently degrading to pass-through for the
|
|
80
|
+
* rest of the package's life (K-2).
|
|
81
|
+
*/
|
|
82
|
+
export function eraseLinesPrefix(count) {
|
|
83
|
+
if (count <= 0)
|
|
84
|
+
return '';
|
|
85
|
+
let out = '';
|
|
86
|
+
for (let i = 0; i < count; i += 1) {
|
|
87
|
+
out += ERASE_LINE + (i < count - 1 ? CURSOR_UP_1 : '');
|
|
88
|
+
}
|
|
89
|
+
return out + CURSOR_LEFT;
|
|
90
|
+
}
|
|
91
|
+
function countErases(prefix) {
|
|
92
|
+
return prefix.split(ERASE_LINE).length - 1;
|
|
93
|
+
}
|
|
94
|
+
export function createFrameDiffer(options) {
|
|
95
|
+
/** Last frame, one entry per row. `null` = no cache, next frame is a full repaint. */
|
|
96
|
+
let prev = null;
|
|
97
|
+
/**
|
|
98
|
+
* Whether any chunk has been seen at all.
|
|
99
|
+
*
|
|
100
|
+
* The session's FIRST write has `previousLineCount === 0`, so `eraseLines(0)`
|
|
101
|
+
* is the empty string and the chunk carries no erase prefix (§3.2.1). That is
|
|
102
|
+
* expected, not a desync, so it must not raise the §5.6 notice on every launch.
|
|
103
|
+
* After it, every chunk Ink produces carries a prefix, so a prefix-less chunk
|
|
104
|
+
* from then on really is foreign.
|
|
105
|
+
*/
|
|
106
|
+
let seenAnyChunk = false;
|
|
107
|
+
const counters = {
|
|
108
|
+
framesTotal: 0,
|
|
109
|
+
framesDiffed: 0,
|
|
110
|
+
framesFull: 0,
|
|
111
|
+
linesWritten: 0,
|
|
112
|
+
bytesWritten: 0,
|
|
113
|
+
fallbacks: 0,
|
|
114
|
+
};
|
|
115
|
+
const invalidate = () => {
|
|
116
|
+
prev = null;
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Write `chunk` verbatim — today's behaviour, and the only failure mode.
|
|
120
|
+
*
|
|
121
|
+
* `counted` is false for the two cases the differ inflicts on itself and knows
|
|
122
|
+
* are harmless (the seed write, degenerate geometry); true means a foreign
|
|
123
|
+
* write desynced absolute addressing, which is what `fallbacks` reports.
|
|
124
|
+
*/
|
|
125
|
+
const passThrough = (counted) => {
|
|
126
|
+
invalidate();
|
|
127
|
+
if (counted) {
|
|
128
|
+
counters.fallbacks += 1;
|
|
129
|
+
if (counters.fallbacks === 1)
|
|
130
|
+
options.onFirstFallback?.();
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
};
|
|
134
|
+
const wrapSync = (payload) => options.sync ? SYNC_BEGIN + payload + SYNC_END : payload;
|
|
135
|
+
const emit = (payload, lines, addressed, full) => {
|
|
136
|
+
prev = lines;
|
|
137
|
+
counters.framesTotal += 1;
|
|
138
|
+
if (full)
|
|
139
|
+
counters.framesFull += 1;
|
|
140
|
+
else
|
|
141
|
+
counters.framesDiffed += 1;
|
|
142
|
+
counters.linesWritten += addressed;
|
|
143
|
+
counters.bytesWritten += payload.length;
|
|
144
|
+
return payload;
|
|
145
|
+
};
|
|
146
|
+
/** `undefined` ⇒ the I-9 stand-down: a viewport too small to address safely. */
|
|
147
|
+
const usableRows = () => {
|
|
148
|
+
const rows = options.rows();
|
|
149
|
+
if (rows === undefined || !Number.isFinite(rows))
|
|
150
|
+
return undefined;
|
|
151
|
+
return rows < MIN_FULLSCREEN_ROWS ? undefined : rows;
|
|
152
|
+
};
|
|
153
|
+
const matchErasePrefix = (chunk) => {
|
|
154
|
+
if (prev !== null) {
|
|
155
|
+
const expected = eraseLinesPrefix(prev.length + 1);
|
|
156
|
+
if (chunk.startsWith(expected))
|
|
157
|
+
return { prefix: expected, erased: prev.length + 1 };
|
|
158
|
+
}
|
|
159
|
+
const m = ERASE_PREFIX_RE.exec(chunk);
|
|
160
|
+
if (m === null)
|
|
161
|
+
return null;
|
|
162
|
+
return { prefix: m[0], erased: countErases(m[0]) };
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* Full repaint — the first diffable frame of a session, every invalidation
|
|
166
|
+
* (including the resize invalidation of I-8), and any line-count change.
|
|
167
|
+
*
|
|
168
|
+
* `CSI J` replaces the H+1 individual line erases with one sequence, and the
|
|
169
|
+
* alternate screen is exactly the region being erased. `\r\n` rather than `\n`
|
|
170
|
+
* is deliberate: Ink relies on the tty's `ONLCR` post-processing to turn `\n`
|
|
171
|
+
* into CRLF, and that dependency is invisible until it is not there.
|
|
172
|
+
*
|
|
173
|
+
* P0-1's blank-then-paint objection does not apply here and the asymmetry with
|
|
174
|
+
* the per-line path is deliberate: a full repaint rewrites every row, so there
|
|
175
|
+
* is no unchanged content that has to survive, and full repaints are rare by
|
|
176
|
+
* construction — one per session, one per resize, one per invalidation.
|
|
177
|
+
*/
|
|
178
|
+
const fullRepaint = (lines) => {
|
|
179
|
+
const payload = cursorTo(1) +
|
|
180
|
+
SGR_RESET +
|
|
181
|
+
ERASE_DOWN +
|
|
182
|
+
lines.join('\r\n') +
|
|
183
|
+
'\r\n' +
|
|
184
|
+
cursorTo(lines.length + 1);
|
|
185
|
+
return emit(wrapSync(payload), lines, lines.length, true);
|
|
186
|
+
};
|
|
187
|
+
const diffRepaint = (lines, previous) => {
|
|
188
|
+
const out = [];
|
|
189
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
190
|
+
// I-4: paint, THEN clear the tail. Never `ERASE_LINE` first.
|
|
191
|
+
if (lines[i] !== previous[i]) {
|
|
192
|
+
out.push(cursorTo(i + 1) + SGR_RESET + lines[i] + SGR_RESET + ERASE_TO_EOL);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// DEFENSIVE ONLY, not a live path (P2-4). `log-update.js:13-15` returns early
|
|
196
|
+
// on an identical frame, so a chunk that reaches here differs by at least one
|
|
197
|
+
// line; the only way to observe zero is a cache that disagrees with the
|
|
198
|
+
// screen, which is the state `invalidate()` exists to prevent.
|
|
199
|
+
if (out.length === 0)
|
|
200
|
+
return emit('', lines, 0, false);
|
|
201
|
+
out.push(cursorTo(lines.length + 1)); // I-5
|
|
202
|
+
return emit(wrapSync(out.join('')), lines, out.length - 1, false);
|
|
203
|
+
};
|
|
204
|
+
const transform = (chunk) => {
|
|
205
|
+
const firstChunk = !seenAnyChunk;
|
|
206
|
+
seenAnyChunk = true;
|
|
207
|
+
const rows = usableRows();
|
|
208
|
+
if (rows === undefined)
|
|
209
|
+
return passThrough(false); // I-9
|
|
210
|
+
const matched = matchErasePrefix(chunk);
|
|
211
|
+
// A prefix-less chunk is the session's seed write (P1-1) or a foreign one.
|
|
212
|
+
if (matched === null)
|
|
213
|
+
return passThrough(!firstChunk);
|
|
214
|
+
const body = chunk.slice(matched.prefix.length);
|
|
215
|
+
if (!body.endsWith('\n'))
|
|
216
|
+
return passThrough(true); // `log.clear()` and friends
|
|
217
|
+
const lines = body.slice(0, -1).split('\n');
|
|
218
|
+
if (lines.length + 1 > rows)
|
|
219
|
+
return passThrough(false); // I-9
|
|
220
|
+
if (prev === null || prev.length !== lines.length || matched.erased !== prev.length + 1) {
|
|
221
|
+
return fullRepaint(lines);
|
|
222
|
+
}
|
|
223
|
+
return diffRepaint(lines, prev);
|
|
224
|
+
};
|
|
225
|
+
return {
|
|
226
|
+
transform,
|
|
227
|
+
invalidate,
|
|
228
|
+
stats: () => ({ ...counters }),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
@@ -33,7 +33,7 @@ const COMMANDS = [
|
|
|
33
33
|
'Todo planning: status, switches, the rail, follow-through',
|
|
34
34
|
],
|
|
35
35
|
['/tools', 'List active tools'],
|
|
36
|
-
['/clear', 'Clear the visible transcript'],
|
|
36
|
+
['/clear', 'Clear the visible transcript and the todo panel'],
|
|
37
37
|
['/reset', 'New conversation'],
|
|
38
38
|
['/cwd [dir]', 'Show or change the tool working directory'],
|
|
39
39
|
['/save [file]', 'Save the session to JSON'],
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stdout frame writer — the `Proxy` that binds `frame-differ.ts` to a real
|
|
3
|
+
* stream (tui-input-flicker-fix §3.2 / §5.5).
|
|
4
|
+
*
|
|
5
|
+
* Ink offers no hook for "how to write a frame": `Ink#log` is constructed
|
|
6
|
+
* internally and `log-update` is a private module. It DOES accept a `stdout` in
|
|
7
|
+
* `render()`'s options, so the whole interposition is one `Proxy` whose only
|
|
8
|
+
* trapped behaviour is `write` — everything else is the real stream.
|
|
9
|
+
*
|
|
10
|
+
* THREE THINGS IN HERE ARE LOAD-BEARING:
|
|
11
|
+
*
|
|
12
|
+
* - The `get` trap MUST bind non-`write` function members to the REAL stream.
|
|
13
|
+
* Without the bind, `stdout.on('resize', …)` executes with `this === proxy`,
|
|
14
|
+
* and `useTerminalSize` registers and removes its listener against a
|
|
15
|
+
* different `_events` object than the one Node emits on — the frame stops
|
|
16
|
+
* resizing and nothing reports why.
|
|
17
|
+
* - The UNTYPED BOUNDARY LIVES HERE, not in the differ (P1-4). A `Uint8Array`
|
|
18
|
+
* chunk, and any `write` whose `encoding` is present and is not UTF-8, is
|
|
19
|
+
* passed straight through after `invalidate()`: in both cases the bytes on
|
|
20
|
+
* the wire are not the string a differ could reason about, and diffing one
|
|
21
|
+
* encoding while writing another is the kind of bug that only shows up on
|
|
22
|
+
* someone else's machine.
|
|
23
|
+
* - The `'resize'` SUBSCRIPTION (I-8 / P0-2). Ink renders SYNCHRONOUSLY from its
|
|
24
|
+
* own `'resize'` handler while the frame's height is still the pre-resize one
|
|
25
|
+
* — `useTerminalSize` debounces by 50 ms — so the line count is unchanged
|
|
26
|
+
* across that window and the differ's line-count guard cannot see the resize.
|
|
27
|
+
* Meanwhile the emulator has re-flowed the alternate screen, so every row the
|
|
28
|
+
* differ believes unchanged is a row it will not repaint, for the rest of the
|
|
29
|
+
* drag. It goes on the REAL stream, not the `Proxy`, and `dispose()` removes
|
|
30
|
+
* it.
|
|
31
|
+
*/
|
|
32
|
+
import type { FrameDiffer, FrameWriterStats } from './frame-differ.js';
|
|
33
|
+
export interface FrameWriterHandle {
|
|
34
|
+
/** Hand this to `render(…, { stdout })`. */
|
|
35
|
+
stdout: NodeJS.WriteStream;
|
|
36
|
+
/** Hand this to `setFrameStatsProvider`. */
|
|
37
|
+
stats(): FrameWriterStats;
|
|
38
|
+
/** Removes the `'resize'` subscription of I-8. Idempotent. */
|
|
39
|
+
dispose(): void;
|
|
40
|
+
}
|
|
41
|
+
export declare function wrapStdoutForFrames(stdout: NodeJS.WriteStream, differ: FrameDiffer): FrameWriterHandle;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stdout frame writer — the `Proxy` that binds `frame-differ.ts` to a real
|
|
3
|
+
* stream (tui-input-flicker-fix §3.2 / §5.5).
|
|
4
|
+
*
|
|
5
|
+
* Ink offers no hook for "how to write a frame": `Ink#log` is constructed
|
|
6
|
+
* internally and `log-update` is a private module. It DOES accept a `stdout` in
|
|
7
|
+
* `render()`'s options, so the whole interposition is one `Proxy` whose only
|
|
8
|
+
* trapped behaviour is `write` — everything else is the real stream.
|
|
9
|
+
*
|
|
10
|
+
* THREE THINGS IN HERE ARE LOAD-BEARING:
|
|
11
|
+
*
|
|
12
|
+
* - The `get` trap MUST bind non-`write` function members to the REAL stream.
|
|
13
|
+
* Without the bind, `stdout.on('resize', …)` executes with `this === proxy`,
|
|
14
|
+
* and `useTerminalSize` registers and removes its listener against a
|
|
15
|
+
* different `_events` object than the one Node emits on — the frame stops
|
|
16
|
+
* resizing and nothing reports why.
|
|
17
|
+
* - The UNTYPED BOUNDARY LIVES HERE, not in the differ (P1-4). A `Uint8Array`
|
|
18
|
+
* chunk, and any `write` whose `encoding` is present and is not UTF-8, is
|
|
19
|
+
* passed straight through after `invalidate()`: in both cases the bytes on
|
|
20
|
+
* the wire are not the string a differ could reason about, and diffing one
|
|
21
|
+
* encoding while writing another is the kind of bug that only shows up on
|
|
22
|
+
* someone else's machine.
|
|
23
|
+
* - The `'resize'` SUBSCRIPTION (I-8 / P0-2). Ink renders SYNCHRONOUSLY from its
|
|
24
|
+
* own `'resize'` handler while the frame's height is still the pre-resize one
|
|
25
|
+
* — `useTerminalSize` debounces by 50 ms — so the line count is unchanged
|
|
26
|
+
* across that window and the differ's line-count guard cannot see the resize.
|
|
27
|
+
* Meanwhile the emulator has re-flowed the alternate screen, so every row the
|
|
28
|
+
* differ believes unchanged is a row it will not repaint, for the rest of the
|
|
29
|
+
* drag. It goes on the REAL stream, not the `Proxy`, and `dispose()` removes
|
|
30
|
+
* it.
|
|
31
|
+
*/
|
|
32
|
+
/** The only encodings for which the chunk string IS the bytes on the wire. */
|
|
33
|
+
function isUtf8Encoding(encoding) {
|
|
34
|
+
const e = encoding.toLowerCase();
|
|
35
|
+
return e === 'utf8' || e === 'utf-8';
|
|
36
|
+
}
|
|
37
|
+
export function wrapStdoutForFrames(stdout, differ) {
|
|
38
|
+
const bound = new Map();
|
|
39
|
+
const writeReal = (chunk, encoding, cb) => {
|
|
40
|
+
if (encoding !== undefined)
|
|
41
|
+
return stdout.write(chunk, encoding, cb);
|
|
42
|
+
return stdout.write(chunk, cb);
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* All three Node overloads: `write(chunk)`, `write(chunk, cb)`,
|
|
46
|
+
* `write(chunk, encoding, cb)`.
|
|
47
|
+
*/
|
|
48
|
+
const write = (chunk, encodingOrCb, maybeCb) => {
|
|
49
|
+
const encoding = typeof encodingOrCb === 'string' ? encodingOrCb : undefined;
|
|
50
|
+
const cb = typeof encodingOrCb === 'function'
|
|
51
|
+
? encodingOrCb
|
|
52
|
+
: typeof maybeCb === 'function'
|
|
53
|
+
? maybeCb
|
|
54
|
+
: undefined;
|
|
55
|
+
// Foreign by construction: Ink never writes Buffers, and a non-UTF-8
|
|
56
|
+
// encoding means the string is not the bytes (P1-4).
|
|
57
|
+
if (typeof chunk !== 'string' || (encoding !== undefined && !isUtf8Encoding(encoding))) {
|
|
58
|
+
differ.invalidate();
|
|
59
|
+
return writeReal(chunk, encoding, cb);
|
|
60
|
+
}
|
|
61
|
+
const out = differ.transform(chunk);
|
|
62
|
+
if (out === null)
|
|
63
|
+
return writeReal(chunk, encoding, cb);
|
|
64
|
+
// Nothing to write, but the callback is still part of the contract — a
|
|
65
|
+
// skipped `cb` strands whatever was waiting on the drain.
|
|
66
|
+
if (out.length === 0) {
|
|
67
|
+
cb?.();
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
return writeReal(out, encoding, cb);
|
|
71
|
+
};
|
|
72
|
+
const onResize = () => differ.invalidate();
|
|
73
|
+
stdout.on('resize', onResize);
|
|
74
|
+
let disposed = false;
|
|
75
|
+
const proxy = new Proxy(stdout, {
|
|
76
|
+
get(target, prop) {
|
|
77
|
+
if (prop === 'write')
|
|
78
|
+
return write;
|
|
79
|
+
const value = Reflect.get(target, prop, target);
|
|
80
|
+
if (typeof value !== 'function')
|
|
81
|
+
return value;
|
|
82
|
+
// Cached so repeated property access does not allocate a new bound
|
|
83
|
+
// function on a hot path (K-9).
|
|
84
|
+
let fn = bound.get(prop);
|
|
85
|
+
if (fn === undefined) {
|
|
86
|
+
fn = value.bind(target);
|
|
87
|
+
bound.set(prop, fn);
|
|
88
|
+
}
|
|
89
|
+
return fn;
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
return {
|
|
93
|
+
stdout: proxy,
|
|
94
|
+
stats: () => differ.stats(),
|
|
95
|
+
dispose() {
|
|
96
|
+
if (disposed)
|
|
97
|
+
return;
|
|
98
|
+
disposed = true;
|
|
99
|
+
stdout.off('resize', onResize);
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aragon-agent/cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.12",
|
|
4
4
|
"description": "AragonAgent CLI — a Claude-Code / Codex-style interactive terminal UI (TUI) on top of the @aragon-agent/core engine. Installable and runnable via npm / npx.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "AragonAgent contributors",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"prepublishOnly": "npm run build"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@aragon-agent/core": "^0.2.
|
|
55
|
+
"@aragon-agent/core": "^0.2.12",
|
|
56
56
|
"chalk": "^5.6.2",
|
|
57
57
|
"cli-highlight": "^2.0.0",
|
|
58
58
|
"commander": "^12.1.0",
|