@yolo-labs/yolobridge 0.16.0 → 0.17.0

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.
@@ -412,10 +412,16 @@ export function computeUsedRows(term) {
412
412
  }
413
413
  return used;
414
414
  }
415
- /** The PTY's grid, which the tile must render at EXACTLY (it cannot be
416
- * resized — the human at the keyboard is watching the same PTY), plus how
415
+ /** The PTY's CURRENT grid, which the tile must render at EXACTLY, plus how
417
416
  * much of that grid is in USE.
418
417
  *
418
+ * The viewer must not resize this grid — the human at the keyboard is
419
+ * watching the same PTY, so a cloud-side resize would reach over and reflow
420
+ * their real screen. But the human themselves CAN resize it, and when they
421
+ * do the ring restarts under a fresh epoch (see `handleLocalResize`), so a
422
+ * viewer holding the old geometry re-seeds rather than replaying old bytes
423
+ * against a grid that no longer exists.
424
+ *
419
425
  * ⚠️ `usedRows` IS NOT A SECOND GEOMETRY. It never changes `cols`/`rows` and
420
426
  * the viewer never resizes its terminal to it: a viewer's terminal that is
421
427
  * not exactly `cols`×`rows` cannot replay the daemon's bytes (relative cursor
@@ -679,6 +685,12 @@ export function startLocalAgent(opts = {}) {
679
685
  // explicitly to disable stdin piping entirely, distinct from omitting the
680
686
  // field (which defaults to wiring up the real `process.stdin`).
681
687
  const inStream = 'stdin' in opts ? opts.stdin : process.stdin;
688
+ // Same defaulting rule as `resolveCols`/`resolveRows`: only reach for the real
689
+ // `process.stdout` when nothing was injected, so a test with a fake sink does
690
+ // not silently pick up the CI runner's terminal.
691
+ const resizeSource = 'resizeSource' in opts
692
+ ? opts.resizeSource
693
+ : (opts.stdout === undefined ? process.stdout : undefined);
682
694
  const ptyProcess = spawnImpl(agentBin, agentArgs, {
683
695
  name: 'xterm-256color',
684
696
  cols,
@@ -706,8 +718,14 @@ export function startLocalAgent(opts = {}) {
706
718
  writeChain: Promise.resolve(),
707
719
  stdin: inStream,
708
720
  rawModeEnabled: false,
721
+ resizeSource,
709
722
  };
710
723
  current = state;
724
+ if (resizeSource && typeof resizeSource.on === 'function') {
725
+ const resizeListener = () => handleLocalResize(state);
726
+ state.resizeListener = resizeListener;
727
+ resizeSource.on('resize', resizeListener);
728
+ }
711
729
  ptyProcess.onData((data) => {
712
730
  state.lastOutputAt = Date.now();
713
731
  outStream.write(data);
@@ -756,7 +774,53 @@ export function startLocalAgent(opts = {}) {
756
774
  * exit code 124. With `stdin.pause()` added below, the same repro exits
757
775
  * cleanly on its own well under a second — no timeout/kill needed.
758
776
  */
777
+ /**
778
+ * The human at the keyboard resized their terminal window.
779
+ *
780
+ * WHY THIS EXISTS: the PTY size used to be treated as fixed at spawn, on the
781
+ * reasoning that "nothing resizes this PTY, least of all the cloud". The cloud
782
+ * half of that is right and still enforced — a viewer must never reach over and
783
+ * reflow the operator's real screen. The other half was simply wrong: the human
784
+ * watching this PTY through `process.stdout` can drag their window, Node raises
785
+ * `'resize'` on SIGWINCH, and we ignored it. The child kept rendering to the old
786
+ * grid, so the operator's own terminal garbled and the tile kept reporting a
787
+ * geometry that no longer matched anything.
788
+ *
789
+ * Everything retained in the ring was captured against a grid that no longer
790
+ * exists — relative cursor moves and the scroll region are defined against the
791
+ * REAL grid, so replaying those bytes at the new size garbles rather than
792
+ * degrades. A fresh epoch is the honest answer: the viewer re-seeds from the
793
+ * current buffer instead of splicing two geometries together.
794
+ */
795
+ function handleLocalResize(state) {
796
+ const src = state.resizeSource;
797
+ if (!src)
798
+ return;
799
+ const cols = src.columns ?? 0;
800
+ const rows = src.rows ?? 0;
801
+ // A stdout that has stopped being a TTY (piped, or the window is gone)
802
+ // reports 0/undefined. Resizing a PTY to zero wedges the child.
803
+ if (cols < 1 || rows < 1)
804
+ return;
805
+ // SIGWINCH also fires for changes that are not a size change. Resizing to the
806
+ // size we already have would burn an epoch and force a pointless re-seed.
807
+ if (cols === state.cols && rows === state.rows)
808
+ return;
809
+ state.cols = cols;
810
+ state.rows = rows;
811
+ state.term.resize(cols, rows);
812
+ rawRing = newRawRing(rows);
813
+ try {
814
+ state.ptyProcess.resize(cols, rows);
815
+ }
816
+ catch {
817
+ // The child is already gone; onExit will clear `current`.
818
+ }
819
+ }
759
820
  function teardownStdio(state) {
821
+ if (state.resizeSource && state.resizeListener && typeof state.resizeSource.removeListener === 'function') {
822
+ state.resizeSource.removeListener('resize', state.resizeListener);
823
+ }
760
824
  const { stdin, stdinListener } = state;
761
825
  if (stdin && stdinListener && typeof stdin.removeListener === 'function') {
762
826
  stdin.removeListener('data', stdinListener);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yolo-labs/yolobridge",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "YoloBridge — local coding-agent daemon that attaches a user's own Claude Code/Codex session to a YOLO Studio workspace as a first-class tile (docs/YOLOBRIDGE_PLAN.md, build-order Phase 5).",
5
5
  "license": "MIT",
6
6
  "type": "module",