@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 +4 -1
- package/dist/tui/input.js +19 -3
- package/dist/tui/render.js +46 -11
- package/package.json +1 -1
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:
|
|
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
|
-
|
|
67
|
+
if (input === 'k' || key.upArrow) {
|
|
67
68
|
state.currentIndex = Math.max(state.currentIndex - 1, 0);
|
|
68
69
|
render();
|
|
70
|
+
return;
|
|
69
71
|
}
|
|
70
|
-
|
|
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
|
-
|
|
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 ──────────────────────────────────────────────────────────────
|
package/dist/tui/render.js
CHANGED
|
@@ -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
|
-
|
|
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}${
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
|
382
|
-
|
|
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
|