@danypops/pi-papyrus 0.45.1 → 0.45.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/extension/src/discuss-ask-view.ts +86 -170
- package/package.json +4 -3
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* dependency on or delegation to another package's registered tool.
|
|
2
|
+
* Discuss's live:true prompt orchestration: searchable single-select, checkbox multi-select,
|
|
3
|
+
* freeform replies, and optional comments docked in Pi's input editor. Multi-select state and
|
|
4
|
+
* viewport behavior come from Vehicle's Pi binding of Malevich; this module owns only
|
|
5
|
+
* Discussion-specific modes and response mapping.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
* full notice in THIRD_PARTY_LICENSES.md)
|
|
10
|
-
* registration, its own event emission, malformed-options recovery for other tools' schemas)
|
|
11
|
-
* removed since Discuss already owns its schema, persistence, and rendering.
|
|
7
|
+
* The single-select/editor flow is adapted from pi-ask-user (MIT, Copyright (c) 2026 Enzo
|
|
8
|
+
* Lucchesi; full notice in THIRD_PARTY_LICENSES.md).
|
|
12
9
|
*/
|
|
10
|
+
import {
|
|
11
|
+
createMultiSelectList,
|
|
12
|
+
type MultiSelectListItem,
|
|
13
|
+
type MultiSelectList as SharedMultiSelectList,
|
|
14
|
+
} from "@danypops/vehicle-client-pi/multi-select-list";
|
|
13
15
|
import type { AgentToolUpdateCallback, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
14
16
|
import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
15
17
|
import {
|
|
@@ -283,185 +285,98 @@ function matchesSelectDown(data: string, keybindings: KeybindingsManager): boole
|
|
|
283
285
|
return keybindings.matches(data, "tui.select.down") || matchesKey(data, Key.tab) || matchesKey(data, VIM_SELECT_DOWN_KEY);
|
|
284
286
|
}
|
|
285
287
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
288
|
+
type DiscussionMultiSelectChoice =
|
|
289
|
+
| { readonly kind: "option"; readonly option: AskOption }
|
|
290
|
+
| { readonly kind: "comment" }
|
|
291
|
+
| { readonly kind: "freeform" };
|
|
292
|
+
|
|
293
|
+
class DiscussionMultiSelectList implements Component {
|
|
294
|
+
private readonly list: SharedMultiSelectList<DiscussionMultiSelectChoice>;
|
|
295
|
+
private readonly commentIndex: number | undefined;
|
|
289
296
|
private commentEnabled = false;
|
|
290
|
-
private cachedWidth?: number;
|
|
291
|
-
private cachedLines?: string[];
|
|
292
297
|
|
|
293
298
|
public onCancel?: () => void;
|
|
294
299
|
public onSubmit?: (result: string[]) => void;
|
|
295
300
|
public onEnterFreeform?: () => void;
|
|
296
301
|
|
|
297
302
|
constructor(
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
private commentToggle: ResolvedShortcut,
|
|
304
|
-
) {
|
|
303
|
+
options: AskOption[],
|
|
304
|
+
allowFreeform: boolean,
|
|
305
|
+
allowComment: boolean,
|
|
306
|
+
theme: Theme,
|
|
307
|
+
keybindings: KeybindingsManager,
|
|
308
|
+
private readonly commentToggle: ResolvedShortcut,
|
|
309
|
+
) {
|
|
310
|
+
const items: Array<MultiSelectListItem<DiscussionMultiSelectChoice>> = options.map((option, index) => ({
|
|
311
|
+
value: { kind: "option", option },
|
|
312
|
+
label: option.title,
|
|
313
|
+
...(option.description ? { description: option.description } : {}),
|
|
314
|
+
...(index < 9 ? { shortcut: String(index + 1) } : {}),
|
|
315
|
+
numberLabel: String(index + 1),
|
|
316
|
+
}));
|
|
317
|
+
if (allowComment) {
|
|
318
|
+
this.commentIndex = items.length;
|
|
319
|
+
items.push({
|
|
320
|
+
value: { kind: "comment" },
|
|
321
|
+
label: COMMENT_TOGGLE_LABEL,
|
|
322
|
+
includeInSelection: false,
|
|
323
|
+
confirmAction: "toggle",
|
|
324
|
+
numberLabel: false,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
if (allowFreeform) {
|
|
328
|
+
items.push({
|
|
329
|
+
value: { kind: "freeform" },
|
|
330
|
+
label: "Type something.",
|
|
331
|
+
description: "Enter a custom response",
|
|
332
|
+
toggleable: false,
|
|
333
|
+
confirmAction: "activate",
|
|
334
|
+
numberLabel: false,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
this.list = createMultiSelectList<DiscussionMultiSelectChoice>({
|
|
338
|
+
items,
|
|
339
|
+
theme,
|
|
340
|
+
keybindings,
|
|
341
|
+
onCancel: () => this.onCancel?.(),
|
|
342
|
+
onToggle: (item: MultiSelectListItem<DiscussionMultiSelectChoice>, checked: boolean) => {
|
|
343
|
+
if (item.value.kind === "comment") this.commentEnabled = checked;
|
|
344
|
+
},
|
|
345
|
+
onActivate: (item: MultiSelectListItem<DiscussionMultiSelectChoice>) => {
|
|
346
|
+
if (item.value.kind === "freeform") this.onEnterFreeform?.();
|
|
347
|
+
},
|
|
348
|
+
onSubmit: (choices: DiscussionMultiSelectChoice[]) => {
|
|
349
|
+
const titles = choices
|
|
350
|
+
.filter((choice): choice is Extract<DiscussionMultiSelectChoice, { kind: "option" }> => choice.kind === "option")
|
|
351
|
+
.map((choice) => choice.option.title);
|
|
352
|
+
if (titles.length > 0) this.onSubmit?.(titles);
|
|
353
|
+
else this.onCancel?.();
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
}
|
|
305
357
|
|
|
306
358
|
public isCommentEnabled(): boolean {
|
|
307
359
|
return this.commentEnabled;
|
|
308
360
|
}
|
|
309
|
-
invalidate(): void {
|
|
310
|
-
this.cachedWidth = undefined;
|
|
311
|
-
this.cachedLines = undefined;
|
|
312
|
-
}
|
|
313
361
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
private getCommentToggleIndex(): number | null {
|
|
318
|
-
return this.allowComment ? this.options.length : null;
|
|
319
|
-
}
|
|
320
|
-
private getFreeformIndex(): number {
|
|
321
|
-
return this.options.length + (this.allowComment ? 1 : 0);
|
|
322
|
-
}
|
|
323
|
-
private isCommentToggleRow(index: number): boolean {
|
|
324
|
-
const i = this.getCommentToggleIndex();
|
|
325
|
-
return i !== null && index === i;
|
|
326
|
-
}
|
|
327
|
-
private isFreeformRow(index: number): boolean {
|
|
328
|
-
return this.allowFreeform && index === this.getFreeformIndex();
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
private toggle(index: number): void {
|
|
332
|
-
if (index < 0 || index >= this.options.length) return;
|
|
333
|
-
if (this.checked.has(index)) this.checked.delete(index);
|
|
334
|
-
else this.checked.add(index);
|
|
362
|
+
setMaxVisibleRows(rows: number): void {
|
|
363
|
+
this.list.setMaxVisibleRows(rows);
|
|
335
364
|
}
|
|
336
365
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
this.commentEnabled = !this.commentEnabled;
|
|
340
|
-
this.invalidate();
|
|
366
|
+
invalidate(): void {
|
|
367
|
+
this.list.invalidate();
|
|
341
368
|
}
|
|
342
369
|
|
|
343
370
|
handleInput(data: string): void {
|
|
344
|
-
if (this.
|
|
345
|
-
this.
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
const count = this.getItemCount();
|
|
349
|
-
if (count === 0) {
|
|
350
|
-
this.onCancel?.();
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
if (this.allowComment && !this.commentToggle.disabled && this.commentToggle.matches(data)) {
|
|
354
|
-
this.toggleComment();
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
if (matchesSelectUp(data, this.keybindings)) {
|
|
359
|
-
this.selectedIndex = this.selectedIndex === 0 ? count - 1 : this.selectedIndex - 1;
|
|
360
|
-
this.invalidate();
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
363
|
-
if (matchesSelectDown(data, this.keybindings)) {
|
|
364
|
-
this.selectedIndex = this.selectedIndex === count - 1 ? 0 : this.selectedIndex + 1;
|
|
365
|
-
this.invalidate();
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const numMatch = data.match(/^[1-9]$/);
|
|
370
|
-
if (numMatch) {
|
|
371
|
-
const idx = Number.parseInt(numMatch[0], 10) - 1;
|
|
372
|
-
if (idx >= 0 && idx < this.options.length) {
|
|
373
|
-
this.toggle(idx);
|
|
374
|
-
this.selectedIndex = Math.min(idx, count - 1);
|
|
375
|
-
this.invalidate();
|
|
376
|
-
}
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
if (matchesKey(data, Key.space)) {
|
|
381
|
-
if (this.isCommentToggleRow(this.selectedIndex)) {
|
|
382
|
-
this.toggleComment();
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
if (this.isFreeformRow(this.selectedIndex)) {
|
|
386
|
-
this.onEnterFreeform?.();
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
|
-
this.toggle(this.selectedIndex);
|
|
390
|
-
this.invalidate();
|
|
371
|
+
if (this.commentIndex !== undefined && !this.commentToggle.disabled && this.commentToggle.matches(data)) {
|
|
372
|
+
this.list.setChecked(this.commentIndex, !this.commentEnabled);
|
|
391
373
|
return;
|
|
392
374
|
}
|
|
393
|
-
|
|
394
|
-
if (this.keybindings.matches(data, "tui.select.confirm")) {
|
|
395
|
-
if (this.isCommentToggleRow(this.selectedIndex)) {
|
|
396
|
-
this.toggleComment();
|
|
397
|
-
return;
|
|
398
|
-
}
|
|
399
|
-
if (this.isFreeformRow(this.selectedIndex)) {
|
|
400
|
-
this.onEnterFreeform?.();
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
const selectedTitles = [...this.checked]
|
|
404
|
-
.sort((a, b) => a - b)
|
|
405
|
-
.map((i) => this.options[i]?.title)
|
|
406
|
-
.filter((t): t is string => !!t);
|
|
407
|
-
const fallback = this.options[this.selectedIndex]?.title;
|
|
408
|
-
const result = selectedTitles.length > 0 ? selectedTitles : fallback ? [fallback] : [];
|
|
409
|
-
if (result.length > 0) this.onSubmit?.(result);
|
|
410
|
-
else this.onCancel?.();
|
|
411
|
-
}
|
|
375
|
+
this.list.handleInput(data);
|
|
412
376
|
}
|
|
413
377
|
|
|
414
378
|
render(width: number): string[] {
|
|
415
|
-
|
|
416
|
-
const theme = this.theme;
|
|
417
|
-
const count = this.getItemCount();
|
|
418
|
-
const maxVisible = Math.min(count, 10);
|
|
419
|
-
if (count === 0) {
|
|
420
|
-
this.cachedLines = [theme.fg("warning", "No options")];
|
|
421
|
-
this.cachedWidth = width;
|
|
422
|
-
return this.cachedLines;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
const startIndex = Math.max(0, Math.min(this.selectedIndex - Math.floor(maxVisible / 2), count - maxVisible));
|
|
426
|
-
const endIndex = Math.min(startIndex + maxVisible, count);
|
|
427
|
-
const lines: string[] = [];
|
|
428
|
-
|
|
429
|
-
for (let i = startIndex; i < endIndex; i++) {
|
|
430
|
-
const isSelected = i === this.selectedIndex;
|
|
431
|
-
const prefix = isSelected ? theme.fg("accent", "→") : " ";
|
|
432
|
-
|
|
433
|
-
if (this.isCommentToggleRow(i)) {
|
|
434
|
-
const checkbox = this.commentEnabled ? theme.fg("success", "[✓]") : theme.fg("dim", "[ ]");
|
|
435
|
-
const label = isSelected
|
|
436
|
-
? theme.fg("accent", theme.bold(COMMENT_TOGGLE_LABEL))
|
|
437
|
-
: theme.fg("text", theme.bold(COMMENT_TOGGLE_LABEL));
|
|
438
|
-
lines.push(truncateToWidth(`${prefix} ${checkbox} ${label}`, width, ""));
|
|
439
|
-
continue;
|
|
440
|
-
}
|
|
441
|
-
if (this.isFreeformRow(i)) {
|
|
442
|
-
const label = theme.fg("text", theme.bold("Type something."));
|
|
443
|
-
const desc = theme.fg("muted", "Enter a custom response");
|
|
444
|
-
lines.push(truncateToWidth(`${prefix} ${label} ${theme.fg("dim", "—")} ${desc}`, width, ""));
|
|
445
|
-
continue;
|
|
446
|
-
}
|
|
447
|
-
const option = this.options[i];
|
|
448
|
-
if (!option) continue;
|
|
449
|
-
const checkbox = this.checked.has(i) ? theme.fg("success", "[✓]") : theme.fg("dim", "[ ]");
|
|
450
|
-
const num = theme.fg("dim", `${i + 1}.`);
|
|
451
|
-
const title = isSelected ? theme.fg("accent", theme.bold(option.title)) : theme.fg("text", theme.bold(option.title));
|
|
452
|
-
lines.push(truncateToWidth(`${prefix} ${num} ${checkbox} ${title}`, width, ""));
|
|
453
|
-
if (option.description) {
|
|
454
|
-
const indent = " ";
|
|
455
|
-
for (const w of wrapTextWithAnsi(option.description, Math.max(10, width - indent.length)))
|
|
456
|
-
lines.push(truncateToWidth(indent + theme.fg("muted", w), width, ""));
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
if (startIndex > 0 || endIndex < count)
|
|
461
|
-
lines.push(theme.fg("dim", truncateToWidth(` (${this.selectedIndex + 1}/${count})`, width, "")));
|
|
462
|
-
this.cachedWidth = width;
|
|
463
|
-
this.cachedLines = lines;
|
|
464
|
-
return lines;
|
|
379
|
+
return this.list.render(width);
|
|
465
380
|
}
|
|
466
381
|
}
|
|
467
382
|
|
|
@@ -740,7 +655,7 @@ class AskComponent extends Container {
|
|
|
740
655
|
private helpText: Text;
|
|
741
656
|
|
|
742
657
|
private singleSelectList?: WrappedSingleSelectList;
|
|
743
|
-
private multiSelectList?:
|
|
658
|
+
private multiSelectList?: DiscussionMultiSelectList;
|
|
744
659
|
private editor?: Editor;
|
|
745
660
|
|
|
746
661
|
private _focused = false;
|
|
@@ -906,7 +821,8 @@ class AskComponent extends Container {
|
|
|
906
821
|
const safeBudget = Math.max(0, Math.floor(budget));
|
|
907
822
|
if (safeBudget <= 0) return [];
|
|
908
823
|
if (this.mode === "select") {
|
|
909
|
-
if (
|
|
824
|
+
if (this.allowMultiple) this.ensureMultiSelectList().setMaxVisibleRows(Math.max(1, safeBudget));
|
|
825
|
+
else this.ensureSingleSelectList().setMaxVisibleRows(Math.max(1, safeBudget));
|
|
910
826
|
return this.limitLines(this.modeContainer.render(width), safeBudget, width, true);
|
|
911
827
|
}
|
|
912
828
|
return this.renderEditorModeLines(width, safeBudget);
|
|
@@ -1119,9 +1035,9 @@ class AskComponent extends Container {
|
|
|
1119
1035
|
return list;
|
|
1120
1036
|
}
|
|
1121
1037
|
|
|
1122
|
-
private ensureMultiSelectList():
|
|
1038
|
+
private ensureMultiSelectList(): DiscussionMultiSelectList {
|
|
1123
1039
|
if (this.multiSelectList) return this.multiSelectList;
|
|
1124
|
-
const list = new
|
|
1040
|
+
const list = new DiscussionMultiSelectList(
|
|
1125
1041
|
this.options,
|
|
1126
1042
|
this.allowFreeform,
|
|
1127
1043
|
this.allowComment,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-papyrus",
|
|
3
|
-
"version": "0.45.
|
|
3
|
+
"version": "0.45.2",
|
|
4
4
|
"description": "Pi host extension for Papyrus: native tools, TUI panels, and context injection over the daemon-backed graph store",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": ["pi-package"],
|
|
@@ -20,13 +20,14 @@
|
|
|
20
20
|
"@danypops/jittor": "^0.14.0",
|
|
21
21
|
"@danypops/papyrus": "^0.44.0",
|
|
22
22
|
"@danypops/vehicle-client": "^0.5.0",
|
|
23
|
-
"@danypops/vehicle-client-pi": "^0.
|
|
23
|
+
"@danypops/vehicle-client-pi": "^0.16.1",
|
|
24
24
|
"@danypops/vehicle-core": "^0.10.0",
|
|
25
25
|
"@danypops/vehicle-server": "^0.13.0",
|
|
26
26
|
"beautiful-mermaid": "1.1.3",
|
|
27
|
-
"malevich-tui-components": "^0.20.
|
|
27
|
+
"malevich-tui-components": "^0.20.3"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
+
"@danypops/pi-tui-harness": "^0.0.2",
|
|
30
31
|
"@earendil-works/pi-coding-agent": "^0.80.10",
|
|
31
32
|
"bun-types": "latest",
|
|
32
33
|
"typebox": "^1.3.6",
|