omniharness-cli 0.1.85 → 0.1.87
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/dist/cli.js +6 -1
- package/dist/ui/debounce.js +18 -0
- package/dist/ui/keys.js +15 -3
- package/dist/ui/resizeDebounce.js +52 -0
- package/dist/ui/terminalInterface.js +39 -12
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import { ownVersion, runUpdate } from './update.js';
|
|
|
7
7
|
import { readActiveCombo } from './config/settings.js';
|
|
8
8
|
import { OmniRouteClient } from './config/omniRoute.js';
|
|
9
9
|
import { doctor, helpText, models } from './doctor.js';
|
|
10
|
+
import { debounceResizeEvents } from './ui/resizeDebounce.js';
|
|
10
11
|
// A crash anywhere below would otherwise surface as a raw Node stack trace, or
|
|
11
12
|
// as an unhandled rejection that terminates the process without saying why.
|
|
12
13
|
// A CLI should fail with a sentence.
|
|
@@ -81,8 +82,12 @@ else {
|
|
|
81
82
|
// The cost is that quitting no longer restores the pre-launch screen. For
|
|
82
83
|
// a tool whose output you are meant to read back, that is the right trade.
|
|
83
84
|
//
|
|
85
|
+
// Coalesce resize delivery before Ink's own internal listener ever sees
|
|
86
|
+
// it — see resizeDebounce.ts. Only meaningful for a real terminal; a
|
|
87
|
+
// non-TTY stdout never emits 'resize' and patching it would be inert.
|
|
88
|
+
const stdout = process.stdout.isTTY ? debounceResizeEvents(process.stdout, 80) : process.stdout;
|
|
84
89
|
// The app owns Ctrl+C so idle quits but an in-flight run is cancelled first.
|
|
85
|
-
const { waitUntilExit } = render(_jsx(TerminalInterface, { engine: engine }), { exitOnCtrlC: false });
|
|
90
|
+
const { waitUntilExit } = render(_jsx(TerminalInterface, { engine: engine }), { stdout, exitOnCtrlC: false });
|
|
86
91
|
await waitUntilExit();
|
|
87
92
|
})();
|
|
88
93
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function debounce(fn, delayMs) {
|
|
2
|
+
let timer;
|
|
3
|
+
const debounced = ((...args) => {
|
|
4
|
+
if (timer)
|
|
5
|
+
clearTimeout(timer);
|
|
6
|
+
timer = setTimeout(() => {
|
|
7
|
+
timer = undefined;
|
|
8
|
+
fn(...args);
|
|
9
|
+
}, delayMs);
|
|
10
|
+
});
|
|
11
|
+
debounced.cancel = () => {
|
|
12
|
+
if (timer)
|
|
13
|
+
clearTimeout(timer);
|
|
14
|
+
timer = undefined;
|
|
15
|
+
};
|
|
16
|
+
return debounced;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=debounce.js.map
|
package/dist/ui/keys.js
CHANGED
|
@@ -102,10 +102,22 @@ export function parseRawKey(chunk) {
|
|
|
102
102
|
return null;
|
|
103
103
|
}
|
|
104
104
|
/**
|
|
105
|
-
* True when `value` is a CSI-u encoded key
|
|
106
|
-
* events, push/pop echoes, query answers),
|
|
105
|
+
* True when `value` is a CSI-u encoded key, a protocol response (kitty key
|
|
106
|
+
* events, push/pop echoes, query answers), or a DECRQM mode-report reply
|
|
107
|
+
* (`CSI ? Ps ; Pm $ y`, e.g. the answer to the synchronized-output query) —
|
|
108
|
+
* never plain text to insert.
|
|
109
|
+
*
|
|
110
|
+
* `value` here is Ink's own parsed form, which strips the leading ESC before
|
|
111
|
+
* handing the rest to `useInput`. A terminal's reply to a query this project
|
|
112
|
+
* sends (synchronized output, in particular) arrives on the same stdin the
|
|
113
|
+
* editor reads from, and if nothing recognises it in this stripped form it is
|
|
114
|
+
* treated as pasted text and typed into whatever the user was editing — the
|
|
115
|
+
* escape sequence itself, landing in the input box. Some terminals are slow
|
|
116
|
+
* to answer that specific query, so this has to keep matching however late
|
|
117
|
+
* the reply arrives, not just during a short probe window at startup.
|
|
107
118
|
*/
|
|
108
119
|
export function isEncodedKey(value) {
|
|
109
|
-
return /^\[\d+(?:;\d+)*u$|^\[[><?]\d+(?:;\d+)*u$/.test(value)
|
|
120
|
+
return /^\[\d+(?:;\d+)*u$|^\[[><?]\d+(?:;\d+)*u$/.test(value)
|
|
121
|
+
|| /^\[\?\d+;\d\$y$/.test(value);
|
|
110
122
|
}
|
|
111
123
|
//# sourceMappingURL=keys.js.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { debounce } from './debounce.js';
|
|
2
|
+
/**
|
|
3
|
+
* Coalesce `resize` event delivery on a TTY stream so every listener — Ink's
|
|
4
|
+
* own internal one included — sees one event per burst instead of one per raw
|
|
5
|
+
* OS-level tick.
|
|
6
|
+
*
|
|
7
|
+
* Ink attaches its own `resize` listener directly to whatever stream is
|
|
8
|
+
* handed to `render()`, and on every single tick it recalculates its Yoga
|
|
9
|
+
* layout and writes a fresh frame — independent of anything application state
|
|
10
|
+
* does. That is not reachable by debouncing a `useState` call in a component:
|
|
11
|
+
* the redraw happens inside Ink regardless.
|
|
12
|
+
*
|
|
13
|
+
* A maximise or restore on Windows Terminal does not deliver one resize
|
|
14
|
+
* event. It animates through the transition and fires a burst of
|
|
15
|
+
* intermediate sizes a few milliseconds apart. Reacting to each one means
|
|
16
|
+
* Ink redraws against a size that is already stale by the time the escape
|
|
17
|
+
* sequences reach the terminal, and the frames land on top of one another
|
|
18
|
+
* instead of replacing one another — duplicated prompt boxes, fragments of an
|
|
19
|
+
* earlier frame still on screen once the window settles.
|
|
20
|
+
*
|
|
21
|
+
* The only place to fix that is before Ink's own listener ever sees the
|
|
22
|
+
* event — so this patches `.on` / `.off` for `resize` specifically, on the
|
|
23
|
+
* same stream object Ink and application code both subscribe to. `write`,
|
|
24
|
+
* `columns`, `rows`, `isTTY` and every other event pass through untouched.
|
|
25
|
+
*/
|
|
26
|
+
export function debounceResizeEvents(stream, delayMs) {
|
|
27
|
+
const realOn = stream.on.bind(stream);
|
|
28
|
+
const realOff = stream.off.bind(stream);
|
|
29
|
+
// Keyed on the caller's own listener, so .off(listener) still finds and
|
|
30
|
+
// cancels the right debounced wrapper — the caller never sees the wrapper.
|
|
31
|
+
const wrapped = new WeakMap();
|
|
32
|
+
stream.on = ((event, listener) => {
|
|
33
|
+
if (event !== 'resize')
|
|
34
|
+
return realOn(event, listener);
|
|
35
|
+
const debounced = debounce(listener, delayMs);
|
|
36
|
+
wrapped.set(listener, debounced);
|
|
37
|
+
return realOn(event, debounced);
|
|
38
|
+
});
|
|
39
|
+
stream.off = ((event, listener) => {
|
|
40
|
+
if (event !== 'resize')
|
|
41
|
+
return realOff(event, listener);
|
|
42
|
+
const debounced = wrapped.get(listener);
|
|
43
|
+
if (debounced) {
|
|
44
|
+
debounced.cancel();
|
|
45
|
+
wrapped.delete(listener);
|
|
46
|
+
return realOff(event, debounced);
|
|
47
|
+
}
|
|
48
|
+
return realOff(event, listener);
|
|
49
|
+
});
|
|
50
|
+
return stream;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=resizeDebounce.js.map
|
|
@@ -320,6 +320,13 @@ export function TerminalInterface({ engine }) {
|
|
|
320
320
|
return () => { alive = false; };
|
|
321
321
|
}, []);
|
|
322
322
|
useEffect(() => {
|
|
323
|
+
// The real fix for a resize storm (maximise/restore firing a burst of
|
|
324
|
+
// intermediate sizes) lives one layer down, in resizeDebounce.ts: it
|
|
325
|
+
// coalesces delivery of the raw 'resize' event on the stream itself,
|
|
326
|
+
// before Ink's own internal listener — which redraws unconditionally on
|
|
327
|
+
// every tick, independent of any component state — ever sees it. This
|
|
328
|
+
// handler can stay a plain listener because of that; by the time it
|
|
329
|
+
// fires, the event has already been coalesced upstream.
|
|
323
330
|
const onResize = () => {
|
|
324
331
|
setWidth(widthOf(stdout));
|
|
325
332
|
setRows(rowsOf(stdout));
|
|
@@ -327,22 +334,41 @@ export function TerminalInterface({ engine }) {
|
|
|
327
334
|
stdout.on('resize', onResize);
|
|
328
335
|
stdout.write(KITTY_PUSH);
|
|
329
336
|
let kittyTimer;
|
|
330
|
-
//
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
+
// Two independent probes, not one shared listener. They used to be a
|
|
338
|
+
// single handler torn down together after 300ms, on the assumption both
|
|
339
|
+
// replies would have arrived by then. Some terminals answer the
|
|
340
|
+
// synchronized-output query well after that — observed landing right
|
|
341
|
+
// around a resize, though the terminal's own reasons for the delay are
|
|
342
|
+
// opaque from here — and a listener already removed cannot catch a late
|
|
343
|
+
// reply. Two things followed from that: synchronized output silently
|
|
344
|
+
// never turned on for the rest of the session, and the reply itself, with
|
|
345
|
+
// nothing left to claim it, reached Ink's own useInput and got typed into
|
|
346
|
+
// the input box as literal text (the isEncodedKey guard now recognises
|
|
347
|
+
// and drops that shape regardless, but the actual fix is to keep
|
|
348
|
+
// listening for it rather than lean on that alone).
|
|
349
|
+
const onSyncProbe = (chunk) => {
|
|
350
|
+
if (syncRestoreRef.current !== null)
|
|
351
|
+
return;
|
|
352
|
+
if (!isSyncOutputReply(chunk.toString()))
|
|
353
|
+
return;
|
|
354
|
+
syncRestoreRef.current = wrapSynchronizedOutput(stdout);
|
|
355
|
+
stdin?.off('data', onSyncProbe);
|
|
356
|
+
};
|
|
357
|
+
// The kitty check is a UI decision — whether Ctrl+letter and a distinct
|
|
358
|
+
// Shift+Tab are available — that has to resolve one way or the other, so
|
|
359
|
+
// it keeps a bounded timeout; "no answer in 300ms" is itself the answer.
|
|
360
|
+
const onKittyProbe = (chunk) => {
|
|
361
|
+
if (!isKittyQueryResponse(chunk.toString()))
|
|
337
362
|
return;
|
|
338
363
|
if (kittyTimer)
|
|
339
364
|
clearTimeout(kittyTimer);
|
|
340
|
-
stdin?.off('data',
|
|
365
|
+
stdin?.off('data', onKittyProbe);
|
|
341
366
|
setKitty(true);
|
|
342
367
|
};
|
|
343
368
|
if (stdin) {
|
|
344
|
-
stdin.on('data',
|
|
345
|
-
|
|
369
|
+
stdin.on('data', onSyncProbe);
|
|
370
|
+
stdin.on('data', onKittyProbe);
|
|
371
|
+
kittyTimer = setTimeout(() => { setKitty(false); stdin.off('data', onKittyProbe); }, 300);
|
|
346
372
|
stdout.write(SYNC_QUERY);
|
|
347
373
|
stdout.write(KITTY_QUERY);
|
|
348
374
|
}
|
|
@@ -425,8 +451,9 @@ export function TerminalInterface({ engine }) {
|
|
|
425
451
|
return () => {
|
|
426
452
|
if (kittyTimer)
|
|
427
453
|
clearTimeout(kittyTimer);
|
|
428
|
-
stdin?.off('data',
|
|
429
|
-
|
|
454
|
+
stdin?.off('data', onSyncProbe);
|
|
455
|
+
stdin?.off('data', onKittyProbe);
|
|
456
|
+
stdout.off('resize', onResize); // resizeDebounce cancels the coalesced timer on .off itself
|
|
430
457
|
const pendingApproval = approvalResolve.current;
|
|
431
458
|
approvalResolve.current = null;
|
|
432
459
|
pendingApproval?.({ approved: false });
|