omniharness-cli 0.1.85 → 0.1.86
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/resizeDebounce.js +52 -0
- package/dist/ui/terminalInterface.js +8 -1
- 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
|
|
@@ -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));
|
|
@@ -426,7 +433,7 @@ export function TerminalInterface({ engine }) {
|
|
|
426
433
|
if (kittyTimer)
|
|
427
434
|
clearTimeout(kittyTimer);
|
|
428
435
|
stdin?.off('data', onProbe);
|
|
429
|
-
stdout.off('resize', onResize);
|
|
436
|
+
stdout.off('resize', onResize); // resizeDebounce cancels the coalesced timer on .off itself
|
|
430
437
|
const pendingApproval = approvalResolve.current;
|
|
431
438
|
approvalResolve.current = null;
|
|
432
439
|
pendingApproval?.({ approved: false });
|