@danypops/papyrus 0.27.13 → 0.27.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.
|
@@ -56,6 +56,9 @@ export type AskDisplayMode = "overlay" | "inline";
|
|
|
56
56
|
export interface AskQuestionParams {
|
|
57
57
|
question: string;
|
|
58
58
|
context?: string;
|
|
59
|
+
/** Plain orientation line ("which discussion is this"), shown dim above the question -- not a
|
|
60
|
+
* labeled section like context. Typically the Discussion's own title. */
|
|
61
|
+
subtitle?: string;
|
|
59
62
|
options?: AskOption[];
|
|
60
63
|
allowMultiple?: boolean;
|
|
61
64
|
allowFreeform?: boolean;
|
|
@@ -621,6 +624,7 @@ class AskComponent extends Container {
|
|
|
621
624
|
constructor(
|
|
622
625
|
private question: string,
|
|
623
626
|
private context: string | undefined,
|
|
627
|
+
private subtitle: string | undefined,
|
|
624
628
|
private options: AskOption[],
|
|
625
629
|
private allowMultiple: boolean,
|
|
626
630
|
private allowFreeform: boolean,
|
|
@@ -658,7 +662,10 @@ class AskComponent extends Container {
|
|
|
658
662
|
this.addChild(new BoxBorderBottom((s) => theme.fg("accent", s)));
|
|
659
663
|
|
|
660
664
|
this.updateStaticText();
|
|
661
|
-
|
|
665
|
+
// A freeform-only ask (no options at all) has no select list to show -- start directly in
|
|
666
|
+
// the freeform editor instead of a select mode that would have nothing to render.
|
|
667
|
+
if (this.options.length === 0) this.showFreeformMode();
|
|
668
|
+
else this.showSelectMode();
|
|
662
669
|
}
|
|
663
670
|
|
|
664
671
|
override invalidate(): void { super.invalidate(); this.updateStaticText(); this.updateHelpText(); }
|
|
@@ -778,6 +785,9 @@ class AskComponent extends Container {
|
|
|
778
785
|
"",
|
|
779
786
|
];
|
|
780
787
|
}
|
|
788
|
+
// Only meaningful when reached by escaping OUT of a real select list -- see showFreeformMode's
|
|
789
|
+
// identical guard for the non-overlay layout.
|
|
790
|
+
if (this.options.length === 0) return [];
|
|
781
791
|
return [...new Text(this.theme.fg("accent", this.theme.bold("Custom answer")), 1, 0).render(width), ""];
|
|
782
792
|
}
|
|
783
793
|
|
|
@@ -861,7 +871,11 @@ class AskComponent extends Container {
|
|
|
861
871
|
|
|
862
872
|
private updateStaticText(): void {
|
|
863
873
|
const theme = this.theme;
|
|
864
|
-
|
|
874
|
+
// Reuses the same slot for two different purposes: a plain "which discussion is this" subtitle
|
|
875
|
+
// normally, or "Optional comment" while in comment mode. A generic "Question" header above the
|
|
876
|
+
// real question text added nothing beyond what the question itself already says, and read
|
|
877
|
+
// confusingly like the question text WAS the header.
|
|
878
|
+
this.titleText.setText(this.mode === "comment" ? theme.fg("accent", theme.bold("Optional comment")) : this.subtitle ? theme.fg("dim", this.subtitle) : "");
|
|
865
879
|
this.questionText.setText(theme.fg("text", theme.bold(this.question)));
|
|
866
880
|
if (this.contextComponent && this.context) {
|
|
867
881
|
if (this.contextComponent instanceof Markdown) (this.contextComponent as Markdown).setText(`**Context:**\n${this.context}`);
|
|
@@ -877,12 +891,13 @@ class AskComponent extends Container {
|
|
|
877
891
|
|
|
878
892
|
if (this.mode === "freeform" || this.mode === "comment") {
|
|
879
893
|
const alternateCancelKeys = this.keybindings.getKeys("tui.select.cancel").filter((key) => key !== "escape" && key !== "esc");
|
|
894
|
+
const canGoBack = this.options.length > 0;
|
|
880
895
|
const hints = [
|
|
881
896
|
keybindingHint(theme, this.keybindings, "tui.input.submit", this.mode === "comment" ? "submit/skip" : "submit"),
|
|
882
897
|
keybindingHint(theme, this.keybindings, "tui.input.newLine", "newline"),
|
|
883
|
-
literalHint(theme, "esc", "back"),
|
|
898
|
+
literalHint(theme, "esc", canGoBack ? "back" : "cancel"),
|
|
884
899
|
overlayHint,
|
|
885
|
-
alternateCancelKeys.length > 0 ? literalHint(theme, formatKeyList(alternateCancelKeys), "cancel") : null,
|
|
900
|
+
canGoBack && alternateCancelKeys.length > 0 ? literalHint(theme, formatKeyList(alternateCancelKeys), "cancel") : null,
|
|
886
901
|
].filter((hint): hint is string => !!hint).join(" • ");
|
|
887
902
|
this.helpText.setText(theme.fg("dim", hints));
|
|
888
903
|
return;
|
|
@@ -981,8 +996,13 @@ class AskComponent extends Container {
|
|
|
981
996
|
const editor = this.ensureEditor();
|
|
982
997
|
this.setEditorText(this.freeformDraft);
|
|
983
998
|
(editor as any).focused = this._focused;
|
|
984
|
-
|
|
985
|
-
|
|
999
|
+
// Only meaningful when reached by escaping OUT of a real select list ("instead of these
|
|
1000
|
+
// options, here's a custom one") -- with no options at all there's nothing to contrast
|
|
1001
|
+
// against, so the label is pure noise.
|
|
1002
|
+
if (this.options.length > 0) {
|
|
1003
|
+
this.modeContainer.addChild(new Text(this.theme.fg("accent", this.theme.bold("Custom answer")), 1, 0));
|
|
1004
|
+
this.modeContainer.addChild(new Spacer(1));
|
|
1005
|
+
}
|
|
986
1006
|
this.modeContainer.addChild(editor);
|
|
987
1007
|
this.updateHelpText();
|
|
988
1008
|
this.invalidate();
|
|
@@ -1031,7 +1051,8 @@ class AskComponent extends Container {
|
|
|
1031
1051
|
handleInput(data: string): void {
|
|
1032
1052
|
if (this.handlePromptScrollInput(data)) { this.tui.requestRender(); return; }
|
|
1033
1053
|
if (this.mode === "freeform" || this.mode === "comment") {
|
|
1034
|
-
|
|
1054
|
+
// A freeform-only ask has no select mode to go back to -- escape cancels outright.
|
|
1055
|
+
if (matchesKey(data, Key.escape)) { if (this.options.length > 0) this.showSelectMode(); else this.onDone(null); return; }
|
|
1035
1056
|
if (this.keybindings.matches(data, "tui.select.cancel")) { this.onDone(null); return; }
|
|
1036
1057
|
this.ensureEditor().handleInput(data);
|
|
1037
1058
|
this.tui.requestRender();
|
|
@@ -1057,6 +1078,11 @@ async function askViaDialogs(
|
|
|
1057
1078
|
const dialogOpts = timeout ? { timeout } : undefined;
|
|
1058
1079
|
const prompt = context ? `${question}\n\nContext:\n${context}` : question;
|
|
1059
1080
|
|
|
1081
|
+
if (options.length === 0) {
|
|
1082
|
+
const answer = (await ui.input(prompt, "Type your answer...", dialogOpts)) as string | undefined;
|
|
1083
|
+
return isCancelledInput(answer) ? null : createFreeformResponse(answer);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1060
1086
|
if (allowMultiple) {
|
|
1061
1087
|
const rawSelections = (await ui.input(`${prompt}\n\nOptions (select one or more):\n${formatOptionsForMessage(options)}`, "Type your selection(s)...", dialogOpts)) as string | undefined;
|
|
1062
1088
|
if (isCancelledInput(rawSelections)) return null;
|
|
@@ -1139,13 +1165,9 @@ async function askQuestionBlocking(
|
|
|
1139
1165
|
displayMode: AskDisplayMode,
|
|
1140
1166
|
normalizedContext: string | undefined,
|
|
1141
1167
|
): Promise<AskAnswer | undefined> {
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
const response = createFreeformResponse(answer);
|
|
1146
|
-
return response ? toAskAnswer(response) : undefined;
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1168
|
+
// A freeform-only ask (no options) still goes through the same rich AskComponent/ctx.ui.custom()
|
|
1169
|
+
// path below, not a bare ctx.ui.input() -- otherwise it renders as a plain, contextless single
|
|
1170
|
+
// line while every options-bearing ask gets the full bordered box, title, and markdown context.
|
|
1149
1171
|
const shortcuts: ResolvedAskShortcuts = {
|
|
1150
1172
|
overlayToggle: resolveShortcut(undefined, process.env["PAPYRUS_DISCUSS_OVERLAY_TOGGLE_KEY"], DEFAULT_OVERLAY_TOGGLE_KEY),
|
|
1151
1173
|
commentToggle: resolveShortcut(undefined, process.env["PAPYRUS_DISCUSS_COMMENT_TOGGLE_KEY"], DEFAULT_COMMENT_TOGGLE_KEY),
|
|
@@ -1159,7 +1181,7 @@ async function askQuestionBlocking(
|
|
|
1159
1181
|
const factory = (tui: TUI, theme: Theme, keybindings: KeybindingsManager, done: (result: AskResponse | null) => void) => {
|
|
1160
1182
|
if (params.signal) params.signal.addEventListener("abort", () => done(null), { once: true });
|
|
1161
1183
|
if (params.timeout && params.timeout > 0) setTimeout(() => done(null), params.timeout);
|
|
1162
|
-
return new AskComponent(params.question, normalizedContext, options, allowMultiple, allowFreeform, allowComment, displayMode, tui, theme, keybindings, shortcuts, done);
|
|
1184
|
+
return new AskComponent(params.question, normalizedContext, params.subtitle, options, allowMultiple, allowFreeform, allowComment, displayMode, tui, theme, keybindings, shortcuts, done);
|
|
1163
1185
|
};
|
|
1164
1186
|
|
|
1165
1187
|
const overlayToggle = shortcuts.overlayToggle;
|
package/extension/src/discuss.ts
CHANGED
|
@@ -82,14 +82,15 @@ export async function showDiscussions(ctx: ExtensionCommandContext): Promise<voi
|
|
|
82
82
|
}
|
|
83
83
|
if (choice === "Reply") {
|
|
84
84
|
const pending = (() => { try { return readDiscussionExtra(discussion.extra); } catch { return undefined; } })();
|
|
85
|
-
|
|
86
|
-
//
|
|
87
|
-
//
|
|
85
|
+
// Same fix as the live discuss tool: the most recent round's own content IS the real
|
|
86
|
+
// question -- the title becomes a plain orientation subtitle, not a labeled-backwards
|
|
87
|
+
// "Context:" section under a generic "Reply to <title>:" wrapper.
|
|
88
88
|
const transcript = await callService<Record<string, unknown>, DiscussionAndRounds>("discuss.show", { id: discussion.id });
|
|
89
|
-
const
|
|
89
|
+
const question = transcript.rounds.at(-1)?.content?.trim() || `Reply to "${discussion.title}":`;
|
|
90
|
+
const subtitle = discussion.title;
|
|
90
91
|
const answer = pending?.pendingOptions && pending.pendingOptions.length > 0 && pending.pendingOptionsMode
|
|
91
|
-
? await askQuestion(commandCtx, { question,
|
|
92
|
-
: await askQuestion(commandCtx, { question,
|
|
92
|
+
? await askQuestion(commandCtx, { question, subtitle, options: pending.pendingOptions.map((title) => ({ title })), allowMultiple: pending.pendingOptionsMode === "multi" })
|
|
93
|
+
: await askQuestion(commandCtx, { question, subtitle });
|
|
93
94
|
if (!answer) return; // canceled
|
|
94
95
|
await callService("discuss.reply", { id: discussion.id, actor: ACTOR, content: answer.content, ...(answer.selected ? { selected: answer.selected } : {}), source: SOURCE });
|
|
95
96
|
commandCtx.ui.notify(answer.selected ? `Selected: ${answer.selected.join(", ")}` : "Reply added.", "info");
|
|
@@ -42,22 +42,23 @@ function text(message: string, details: unknown = {}) {
|
|
|
42
42
|
async function liveAnswer(ctx: ExtensionContext, discussion: Artifact, latestContent: string | undefined, onUpdate: AgentToolUpdateCallback | undefined, signal: AbortSignal | undefined): Promise<{ content: string; selected?: string[] } | undefined> {
|
|
43
43
|
if (!ctx.hasUI) return undefined;
|
|
44
44
|
const pending = (() => { try { return readDiscussionExtra(discussion.extra); } catch { return undefined; } })();
|
|
45
|
-
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
const
|
|
45
|
+
// The just-recorded round's own content IS the real question -- a generic "Reply to <title>:"
|
|
46
|
+
// wrapper as the primary question, with the real content demoted to "Context:", left a human
|
|
47
|
+
// staring at a labeled-backwards prompt (live-observed). The wrapper is now only a fallback for
|
|
48
|
+
// the degenerate case of empty content; the title becomes a plain orientation subtitle instead.
|
|
49
|
+
const question = latestContent?.trim() || `Reply to "${discussion.title}":`;
|
|
50
|
+
const subtitle = discussion.title;
|
|
50
51
|
if (pending?.pendingOptions && pending.pendingOptions.length > 0 && pending.pendingOptionsMode) {
|
|
51
52
|
return askQuestion(ctx, {
|
|
52
53
|
question,
|
|
53
|
-
|
|
54
|
+
subtitle,
|
|
54
55
|
options: pending.pendingOptions.map((title) => ({ title })),
|
|
55
56
|
allowMultiple: pending.pendingOptionsMode === "multi",
|
|
56
57
|
onUpdate,
|
|
57
58
|
signal,
|
|
58
59
|
});
|
|
59
60
|
}
|
|
60
|
-
return askQuestion(ctx, { question,
|
|
61
|
+
return askQuestion(ctx, { question, subtitle, onUpdate, signal });
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
/**
|
package/package.json
CHANGED