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 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 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omniharness-cli",
3
- "version": "0.1.85",
3
+ "version": "0.1.86",
4
4
  "description": "OmniHarness — local-first agent orchestration harness for OmniRoute.",
5
5
  "license": "MIT",
6
6
  "type": "module",