@crouton-kit/humanloop 0.3.21 → 0.3.22

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/inbox/tui.js CHANGED
@@ -113,17 +113,24 @@ export function pickFromInbox(items, opts) {
113
113
  const flush = () => {
114
114
  const { cols: currentCols, rows: currentRows } = getTerminalSize();
115
115
  const lines = buildInboxLines(items, currentCols, selectedIndex);
116
- const { writes, nextPrevFrame } = diffFrame(prevFrame, lines, currentRows);
116
+ const { writes, nextPrevFrame } = diffFrame(prevFrame, lines, currentRows, currentCols);
117
117
  process.stdout.write('\x1b[?2026h');
118
118
  for (const w of writes)
119
119
  process.stdout.write(w);
120
120
  process.stdout.write('\x1b[?2026l');
121
121
  prevFrame = nextPrevFrame;
122
122
  };
123
+ // Resize reflows/scrolls what's already on screen, invalidating the diff
124
+ // model — clear and redraw from scratch at the new size.
125
+ const onResize = () => {
126
+ prevFrame = [];
127
+ process.stdout.write('\x1b[2J\x1b[H');
128
+ flush();
129
+ };
123
130
  const done = (result) => {
124
131
  restoreTerminal();
125
132
  process.stdin.removeListener('data', onData);
126
- process.stdout.removeListener('resize', flush);
133
+ process.stdout.removeListener('resize', onResize);
127
134
  resolve(result);
128
135
  };
129
136
  setupTerminal();
@@ -151,6 +158,6 @@ export function pickFromInbox(items, opts) {
151
158
  }
152
159
  };
153
160
  process.stdin.on('data', onData);
154
- process.stdout.on('resize', flush);
161
+ process.stdout.on('resize', onResize);
155
162
  });
156
163
  }
package/dist/tui/app.js CHANGED
@@ -254,14 +254,26 @@ export async function resolveInteractionDir(dir, deck, opts = {}) {
254
254
  let currentDeck = deck;
255
255
  let deckWatch = null;
256
256
  const flushHost = (lines) => {
257
- const { rows: currentRows } = getTerminalSize();
258
- const { writes, nextPrevFrame } = diffFrame(prevFrameLocal, lines, currentRows);
257
+ const { cols: currentCols, rows: currentRows } = getTerminalSize();
258
+ const { writes, nextPrevFrame } = diffFrame(prevFrameLocal, lines, currentRows, currentCols);
259
259
  process.stdout.write('\x1b[?2026h');
260
260
  for (const w of writes)
261
261
  process.stdout.write(w);
262
262
  process.stdout.write('\x1b[?2026l');
263
263
  prevFrameLocal = nextPrevFrame;
264
264
  };
265
+ // On resize the terminal reflows/scrolls existing content, so the diff
266
+ // model no longer matches the screen: re-layout at the new size, clear
267
+ // everything, and redraw from scratch.
268
+ const onResize = () => {
269
+ if (panel === null)
270
+ return;
271
+ const { cols: c, rows: r } = getTerminalSize();
272
+ const lines = panel.handleResize(c, r);
273
+ prevFrameLocal = [];
274
+ process.stdout.write('\x1b[2J\x1b[H');
275
+ flushHost(lines);
276
+ };
265
277
  const finalize = (responses) => {
266
278
  if (deckWatch !== null) {
267
279
  clearInterval(deckWatch);
@@ -269,6 +281,7 @@ export async function resolveInteractionDir(dir, deck, opts = {}) {
269
281
  }
270
282
  restoreTerminal();
271
283
  process.stdin.removeListener('data', onData);
284
+ process.stdout.removeListener('resize', onResize);
272
285
  panel?.unmount();
273
286
  const completedAt = new Date().toISOString();
274
287
  // Resolved supersedes in-progress: write response.json, drop progress.json.
@@ -342,6 +355,7 @@ export async function resolveInteractionDir(dir, deck, opts = {}) {
342
355
  flushHost(panel.render());
343
356
  };
344
357
  process.stdin.on('data', onData);
358
+ process.stdout.on('resize', onResize);
345
359
  });
346
360
  }
347
361
  // ── launchTui — file-path entry over the dir resolver (a kept public export
@@ -1,6 +1,14 @@
1
1
  import type { TuiState, Interaction, InteractionResponse } from '../types.js';
2
2
  export declare function sanitize(text: string): string;
3
- export declare function diffFrame(prevFrame: string[], nextLines: string[], rows: number): {
3
+ /**
4
+ * ANSI-aware clip: truncate a line's *visible* width to `maxWidth`, passing
5
+ * escape sequences through untouched. Every line written to the terminal must
6
+ * fit within the columns, or it physically wraps onto the next row — which
7
+ * breaks diffFrame's one-logical-line-per-row model and strands spillover text
8
+ * on rows the differ believes are empty (so it never erases them).
9
+ */
10
+ export declare function clipLine(line: string, maxWidth: number): string;
11
+ export declare function diffFrame(prevFrame: string[], nextLines: string[], rows: number, cols?: number): {
4
12
  writes: string[];
5
13
  nextPrevFrame: string[];
6
14
  };
@@ -143,15 +143,54 @@ function centerHorizontal(lines, cols, contentWidth) {
143
143
  return lines.map((line) => (line === '' ? '' : pad + line));
144
144
  }
145
145
  // ── Frame buffer ─────────────────────────────────────────────────────────────
146
- export function diffFrame(prevFrame, nextLines, rows) {
146
+ /**
147
+ * ANSI-aware clip: truncate a line's *visible* width to `maxWidth`, passing
148
+ * escape sequences through untouched. Every line written to the terminal must
149
+ * fit within the columns, or it physically wraps onto the next row — which
150
+ * breaks diffFrame's one-logical-line-per-row model and strands spillover text
151
+ * on rows the differ believes are empty (so it never erases them).
152
+ */
153
+ export function clipLine(line, maxWidth) {
154
+ if (maxWidth < 1)
155
+ return '';
156
+ if (stringWidth(line) <= maxWidth)
157
+ return line; // string-width ignores ANSI
158
+ let out = '';
159
+ let w = 0;
160
+ let i = 0;
161
+ let sawAnsi = false;
162
+ while (i < line.length) {
163
+ if (line[i] === '\x1b') {
164
+ const m = /^\x1b\[[0-9;?]*[a-zA-Z]|^\x1b[@-_]/.exec(line.slice(i));
165
+ if (m !== null) {
166
+ out += m[0];
167
+ i += m[0].length;
168
+ sawAnsi = true;
169
+ continue;
170
+ }
171
+ }
172
+ const ch = String.fromCodePoint(line.codePointAt(i));
173
+ const cw = stringWidth(ch);
174
+ if (w + cw > maxWidth)
175
+ break;
176
+ out += ch;
177
+ w += cw;
178
+ i += ch.length;
179
+ }
180
+ return sawAnsi ? out + RESET : out;
181
+ }
182
+ export function diffFrame(prevFrame, nextLines, rows, cols) {
183
+ const clipped = cols !== undefined
184
+ ? nextLines.map((l) => clipLine(l, cols))
185
+ : nextLines;
147
186
  const writes = [];
148
187
  for (let i = 0; i < rows; i++) {
149
- const line = i < nextLines.length ? nextLines[i] : '';
188
+ const line = i < clipped.length ? clipped[i] : '';
150
189
  if (prevFrame[i] !== line) {
151
190
  writes.push(`${ESC}${i + 1};1H${ESC}2K${line}`);
152
191
  }
153
192
  }
154
- return { writes, nextPrevFrame: [...nextLines] };
193
+ return { writes, nextPrevFrame: [...clipped] };
155
194
  }
156
195
  // ── Renderers ────────────────────────────────────────────────────────────────
157
196
  export function renderOverview(state, cols, rows) {
@@ -301,12 +340,14 @@ export function renderItemReview(state, cols, rows) {
301
340
  ? opts.find((o) => o.id === attachedId)
302
341
  : undefined;
303
342
  const valueText = attached !== undefined
304
- ? `${CYAN}${singleLine(attached.label)}${RESET}`
343
+ ? `${CYAN}${truncate(singleLine(attached.label), Math.max(10, maxW - 28))}${RESET}`
305
344
  : `${DIM}none (overall)${RESET}`;
306
345
  attachedLine = ` ${DIM}attached:${RESET} ${valueText} ${DIM}[tab to cycle]${RESET}`;
307
346
  }
308
347
  }
309
- postLines.push(` ${YELLOW}${label}:${RESET}`);
348
+ for (const labelLine of wrap(`${singleLine(label)}:`, maxW)) {
349
+ postLines.push(` ${YELLOW}${labelLine}${RESET}`);
350
+ }
310
351
  const bufLines = hardWrap(state.inputMode.buffer, maxW - 1);
311
352
  for (let i = 0; i < bufLines.length; i++) {
312
353
  const isLast = i === bufLines.length - 1;
@@ -326,7 +367,9 @@ export function renderItemReview(state, cols, rows) {
326
367
  // just above the footer; cleared on the next keypress.
327
368
  if (state.hint !== undefined && state.hint.length > 0) {
328
369
  postLines.push('');
329
- postLines.push(` ${YELLOW}${sanitize(state.hint)}${RESET}`);
370
+ for (const hintLine of wrap(sanitize(state.hint), maxW)) {
371
+ postLines.push(` ${YELLOW}${hintLine}${RESET}`);
372
+ }
330
373
  }
331
374
  // Window the body
332
375
  const reservedRows = preLines.length + postLines.length + 1; // +1 for footer
@@ -428,15 +471,25 @@ function renderActions(interaction, selectedAction, maxW, existing) {
428
471
  label = 'Add overall comment (c on an option for per-option)';
429
472
  else
430
473
  label = 'Add comment';
431
- lines.push(` ${cursor} ${DIM}[c]${RESET} ${label}`);
474
+ const ftLines = wrap(sanitize(label), contentMax);
475
+ for (let j = 0; j < ftLines.length; j++) {
476
+ const prefix = j === 0 ? ` ${cursor} ${DIM}[c]${RESET} ` : ' '.repeat(8);
477
+ lines.push(`${prefix}${ftLines[j]}`);
478
+ }
432
479
  }
433
480
  else if (interaction.allowFreetext && opts.length === 0) {
434
481
  const ftLabel = interaction.freetextLabel !== undefined ? interaction.freetextLabel : 'Enter response';
435
- lines.push(` ${DIM}[r]${RESET} ${ftLabel}`);
482
+ const ftLines = wrap(sanitize(ftLabel), contentMax);
483
+ for (let j = 0; j < ftLines.length; j++) {
484
+ const prefix = j === 0 ? ` ${DIM}[r]${RESET} ` : ' '.repeat(6);
485
+ lines.push(`${prefix}${ftLines[j]}`);
486
+ }
436
487
  }
437
488
  if (existing) {
438
489
  lines.push('');
439
- lines.push(` ${GREEN}Current: ${responseSummary(existing, interaction)}${RESET}`);
490
+ for (const curLine of wrap(`Current: ${responseSummary(existing, interaction)}`, maxW)) {
491
+ lines.push(` ${GREEN}${curLine}${RESET}`);
492
+ }
440
493
  }
441
494
  return lines;
442
495
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crouton-kit/humanloop",
3
- "version": "0.3.21",
3
+ "version": "0.3.22",
4
4
  "description": "Human-in-the-loop decision TUI — agents write questions, humans answer them",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",