@yolo-labs/yolobridge 0.12.0 → 0.13.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.
@@ -199,6 +199,7 @@ export async function postReadOutputReply(cfg, workspaceId, attachmentId, reques
199
199
  output,
200
200
  busy,
201
201
  ...(extra?.cols && extra?.rows ? { cols: extra.cols, rows: extra.rows } : {}),
202
+ ...(extra?.usedRows ? { usedRows: extra.usedRows } : {}),
202
203
  ...(extra?.raw
203
204
  ? {
204
205
  raw: extra.raw.data,
@@ -245,6 +246,7 @@ export async function postOutputChunk(cfg, workspaceId, attachmentId, chunk) {
245
246
  ...(chunk.epoch ? { epoch: chunk.epoch } : {}),
246
247
  ...(typeof chunk.startOffset === 'number' ? { startOffset: chunk.startOffset } : {}),
247
248
  ...(chunk.cols && chunk.rows ? { cols: chunk.cols, rows: chunk.rows } : {}),
249
+ ...(chunk.usedRows ? { usedRows: chunk.usedRows } : {}),
248
250
  });
249
251
  return Boolean(body?.relayed);
250
252
  }
@@ -642,6 +642,7 @@ export async function runAttachDaemon(deps) {
642
642
  startOffset: batch.startOffset,
643
643
  cols: geometry?.cols,
644
644
  rows: geometry?.rows,
645
+ usedRows: geometry?.usedRows,
645
646
  });
646
647
  if (outputStream === session)
647
648
  session.seq += 1;
@@ -875,6 +876,7 @@ export async function runAttachDaemon(deps) {
875
876
  .postReadOutputReply(scopedCfg(), workspaceId, attachmentId, action.requestId, captured.output, captured.busy, {
876
877
  cols: raw?.cols ?? captured.cols,
877
878
  rows: raw?.rows ?? captured.rows,
879
+ usedRows: raw?.usedRows ?? captured.usedRows,
878
880
  raw: raw
879
881
  ? {
880
882
  epoch: raw.epoch,
@@ -294,6 +294,7 @@ export function takeRawSeed() {
294
294
  data,
295
295
  cols: current.cols,
296
296
  rows: current.rows,
297
+ usedRows: computeUsedRows(current.term),
297
298
  truncated,
298
299
  // Only a truncated replay is missing state that predates it.
299
300
  ...(truncated ? { prologue: buildModePrologue(observedModes(current.term), rawRing.modes) } : {}),
@@ -337,12 +338,93 @@ export function primeRawStream(maxBytes = RAW_STREAM_PRIME_BYTES) {
337
338
  : all;
338
339
  return { epoch: rawRing.epoch, startOffset: groundStart, data };
339
340
  }
341
+ /**
342
+ * Is this cell blank — nothing a viewer could see in it?
343
+ *
344
+ * "Blank" is deliberately the SAME notion `serializeTerminalBuffer`'s
345
+ * right-trim uses: an unwritten or space cell whose PAINT is also default. A
346
+ * run of spaces carrying a background colour (a status bar, a selection, a
347
+ * progress bar's filled portion) is real, visible output, and calling it blank
348
+ * would crop it off the bottom of the screen.
349
+ */
350
+ function cellIsBlank(cell) {
351
+ const chars = cell.getChars();
352
+ if (chars !== '' && chars !== ' ')
353
+ return false;
354
+ return (!!cell.isBgDefault() && !cell.isInverse() && !cell.isUnderline() && !cell.isStrikethrough());
355
+ }
356
+ /**
357
+ * How many of the grid's `rows` the session is actually USING — the last row
358
+ * with anything visible on it (or the cursor's row), whichever is lower down.
359
+ *
360
+ * ⚠️ WHY THE DAEMON COMPUTES THIS AND NOT THE VIEWER. The tile only ever
361
+ * receives a byte stream; reconstructing "how much of the screen is in use"
362
+ * from it means re-implementing the parser's own bookkeeping. The daemon
363
+ * already holds the authoritative `@xterm/headless` buffer, so the answer is a
364
+ * read rather than an inference.
365
+ *
366
+ * ⚠️ ALTERNATE SCREEN ⇒ `rows`, ALWAYS. A full-screen TUI legitimately paints
367
+ * every row it was given, and plenty of those rows look blank by this
368
+ * predicate (a padded pane, an empty list body, the gap above a footer).
369
+ * Cropping there would clip live UI off the bottom of a viewer's tile, so the
370
+ * alternate screen simply opts out: the whole grid is in use by definition.
371
+ *
372
+ * COST: bounded by the number of TRAILING BLANK rows — the walk starts at the
373
+ * bottom and returns at the first row with content, so a busy screen costs one
374
+ * row's cells and a mostly-empty one costs `(rows − usedRows) × cols` cell
375
+ * reads with no allocation. Nothing is serialized: this is called on every
376
+ * relayed chunk and a screen dump per chunk would be exactly the cost the raw
377
+ * stream exists to avoid.
378
+ *
379
+ * Never returns 0 — a terminal is always showing at least one row, and a zero
380
+ * would divide straight through the viewer's scale computation.
381
+ */
382
+ export function computeUsedRows(term) {
383
+ const buffer = term.buffer.active;
384
+ const rows = Math.max(1, term.rows);
385
+ if (buffer.type === 'alternate')
386
+ return rows;
387
+ // The viewport's first line within the buffer. The daemon's terminal is
388
+ // never scrolled by a human (nobody scrolls it — it is fed and read
389
+ // programmatically), so the base IS the viewport.
390
+ const top = buffer.baseY;
391
+ // The cursor is the session's live edge even on a row that is still empty
392
+ // (a fresh prompt line, a cleared input box), so it is a floor.
393
+ let used = Math.min(rows, Math.max(1, buffer.cursorY + 1));
394
+ for (let y = rows - 1; y >= used; y--) {
395
+ const line = buffer.getLine(top + y);
396
+ if (!line)
397
+ continue;
398
+ let blank = true;
399
+ for (let x = 0; x < line.length; x++) {
400
+ const cell = line.getCell(x);
401
+ if (!cell)
402
+ continue;
403
+ if (!cellIsBlank(cell)) {
404
+ blank = false;
405
+ break;
406
+ }
407
+ }
408
+ if (!blank) {
409
+ used = y + 1;
410
+ break;
411
+ }
412
+ }
413
+ return used;
414
+ }
340
415
  /** The PTY's grid, which the tile must render at EXACTLY (it cannot be
341
- * resized — the human at the keyboard is watching the same PTY). */
416
+ * resized — the human at the keyboard is watching the same PTY), plus how
417
+ * much of that grid is in USE.
418
+ *
419
+ * ⚠️ `usedRows` IS NOT A SECOND GEOMETRY. It never changes `cols`/`rows` and
420
+ * the viewer never resizes its terminal to it: a viewer's terminal that is
421
+ * not exactly `cols`×`rows` cannot replay the daemon's bytes (relative cursor
422
+ * moves and the scroll region are defined against the real grid). It exists
423
+ * so the viewer can decide how large to draw the text. */
342
424
  export function getLocalAgentGeometry() {
343
425
  if (!current)
344
426
  return undefined;
345
- return { cols: current.cols, rows: current.rows };
427
+ return { cols: current.cols, rows: current.rows, usedRows: computeUsedRows(current.term) };
346
428
  }
347
429
  /** The same recent-activity heuristic `captureLocalAgentOutput` reports, without
348
430
  * paying for a buffer serialization. */
@@ -872,5 +954,13 @@ export async function captureLocalAgentOutput() {
872
954
  // The grid the screen above was laid out FOR. A viewer that renders it at
873
955
  // any other width re-wraps every long line — the dump has no width of its
874
956
  // own, only the one the PTY composed it at.
875
- return { output, busy, cols: current.cols, rows: current.rows };
957
+ return {
958
+ output,
959
+ busy,
960
+ cols: current.cols,
961
+ rows: current.rows,
962
+ // How much of that grid is in use, so the viewer can scale the text to the
963
+ // rows that carry content instead of to a screenful of blank tail.
964
+ usedRows: computeUsedRows(current.term),
965
+ };
876
966
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yolo-labs/yolobridge",
3
- "version": "0.12.0",
3
+ "version": "0.13.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",