@danypops/papyrus 0.25.0 → 0.27.0
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/active-task-continuation.ts +1 -1
- package/extension/src/artifact-browser.ts +4 -4
- package/extension/src/discuss-ask-layout.ts +193 -0
- package/extension/src/discuss-ask-view.ts +1125 -0
- package/extension/src/discuss.ts +8 -20
- package/extension/src/docs.ts +2 -2
- package/extension/src/domain-tools.ts +59 -38
- package/extension/src/index.ts +37 -9
- package/extension/src/playbooks.ts +2 -2
- package/extension/src/rules.ts +1 -1
- package/extension/src/skills.ts +4 -4
- package/extension/src/tasks.ts +20 -9
- package/extension/src/tool-rendering/artifact-card.ts +1 -1
- package/package.json +1 -1
- package/src/constants.ts +0 -3
- package/src/domain-services.ts +9 -9
- package/src/task-context.ts +4 -4
- package/extension/src/discussion-picker.ts +0 -148
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* discussion-picker.ts — the structured-choice picker for /discuss's "Reply" action and the
|
|
3
|
-
* discuss tool's own live:true synchronous ask.
|
|
4
|
-
*
|
|
5
|
-
* "single" mode (mutually exclusive) needs nothing bespoke for the pick list itself: the Pi
|
|
6
|
-
* extension UI already provides exactly that (ctx.ui.select). "multi" (allow several) has no
|
|
7
|
-
* native equivalent anywhere in @earendil-works/pi-coding-agent or pi-tui (checked both) -- so
|
|
8
|
-
* that one is a small, genuinely domain-specific checkbox-list component, not a generic library
|
|
9
|
-
* replacement. Both modes get a numbered quick-select (press the row's digit instead of
|
|
10
|
-
* scrolling with arrows) and an appended "type your own answer" row, itself numbered the same
|
|
11
|
-
* way -- a genuinely open question is exactly as valid an answer as any of the posed options.
|
|
12
|
-
*/
|
|
13
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
14
|
-
import { matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
|
|
15
|
-
import { DISCUSSION_PICKER_IDLE_TIMEOUT_MS, DISCUSSION_PICKER_TICK_MS } from "../../src/constants.ts";
|
|
16
|
-
import type { DiscussionOptionsMode } from "../../src/domain/discussion.ts";
|
|
17
|
-
|
|
18
|
-
const FREEFORM_LABEL = "Something else (type your own answer)";
|
|
19
|
-
|
|
20
|
-
export type DiscussionPickResult = { kind: "selected"; selected: string[] } | { kind: "freeform"; text: string };
|
|
21
|
-
|
|
22
|
-
/** digit "1".."9" -> index 0-8, "0" -> index 9 (DISCUSSION_OPTIONS_MAX_COUNT is 10) -- standard terminal-menu numbering, not 0-indexed. */
|
|
23
|
-
function digitToIndex(data: string, rowCount: number): number | undefined {
|
|
24
|
-
if (data === "0") return rowCount >= 10 ? 9 : undefined;
|
|
25
|
-
if (data.length === 1 && data >= "1" && data <= "9") {
|
|
26
|
-
const index = Number(data) - 1;
|
|
27
|
-
return index < rowCount ? index : undefined;
|
|
28
|
-
}
|
|
29
|
-
return undefined;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function rowNumberLabel(index: number): string {
|
|
33
|
-
if (index < 9) return `${index + 1}`;
|
|
34
|
-
if (index === 9) return "0";
|
|
35
|
-
return " "; // beyond the 1-9,0 quick-select range (should not happen given DISCUSSION_OPTIONS_MAX_COUNT=10, but never crash rendering)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async function promptFreeformAnswer(ctx: ExtensionContext): Promise<DiscussionPickResult | undefined> {
|
|
39
|
-
const text = await ctx.ui.input("Your answer:", "");
|
|
40
|
-
return text ? { kind: "freeform", text } : undefined;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Toggle with space or a row's number, confirm with enter (refuses an empty confirm -- at least
|
|
45
|
-
* one pick is required), cancel with escape. Picking the freeform row exits the checkbox flow
|
|
46
|
-
* entirely rather than adding it to the selection.
|
|
47
|
-
*
|
|
48
|
-
* Idle countdown: auto-cancels after DISCUSSION_PICKER_IDLE_TIMEOUT_MS of no input at all --
|
|
49
|
-
* the first keystroke of any kind stops it permanently (not a pause; it never resumes for this
|
|
50
|
-
* picker instance), since a countdown ticking while someone is actively engaging is pressure,
|
|
51
|
-
* not a nudge. The same tick also drives a slow, deliberately noticeable blink on the cursor
|
|
52
|
-
* row -- checked rows stay steadily highlighted, everything else stays dimmed, so the eye reads
|
|
53
|
-
* "what's chosen" at a glance independent of where the cursor happens to be.
|
|
54
|
-
*/
|
|
55
|
-
async function pickMultiple(ctx: ExtensionContext, title: string, options: string[], allowFreeform: boolean, idleTimeoutMs: number, tickMs: number): Promise<DiscussionPickResult | undefined> {
|
|
56
|
-
return ctx.ui.custom<DiscussionPickResult | undefined>((tui, theme, _keybindings, done) => {
|
|
57
|
-
const rows = allowFreeform ? [...options, FREEFORM_LABEL] : options;
|
|
58
|
-
const freeformIndex = allowFreeform ? rows.length - 1 : -1;
|
|
59
|
-
const checked = new Set<number>();
|
|
60
|
-
let selectedIndex = 0;
|
|
61
|
-
let hasInteracted = false;
|
|
62
|
-
let remainingMs = idleTimeoutMs;
|
|
63
|
-
let blinkOn = true;
|
|
64
|
-
const tick = setInterval(() => {
|
|
65
|
-
blinkOn = !blinkOn;
|
|
66
|
-
if (!hasInteracted) {
|
|
67
|
-
remainingMs -= tickMs;
|
|
68
|
-
if (remainingMs <= 0) { finish(undefined); return; }
|
|
69
|
-
}
|
|
70
|
-
tui.requestRender();
|
|
71
|
-
}, tickMs);
|
|
72
|
-
const finish = (value: DiscussionPickResult | undefined) => { clearInterval(tick); done(value); };
|
|
73
|
-
const chooseFreeform = () => { promptFreeformAnswer(ctx).then(finish); };
|
|
74
|
-
const toggle = (index: number) => {
|
|
75
|
-
if (index === freeformIndex) { chooseFreeform(); return; }
|
|
76
|
-
if (checked.has(index)) checked.delete(index); else checked.add(index);
|
|
77
|
-
tui.requestRender();
|
|
78
|
-
};
|
|
79
|
-
return {
|
|
80
|
-
invalidate() {},
|
|
81
|
-
render(width: number): string[] {
|
|
82
|
-
const lines: string[] = [
|
|
83
|
-
theme.bold(title),
|
|
84
|
-
theme.fg("muted", "number/space toggle \u00b7 enter confirm \u00b7 esc cancel"),
|
|
85
|
-
"",
|
|
86
|
-
];
|
|
87
|
-
rows.forEach((option, index) => {
|
|
88
|
-
const isCursor = index === selectedIndex;
|
|
89
|
-
const isChecked = checked.has(index);
|
|
90
|
-
const cursorGlyph = isCursor && blinkOn ? theme.fg("accent", "\u276f") : " ";
|
|
91
|
-
const box = index === freeformIndex ? " " : isChecked ? theme.fg("success", "[x]") : "[ ]";
|
|
92
|
-
let label = option;
|
|
93
|
-
if (isChecked) label = theme.bold(theme.fg("success", label));
|
|
94
|
-
else if (isCursor) label = theme.bold(theme.fg("accent", label));
|
|
95
|
-
else label = theme.fg("dim", label);
|
|
96
|
-
lines.push(truncateToWidth(`${cursorGlyph} ${rowNumberLabel(index)}. ${box} ${label}`, width, ""));
|
|
97
|
-
});
|
|
98
|
-
lines.push("");
|
|
99
|
-
lines.push(theme.fg("dim", `${checked.size} selected`));
|
|
100
|
-
if (!hasInteracted) lines.push(theme.fg("dim", `auto-cancels in ${Math.max(0, Math.ceil(remainingMs / 1000))}s (press any key to stop)`));
|
|
101
|
-
return lines;
|
|
102
|
-
},
|
|
103
|
-
handleInput(data: string) {
|
|
104
|
-
hasInteracted = true;
|
|
105
|
-
const digit = digitToIndex(data, rows.length);
|
|
106
|
-
if (digit !== undefined) { selectedIndex = digit; toggle(digit); return; }
|
|
107
|
-
if (matchesKey(data, "up")) selectedIndex = (selectedIndex - 1 + rows.length) % rows.length;
|
|
108
|
-
else if (matchesKey(data, "down")) selectedIndex = (selectedIndex + 1) % rows.length;
|
|
109
|
-
else if (data === " ") { toggle(selectedIndex); return; }
|
|
110
|
-
else if (matchesKey(data, "enter")) {
|
|
111
|
-
if (selectedIndex === freeformIndex && checked.size === 0) { chooseFreeform(); return; }
|
|
112
|
-
if (checked.size === 0) return; // refuse an empty confirm -- selecting nothing isn't a valid answer
|
|
113
|
-
finish({ kind: "selected", selected: [...checked].sort((a, b) => a - b).map((index) => rows[index]!) });
|
|
114
|
-
return;
|
|
115
|
-
} else if (matchesKey(data, "escape")) { finish(undefined); return; }
|
|
116
|
-
else return;
|
|
117
|
-
tui.requestRender();
|
|
118
|
-
},
|
|
119
|
-
};
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Picks one (single) or several (multi) of the given options, or a freeform typed answer
|
|
125
|
-
* instead, or undefined if the user cancels. Takes the base ExtensionContext (just .ui) rather
|
|
126
|
-
* than the wider ExtensionCommandContext, since a tool's execute() only ever receives the
|
|
127
|
-
* former -- the discuss tool's own live mode reuses this same picker, not just the /discuss
|
|
128
|
-
* TUI panel. Single mode's numbered quick-select is not guaranteed: it delegates to Pi's own
|
|
129
|
-
* native ctx.ui.select, which this package does not control the key handling of.
|
|
130
|
-
*/
|
|
131
|
-
export async function pickDiscussionOptions(
|
|
132
|
-
ctx: ExtensionContext,
|
|
133
|
-
mode: DiscussionOptionsMode,
|
|
134
|
-
options: string[],
|
|
135
|
-
allowFreeform = true,
|
|
136
|
-
/** Test seam: real timers, not faked global time -- pass tiny values to exercise the idle-cancel/blink logic quickly and deterministically. */
|
|
137
|
-
idleTimeoutMs = DISCUSSION_PICKER_IDLE_TIMEOUT_MS,
|
|
138
|
-
tickMs = DISCUSSION_PICKER_TICK_MS,
|
|
139
|
-
): Promise<DiscussionPickResult | undefined> {
|
|
140
|
-
if (mode === "single") {
|
|
141
|
-
const rows = allowFreeform ? [...options, FREEFORM_LABEL] : options;
|
|
142
|
-
const pick = await ctx.ui.select("Pick one:", rows);
|
|
143
|
-
if (!pick) return undefined;
|
|
144
|
-
if (allowFreeform && pick === FREEFORM_LABEL) return promptFreeformAnswer(ctx);
|
|
145
|
-
return { kind: "selected", selected: [pick] };
|
|
146
|
-
}
|
|
147
|
-
return pickMultiple(ctx, "Pick one or more:", options, allowFreeform, idleTimeoutMs, tickMs);
|
|
148
|
-
}
|