@yolo-labs/yolobridge 0.8.0 → 0.9.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.
Files changed (2) hide show
  1. package/dist/local-agent.js +164 -9
  2. package/package.json +1 -1
@@ -124,22 +124,177 @@ function resolveRows(opts) {
124
124
  return DEFAULT_ROWS;
125
125
  return process.stdout.rows || DEFAULT_ROWS;
126
126
  }
127
+ const DEFAULT_ATTRS = {
128
+ bold: false,
129
+ dim: false,
130
+ italic: false,
131
+ underline: false,
132
+ inverse: false,
133
+ strikethrough: false,
134
+ fgMode: 'default',
135
+ fgColor: 0,
136
+ bgMode: 'default',
137
+ bgColor: 0,
138
+ };
139
+ function attrsAreDefault(a) {
140
+ return (!a.bold &&
141
+ !a.dim &&
142
+ !a.italic &&
143
+ !a.underline &&
144
+ !a.inverse &&
145
+ !a.strikethrough &&
146
+ a.fgMode === 'default' &&
147
+ a.bgMode === 'default');
148
+ }
149
+ /** `38;…` / `48;…` (or the compact 30-37/90-97/40-47/100-107 forms). */
150
+ function colorCodes(mode, color, fg) {
151
+ if (mode === 'default')
152
+ return [fg ? '39' : '49'];
153
+ if (mode === 'rgb') {
154
+ // Typings: RGB mode packs the colour as 0xRRGGBB.
155
+ const r = (color >> 16) & 0xff;
156
+ const g = (color >> 8) & 0xff;
157
+ const b = color & 0xff;
158
+ return [`${fg ? 38 : 48};2;${r};${g};${b}`];
159
+ }
160
+ // Palette: 0-255. 0-7 and 8-15 have compact single-code forms; the rest
161
+ // need the indexed form.
162
+ if (color < 8)
163
+ return [String((fg ? 30 : 40) + color)];
164
+ if (color < 16)
165
+ return [String((fg ? 90 : 100) + (color - 8))];
166
+ return [`${fg ? 38 : 48};5;${color}`];
167
+ }
168
+ /**
169
+ * The SGR parameters that move `prev` to `next` — EMPTY when nothing
170
+ * changed, which is what keeps the payload small (see
171
+ * `serializeTerminalBuffer`).
172
+ */
173
+ function sgrDiff(prev, next) {
174
+ const codes = [];
175
+ // Bold and dim share one "off" code (22), so turning either off means
176
+ // re-asserting whichever of the two survives.
177
+ if ((prev.bold && !next.bold) || (prev.dim && !next.dim)) {
178
+ codes.push('22');
179
+ if (next.bold)
180
+ codes.push('1');
181
+ if (next.dim)
182
+ codes.push('2');
183
+ }
184
+ else {
185
+ if (!prev.bold && next.bold)
186
+ codes.push('1');
187
+ if (!prev.dim && next.dim)
188
+ codes.push('2');
189
+ }
190
+ if (prev.italic !== next.italic)
191
+ codes.push(next.italic ? '3' : '23');
192
+ if (prev.underline !== next.underline)
193
+ codes.push(next.underline ? '4' : '24');
194
+ if (prev.inverse !== next.inverse)
195
+ codes.push(next.inverse ? '7' : '27');
196
+ if (prev.strikethrough !== next.strikethrough)
197
+ codes.push(next.strikethrough ? '9' : '29');
198
+ if (prev.fgMode !== next.fgMode || prev.fgColor !== next.fgColor) {
199
+ codes.push(...colorCodes(next.fgMode, next.fgColor, true));
200
+ }
201
+ if (prev.bgMode !== next.bgMode || prev.bgColor !== next.bgColor) {
202
+ codes.push(...colorCodes(next.bgMode, next.bgColor, false));
203
+ }
204
+ return codes;
205
+ }
127
206
  /**
128
- * Serializes the terminal's current buffer (scrollback + viewport) to
129
- * plain text no ANSI/SGR escape codes. Deliberately not using
130
- * `@xterm/addon-serialize`: that addon's `serialize()` reconstructs a
131
- * VT100-replayable stream (colors, cursor moves included) for re-feeding
132
- * into another terminal, which is the wrong shape for `read_tile_output`
133
- * — the consumer on the other end (an orchestrator tile, possibly an
134
- * LLM) wants clean text, not escape sequences. Walking `buffer.active`
135
- * directly and calling `translateToString` per line gives exactly that.
207
+ * Serializes the terminal's current buffer (scrollback + viewport) to text
208
+ * that keeps the agent's COLOUR and text styling, as SGR escapes only.
209
+ *
210
+ * Still deliberately not `@xterm/addon-serialize`: that addon reconstructs
211
+ * a fully VT100-replayable stream cursor moves, scroll regions, mode
212
+ * switches for re-feeding into another terminal, which is the wrong
213
+ * shape for `read_tile_output`. Its consumers (the browser tile, and an
214
+ * orchestrator/LLM reading the same capture) want the SCREEN as lines,
215
+ * with the styling that makes an agent's output readable, and nothing
216
+ * that repositions a cursor. So we walk `buffer.active` cell by cell and
217
+ * re-emit just the SGR state.
218
+ *
219
+ * Payload discipline is the reason this walks cells rather than emitting
220
+ * per cell: an escape is written ONLY where the attribute state actually
221
+ * changes, so a screen of unstyled text emits ZERO escapes and is
222
+ * byte-identical to what the old `translateToString(true)` produced. That
223
+ * matters — this capture is polled on an interval and crosses the
224
+ * network on every poll.
225
+ *
226
+ * Each line is self-contained: any line that ends with non-default
227
+ * attributes is closed with a reset, so state cannot bleed into the next
228
+ * line (the webapp splits this on `\n` and renders lines independently).
229
+ *
230
+ * NOT preserved, by design: cursor position, the alternate-screen flag,
231
+ * scroll regions, hyperlinks (OSC 8), and blink/invisible/overline — none
232
+ * of them survive into a static, line-split view.
136
233
  */
137
234
  export function serializeTerminalBuffer(term) {
138
235
  const buffer = term.buffer.active;
139
236
  const lines = [];
140
237
  for (let i = 0; i < buffer.length; i++) {
141
238
  const line = buffer.getLine(i);
142
- lines.push(line ? line.translateToString(true) : '');
239
+ if (!line) {
240
+ lines.push('');
241
+ continue;
242
+ }
243
+ // Collect first, so trailing blanks can be trimmed before any escape
244
+ // is emitted for them (matching the old `translateToString(true)`).
245
+ const cells = [];
246
+ for (let x = 0; x < line.length; x++) {
247
+ const cell = line.getCell(x);
248
+ if (!cell)
249
+ continue;
250
+ // Width 0 = the right half of a wide (CJK/emoji) glyph; its content
251
+ // already came out of the width-2 cell before it. Emitting it too
252
+ // would duplicate the character.
253
+ if (cell.getWidth() === 0)
254
+ continue;
255
+ // An untouched cell has no content at all; it renders as a space.
256
+ const chars = cell.getChars();
257
+ cells.push({
258
+ text: chars === '' ? ' ' : chars,
259
+ attrs: {
260
+ bold: !!cell.isBold(),
261
+ dim: !!cell.isDim(),
262
+ italic: !!cell.isItalic(),
263
+ underline: !!cell.isUnderline(),
264
+ inverse: !!cell.isInverse(),
265
+ strikethrough: !!cell.isStrikethrough(),
266
+ fgMode: cell.isFgRGB() ? 'rgb' : cell.isFgPalette() ? 'palette' : 'default',
267
+ // In default mode the colour NUMBER is meaningless (the typings
268
+ // say "should be 0"; the runtime actually reports -1). Normalise
269
+ // it so two default cells compare equal and emit no escape.
270
+ fgColor: cell.isFgDefault() ? 0 : cell.getFgColor(),
271
+ bgMode: cell.isBgRGB() ? 'rgb' : cell.isBgPalette() ? 'palette' : 'default',
272
+ bgColor: cell.isBgDefault() ? 0 : cell.getBgColor(),
273
+ },
274
+ });
275
+ }
276
+ // Right-trim, as before — but only cells that are blank AND unstyled.
277
+ // A run of spaces carrying a background colour is real, visible output
278
+ // (a status bar, a selection); dropping it would lose the paint.
279
+ while (cells.length > 0) {
280
+ const last = cells[cells.length - 1];
281
+ if (last.text === ' ' && attrsAreDefault(last.attrs))
282
+ cells.pop();
283
+ else
284
+ break;
285
+ }
286
+ let out = '';
287
+ let state = DEFAULT_ATTRS;
288
+ for (const cell of cells) {
289
+ const codes = sgrDiff(state, cell.attrs);
290
+ if (codes.length > 0)
291
+ out += `\x1b[${codes.join(';')}m`;
292
+ out += cell.text;
293
+ state = cell.attrs;
294
+ }
295
+ if (!attrsAreDefault(state))
296
+ out += '\x1b[0m';
297
+ lines.push(out);
143
298
  }
144
299
  while (lines.length > 0 && lines[lines.length - 1] === '')
145
300
  lines.pop();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yolo-labs/yolobridge",
3
- "version": "0.8.0",
3
+ "version": "0.9.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",