@howaboua/pi-ask 0.0.3 → 0.0.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/README.md +7 -1
- package/ask/tui.ts +116 -39
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -42,10 +42,16 @@ Set either value to `false`, then run `/reload`, to hide that template. The same
|
|
|
42
42
|
|
|
43
43
|
## TUI controls
|
|
44
44
|
|
|
45
|
+
The panel uses Pi's active keybindings. The defaults are:
|
|
46
|
+
|
|
45
47
|
- `Up` / `Down` — move through choices, free text, and comments
|
|
46
|
-
- `Enter` — choose or edit the focused item
|
|
48
|
+
- `Enter` — choose or edit the focused item, or save an edit
|
|
47
49
|
- `Left` / `Right` — move between prompts and the review tab
|
|
48
50
|
- `Tab` — advance, using `Other/rephrase` when the current prompt is unanswered
|
|
49
51
|
- `Esc` — dismiss the panel
|
|
50
52
|
|
|
53
|
+
Configure the corresponding `tui.select.*`, `tui.editor.cursorLeft`,
|
|
54
|
+
`tui.editor.cursorRight`, and `tui.input.*` actions in Pi's
|
|
55
|
+
`keybindings.json`.
|
|
56
|
+
|
|
51
57
|
The tool requires Pi's interactive TUI or RPC UI. It returns dismissal as a distinct result so the agent can stop asking rather than guessing.
|
package/ask/tui.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
type ExtensionContext,
|
|
3
|
+
type KeybindingsManager,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
2
5
|
import {
|
|
3
6
|
Editor,
|
|
4
|
-
Key,
|
|
5
|
-
matchesKey,
|
|
6
7
|
truncateToWidth,
|
|
7
8
|
wrapTextWithAnsi,
|
|
8
9
|
} from "@earendil-works/pi-tui";
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
} from "./state.js";
|
|
25
26
|
|
|
26
27
|
type AskTheme = ExtensionContext["ui"]["theme"];
|
|
28
|
+
type AskKeybinding = Parameters<KeybindingsManager["getKeys"]>[0];
|
|
27
29
|
type AddLine = (line?: string) => void;
|
|
28
30
|
type EditingKind = "other" | "comment";
|
|
29
31
|
|
|
@@ -44,6 +46,7 @@ interface RenderTabsOptions {
|
|
|
44
46
|
interface RenderReviewOptions {
|
|
45
47
|
add: AddLine;
|
|
46
48
|
handoff: boolean;
|
|
49
|
+
keybindings: KeybindingsManager;
|
|
47
50
|
promptStates: PromptState[];
|
|
48
51
|
prompts: AskPrompt[];
|
|
49
52
|
theme: AskTheme;
|
|
@@ -64,6 +67,7 @@ interface RenderPromptOptions {
|
|
|
64
67
|
editor: Editor;
|
|
65
68
|
handoff: boolean;
|
|
66
69
|
isEditing: EditingKind | null;
|
|
70
|
+
keybindings: KeybindingsManager;
|
|
67
71
|
choices: PromptChoice[];
|
|
68
72
|
prompt: AskPrompt;
|
|
69
73
|
promptState: PromptState | undefined;
|
|
@@ -86,6 +90,18 @@ function addWrapped(
|
|
|
86
90
|
}
|
|
87
91
|
}
|
|
88
92
|
|
|
93
|
+
function renderKeyHint(
|
|
94
|
+
theme: AskTheme,
|
|
95
|
+
keybindings: KeybindingsManager,
|
|
96
|
+
action: AskKeybinding,
|
|
97
|
+
description: string,
|
|
98
|
+
): string {
|
|
99
|
+
return (
|
|
100
|
+
theme.fg("dim", keybindings.getKeys(action).join("/")) +
|
|
101
|
+
theme.fg("muted", ` ${description}`)
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
89
105
|
function renderAskTabs({
|
|
90
106
|
handoff,
|
|
91
107
|
prompts,
|
|
@@ -110,17 +126,13 @@ function renderAskTabs({
|
|
|
110
126
|
function renderAskReview({
|
|
111
127
|
add,
|
|
112
128
|
handoff,
|
|
129
|
+
keybindings,
|
|
113
130
|
promptStates,
|
|
114
131
|
prompts,
|
|
115
132
|
theme,
|
|
116
133
|
width,
|
|
117
134
|
}: RenderReviewOptions): void {
|
|
118
|
-
add(
|
|
119
|
-
theme.fg(
|
|
120
|
-
handoff ? "warning" : "accent",
|
|
121
|
-
handoff ? " Resume agent" : " Review",
|
|
122
|
-
),
|
|
123
|
-
);
|
|
135
|
+
add(theme.fg("accent", handoff ? " Resume agent" : " Review"));
|
|
124
136
|
add();
|
|
125
137
|
prompts.forEach((prompt, index) => {
|
|
126
138
|
const promptState = promptStates[index];
|
|
@@ -147,13 +159,32 @@ function renderAskReview({
|
|
|
147
159
|
}
|
|
148
160
|
});
|
|
149
161
|
add();
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
162
|
+
addWrapped(
|
|
163
|
+
add,
|
|
164
|
+
" " +
|
|
165
|
+
renderKeyHint(
|
|
166
|
+
theme,
|
|
167
|
+
keybindings,
|
|
168
|
+
"tui.select.confirm",
|
|
169
|
+
handoff ? "return control" : "submit",
|
|
170
|
+
) +
|
|
171
|
+
" • " +
|
|
172
|
+
renderKeyHint(
|
|
173
|
+
theme,
|
|
174
|
+
keybindings,
|
|
175
|
+
"tui.editor.cursorLeft",
|
|
176
|
+
"previous prompt",
|
|
177
|
+
) +
|
|
178
|
+
" • " +
|
|
179
|
+
renderKeyHint(
|
|
180
|
+
theme,
|
|
181
|
+
keybindings,
|
|
182
|
+
"tui.editor.cursorRight",
|
|
183
|
+
"next prompt",
|
|
184
|
+
) +
|
|
185
|
+
" • " +
|
|
186
|
+
renderKeyHint(theme, keybindings, "tui.select.cancel", "dismiss"),
|
|
187
|
+
width,
|
|
157
188
|
);
|
|
158
189
|
}
|
|
159
190
|
|
|
@@ -178,6 +209,7 @@ function renderAskPrompt({
|
|
|
178
209
|
editor,
|
|
179
210
|
handoff,
|
|
180
211
|
isEditing,
|
|
212
|
+
keybindings,
|
|
181
213
|
choices,
|
|
182
214
|
prompt,
|
|
183
215
|
promptState,
|
|
@@ -186,7 +218,7 @@ function renderAskPrompt({
|
|
|
186
218
|
width,
|
|
187
219
|
}: RenderPromptOptions): void {
|
|
188
220
|
if (handoff) {
|
|
189
|
-
add(theme.fg("
|
|
221
|
+
add(theme.fg("accent", " Human action needed"));
|
|
190
222
|
add();
|
|
191
223
|
}
|
|
192
224
|
addWrapped(add, theme.fg("text", prompt.title), width, " ");
|
|
@@ -237,17 +269,39 @@ function renderAskPrompt({
|
|
|
237
269
|
for (const line of editor.render(width - 2)) add(` ${line}`);
|
|
238
270
|
}
|
|
239
271
|
add();
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
"
|
|
244
|
-
|
|
272
|
+
if (isEditing) {
|
|
273
|
+
addWrapped(
|
|
274
|
+
add,
|
|
275
|
+
" " +
|
|
276
|
+
renderKeyHint(theme, keybindings, "tui.input.submit", "save") +
|
|
277
|
+
" • " +
|
|
278
|
+
renderKeyHint(theme, keybindings, "tui.input.tab", "next/default") +
|
|
279
|
+
" • " +
|
|
280
|
+
renderKeyHint(theme, keybindings, "tui.select.cancel", "cancel edit"),
|
|
281
|
+
width,
|
|
282
|
+
);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
addWrapped(
|
|
286
|
+
add,
|
|
287
|
+
" " +
|
|
288
|
+
renderKeyHint(theme, keybindings, "tui.select.up", "up") +
|
|
289
|
+
" • " +
|
|
290
|
+
renderKeyHint(theme, keybindings, "tui.select.down", "down") +
|
|
291
|
+
" • " +
|
|
292
|
+
renderKeyHint(theme, keybindings, "tui.select.confirm", "choose/type") +
|
|
293
|
+
" • " +
|
|
294
|
+
renderKeyHint(theme, keybindings, "tui.input.tab", "next/default") +
|
|
295
|
+
" • blank Other/rephrase = follow-up • " +
|
|
296
|
+
renderKeyHint(theme, keybindings, "tui.select.cancel", "close"),
|
|
297
|
+
width,
|
|
245
298
|
);
|
|
246
299
|
}
|
|
247
300
|
|
|
248
301
|
interface EditingInputOptions {
|
|
249
302
|
advance: () => void;
|
|
250
303
|
editor: Editor;
|
|
304
|
+
keybindings: KeybindingsManager;
|
|
251
305
|
refresh: () => void;
|
|
252
306
|
saveEditing: (submittedText?: string) => void;
|
|
253
307
|
state: AskUiState;
|
|
@@ -255,15 +309,22 @@ interface EditingInputOptions {
|
|
|
255
309
|
|
|
256
310
|
function handleAskEditingInput(
|
|
257
311
|
data: string,
|
|
258
|
-
{
|
|
312
|
+
{
|
|
313
|
+
advance,
|
|
314
|
+
editor,
|
|
315
|
+
keybindings,
|
|
316
|
+
refresh,
|
|
317
|
+
saveEditing,
|
|
318
|
+
state,
|
|
319
|
+
}: EditingInputOptions,
|
|
259
320
|
): void {
|
|
260
|
-
if (
|
|
321
|
+
if (keybindings.matches(data, "tui.input.tab")) {
|
|
261
322
|
saveEditing(editor.getText());
|
|
262
323
|
state.editing = null;
|
|
263
324
|
advance();
|
|
264
325
|
return;
|
|
265
326
|
}
|
|
266
|
-
if (
|
|
327
|
+
if (keybindings.matches(data, "tui.select.cancel")) {
|
|
267
328
|
state.editing = null;
|
|
268
329
|
refresh();
|
|
269
330
|
return;
|
|
@@ -276,6 +337,7 @@ interface PromptInputOptions {
|
|
|
276
337
|
advanceWithDefault: () => void;
|
|
277
338
|
choices: () => PromptChoice[];
|
|
278
339
|
count: () => number;
|
|
340
|
+
keybindings: KeybindingsManager;
|
|
279
341
|
pick: (index: number) => void;
|
|
280
342
|
refresh: () => void;
|
|
281
343
|
selectOther: () => void;
|
|
@@ -289,6 +351,7 @@ function handleAskPromptInput(
|
|
|
289
351
|
advanceWithDefault,
|
|
290
352
|
choices,
|
|
291
353
|
count,
|
|
354
|
+
keybindings,
|
|
292
355
|
pick,
|
|
293
356
|
refresh,
|
|
294
357
|
selectOther,
|
|
@@ -296,21 +359,21 @@ function handleAskPromptInput(
|
|
|
296
359
|
state,
|
|
297
360
|
}: PromptInputOptions,
|
|
298
361
|
): void {
|
|
299
|
-
if (
|
|
362
|
+
if (keybindings.matches(data, "tui.select.up")) {
|
|
300
363
|
state.focus = Math.max(0, state.focus - 1);
|
|
301
364
|
refresh();
|
|
302
365
|
return;
|
|
303
366
|
}
|
|
304
|
-
if (
|
|
367
|
+
if (keybindings.matches(data, "tui.select.down")) {
|
|
305
368
|
state.focus = Math.min(count() - 1, state.focus + 1);
|
|
306
369
|
refresh();
|
|
307
370
|
return;
|
|
308
371
|
}
|
|
309
|
-
if (
|
|
372
|
+
if (keybindings.matches(data, "tui.input.tab")) {
|
|
310
373
|
advanceWithDefault();
|
|
311
374
|
return;
|
|
312
375
|
}
|
|
313
|
-
if (!
|
|
376
|
+
if (!keybindings.matches(data, "tui.select.confirm")) return;
|
|
314
377
|
if (state.focus === choices().length) {
|
|
315
378
|
selectOther();
|
|
316
379
|
return;
|
|
@@ -325,6 +388,7 @@ function handleAskPromptInput(
|
|
|
325
388
|
interface NavigationInputOptions {
|
|
326
389
|
done: (responses: AskResponse[] | null) => void;
|
|
327
390
|
isReview: () => boolean;
|
|
391
|
+
keybindings: KeybindingsManager;
|
|
328
392
|
promptStates: PromptState[];
|
|
329
393
|
prompts: AskPrompt[];
|
|
330
394
|
setTab: (next: number) => void;
|
|
@@ -336,26 +400,27 @@ function handleAskNavigationInput(
|
|
|
336
400
|
{
|
|
337
401
|
done,
|
|
338
402
|
isReview,
|
|
403
|
+
keybindings,
|
|
339
404
|
promptStates,
|
|
340
405
|
prompts,
|
|
341
406
|
setTab,
|
|
342
407
|
state,
|
|
343
408
|
}: NavigationInputOptions,
|
|
344
409
|
): boolean {
|
|
345
|
-
if (
|
|
410
|
+
if (keybindings.matches(data, "tui.select.cancel")) {
|
|
346
411
|
done(null);
|
|
347
412
|
return true;
|
|
348
413
|
}
|
|
349
|
-
if (
|
|
414
|
+
if (keybindings.matches(data, "tui.editor.cursorLeft")) {
|
|
350
415
|
setTab(state.tab - 1);
|
|
351
416
|
return true;
|
|
352
417
|
}
|
|
353
|
-
if (
|
|
418
|
+
if (keybindings.matches(data, "tui.editor.cursorRight")) {
|
|
354
419
|
setTab(state.tab + 1);
|
|
355
420
|
return true;
|
|
356
421
|
}
|
|
357
422
|
if (!isReview()) return false;
|
|
358
|
-
if (
|
|
423
|
+
if (keybindings.matches(data, "tui.select.confirm")) {
|
|
359
424
|
done(promptStatesToResponses(prompts, promptStates));
|
|
360
425
|
}
|
|
361
426
|
return true;
|
|
@@ -367,7 +432,7 @@ export async function askInTui(
|
|
|
367
432
|
{ handoff = false, signal }: { handoff?: boolean; signal?: AbortSignal } = {},
|
|
368
433
|
): Promise<AskResponse[] | null> {
|
|
369
434
|
if (!ctx.hasUI || signal?.aborted) return null;
|
|
370
|
-
return await ctx.ui.custom<AskResponse[] | null>((tui, theme,
|
|
435
|
+
return await ctx.ui.custom<AskResponse[] | null>((tui, theme, kb, done) => {
|
|
371
436
|
let settled = false;
|
|
372
437
|
const finish = (result: AskResponse[] | null) => {
|
|
373
438
|
if (settled) return;
|
|
@@ -384,13 +449,13 @@ export async function askInTui(
|
|
|
384
449
|
// transitions easy to desynchronise as this tool grows.
|
|
385
450
|
const promptStates = prompts.map(createPromptState);
|
|
386
451
|
const editor = new Editor(tui, {
|
|
387
|
-
borderColor: (text) => theme.fg(
|
|
452
|
+
borderColor: (text) => theme.fg("accent", text),
|
|
388
453
|
selectList: {
|
|
389
454
|
selectedPrefix: (text) => theme.fg("accent", text),
|
|
390
455
|
selectedText: (text) => theme.fg("accent", text),
|
|
391
456
|
description: (text) => theme.fg("muted", text),
|
|
392
457
|
scrollInfo: (text) => theme.fg("dim", text),
|
|
393
|
-
noMatch: (text) => theme.fg("
|
|
458
|
+
noMatch: (text) => theme.fg("muted", text),
|
|
394
459
|
},
|
|
395
460
|
});
|
|
396
461
|
|
|
@@ -468,6 +533,7 @@ export async function askInTui(
|
|
|
468
533
|
handleAskEditingInput(data, {
|
|
469
534
|
advance,
|
|
470
535
|
editor,
|
|
536
|
+
keybindings: kb,
|
|
471
537
|
refresh,
|
|
472
538
|
saveEditing,
|
|
473
539
|
state,
|
|
@@ -478,6 +544,7 @@ export async function askInTui(
|
|
|
478
544
|
handleAskNavigationInput(data, {
|
|
479
545
|
done: finish,
|
|
480
546
|
isReview,
|
|
547
|
+
keybindings: kb,
|
|
481
548
|
promptStates,
|
|
482
549
|
prompts,
|
|
483
550
|
setTab,
|
|
@@ -490,6 +557,7 @@ export async function askInTui(
|
|
|
490
557
|
advanceWithDefault,
|
|
491
558
|
count,
|
|
492
559
|
choices,
|
|
560
|
+
keybindings: kb,
|
|
493
561
|
pick,
|
|
494
562
|
refresh,
|
|
495
563
|
selectOther,
|
|
@@ -502,13 +570,21 @@ export async function askInTui(
|
|
|
502
570
|
const lines: string[] = [];
|
|
503
571
|
const add: AddLine = (line = "") =>
|
|
504
572
|
lines.push(truncateToWidth(line, width));
|
|
505
|
-
add(theme.fg(
|
|
573
|
+
add(theme.fg("accent", "─".repeat(width)));
|
|
506
574
|
add(
|
|
507
575
|
renderAskTabs({ handoff, prompts, responded, tab: state.tab, theme }),
|
|
508
576
|
);
|
|
509
577
|
add();
|
|
510
578
|
if (isReview()) {
|
|
511
|
-
renderAskReview({
|
|
579
|
+
renderAskReview({
|
|
580
|
+
add,
|
|
581
|
+
handoff,
|
|
582
|
+
keybindings: kb,
|
|
583
|
+
promptStates,
|
|
584
|
+
prompts,
|
|
585
|
+
theme,
|
|
586
|
+
width,
|
|
587
|
+
});
|
|
512
588
|
} else {
|
|
513
589
|
const prompt = current();
|
|
514
590
|
if (prompt) {
|
|
@@ -517,6 +593,7 @@ export async function askInTui(
|
|
|
517
593
|
editor,
|
|
518
594
|
handoff,
|
|
519
595
|
isEditing: state.editing,
|
|
596
|
+
keybindings: kb,
|
|
520
597
|
choices: choices(),
|
|
521
598
|
prompt,
|
|
522
599
|
promptState: currentPromptState(),
|
|
@@ -526,7 +603,7 @@ export async function askInTui(
|
|
|
526
603
|
});
|
|
527
604
|
}
|
|
528
605
|
}
|
|
529
|
-
add(theme.fg(
|
|
606
|
+
add(theme.fg("accent", "─".repeat(width)));
|
|
530
607
|
cached = lines;
|
|
531
608
|
return lines;
|
|
532
609
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@howaboua/pi-ask",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "Interactive decisions, review triage, and human handoffs for Pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@biomejs/biome": "^2.4.16",
|
|
52
|
-
"@earendil-works/pi-coding-agent": "0.80.
|
|
53
|
-
"@earendil-works/pi-tui": "0.80.
|
|
52
|
+
"@earendil-works/pi-coding-agent": "0.80.8",
|
|
53
|
+
"@earendil-works/pi-tui": "0.80.8",
|
|
54
54
|
"typebox": "1.1.38",
|
|
55
55
|
"typescript": "^6.0.3"
|
|
56
56
|
},
|