@crouton-kit/humanloop 0.1.3 → 0.1.4

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/tui/app.js CHANGED
@@ -103,8 +103,11 @@ export function validateInput(parsed) {
103
103
  }
104
104
  // ── Internal helpers ──────────────────────────────────────────────────────────
105
105
  function buildInitialState(deck) {
106
+ // Single-question decks skip the overview list — there's nothing to overview,
107
+ // and overview hides the option hotkeys so users press 'y' and nothing happens.
108
+ const initialPhase = deck.interactions.length === 1 ? 'item-review' : 'overview';
106
109
  return {
107
- phase: 'overview',
110
+ phase: initialPhase,
108
111
  currentIndex: 0,
109
112
  interactions: deck.interactions,
110
113
  responses: new Map(),
package/dist/tui/input.js CHANGED
@@ -62,18 +62,21 @@ function handleOverview(input, key, state, render, exit) {
62
62
  if (input === 'j' || key.downArrow) {
63
63
  state.currentIndex = Math.min(state.currentIndex + 1, state.interactions.length - 1);
64
64
  render();
65
+ return;
65
66
  }
66
- else if (input === 'k' || key.upArrow) {
67
+ if (input === 'k' || key.upArrow) {
67
68
  state.currentIndex = Math.max(state.currentIndex - 1, 0);
68
69
  render();
70
+ return;
69
71
  }
70
- else if (key.return) {
72
+ if (key.return || input === ' ') {
71
73
  state.phase = 'item-review';
72
74
  state.selectedAction = 0;
73
75
  state.detailExpanded = false;
74
76
  render();
77
+ return;
75
78
  }
76
- else if (input === 'q') {
79
+ if (input === 'q') {
77
80
  if (state.responses.size >= state.interactions.length) {
78
81
  exit();
79
82
  }
@@ -81,6 +84,19 @@ function handleOverview(input, key, state, render, exit) {
81
84
  state.phase = 'final';
82
85
  render();
83
86
  }
87
+ return;
88
+ }
89
+ // Quick-answer: option shortcut for the focused interaction. Lets users
90
+ // answer from the overview list without pressing Enter first.
91
+ const interaction = state.interactions[state.currentIndex];
92
+ if (interaction !== undefined) {
93
+ const matched = interaction.options.find((o) => o.shortcut === input);
94
+ if (matched !== undefined) {
95
+ submitOption(state, interaction, matched.id, undefined);
96
+ // Don't auto-advance the cursor — users may want to re-answer the same
97
+ // question. The response icon flips ✓ and they can j/k away when ready.
98
+ render();
99
+ }
84
100
  }
85
101
  }
86
102
  // ── Item Review ──────────────────────────────────────────────────────────────
@@ -166,6 +166,22 @@ function hardWrap(text, maxWidth) {
166
166
  }
167
167
  return out;
168
168
  }
169
+ // ── Horizontal centering ─────────────────────────────────────────────────────
170
+ /**
171
+ * Pad each non-empty line with leading spaces to horizontally center the
172
+ * `contentWidth`-wide block within `cols`. Wide terminals (dashboard, full
173
+ * screen) get visual breathing room; narrow panes (split tmux pane next to a
174
+ * spawning agent) skip centering because there's nothing to center.
175
+ *
176
+ * Empty lines stay empty so frame diffing can keep them as cheap no-ops.
177
+ */
178
+ function centerHorizontal(lines, cols, contentWidth) {
179
+ const extraPad = Math.max(0, Math.floor((cols - contentWidth) / 2));
180
+ if (extraPad === 0)
181
+ return lines;
182
+ const pad = ' '.repeat(extraPad);
183
+ return lines.map((line) => (line === '' ? '' : pad + line));
184
+ }
169
185
  // ── Frame buffer ─────────────────────────────────────────────────────────────
170
186
  export function diffFrame(prevFrame, nextLines, rows) {
171
187
  const writes = [];
@@ -237,7 +253,10 @@ export function renderOverview(state, cols, rows) {
237
253
  lines.push(` ${DIM}enter${RESET} review ${DIM}j/k${RESET} navigate ${DIM}q${RESET} finish`);
238
254
  while (lines.length < rows)
239
255
  lines.push('');
240
- return lines.slice(0, rows);
256
+ // Overview content extends roughly cols-16 wide for option labels; center
257
+ // against a 60-col cap (the divider width) when the terminal is much wider.
258
+ const centered = centerHorizontal(lines.slice(0, rows), cols, Math.min(cols, 60) + 2);
259
+ return centered;
241
260
  }
242
261
  export function renderItemReview(state, cols, rows) {
243
262
  const interaction = state.interactions[state.currentIndex];
@@ -307,7 +326,7 @@ export function renderItemReview(state, cols, rows) {
307
326
  ? opts.find((o) => o.id === attachedId)
308
327
  : undefined;
309
328
  const valueText = attached !== undefined
310
- ? `${CYAN}${sanitize(attached.label)}${RESET}`
329
+ ? `${CYAN}${singleLine(attached.label)}${RESET}`
311
330
  : `${DIM}none${RESET}`;
312
331
  attachedLine = ` ${DIM}attached:${RESET} ${valueText} ${DIM}[tab to cycle]${RESET}`;
313
332
  }
@@ -326,7 +345,7 @@ export function renderItemReview(state, cols, rows) {
326
345
  postLines.push(` ${DIM}enter${RESET} submit ${DIM}esc${RESET} cancel`);
327
346
  }
328
347
  else {
329
- postLines.push(...renderActions(interaction, state.selectedAction, response));
348
+ postLines.push(...renderActions(interaction, state.selectedAction, maxW, response));
330
349
  }
331
350
  // Window the body
332
351
  const reservedRows = preLines.length + postLines.length + 1; // +1 for footer
@@ -365,21 +384,37 @@ export function renderItemReview(state, cols, rows) {
365
384
  lines.push('');
366
385
  lines.push(footer);
367
386
  // Final clamp (safety net for very small terminals)
368
- if (lines.length > rows) {
369
- return [...lines.slice(0, rows - 1), footer];
370
- }
371
- return lines;
387
+ const clamped = lines.length > rows
388
+ ? [...lines.slice(0, rows - 1), footer]
389
+ : lines;
390
+ // Content occupies maxW cols of body + 2 cols of left prefix — center the
391
+ // whole block when the terminal is wider than that.
392
+ return centerHorizontal(clamped, cols, maxW + 2);
372
393
  }
373
- function renderActions(interaction, selectedAction, existing) {
394
+ function renderActions(interaction, selectedAction, maxW, existing) {
374
395
  const lines = [];
375
396
  const opts = interaction.options;
397
+ // Prefix on first row: " X [s] " — 2 + 1 (cursor) + 1 + 3 ([s]) + 1 = 8 visible cols.
398
+ // Continuation rows align under the label so each option reads as a block.
399
+ const prefixWidth = 8;
400
+ const indent = ' '.repeat(prefixWidth);
401
+ const contentMax = Math.max(20, maxW - prefixWidth);
376
402
  for (let i = 0; i < opts.length; i++) {
377
403
  const o = opts[i];
378
404
  const cursor = i === selectedAction ? `${CYAN}▸${RESET}` : ' ';
379
405
  const sc = o.shortcut ?? ' ';
380
406
  const keyBadge = `${DIM}[${sc}]${RESET}`;
381
- const desc = o.description ? ` ${DIM}— ${sanitize(o.description)}${RESET}` : '';
382
- lines.push(` ${cursor} ${keyBadge} ${sanitize(o.label)}${desc}`);
407
+ const labelLines = wrap(sanitize(o.label), contentMax);
408
+ for (let j = 0; j < labelLines.length; j++) {
409
+ const prefix = j === 0 ? ` ${cursor} ${keyBadge} ` : indent;
410
+ lines.push(`${prefix}${labelLines[j]}`);
411
+ }
412
+ if (o.description) {
413
+ const descLines = wrap(`— ${sanitize(o.description)}`, contentMax);
414
+ for (const dl of descLines) {
415
+ lines.push(`${indent}${DIM}${dl}${RESET}`);
416
+ }
417
+ }
383
418
  }
384
419
  if (interaction.allowFreetext && opts.length > 0) {
385
420
  const cursor = opts.length === selectedAction ? `${CYAN}▸${RESET}` : ' ';
@@ -435,7 +470,7 @@ export function renderFinal(state, cols, rows) {
435
470
  const lines = [...header, ...visible, ...footer];
436
471
  while (lines.length < rows)
437
472
  lines.push('');
438
- return lines.slice(0, rows);
473
+ return centerHorizontal(lines.slice(0, rows), cols, maxW + 2);
439
474
  }
440
475
  export function responseSummary(r, interaction) {
441
476
  const opt = r.selectedOptionId
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crouton-kit/humanloop",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
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",