@crouton-kit/humanloop 0.1.0 → 0.1.3
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/cli.js +82 -89
- package/dist/index.d.ts +5 -0
- package/dist/index.js +2 -0
- package/dist/tui/app.d.ts +7 -2
- package/dist/tui/app.js +303 -105
- package/dist/tui/input.d.ts +2 -1
- package/dist/tui/input.js +129 -115
- package/dist/tui/render.d.ts +10 -5
- package/dist/tui/render.js +329 -157
- package/dist/tui/terminal.js +5 -0
- package/dist/tui/tmux.d.ts +6 -2
- package/dist/tui/tmux.js +20 -2
- package/dist/types.d.ts +57 -45
- package/dist/types.js +1 -1
- package/dist/visuals/generate.d.ts +9 -4
- package/dist/visuals/generate.js +30 -39
- package/package.json +14 -2
package/dist/tui/input.js
CHANGED
|
@@ -1,3 +1,34 @@
|
|
|
1
|
+
const RESERVED = new Set(['c', 'r', 'n', 'p', 'q', 'j', 'k', 'u', 'd', ' ']);
|
|
2
|
+
export function assignShortcuts(interactions) {
|
|
3
|
+
for (const it of interactions) {
|
|
4
|
+
const used = new Set(it.options.map((o) => o.shortcut).filter((s) => s !== undefined));
|
|
5
|
+
for (const opt of it.options) {
|
|
6
|
+
if (opt.shortcut !== undefined)
|
|
7
|
+
continue;
|
|
8
|
+
const letters = [...opt.label.toLowerCase()].filter((c) => /[a-z]/.test(c));
|
|
9
|
+
let chosen;
|
|
10
|
+
for (const letter of letters) {
|
|
11
|
+
if (!used.has(letter) && !RESERVED.has(letter)) {
|
|
12
|
+
chosen = letter;
|
|
13
|
+
break;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (chosen === undefined) {
|
|
17
|
+
for (let d = 1; d <= 9; d++) {
|
|
18
|
+
const s = String(d);
|
|
19
|
+
if (!used.has(s)) {
|
|
20
|
+
chosen = s;
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (chosen !== undefined) {
|
|
26
|
+
opt.shortcut = chosen;
|
|
27
|
+
used.add(chosen);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
1
32
|
export function handleKeypress(input, key, state, render, exit) {
|
|
2
33
|
if (key.ctrl && input === 'c') {
|
|
3
34
|
exit();
|
|
@@ -22,14 +53,14 @@ export function handleKeypress(input, key, state, render, exit) {
|
|
|
22
53
|
}
|
|
23
54
|
}
|
|
24
55
|
function checkAutoExit(state, exit) {
|
|
25
|
-
if (state.phase === 'final' && state.
|
|
56
|
+
if (state.phase === 'final' && state.responses.size >= state.interactions.length) {
|
|
26
57
|
exit();
|
|
27
58
|
}
|
|
28
59
|
}
|
|
29
60
|
// ── Overview ─────────────────────────────────────────────────────────────────
|
|
30
61
|
function handleOverview(input, key, state, render, exit) {
|
|
31
62
|
if (input === 'j' || key.downArrow) {
|
|
32
|
-
state.currentIndex = Math.min(state.currentIndex + 1, state.
|
|
63
|
+
state.currentIndex = Math.min(state.currentIndex + 1, state.interactions.length - 1);
|
|
33
64
|
render();
|
|
34
65
|
}
|
|
35
66
|
else if (input === 'k' || key.upArrow) {
|
|
@@ -43,7 +74,7 @@ function handleOverview(input, key, state, render, exit) {
|
|
|
43
74
|
render();
|
|
44
75
|
}
|
|
45
76
|
else if (input === 'q') {
|
|
46
|
-
if (state.
|
|
77
|
+
if (state.responses.size >= state.interactions.length) {
|
|
47
78
|
exit();
|
|
48
79
|
}
|
|
49
80
|
else {
|
|
@@ -54,8 +85,7 @@ function handleOverview(input, key, state, render, exit) {
|
|
|
54
85
|
}
|
|
55
86
|
// ── Item Review ──────────────────────────────────────────────────────────────
|
|
56
87
|
function handleItemReview(input, key, state, render) {
|
|
57
|
-
const
|
|
58
|
-
// Navigation
|
|
88
|
+
const interaction = state.interactions[state.currentIndex];
|
|
59
89
|
if (input === 'n') {
|
|
60
90
|
advanceItem(state, 1);
|
|
61
91
|
render();
|
|
@@ -76,9 +106,22 @@ function handleItemReview(input, key, state, render) {
|
|
|
76
106
|
render();
|
|
77
107
|
return;
|
|
78
108
|
}
|
|
79
|
-
//
|
|
109
|
+
// Body scroll: u/d or Ctrl+D / Ctrl+U (half-page), Ctrl+E / Ctrl+Y (line).
|
|
110
|
+
// Plain u/d exists because tmux configs commonly bind C-d/C-u for pane scroll
|
|
111
|
+
// and intercept them before they reach the app. Render clamps state.scrollOffset,
|
|
112
|
+
// so over-scroll past the bottom is harmless.
|
|
113
|
+
if (input === 'd' || (key.ctrl && (input === 'd' || input === 'e'))) {
|
|
114
|
+
state.scrollOffset = (state.scrollOffset ?? 0) + (input === 'e' ? 1 : 10);
|
|
115
|
+
render();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (input === 'u' || (key.ctrl && (input === 'u' || input === 'y'))) {
|
|
119
|
+
state.scrollOffset = Math.max(0, (state.scrollOffset ?? 0) - (input === 'y' ? 1 : 10));
|
|
120
|
+
render();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
80
123
|
if (input === 'j' || key.downArrow) {
|
|
81
|
-
const max = actionCount(
|
|
124
|
+
const max = actionCount(interaction) - 1;
|
|
82
125
|
state.selectedAction = Math.min(state.selectedAction + 1, max);
|
|
83
126
|
render();
|
|
84
127
|
return;
|
|
@@ -88,82 +131,55 @@ function handleItemReview(input, key, state, render) {
|
|
|
88
131
|
render();
|
|
89
132
|
return;
|
|
90
133
|
}
|
|
91
|
-
|
|
92
|
-
if (q.type === 'validation') {
|
|
93
|
-
handleValidationAction(input, key, state, q, render);
|
|
94
|
-
}
|
|
95
|
-
else if (q.type === 'choice') {
|
|
96
|
-
handleChoiceAction(input, key, state, q, render);
|
|
97
|
-
}
|
|
98
|
-
else {
|
|
99
|
-
handleFreetextAction(input, key, state, render);
|
|
100
|
-
}
|
|
134
|
+
handleInteractionAction(input, key, state, interaction, render);
|
|
101
135
|
}
|
|
102
|
-
function
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
136
|
+
function handleInteractionAction(input, key, state, interaction, render) {
|
|
137
|
+
const opts = interaction.options;
|
|
138
|
+
// Match by shortcut
|
|
139
|
+
const matched = opts.find((o) => o.shortcut === input);
|
|
140
|
+
if (matched !== undefined) {
|
|
141
|
+
submitOption(state, interaction, matched.id, undefined);
|
|
106
142
|
advanceItem(state, 1);
|
|
107
143
|
render();
|
|
144
|
+
return;
|
|
108
145
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
state.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
advanceItem(state, 1);
|
|
146
|
+
// Comment mode: allowFreetext + options exist
|
|
147
|
+
// If the cursor is on an option row, pre-attach that option to the comment.
|
|
148
|
+
if (input === 'c' && interaction.allowFreetext && opts.length > 0) {
|
|
149
|
+
const preselected = state.selectedAction < opts.length
|
|
150
|
+
? opts[state.selectedAction].id
|
|
151
|
+
: undefined;
|
|
152
|
+
state.inputMode = preselected !== undefined
|
|
153
|
+
? { kind: 'comment', buffer: '', selectedOptionId: preselected }
|
|
154
|
+
: { kind: 'comment', buffer: '' };
|
|
119
155
|
render();
|
|
156
|
+
return;
|
|
120
157
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
158
|
+
// Freetext-only: 'r' or enter opens input mode
|
|
159
|
+
if (interaction.allowFreetext && opts.length === 0) {
|
|
160
|
+
if (input === 'r' || key.return) {
|
|
161
|
+
const existing = state.responses.get(interaction.id);
|
|
162
|
+
const prefill = existing !== undefined && existing.freetext !== undefined ? existing.freetext : '';
|
|
163
|
+
state.inputMode = { kind: 'freetext', buffer: prefill };
|
|
164
|
+
render();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
126
167
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (digit >= 1 && digit <= numOptions) {
|
|
132
|
-
state.answers.set(q.id, {
|
|
133
|
-
id: q.id,
|
|
134
|
-
type: 'choice',
|
|
135
|
-
selected: q.options[digit - 1],
|
|
136
|
-
isCustom: false,
|
|
137
|
-
});
|
|
138
|
-
state.persist?.();
|
|
168
|
+
// Enter on selected option row
|
|
169
|
+
if (key.return && state.selectedAction < opts.length) {
|
|
170
|
+
const o = opts[state.selectedAction];
|
|
171
|
+
submitOption(state, interaction, o.id, undefined);
|
|
139
172
|
advanceItem(state, 1);
|
|
140
173
|
render();
|
|
141
174
|
return;
|
|
142
175
|
}
|
|
143
|
-
|
|
144
|
-
|
|
176
|
+
// Enter on the [c] row (allowFreetext + options exist)
|
|
177
|
+
if (key.return && state.selectedAction === opts.length
|
|
178
|
+
&& interaction.allowFreetext && opts.length > 0) {
|
|
179
|
+
state.inputMode = { kind: 'comment', buffer: '' };
|
|
145
180
|
render();
|
|
146
181
|
return;
|
|
147
182
|
}
|
|
148
|
-
if (key.return && state.selectedAction < numOptions) {
|
|
149
|
-
state.answers.set(q.id, {
|
|
150
|
-
id: q.id,
|
|
151
|
-
type: 'choice',
|
|
152
|
-
selected: q.options[state.selectedAction],
|
|
153
|
-
isCustom: false,
|
|
154
|
-
});
|
|
155
|
-
state.persist?.();
|
|
156
|
-
advanceItem(state, 1);
|
|
157
|
-
render();
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
function handleFreetextAction(input, key, state, render) {
|
|
161
|
-
if (input === 'r' || key.return) {
|
|
162
|
-
const existing = state.answers.get(state.questions[state.currentIndex].id);
|
|
163
|
-
const prefill = existing?.type === 'freetext' ? existing.response : '';
|
|
164
|
-
state.inputMode = { kind: 'freetext', buffer: prefill };
|
|
165
|
-
render();
|
|
166
|
-
}
|
|
167
183
|
}
|
|
168
184
|
// ── Input Mode ───────────────────────────────────────────────────────────────
|
|
169
185
|
function handleInputMode(input, key, state, render) {
|
|
@@ -173,57 +189,49 @@ function handleInputMode(input, key, state, render) {
|
|
|
173
189
|
render();
|
|
174
190
|
return;
|
|
175
191
|
}
|
|
192
|
+
// Tab cycles attached option in comment mode: (none) → opt1 → opt2 → ... → (none)
|
|
193
|
+
if (key.tab && mode.kind === 'comment') {
|
|
194
|
+
const interaction = state.interactions[state.currentIndex];
|
|
195
|
+
const opts = interaction.options;
|
|
196
|
+
if (opts.length > 0) {
|
|
197
|
+
const cur = mode.selectedOptionId;
|
|
198
|
+
const curIdx = cur === undefined ? -1 : opts.findIndex((o) => o.id === cur);
|
|
199
|
+
const nextIdx = curIdx + 1; // -1 → 0, last → length (which we map to none)
|
|
200
|
+
if (nextIdx >= opts.length) {
|
|
201
|
+
delete mode.selectedOptionId;
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
mode.selectedOptionId = opts[nextIdx].id;
|
|
205
|
+
}
|
|
206
|
+
render();
|
|
207
|
+
}
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
176
210
|
if (key.return) {
|
|
177
|
-
|
|
211
|
+
const interaction = state.interactions[state.currentIndex];
|
|
212
|
+
const attached = mode.kind === 'comment' ? mode.selectedOptionId : undefined;
|
|
213
|
+
submitOption(state, interaction, attached, mode.buffer);
|
|
178
214
|
state.inputMode = null;
|
|
179
215
|
advanceItem(state, 1);
|
|
180
216
|
render();
|
|
181
217
|
return;
|
|
182
218
|
}
|
|
183
219
|
if (key.backspace) {
|
|
184
|
-
|
|
220
|
+
const chars = [...mode.buffer];
|
|
221
|
+
chars.pop();
|
|
222
|
+
mode.buffer = chars.join('');
|
|
185
223
|
render();
|
|
186
224
|
return;
|
|
187
225
|
}
|
|
188
|
-
|
|
189
|
-
|
|
226
|
+
const cleaned = input
|
|
227
|
+
.replace(/\x1b\[20[01]~/g, '')
|
|
228
|
+
.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, '')
|
|
229
|
+
.replace(/[\x00-\x1F\x7F]/g, '');
|
|
230
|
+
if (cleaned.length > 0) {
|
|
231
|
+
mode.buffer += cleaned;
|
|
190
232
|
render();
|
|
191
233
|
}
|
|
192
234
|
}
|
|
193
|
-
function commitInput(state) {
|
|
194
|
-
const q = state.questions[state.currentIndex];
|
|
195
|
-
const mode = state.inputMode;
|
|
196
|
-
if (mode.kind === 'comment') {
|
|
197
|
-
const existing = state.answers.get(q.id);
|
|
198
|
-
const approved = existing ? existing.approved : false;
|
|
199
|
-
state.answers.set(q.id, {
|
|
200
|
-
id: q.id,
|
|
201
|
-
type: 'validation',
|
|
202
|
-
approved,
|
|
203
|
-
comment: mode.buffer || undefined,
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
else if (mode.kind === 'custom-option') {
|
|
207
|
-
if (mode.buffer) {
|
|
208
|
-
state.answers.set(q.id, {
|
|
209
|
-
id: q.id,
|
|
210
|
-
type: 'choice',
|
|
211
|
-
selected: mode.buffer,
|
|
212
|
-
isCustom: true,
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
else if (mode.kind === 'freetext') {
|
|
217
|
-
if (mode.buffer) {
|
|
218
|
-
state.answers.set(q.id, {
|
|
219
|
-
id: q.id,
|
|
220
|
-
type: 'freetext',
|
|
221
|
-
response: mode.buffer,
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
state.persist?.();
|
|
226
|
-
}
|
|
227
235
|
// ── Final ────────────────────────────────────────────────────────────────────
|
|
228
236
|
function handleFinal(input, key, state, render, exit) {
|
|
229
237
|
if (key.return) {
|
|
@@ -231,7 +239,7 @@ function handleFinal(input, key, state, render, exit) {
|
|
|
231
239
|
}
|
|
232
240
|
else if (input === 'p') {
|
|
233
241
|
state.phase = 'item-review';
|
|
234
|
-
state.currentIndex = state.
|
|
242
|
+
state.currentIndex = state.interactions.length - 1;
|
|
235
243
|
render();
|
|
236
244
|
}
|
|
237
245
|
}
|
|
@@ -240,18 +248,24 @@ function advanceItem(state, direction) {
|
|
|
240
248
|
const next = state.currentIndex + direction;
|
|
241
249
|
if (next < 0)
|
|
242
250
|
return;
|
|
243
|
-
if (next >= state.
|
|
251
|
+
if (next >= state.interactions.length) {
|
|
244
252
|
state.phase = 'final';
|
|
245
253
|
return;
|
|
246
254
|
}
|
|
247
255
|
state.currentIndex = next;
|
|
248
256
|
state.selectedAction = 0;
|
|
249
257
|
state.detailExpanded = false;
|
|
258
|
+
state.scrollOffset = 0;
|
|
250
259
|
}
|
|
251
|
-
function actionCount(
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
260
|
+
function actionCount(interaction) {
|
|
261
|
+
return interaction.options.length + (interaction.allowFreetext && interaction.options.length > 0 ? 1 : 0);
|
|
262
|
+
}
|
|
263
|
+
function submitOption(state, interaction, selectedOptionId, freetext) {
|
|
264
|
+
const response = { id: interaction.id };
|
|
265
|
+
if (selectedOptionId !== undefined)
|
|
266
|
+
response.selectedOptionId = selectedOptionId;
|
|
267
|
+
if (freetext !== undefined)
|
|
268
|
+
response.freetext = freetext;
|
|
269
|
+
state.responses.set(interaction.id, response);
|
|
270
|
+
state.persist?.();
|
|
257
271
|
}
|
package/dist/tui/render.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import type { TuiState } from '../types.js';
|
|
2
|
-
export declare function
|
|
3
|
-
export declare function
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import type { TuiState, Interaction, InteractionResponse } from '../types.js';
|
|
2
|
+
export declare function sanitize(text: string): string;
|
|
3
|
+
export declare function diffFrame(prevFrame: string[], nextLines: string[], rows: number): {
|
|
4
|
+
writes: string[];
|
|
5
|
+
nextPrevFrame: string[];
|
|
6
|
+
};
|
|
7
|
+
export declare function renderOverview(state: TuiState, cols: number, rows: number): string[];
|
|
8
|
+
export declare function renderItemReview(state: TuiState, cols: number, rows: number): string[];
|
|
9
|
+
export declare function renderFinal(state: TuiState, cols: number, rows: number): string[];
|
|
10
|
+
export declare function responseSummary(r: InteractionResponse, interaction: Interaction): string;
|