@crouton-kit/humanloop 0.1.4 → 0.3.1
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/api.d.ts +35 -0
- package/dist/api.js +127 -0
- package/dist/cli.js +858 -183
- package/dist/editor/review.d.ts +24 -0
- package/dist/editor/review.js +425 -0
- package/dist/inbox/convention.d.ts +17 -0
- package/dist/inbox/convention.js +87 -0
- package/dist/inbox/deck-schema.d.ts +42 -0
- package/dist/inbox/deck-schema.js +110 -0
- package/dist/inbox/scan.d.ts +2 -0
- package/dist/inbox/scan.js +62 -0
- package/dist/inbox/tui.d.ts +9 -0
- package/dist/inbox/tui.js +158 -0
- package/dist/index.d.ts +11 -1
- package/dist/index.js +13 -0
- package/dist/render/termrender.d.ts +36 -0
- package/dist/render/termrender.js +271 -0
- package/dist/render/version.d.ts +1 -0
- package/dist/render/version.js +1 -0
- package/dist/scripts/install-renderer.d.ts +2 -0
- package/dist/scripts/install-renderer.js +16 -0
- package/dist/surfaces/display.d.ts +5 -0
- package/dist/surfaces/display.js +19 -0
- package/dist/tui/app.d.ts +24 -1
- package/dist/tui/app.js +48 -113
- package/dist/tui/input.js +67 -8
- package/dist/tui/render.js +34 -51
- package/dist/tui/tmux.d.ts +4 -6
- package/dist/tui/tmux.js +6 -4
- package/dist/types.d.ts +71 -0
- package/dist/visuals/generate.js +2 -27
- package/package.json +4 -2
package/dist/tui/input.js
CHANGED
|
@@ -92,7 +92,12 @@ function handleOverview(input, key, state, render, exit) {
|
|
|
92
92
|
if (interaction !== undefined) {
|
|
93
93
|
const matched = interaction.options.find((o) => o.shortcut === input);
|
|
94
94
|
if (matched !== undefined) {
|
|
95
|
-
|
|
95
|
+
if (interaction.multiSelect) {
|
|
96
|
+
toggleMulti(state, interaction, matched.id);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
submitOption(state, interaction, matched.id, undefined);
|
|
100
|
+
}
|
|
96
101
|
// Don't auto-advance the cursor — users may want to re-answer the same
|
|
97
102
|
// question. The response icon flips ✓ and they can j/k away when ready.
|
|
98
103
|
render();
|
|
@@ -117,6 +122,13 @@ function handleItemReview(input, key, state, render) {
|
|
|
117
122
|
render();
|
|
118
123
|
return;
|
|
119
124
|
}
|
|
125
|
+
// Space toggles the focused option for multi-select; otherwise expand context.
|
|
126
|
+
if (input === ' ' && interaction.multiSelect
|
|
127
|
+
&& state.selectedAction < interaction.options.length) {
|
|
128
|
+
toggleMulti(state, interaction, interaction.options[state.selectedAction].id);
|
|
129
|
+
render();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
120
132
|
if (input === ' ') {
|
|
121
133
|
state.detailExpanded = !state.detailExpanded;
|
|
122
134
|
render();
|
|
@@ -151,18 +163,23 @@ function handleItemReview(input, key, state, render) {
|
|
|
151
163
|
}
|
|
152
164
|
function handleInteractionAction(input, key, state, interaction, render) {
|
|
153
165
|
const opts = interaction.options;
|
|
154
|
-
// Match by shortcut
|
|
166
|
+
// Match by shortcut. Multi-select toggles (stay put); single-select submits.
|
|
155
167
|
const matched = opts.find((o) => o.shortcut === input);
|
|
156
168
|
if (matched !== undefined) {
|
|
169
|
+
if (interaction.multiSelect) {
|
|
170
|
+
toggleMulti(state, interaction, matched.id);
|
|
171
|
+
render();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
157
174
|
submitOption(state, interaction, matched.id, undefined);
|
|
158
175
|
advanceItem(state, 1);
|
|
159
176
|
render();
|
|
160
177
|
return;
|
|
161
178
|
}
|
|
162
|
-
// Comment mode: allowFreetext + options exist
|
|
163
|
-
//
|
|
179
|
+
// Comment mode: allowFreetext + options exist. Multi-select carries its
|
|
180
|
+
// checked set on submit, so don't pre-attach a single option.
|
|
164
181
|
if (input === 'c' && interaction.allowFreetext && opts.length > 0) {
|
|
165
|
-
const preselected = state.selectedAction < opts.length
|
|
182
|
+
const preselected = !interaction.multiSelect && state.selectedAction < opts.length
|
|
166
183
|
? opts[state.selectedAction].id
|
|
167
184
|
: undefined;
|
|
168
185
|
state.inputMode = preselected !== undefined
|
|
@@ -181,8 +198,15 @@ function handleInteractionAction(input, key, state, interaction, render) {
|
|
|
181
198
|
return;
|
|
182
199
|
}
|
|
183
200
|
}
|
|
184
|
-
// Enter on selected option row
|
|
201
|
+
// Enter on selected option row. Multi-select confirms the accumulated set
|
|
202
|
+
// and advances; single-select picks that one option.
|
|
185
203
|
if (key.return && state.selectedAction < opts.length) {
|
|
204
|
+
if (interaction.multiSelect) {
|
|
205
|
+
commitMulti(state, interaction);
|
|
206
|
+
advanceItem(state, 1);
|
|
207
|
+
render();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
186
210
|
const o = opts[state.selectedAction];
|
|
187
211
|
submitOption(state, interaction, o.id, undefined);
|
|
188
212
|
advanceItem(state, 1);
|
|
@@ -225,8 +249,13 @@ function handleInputMode(input, key, state, render) {
|
|
|
225
249
|
}
|
|
226
250
|
if (key.return) {
|
|
227
251
|
const interaction = state.interactions[state.currentIndex];
|
|
228
|
-
|
|
229
|
-
|
|
252
|
+
if (interaction.multiSelect) {
|
|
253
|
+
commitMulti(state, interaction, mode.buffer);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
const attached = mode.kind === 'comment' ? mode.selectedOptionId : undefined;
|
|
257
|
+
submitOption(state, interaction, attached, mode.buffer);
|
|
258
|
+
}
|
|
230
259
|
state.inputMode = null;
|
|
231
260
|
advanceItem(state, 1);
|
|
232
261
|
render();
|
|
@@ -285,3 +314,33 @@ function submitOption(state, interaction, selectedOptionId, freetext) {
|
|
|
285
314
|
state.responses.set(interaction.id, response);
|
|
286
315
|
state.persist?.();
|
|
287
316
|
}
|
|
317
|
+
// ── Multi-select ─────────────────────────────────────────────────────────────
|
|
318
|
+
// Toggle/commit write progressively into state.responses (mirrors single-select
|
|
319
|
+
// submitOption immediacy); Enter just confirms the accumulated set + advances.
|
|
320
|
+
function toggleMulti(state, interaction, optionId) {
|
|
321
|
+
const existing = state.responses.get(interaction.id);
|
|
322
|
+
const set = new Set(existing?.selectedOptionIds ?? []);
|
|
323
|
+
if (set.has(optionId))
|
|
324
|
+
set.delete(optionId);
|
|
325
|
+
else
|
|
326
|
+
set.add(optionId);
|
|
327
|
+
const response = { id: interaction.id, selectedOptionIds: [...set] };
|
|
328
|
+
if (existing?.freetext !== undefined)
|
|
329
|
+
response.freetext = existing.freetext;
|
|
330
|
+
state.responses.set(interaction.id, response);
|
|
331
|
+
state.persist?.();
|
|
332
|
+
}
|
|
333
|
+
/** Ensure a (possibly empty) response exists so the interaction counts as
|
|
334
|
+
* answered, optionally setting/replacing freetext. */
|
|
335
|
+
function commitMulti(state, interaction, freetext) {
|
|
336
|
+
const existing = state.responses.get(interaction.id);
|
|
337
|
+
const response = {
|
|
338
|
+
id: interaction.id,
|
|
339
|
+
selectedOptionIds: [...(existing?.selectedOptionIds ?? [])],
|
|
340
|
+
};
|
|
341
|
+
const ft = freetext ?? existing?.freetext;
|
|
342
|
+
if (ft !== undefined)
|
|
343
|
+
response.freetext = ft;
|
|
344
|
+
state.responses.set(interaction.id, response);
|
|
345
|
+
state.persist?.();
|
|
346
|
+
}
|
package/dist/tui/render.js
CHANGED
|
@@ -1,45 +1,5 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process';
|
|
2
1
|
import stringWidth from 'string-width';
|
|
3
|
-
|
|
4
|
-
let _termrenderAvail = null;
|
|
5
|
-
function isTermrenderAvailable() {
|
|
6
|
-
if (_termrenderAvail !== null)
|
|
7
|
-
return _termrenderAvail;
|
|
8
|
-
try {
|
|
9
|
-
execFileSync('termrender', ['--version'], { stdio: 'pipe', timeout: 3000 });
|
|
10
|
-
_termrenderAvail = true;
|
|
11
|
-
}
|
|
12
|
-
catch {
|
|
13
|
-
_termrenderAvail = false;
|
|
14
|
-
}
|
|
15
|
-
return _termrenderAvail;
|
|
16
|
-
}
|
|
17
|
-
const _bodyCache = new Map();
|
|
18
|
-
function renderBody(text, width) {
|
|
19
|
-
const key = `${text}\0${width}`;
|
|
20
|
-
const cached = _bodyCache.get(key);
|
|
21
|
-
if (cached)
|
|
22
|
-
return cached;
|
|
23
|
-
if (isTermrenderAvailable()) {
|
|
24
|
-
try {
|
|
25
|
-
const out = execFileSync('termrender', ['--width', String(width)], {
|
|
26
|
-
input: text,
|
|
27
|
-
encoding: 'utf-8',
|
|
28
|
-
timeout: 5000,
|
|
29
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
30
|
-
});
|
|
31
|
-
const lines = out.split('\n');
|
|
32
|
-
if (lines.length > 0 && lines[lines.length - 1] === '')
|
|
33
|
-
lines.pop();
|
|
34
|
-
_bodyCache.set(key, lines);
|
|
35
|
-
return lines;
|
|
36
|
-
}
|
|
37
|
-
catch { /* fall through */ }
|
|
38
|
-
}
|
|
39
|
-
const fallback = wrap(sanitize(text), width);
|
|
40
|
-
_bodyCache.set(key, fallback);
|
|
41
|
-
return fallback;
|
|
42
|
-
}
|
|
2
|
+
import { renderMarkdown } from '../render/termrender.js';
|
|
43
3
|
// ── ANSI helpers ─────────────────────────────────────────────────────────────
|
|
44
4
|
const ESC = '\x1b[';
|
|
45
5
|
const RESET = `${ESC}0m`;
|
|
@@ -282,7 +242,7 @@ export function renderItemReview(state, cols, rows) {
|
|
|
282
242
|
const bodyLines = [];
|
|
283
243
|
if (interaction.body) {
|
|
284
244
|
bodyLines.push('');
|
|
285
|
-
for (const line of
|
|
245
|
+
for (const line of renderMarkdown(interaction.body, maxW)) {
|
|
286
246
|
bodyLines.push(` ${line}`);
|
|
287
247
|
}
|
|
288
248
|
}
|
|
@@ -316,9 +276,10 @@ export function renderItemReview(state, cols, rows) {
|
|
|
316
276
|
const label = interaction.freetextLabel !== undefined
|
|
317
277
|
? interaction.freetextLabel
|
|
318
278
|
: state.inputMode.kind === 'comment' ? 'Comment' : 'Response';
|
|
319
|
-
// Show attached option (comment mode only) — Tab cycles
|
|
279
|
+
// Show attached option (single-select comment mode only) — Tab cycles.
|
|
280
|
+
// Multi-select comments carry the checked set, so no attach line.
|
|
320
281
|
let attachedLine;
|
|
321
|
-
if (state.inputMode.kind === 'comment') {
|
|
282
|
+
if (state.inputMode.kind === 'comment' && !interaction.multiSelect) {
|
|
322
283
|
const attachedId = state.inputMode.selectedOptionId;
|
|
323
284
|
const opts = interaction.options;
|
|
324
285
|
if (opts.length > 0) {
|
|
@@ -370,11 +331,18 @@ export function renderItemReview(state, cols, rows) {
|
|
|
370
331
|
visibleBody = bodyLines;
|
|
371
332
|
}
|
|
372
333
|
// Footer hint — mention scroll keys when body overflows
|
|
373
|
-
const footerParts =
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
334
|
+
const footerParts = interaction.multiSelect === true
|
|
335
|
+
? [
|
|
336
|
+
`${DIM}n/p${RESET} prev/next`,
|
|
337
|
+
`${DIM}space${RESET} toggle`,
|
|
338
|
+
`${DIM}enter${RESET} confirm`,
|
|
339
|
+
`${DIM}q${RESET} overview`,
|
|
340
|
+
]
|
|
341
|
+
: [
|
|
342
|
+
`${DIM}n/p${RESET} prev/next`,
|
|
343
|
+
`${DIM}space${RESET} expand`,
|
|
344
|
+
`${DIM}q${RESET} overview`,
|
|
345
|
+
];
|
|
378
346
|
if (overflows)
|
|
379
347
|
footerParts.unshift(`${DIM}u/d${RESET} scroll`);
|
|
380
348
|
const footer = ` ${footerParts.join(' ')}`;
|
|
@@ -396,7 +364,9 @@ function renderActions(interaction, selectedAction, maxW, existing) {
|
|
|
396
364
|
const opts = interaction.options;
|
|
397
365
|
// Prefix on first row: " X [s] " — 2 + 1 (cursor) + 1 + 3 ([s]) + 1 = 8 visible cols.
|
|
398
366
|
// Continuation rows align under the label so each option reads as a block.
|
|
399
|
-
const
|
|
367
|
+
const multi = interaction.multiSelect === true;
|
|
368
|
+
const checked = new Set(existing?.selectedOptionIds ?? []);
|
|
369
|
+
const prefixWidth = multi ? 12 : 8;
|
|
400
370
|
const indent = ' '.repeat(prefixWidth);
|
|
401
371
|
const contentMax = Math.max(20, maxW - prefixWidth);
|
|
402
372
|
for (let i = 0; i < opts.length; i++) {
|
|
@@ -404,9 +374,12 @@ function renderActions(interaction, selectedAction, maxW, existing) {
|
|
|
404
374
|
const cursor = i === selectedAction ? `${CYAN}▸${RESET}` : ' ';
|
|
405
375
|
const sc = o.shortcut ?? ' ';
|
|
406
376
|
const keyBadge = `${DIM}[${sc}]${RESET}`;
|
|
377
|
+
const box = multi
|
|
378
|
+
? (checked.has(o.id) ? `${GREEN}[x]${RESET}` : `${DIM}[ ]${RESET}`) + ' '
|
|
379
|
+
: '';
|
|
407
380
|
const labelLines = wrap(sanitize(o.label), contentMax);
|
|
408
381
|
for (let j = 0; j < labelLines.length; j++) {
|
|
409
|
-
const prefix = j === 0 ? ` ${cursor} ${keyBadge} ` : indent;
|
|
382
|
+
const prefix = j === 0 ? ` ${cursor} ${box}${keyBadge} ` : indent;
|
|
410
383
|
lines.push(`${prefix}${labelLines[j]}`);
|
|
411
384
|
}
|
|
412
385
|
if (o.description) {
|
|
@@ -473,6 +446,16 @@ export function renderFinal(state, cols, rows) {
|
|
|
473
446
|
return centerHorizontal(lines.slice(0, rows), cols, maxW + 2);
|
|
474
447
|
}
|
|
475
448
|
export function responseSummary(r, interaction) {
|
|
449
|
+
if (r.selectedOptionIds !== undefined) {
|
|
450
|
+
const labels = r.selectedOptionIds
|
|
451
|
+
.map((id) => interaction.options.find((o) => o.id === id))
|
|
452
|
+
.filter((o) => o !== undefined)
|
|
453
|
+
.map((o) => sanitize(o.label));
|
|
454
|
+
const picks = labels.length > 0 ? labels.join(', ') : '(none)';
|
|
455
|
+
if (r.freetext)
|
|
456
|
+
return `${picks}: "${sanitize(r.freetext)}"`;
|
|
457
|
+
return picks;
|
|
458
|
+
}
|
|
476
459
|
const opt = r.selectedOptionId
|
|
477
460
|
? interaction.options.find((o) => o.id === r.selectedOptionId)
|
|
478
461
|
: undefined;
|
package/dist/tui/tmux.d.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export interface TuiOutput {
|
|
3
|
-
responses: InteractionResponse[];
|
|
4
|
-
completedAt: string;
|
|
5
|
-
}
|
|
1
|
+
import type { ResolutionEnvelope } from '../types.js';
|
|
6
2
|
export interface TmuxDispatchOpts {
|
|
7
3
|
sessionId?: string;
|
|
8
4
|
visuals: boolean;
|
|
5
|
+
/** Interaction dir forwarded to the child so response.json lands there. */
|
|
6
|
+
dir: string;
|
|
9
7
|
}
|
|
10
|
-
export declare function dispatchToTmuxPane(file: string, opts: TmuxDispatchOpts): Promise<
|
|
8
|
+
export declare function dispatchToTmuxPane(file: string, opts: TmuxDispatchOpts): Promise<ResolutionEnvelope>;
|
package/dist/tui/tmux.js
CHANGED
|
@@ -15,8 +15,10 @@ function buildChildCmd(file, resultPath, opts) {
|
|
|
15
15
|
const parts = [
|
|
16
16
|
shellQuote(process.execPath),
|
|
17
17
|
shellQuote(scriptPath),
|
|
18
|
-
'
|
|
18
|
+
'ask',
|
|
19
19
|
shellQuote(file),
|
|
20
|
+
'--dir',
|
|
21
|
+
shellQuote(opts.dir),
|
|
20
22
|
'--write-to',
|
|
21
23
|
shellQuote(resultPath),
|
|
22
24
|
];
|
|
@@ -29,8 +31,8 @@ function buildChildCmd(file, resultPath, opts) {
|
|
|
29
31
|
return parts.join(' ');
|
|
30
32
|
}
|
|
31
33
|
export async function dispatchToTmuxPane(file, opts) {
|
|
32
|
-
const
|
|
33
|
-
const resultPath = join(
|
|
34
|
+
const parentTmp = mkdtempSync(join(tmpdir(), 'hl-'));
|
|
35
|
+
const resultPath = join(parentTmp, 'result.json');
|
|
34
36
|
const cmd = buildChildCmd(file, resultPath, opts);
|
|
35
37
|
// Capture the spawned pane id so we can detect if the user closes it
|
|
36
38
|
// without finishing — otherwise the parent would poll forever.
|
|
@@ -65,7 +67,7 @@ export async function dispatchToTmuxPane(file, opts) {
|
|
|
65
67
|
}
|
|
66
68
|
catch { /* ignore */ }
|
|
67
69
|
try {
|
|
68
|
-
rmdirSync(
|
|
70
|
+
rmdirSync(parentTmp);
|
|
69
71
|
}
|
|
70
72
|
catch { /* ignore */ }
|
|
71
73
|
return JSON.parse(json);
|
package/dist/types.d.ts
CHANGED
|
@@ -13,13 +13,19 @@ export interface Interaction {
|
|
|
13
13
|
body?: string;
|
|
14
14
|
bodyPath?: string;
|
|
15
15
|
options: InteractionOption[];
|
|
16
|
+
/** When true the human can check multiple options; the response carries
|
|
17
|
+
* `selectedOptionIds`. Absent/false = single-select (unchanged). */
|
|
18
|
+
multiSelect?: boolean;
|
|
16
19
|
allowFreetext?: boolean;
|
|
17
20
|
freetextLabel?: string;
|
|
18
21
|
kind?: InteractionKind;
|
|
19
22
|
}
|
|
20
23
|
export interface InteractionResponse {
|
|
21
24
|
id: string;
|
|
25
|
+
/** Single-select pick. */
|
|
22
26
|
selectedOptionId?: string;
|
|
27
|
+
/** Multi-select picks (set only for `multiSelect` interactions). */
|
|
28
|
+
selectedOptionIds?: string[];
|
|
23
29
|
freetext?: string;
|
|
24
30
|
}
|
|
25
31
|
export interface DeckSource {
|
|
@@ -32,6 +38,32 @@ export interface Deck {
|
|
|
32
38
|
source?: DeckSource;
|
|
33
39
|
interactions: Interaction[];
|
|
34
40
|
}
|
|
41
|
+
export interface FeedbackComment {
|
|
42
|
+
id: string;
|
|
43
|
+
/** 1-based source line where the comment is anchored (start). */
|
|
44
|
+
line: number;
|
|
45
|
+
/** 1-based source line where the anchored range ends (== line for one line). */
|
|
46
|
+
endLine: number;
|
|
47
|
+
/** Exact selected substring when the human made a visual selection. */
|
|
48
|
+
quote?: string;
|
|
49
|
+
/** 0-based byte column where a partial (charwise) selection starts on `line`. */
|
|
50
|
+
colStart?: number;
|
|
51
|
+
/** 0-based exclusive byte column where the selection ends on `endLine`. */
|
|
52
|
+
colEnd?: number;
|
|
53
|
+
/** Full source text of the anchored line(s) — context for the agent. */
|
|
54
|
+
lineText: string;
|
|
55
|
+
comment: string;
|
|
56
|
+
createdAt: string;
|
|
57
|
+
}
|
|
58
|
+
export interface FeedbackResult {
|
|
59
|
+
file: string;
|
|
60
|
+
submitted: boolean;
|
|
61
|
+
/** True when submitted with zero comments — human signalled "looks good". */
|
|
62
|
+
approved: boolean;
|
|
63
|
+
comments: FeedbackComment[];
|
|
64
|
+
submittedAt?: string;
|
|
65
|
+
savedAt: string;
|
|
66
|
+
}
|
|
35
67
|
export interface VisualBlock {
|
|
36
68
|
questionId: string;
|
|
37
69
|
content: string;
|
|
@@ -58,6 +90,45 @@ export interface TuiState {
|
|
|
58
90
|
scrollOffset: number;
|
|
59
91
|
persist?: () => void;
|
|
60
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Resolution contract returned by `ask`/`inbox`. On-disk `response.json` stays
|
|
95
|
+
* `{ responses, completedAt }`; `responsePath` points at it. `hl schema
|
|
96
|
+
* response` returns the JSON Schema this `schema` id names.
|
|
97
|
+
*/
|
|
98
|
+
export interface ResolutionEnvelope {
|
|
99
|
+
/** 1 line/interaction "<title>: <option label>[ — <freetext>]"; deterministic, no LLM. */
|
|
100
|
+
summary: string;
|
|
101
|
+
/** Absolute path to response.json. */
|
|
102
|
+
responsePath: string;
|
|
103
|
+
schema: 'humanloop.response/v2';
|
|
104
|
+
/** Inline (small). */
|
|
105
|
+
responses: InteractionResponse[];
|
|
106
|
+
/** ISO timestamp. */
|
|
107
|
+
completedAt: string;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* One pending interaction discovered by `scanInbox`. Read from the
|
|
111
|
+
* `deck.json` header only — never the full deck.
|
|
112
|
+
*/
|
|
113
|
+
export interface InboxItem {
|
|
114
|
+
dir: string;
|
|
115
|
+
id: string;
|
|
116
|
+
title?: string;
|
|
117
|
+
subtitle?: string;
|
|
118
|
+
kind?: InteractionKind;
|
|
119
|
+
/** `deck.source.blockedSince` ?? `statSync(deck.json).mtime` (ISO). */
|
|
120
|
+
blockedSince: string;
|
|
121
|
+
source?: DeckSource;
|
|
122
|
+
}
|
|
123
|
+
/** Options for `display()` — the live-watch tmux pane surface. */
|
|
124
|
+
export interface DisplayOpts {
|
|
125
|
+
/** Pass `--watch` so the pane live-updates on edits. Default true. */
|
|
126
|
+
watch?: boolean;
|
|
127
|
+
/** `'auto'` (default) splits until the pane budget, then opens a new window. */
|
|
128
|
+
window?: 'auto' | 'split' | 'new';
|
|
129
|
+
/** Pane budget per window before `'auto'` opens a new window. Default 3. */
|
|
130
|
+
maxPanes?: number;
|
|
131
|
+
}
|
|
61
132
|
export type GenerateVisual = (interaction: Interaction) => Promise<{
|
|
62
133
|
ok: true;
|
|
63
134
|
ansi: string;
|
package/dist/visuals/generate.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { query } from '@r-cli/sdk';
|
|
2
|
-
import {
|
|
2
|
+
import { renderMarkdown } from '../render/termrender.js';
|
|
3
3
|
const VISUAL_SYSTEM_PROMPT = `You're briefing a CTO-level engineer in the 30 seconds before they decide. They've been off this problem for days; they need a fast re-ground in what *already exists* — the files, data flow, or constraint they're deciding inside of — not a lecture on tradeoffs.
|
|
4
4
|
|
|
5
5
|
# Length
|
|
@@ -68,31 +68,6 @@ async function callHaiku(prompt, systemPrompt) {
|
|
|
68
68
|
return null;
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
|
-
function renderWithTermrender(markdown, width) {
|
|
72
|
-
// First attempt
|
|
73
|
-
const result = tryTermrender(markdown, width);
|
|
74
|
-
if (result !== null)
|
|
75
|
-
return result;
|
|
76
|
-
// Fallback: strip all directives and render as plain markdown
|
|
77
|
-
const stripped = markdown.replace(/^:{3,}\w*.*$/gm, '').trim();
|
|
78
|
-
const fallback = tryTermrender(stripped, width);
|
|
79
|
-
return fallback ?? markdown;
|
|
80
|
-
}
|
|
81
|
-
function tryTermrender(markdown, width) {
|
|
82
|
-
try {
|
|
83
|
-
return execSync(`termrender -w ${width}`, {
|
|
84
|
-
input: markdown,
|
|
85
|
-
encoding: 'utf8',
|
|
86
|
-
timeout: 5000,
|
|
87
|
-
env: { ...process.env, TERMRENDER_COLOR: '1' },
|
|
88
|
-
}).trimEnd();
|
|
89
|
-
}
|
|
90
|
-
catch (err) {
|
|
91
|
-
const stderr = err.stderr || '';
|
|
92
|
-
process.stderr.write(`[hl] termrender: ${stderr.split('\n')[0]}\n`);
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
71
|
// defaultGenerateVisual matches the GenerateVisual contract for use with
|
|
97
72
|
// mountPanel. Width is read from process.stdout.columns so callers that
|
|
98
73
|
// embed humanloop in a sub-region should supply their own closure that bakes
|
|
@@ -113,7 +88,7 @@ export async function defaultGenerateVisual(interaction, conversationContext) {
|
|
|
113
88
|
.replace(/^```[\w]*\n?/gm, '')
|
|
114
89
|
.replace(/^```\s*$/gm, '')
|
|
115
90
|
.trim();
|
|
116
|
-
const ansi =
|
|
91
|
+
const ansi = renderMarkdown(markdown, width).join('\n');
|
|
117
92
|
return { ok: true, ansi, markdown };
|
|
118
93
|
}
|
|
119
94
|
return { ok: false, error: 'haiku returned no output' };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crouton-kit/humanloop",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.3.1",
|
|
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",
|
|
@@ -25,12 +25,14 @@
|
|
|
25
25
|
"build": "tsc",
|
|
26
26
|
"dev": "tsx src/cli.ts",
|
|
27
27
|
"link": "npm link",
|
|
28
|
+
"postinstall": "node dist/scripts/install-renderer.js || true",
|
|
28
29
|
"test": "tsx src/__tests__/mount-panel.test.ts"
|
|
29
30
|
},
|
|
30
31
|
"dependencies": {
|
|
31
32
|
"@r-cli/sdk": "^1.3.0",
|
|
32
33
|
"commander": "^13.0.0",
|
|
33
|
-
"string-width": "^7.0.0"
|
|
34
|
+
"string-width": "^7.0.0",
|
|
35
|
+
"zod": "^4.3.6"
|
|
34
36
|
},
|
|
35
37
|
"devDependencies": {
|
|
36
38
|
"@types/node": "^22.0.0",
|