@crouton-kit/humanloop 0.3.13 → 0.3.15
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 +2 -1
- package/dist/cli.js +2 -2
- package/dist/inbox/convention.d.ts +10 -1
- package/dist/inbox/convention.js +17 -0
- package/dist/inbox/deck-schema.d.ts +1 -2
- package/dist/inbox/deck-schema.js +5 -1
- package/dist/tui/input.js +28 -3
- package/dist/tui/render.js +6 -0
- package/dist/types.d.ts +10 -0
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -4,7 +4,7 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { resolveInteractionDir } from './tui/app.js';
|
|
5
5
|
import { scanInbox } from './inbox/scan.js';
|
|
6
6
|
import { pickFromInbox } from './inbox/tui.js';
|
|
7
|
-
import { deckPath, atomicWriteJson, readJson } from './inbox/convention.js';
|
|
7
|
+
import { deckPath, atomicWriteJson, readJson, stampCanvasNode } from './inbox/convention.js';
|
|
8
8
|
import { getTerminalSize } from './tui/terminal.js';
|
|
9
9
|
import { approveDeck, notifyDeck } from './inbox/deck-factories.js';
|
|
10
10
|
const RESPONSE_SCHEMA_ID = 'humanloop.response/v2';
|
|
@@ -61,6 +61,7 @@ function buildSummary(deck, responses) {
|
|
|
61
61
|
export async function ask(deck, opts = {}) {
|
|
62
62
|
const dir = opts.dir ?? managedDir();
|
|
63
63
|
mkdirSync(dir, { recursive: true });
|
|
64
|
+
stampCanvasNode(deck);
|
|
64
65
|
atomicWriteJson(deckPath(dir), deck);
|
|
65
66
|
const { responses, completedAt, responsePath, deck: answeredDeck } = await resolveInteractionDir(dir, deck, {
|
|
66
67
|
sessionId: opts.sessionId,
|
package/dist/cli.js
CHANGED
|
@@ -11,7 +11,7 @@ import { ask, inbox } from './api.js';
|
|
|
11
11
|
import { display } from './surfaces/display.js';
|
|
12
12
|
import { renderMarkdown, checkMarkdown } from './render/termrender.js';
|
|
13
13
|
import { scanInbox } from './inbox/scan.js';
|
|
14
|
-
import { deckPath, atomicWriteJson, readJson, responsePath, } from './inbox/convention.js';
|
|
14
|
+
import { deckPath, atomicWriteJson, readJson, responsePath, stampCanvasNode, } from './inbox/convention.js';
|
|
15
15
|
// ── Version ───────────────────────────────────────────────────────────────────
|
|
16
16
|
const HL_VERSION = '0.2.1';
|
|
17
17
|
function emitError(err, exitCode = 1) {
|
|
@@ -212,7 +212,6 @@ const REQUEST_SCHEMA = {
|
|
|
212
212
|
id: { type: 'string' },
|
|
213
213
|
label: { type: 'string' },
|
|
214
214
|
description: { type: 'string' },
|
|
215
|
-
shortcut: { type: 'string', description: 'Single char. Auto-assigned if absent.' },
|
|
216
215
|
},
|
|
217
216
|
},
|
|
218
217
|
},
|
|
@@ -372,6 +371,7 @@ deckCmd
|
|
|
372
371
|
}
|
|
373
372
|
const dir = input.dir ? resolve(input.dir) : mkdtempSync(join(tmpdir(), 'hl-ix-'));
|
|
374
373
|
mkdirSync(dir, { recursive: true });
|
|
374
|
+
stampCanvasNode(deck);
|
|
375
375
|
atomicWriteJson(deckPath(dir), deck);
|
|
376
376
|
const jobId = basename(dir);
|
|
377
377
|
const visuals = input.visuals !== false;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { InteractionResponse } from '../types.js';
|
|
1
|
+
import type { Deck, InteractionResponse } from '../types.js';
|
|
2
2
|
export declare function deckPath(dir: string): string;
|
|
3
3
|
export declare function responsePath(dir: string): string;
|
|
4
4
|
export declare function progressPath(dir: string): string;
|
|
@@ -10,6 +10,15 @@ export declare function interactionState(dir: string): InteractionState;
|
|
|
10
10
|
export declare function isResolved(dir: string): boolean;
|
|
11
11
|
/** Returns true if a live resolver owns this dir (progress.json mtime < 300s). */
|
|
12
12
|
export declare function isClaimed(dir: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Stamp the originating canvas node id onto a deck's `source` so per-node
|
|
15
|
+
* attention scoping (crouter's nav chrome) can attribute the ask to the node
|
|
16
|
+
* that raised it rather than every sibling node sharing the same cwd.
|
|
17
|
+
*
|
|
18
|
+
* No-op when not inside a canvas node (CRTR_NODE_ID unset) or when the deck
|
|
19
|
+
* already carries a nodeId. Mutates `deck` in place.
|
|
20
|
+
*/
|
|
21
|
+
export declare function stampCanvasNode(deck: Deck): void;
|
|
13
22
|
export declare function atomicWriteJson(path: string, value: unknown): void;
|
|
14
23
|
export declare function readJson<T>(path: string): T | null;
|
|
15
24
|
export declare function writeResponse(dir: string, responses: InteractionResponse[], completedAt: string): string;
|
package/dist/inbox/convention.js
CHANGED
|
@@ -47,6 +47,23 @@ export function isClaimed(dir) {
|
|
|
47
47
|
return false;
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
+
// ── Canvas-node attribution ─────────────────────────────────────────
|
|
51
|
+
/**
|
|
52
|
+
* Stamp the originating canvas node id onto a deck's `source` so per-node
|
|
53
|
+
* attention scoping (crouter's nav chrome) can attribute the ask to the node
|
|
54
|
+
* that raised it rather than every sibling node sharing the same cwd.
|
|
55
|
+
*
|
|
56
|
+
* No-op when not inside a canvas node (CRTR_NODE_ID unset) or when the deck
|
|
57
|
+
* already carries a nodeId. Mutates `deck` in place.
|
|
58
|
+
*/
|
|
59
|
+
export function stampCanvasNode(deck) {
|
|
60
|
+
const id = process.env['CRTR_NODE_ID'];
|
|
61
|
+
if (id === undefined || id.trim() === '')
|
|
62
|
+
return;
|
|
63
|
+
if (deck.source?.nodeId != null && deck.source.nodeId !== '')
|
|
64
|
+
return;
|
|
65
|
+
deck.source = { ...(deck.source ?? {}), nodeId: id };
|
|
66
|
+
}
|
|
50
67
|
// ── Atomic I/O ────────────────────────────────────────────────────────────────
|
|
51
68
|
export function atomicWriteJson(path, value) {
|
|
52
69
|
const payload = JSON.stringify(value, null, 2);
|
|
@@ -4,7 +4,6 @@ export declare const interactionOptionSchema: z.ZodObject<{
|
|
|
4
4
|
id: z.ZodString;
|
|
5
5
|
label: z.ZodString;
|
|
6
6
|
description: z.ZodOptional<z.ZodString>;
|
|
7
|
-
shortcut: z.ZodOptional<z.ZodString>;
|
|
8
7
|
}, z.core.$strip>;
|
|
9
8
|
export declare const preAnswerSchema: z.ZodObject<{
|
|
10
9
|
selectedOptionId: z.ZodOptional<z.ZodString>;
|
|
@@ -18,6 +17,7 @@ export declare const deckSchema: z.ZodObject<{
|
|
|
18
17
|
sessionName: z.ZodOptional<z.ZodString>;
|
|
19
18
|
askedBy: z.ZodOptional<z.ZodString>;
|
|
20
19
|
blockedSince: z.ZodOptional<z.ZodString>;
|
|
20
|
+
nodeId: z.ZodOptional<z.ZodString>;
|
|
21
21
|
}, z.core.$strip>>;
|
|
22
22
|
interactions: z.ZodArray<z.ZodObject<{
|
|
23
23
|
id: z.ZodString;
|
|
@@ -29,7 +29,6 @@ export declare const deckSchema: z.ZodObject<{
|
|
|
29
29
|
id: z.ZodString;
|
|
30
30
|
label: z.ZodString;
|
|
31
31
|
description: z.ZodOptional<z.ZodString>;
|
|
32
|
-
shortcut: z.ZodOptional<z.ZodString>;
|
|
33
32
|
}, z.core.$strip>>;
|
|
34
33
|
multiSelect: z.ZodOptional<z.ZodBoolean>;
|
|
35
34
|
allowFreetext: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -8,7 +8,10 @@ export const interactionOptionSchema = z.object({
|
|
|
8
8
|
id: z.string().min(1),
|
|
9
9
|
label: z.string().min(1),
|
|
10
10
|
description: z.string().optional(),
|
|
11
|
-
shortcut
|
|
11
|
+
// No author-settable `shortcut`: option shortcuts are auto-assigned by the
|
|
12
|
+
// TUI (assignShortcuts). zod strips unknown keys, so any author-supplied
|
|
13
|
+
// `shortcut` is silently dropped here — this is the enforcement boundary that
|
|
14
|
+
// stops a deck from shadowing a reserved key (e.g. `c` = comment).
|
|
12
15
|
});
|
|
13
16
|
export const preAnswerSchema = z.object({
|
|
14
17
|
selectedOptionId: z.string().optional(),
|
|
@@ -33,6 +36,7 @@ const deckSourceSchema = z.object({
|
|
|
33
36
|
sessionName: z.string().optional(),
|
|
34
37
|
askedBy: z.string().optional(),
|
|
35
38
|
blockedSince: z.string().optional(),
|
|
39
|
+
nodeId: z.string().optional(),
|
|
36
40
|
});
|
|
37
41
|
export const deckSchema = z.object({
|
|
38
42
|
title: z.string().optional(),
|
package/dist/tui/input.js
CHANGED
|
@@ -34,6 +34,10 @@ export function handleKeypress(input, key, state, render, exit) {
|
|
|
34
34
|
exit();
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
|
+
// Clear any transient hint from the previous keypress. Handlers below may set
|
|
38
|
+
// a fresh one (e.g. an empty multi-select Enter), so it survives exactly one
|
|
39
|
+
// render cycle.
|
|
40
|
+
state.hint = undefined;
|
|
37
41
|
if (state.inputMode) {
|
|
38
42
|
handleInputMode(input, key, state, render);
|
|
39
43
|
checkAutoExit(state, exit);
|
|
@@ -53,9 +57,21 @@ export function handleKeypress(input, key, state, render, exit) {
|
|
|
53
57
|
}
|
|
54
58
|
}
|
|
55
59
|
function checkAutoExit(state, exit) {
|
|
56
|
-
if (state.phase
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
if (state.phase !== 'final')
|
|
61
|
+
return;
|
|
62
|
+
if (state.responses.size < state.interactions.length)
|
|
63
|
+
return;
|
|
64
|
+
// Multi-select commits route THROUGH the Summary/confirm screen instead of
|
|
65
|
+
// auto-exiting on the first Enter: the commit advances the deck to `final`,
|
|
66
|
+
// but the human must press Enter again (handleFinal) to actually submit.
|
|
67
|
+
// The interaction that pushed us into `final` is still at currentIndex — the
|
|
68
|
+
// advance helpers leave it there when they fall through to `final` — so a
|
|
69
|
+
// multi-select there means "just confirmed a set, await deliberate submit".
|
|
70
|
+
// Single-select keeps its submit-on-pick fast path (auto-exit below).
|
|
71
|
+
const justCommitted = state.interactions[state.currentIndex];
|
|
72
|
+
if (justCommitted?.multiSelect === true)
|
|
73
|
+
return;
|
|
74
|
+
exit();
|
|
59
75
|
}
|
|
60
76
|
// ── Overview ─────────────────────────────────────────────────────────────────
|
|
61
77
|
function handleOverview(input, key, state, render, exit) {
|
|
@@ -225,6 +241,15 @@ function handleInteractionAction(input, key, state, interaction, render) {
|
|
|
225
241
|
// and advances; single-select picks that one option.
|
|
226
242
|
if (key.return && state.selectedAction < opts.length) {
|
|
227
243
|
if (interaction.multiSelect) {
|
|
244
|
+
const checked = state.responses.get(interaction.id)?.selectedOptionIds ?? [];
|
|
245
|
+
if (checked.length === 0) {
|
|
246
|
+
// Accidental Enter with nothing toggled is a no-op: don't finalize or
|
|
247
|
+
// advance. A deliberate empty finish is still reachable via `q` →
|
|
248
|
+
// overview → finish (partial); freetext-only via the [c] row.
|
|
249
|
+
state.hint = 'Select at least one option (space to toggle), or q to skip';
|
|
250
|
+
render();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
228
253
|
commitMulti(state, interaction);
|
|
229
254
|
advanceToNextUnanswered(state);
|
|
230
255
|
render();
|
package/dist/tui/render.js
CHANGED
|
@@ -322,6 +322,12 @@ export function renderItemReview(state, cols, rows) {
|
|
|
322
322
|
else {
|
|
323
323
|
postLines.push(...renderActions(interaction, state.selectedAction, maxW, response));
|
|
324
324
|
}
|
|
325
|
+
// Transient hint (e.g. an empty multi-select Enter that was rejected). Sits
|
|
326
|
+
// just above the footer; cleared on the next keypress.
|
|
327
|
+
if (state.hint !== undefined && state.hint.length > 0) {
|
|
328
|
+
postLines.push('');
|
|
329
|
+
postLines.push(` ${YELLOW}${sanitize(state.hint)}${RESET}`);
|
|
330
|
+
}
|
|
325
331
|
// Window the body
|
|
326
332
|
const reservedRows = preLines.length + postLines.length + 1; // +1 for footer
|
|
327
333
|
const bodyHeight = Math.max(1, rows - reservedRows);
|
package/dist/types.d.ts
CHANGED
|
@@ -4,6 +4,9 @@ export interface InteractionOption {
|
|
|
4
4
|
id: string;
|
|
5
5
|
label: string;
|
|
6
6
|
description?: string;
|
|
7
|
+
/** Auto-assigned by the TUI (assignShortcuts) — NOT author-settable. Omitted
|
|
8
|
+
* from the deck schema so a deck can't create a shortcut that shadows a
|
|
9
|
+
* reserved key (e.g. `c` = comment, which would submit+close on single-select). */
|
|
7
10
|
shortcut?: string;
|
|
8
11
|
}
|
|
9
12
|
/**
|
|
@@ -54,6 +57,10 @@ export interface DeckSource {
|
|
|
54
57
|
sessionName?: string;
|
|
55
58
|
askedBy?: string;
|
|
56
59
|
blockedSince?: string;
|
|
60
|
+
/** Originating canvas node id (CRTR_NODE_ID) when the ask was raised inside
|
|
61
|
+
* a crouter canvas node. Lets per-node attention scoping attribute the ask
|
|
62
|
+
* to the node that raised it rather than every sibling sharing the cwd. */
|
|
63
|
+
nodeId?: string;
|
|
57
64
|
}
|
|
58
65
|
export interface Deck {
|
|
59
66
|
title?: string;
|
|
@@ -114,6 +121,9 @@ export interface TuiState {
|
|
|
114
121
|
selectedAction: number;
|
|
115
122
|
detailExpanded: boolean;
|
|
116
123
|
scrollOffset: number;
|
|
124
|
+
/** Transient one-line notice shown in item-review (e.g. an empty multi-select
|
|
125
|
+
* Enter that was rejected). Cleared on the next keypress. */
|
|
126
|
+
hint?: string;
|
|
117
127
|
persist?: () => void;
|
|
118
128
|
}
|
|
119
129
|
/**
|