@crouton-kit/humanloop 0.2.1 → 0.3.2
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.js +15 -7
- package/dist/cli.js +809 -385
- package/dist/inbox/deck-schema.d.ts +1 -0
- package/dist/inbox/deck-schema.js +1 -0
- package/dist/render/termrender.d.ts +2 -2
- package/dist/render/termrender.js +80 -24
- package/dist/render/version.d.ts +1 -1
- package/dist/render/version.js +1 -1
- package/dist/tui/input.js +67 -8
- package/dist/tui/render.js +32 -9
- package/dist/types.d.ts +6 -0
- package/package.json +1 -1
|
@@ -25,6 +25,7 @@ export declare const deckSchema: z.ZodObject<{
|
|
|
25
25
|
description: z.ZodOptional<z.ZodString>;
|
|
26
26
|
shortcut: z.ZodOptional<z.ZodString>;
|
|
27
27
|
}, z.core.$strip>>;
|
|
28
|
+
multiSelect: z.ZodOptional<z.ZodBoolean>;
|
|
28
29
|
allowFreetext: z.ZodOptional<z.ZodBoolean>;
|
|
29
30
|
freetextLabel: z.ZodOptional<z.ZodString>;
|
|
30
31
|
kind: z.ZodOptional<z.ZodEnum<{
|
|
@@ -17,6 +17,7 @@ const interactionSchema = z.object({
|
|
|
17
17
|
body: z.string().optional(),
|
|
18
18
|
bodyPath: z.string().optional(),
|
|
19
19
|
options: z.array(interactionOptionSchema),
|
|
20
|
+
multiSelect: z.boolean().optional(),
|
|
20
21
|
allowFreetext: z.boolean().optional(),
|
|
21
22
|
freetextLabel: z.string().optional(),
|
|
22
23
|
kind: z.enum(['notify', 'validation', 'decision', 'context', 'error']).optional(),
|
|
@@ -13,7 +13,7 @@ export declare function ensureRenderer(): void;
|
|
|
13
13
|
export declare function isRendererReady(): boolean;
|
|
14
14
|
/** Render markdown to terminal lines via the pinned binary; plaintext fallback. */
|
|
15
15
|
export declare function renderMarkdown(md: string, width: number): string[];
|
|
16
|
-
/** Validate markdown via `termrender
|
|
16
|
+
/** Validate markdown via `termrender doc check`. */
|
|
17
17
|
export declare function checkMarkdown(md: string): {
|
|
18
18
|
ok: true;
|
|
19
19
|
} | {
|
|
@@ -21,7 +21,7 @@ export declare function checkMarkdown(md: string): {
|
|
|
21
21
|
error: string;
|
|
22
22
|
};
|
|
23
23
|
export interface DisplayInPaneOpts {
|
|
24
|
-
/** Pass
|
|
24
|
+
/** Pass watch so the pane live-updates on file edits. Default true. */
|
|
25
25
|
watch?: boolean;
|
|
26
26
|
/** Open in a new tmux window instead of splitting the current one. */
|
|
27
27
|
newWindow?: boolean;
|
|
@@ -32,18 +32,34 @@ function binaryOk() {
|
|
|
32
32
|
if (!existsSync(VENV_BIN))
|
|
33
33
|
return false;
|
|
34
34
|
try {
|
|
35
|
-
|
|
35
|
+
// v2 contract: no --version flag; use -h (exit 0) as a liveness check.
|
|
36
|
+
execFileSync(VENV_BIN, ['-h'], {
|
|
36
37
|
encoding: 'utf8',
|
|
37
38
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
38
39
|
timeout: 5000,
|
|
39
40
|
});
|
|
40
|
-
|
|
41
|
-
return m?.[0] === TERMRENDER_VERSION;
|
|
41
|
+
return true;
|
|
42
42
|
}
|
|
43
43
|
catch {
|
|
44
44
|
return false;
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
+
// Returns the termrender version installed in the managed venv (via
|
|
48
|
+
// importlib.metadata), or null if the venv is missing/broken or termrender is
|
|
49
|
+
// not installed. Used by ensureRenderer() to detect drift from the pin and
|
|
50
|
+
// trigger a reinstall — otherwise a venv provisioned at an older pin sticks
|
|
51
|
+
// forever (binaryOk passes for any working binary, regardless of version).
|
|
52
|
+
function installedVersion() {
|
|
53
|
+
if (!existsSync(VENV_PYTHON))
|
|
54
|
+
return null;
|
|
55
|
+
try {
|
|
56
|
+
const out = execFileSync(VENV_PYTHON, ['-c', 'import importlib.metadata as m; print(m.version("termrender"))'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 5000 });
|
|
57
|
+
return out.trim() || null;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
47
63
|
function uvAvailable() {
|
|
48
64
|
try {
|
|
49
65
|
execFileSync('uv', ['--version'], { stdio: 'pipe', timeout: 5000 });
|
|
@@ -70,7 +86,7 @@ export function ensureRenderer() {
|
|
|
70
86
|
rendererState = 'unavailable';
|
|
71
87
|
return;
|
|
72
88
|
}
|
|
73
|
-
if (binaryOk()) {
|
|
89
|
+
if (binaryOk() && installedVersion() === TERMRENDER_VERSION) {
|
|
74
90
|
rendererState = 'ready';
|
|
75
91
|
return;
|
|
76
92
|
}
|
|
@@ -81,7 +97,12 @@ export function ensureRenderer() {
|
|
|
81
97
|
return;
|
|
82
98
|
}
|
|
83
99
|
try {
|
|
84
|
-
|
|
100
|
+
// Skip venv creation on drift (venv exists with wrong termrender version)
|
|
101
|
+
// — `uv pip install` into the existing venv replaces it in place. Only
|
|
102
|
+
// create when the venv directory is genuinely absent.
|
|
103
|
+
if (!existsSync(VENV_DIR)) {
|
|
104
|
+
execFileSync('uv', ['venv', VENV_DIR], { stdio: 'pipe', timeout: 60000 });
|
|
105
|
+
}
|
|
85
106
|
execFileSync('uv', ['pip', 'install', '--python', VENV_PYTHON, `termrender==${TERMRENDER_VERSION}`], { stdio: 'pipe', timeout: 120000 });
|
|
86
107
|
}
|
|
87
108
|
catch (err) {
|
|
@@ -89,9 +110,9 @@ export function ensureRenderer() {
|
|
|
89
110
|
rendererState = 'unavailable';
|
|
90
111
|
return;
|
|
91
112
|
}
|
|
92
|
-
rendererState = binaryOk() ? 'ready' : 'unavailable';
|
|
113
|
+
rendererState = (binaryOk() && installedVersion() === TERMRENDER_VERSION) ? 'ready' : 'unavailable';
|
|
93
114
|
if (rendererState === 'unavailable') {
|
|
94
|
-
process.stderr.write('[hl] termrender install completed but
|
|
115
|
+
process.stderr.write('[hl] termrender install completed but health check failed; using plaintext fallback\n');
|
|
95
116
|
}
|
|
96
117
|
}
|
|
97
118
|
/** Cheap predicate — true when the pinned managed binary is present and correct. Does not install. */
|
|
@@ -172,12 +193,12 @@ export function renderMarkdown(md, width) {
|
|
|
172
193
|
ensureRenderer();
|
|
173
194
|
if (rendererState === 'ready') {
|
|
174
195
|
try {
|
|
175
|
-
const
|
|
176
|
-
|
|
196
|
+
const input = JSON.stringify({ source: md, width, color: true });
|
|
197
|
+
const out = execFileSync(VENV_BIN, ['doc', 'render'], {
|
|
198
|
+
input,
|
|
177
199
|
encoding: 'utf-8',
|
|
178
200
|
timeout: 5000,
|
|
179
201
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
180
|
-
env: { ...process.env, TERMRENDER_COLOR: '1' },
|
|
181
202
|
});
|
|
182
203
|
const lines = out.split('\n');
|
|
183
204
|
if (lines.length > 0 && lines[lines.length - 1] === '')
|
|
@@ -193,23 +214,42 @@ export function renderMarkdown(md, width) {
|
|
|
193
214
|
_bodyCache.set(key, fallback);
|
|
194
215
|
return fallback;
|
|
195
216
|
}
|
|
196
|
-
/** Validate markdown via `termrender
|
|
217
|
+
/** Validate markdown via `termrender doc check`. */
|
|
197
218
|
export function checkMarkdown(md) {
|
|
198
219
|
ensureRenderer();
|
|
199
220
|
// Renderer unavailable → don't block validation; the body just renders as
|
|
200
221
|
// plaintext later. Bricking deck validation here would be the wrong default.
|
|
201
222
|
if (rendererState !== 'ready')
|
|
202
223
|
return { ok: true };
|
|
203
|
-
const
|
|
204
|
-
|
|
224
|
+
const input = JSON.stringify({ source: md });
|
|
225
|
+
const result = spawnSync(VENV_BIN, ['doc', 'check'], {
|
|
226
|
+
input,
|
|
205
227
|
encoding: 'utf-8',
|
|
206
228
|
timeout: 5000,
|
|
207
229
|
});
|
|
208
230
|
if (result.error) {
|
|
209
|
-
return { ok: false, error: `termrender invocation failed: ${result.error.message}` };
|
|
231
|
+
return { ok: false, error: `termrender: invocation failed: ${result.error.message}` };
|
|
232
|
+
}
|
|
233
|
+
let parsed = null;
|
|
234
|
+
const rawStdout = typeof result.stdout === 'string' ? result.stdout : '';
|
|
235
|
+
if (rawStdout) {
|
|
236
|
+
try {
|
|
237
|
+
parsed = JSON.parse(rawStdout.trim());
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
// stdout not parseable — fall through to exit-code handling
|
|
241
|
+
}
|
|
210
242
|
}
|
|
243
|
+
if (parsed !== null) {
|
|
244
|
+
if (parsed.ok)
|
|
245
|
+
return { ok: true };
|
|
246
|
+
const first = Array.isArray(parsed.errors) ? parsed.errors[0] : undefined;
|
|
247
|
+
const msg = (first && typeof first.message === 'string' && first.message) ? first.message : 'invalid markdown';
|
|
248
|
+
return { ok: false, error: `termrender: ${msg}` };
|
|
249
|
+
}
|
|
250
|
+
// exit code 2 = invalid per contract; any non-zero is an error
|
|
211
251
|
if (result.status !== 0) {
|
|
212
|
-
return { ok: false, error: `termrender
|
|
252
|
+
return { ok: false, error: `termrender: doc check exited ${result.status}` };
|
|
213
253
|
}
|
|
214
254
|
return { ok: true };
|
|
215
255
|
}
|
|
@@ -222,15 +262,31 @@ export function displayInPane(path, opts = {}) {
|
|
|
222
262
|
ensureRenderer();
|
|
223
263
|
if (rendererState !== 'ready')
|
|
224
264
|
return {};
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
265
|
+
const input = JSON.stringify({
|
|
266
|
+
path,
|
|
267
|
+
watch: opts.watch !== false,
|
|
268
|
+
window: opts.newWindow ? 'new' : 'split',
|
|
269
|
+
});
|
|
270
|
+
const result = spawnSync(VENV_BIN, ['pane', 'open'], {
|
|
271
|
+
input,
|
|
272
|
+
encoding: 'utf-8',
|
|
273
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
274
|
+
});
|
|
232
275
|
if (result.error || result.status !== 0)
|
|
233
276
|
return {};
|
|
234
|
-
|
|
235
|
-
|
|
277
|
+
// `encoding: 'utf-8'` makes spawnSync return stdout as a string.
|
|
278
|
+
const rawStdout = result.stdout;
|
|
279
|
+
if (!rawStdout)
|
|
280
|
+
return {};
|
|
281
|
+
let parsed = null;
|
|
282
|
+
try {
|
|
283
|
+
parsed = JSON.parse(rawStdout.trim());
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return {};
|
|
287
|
+
}
|
|
288
|
+
if (parsed && typeof parsed.pane_id === 'string' && parsed.pane_id) {
|
|
289
|
+
return { paneId: parsed.pane_id };
|
|
290
|
+
}
|
|
291
|
+
return {};
|
|
236
292
|
}
|
package/dist/render/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const TERMRENDER_VERSION = "1.0
|
|
1
|
+
export declare const TERMRENDER_VERSION = "2.1.0";
|
package/dist/render/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const TERMRENDER_VERSION = '1.0
|
|
1
|
+
export const TERMRENDER_VERSION = '2.1.0';
|
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
|
@@ -276,9 +276,10 @@ export function renderItemReview(state, cols, rows) {
|
|
|
276
276
|
const label = interaction.freetextLabel !== undefined
|
|
277
277
|
? interaction.freetextLabel
|
|
278
278
|
: state.inputMode.kind === 'comment' ? 'Comment' : 'Response';
|
|
279
|
-
// 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.
|
|
280
281
|
let attachedLine;
|
|
281
|
-
if (state.inputMode.kind === 'comment') {
|
|
282
|
+
if (state.inputMode.kind === 'comment' && !interaction.multiSelect) {
|
|
282
283
|
const attachedId = state.inputMode.selectedOptionId;
|
|
283
284
|
const opts = interaction.options;
|
|
284
285
|
if (opts.length > 0) {
|
|
@@ -330,11 +331,18 @@ export function renderItemReview(state, cols, rows) {
|
|
|
330
331
|
visibleBody = bodyLines;
|
|
331
332
|
}
|
|
332
333
|
// Footer hint — mention scroll keys when body overflows
|
|
333
|
-
const footerParts =
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
+
];
|
|
338
346
|
if (overflows)
|
|
339
347
|
footerParts.unshift(`${DIM}u/d${RESET} scroll`);
|
|
340
348
|
const footer = ` ${footerParts.join(' ')}`;
|
|
@@ -356,7 +364,9 @@ function renderActions(interaction, selectedAction, maxW, existing) {
|
|
|
356
364
|
const opts = interaction.options;
|
|
357
365
|
// Prefix on first row: " X [s] " — 2 + 1 (cursor) + 1 + 3 ([s]) + 1 = 8 visible cols.
|
|
358
366
|
// Continuation rows align under the label so each option reads as a block.
|
|
359
|
-
const
|
|
367
|
+
const multi = interaction.multiSelect === true;
|
|
368
|
+
const checked = new Set(existing?.selectedOptionIds ?? []);
|
|
369
|
+
const prefixWidth = multi ? 12 : 8;
|
|
360
370
|
const indent = ' '.repeat(prefixWidth);
|
|
361
371
|
const contentMax = Math.max(20, maxW - prefixWidth);
|
|
362
372
|
for (let i = 0; i < opts.length; i++) {
|
|
@@ -364,9 +374,12 @@ function renderActions(interaction, selectedAction, maxW, existing) {
|
|
|
364
374
|
const cursor = i === selectedAction ? `${CYAN}▸${RESET}` : ' ';
|
|
365
375
|
const sc = o.shortcut ?? ' ';
|
|
366
376
|
const keyBadge = `${DIM}[${sc}]${RESET}`;
|
|
377
|
+
const box = multi
|
|
378
|
+
? (checked.has(o.id) ? `${GREEN}[x]${RESET}` : `${DIM}[ ]${RESET}`) + ' '
|
|
379
|
+
: '';
|
|
367
380
|
const labelLines = wrap(sanitize(o.label), contentMax);
|
|
368
381
|
for (let j = 0; j < labelLines.length; j++) {
|
|
369
|
-
const prefix = j === 0 ? ` ${cursor} ${keyBadge} ` : indent;
|
|
382
|
+
const prefix = j === 0 ? ` ${cursor} ${box}${keyBadge} ` : indent;
|
|
370
383
|
lines.push(`${prefix}${labelLines[j]}`);
|
|
371
384
|
}
|
|
372
385
|
if (o.description) {
|
|
@@ -433,6 +446,16 @@ export function renderFinal(state, cols, rows) {
|
|
|
433
446
|
return centerHorizontal(lines.slice(0, rows), cols, maxW + 2);
|
|
434
447
|
}
|
|
435
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
|
+
}
|
|
436
459
|
const opt = r.selectedOptionId
|
|
437
460
|
? interaction.options.find((o) => o.id === r.selectedOptionId)
|
|
438
461
|
: undefined;
|
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 {
|