@danypops/papyrus 0.27.14 → 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,
@@ -781,6 +785,9 @@ class AskComponent extends Container {
781
785
  "",
782
786
  ];
783
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 [];
784
791
  return [...new Text(this.theme.fg("accent", this.theme.bold("Custom answer")), 1, 0).render(width), ""];
785
792
  }
786
793
 
@@ -864,7 +871,11 @@ class AskComponent extends Container {
864
871
 
865
872
  private updateStaticText(): void {
866
873
  const theme = this.theme;
867
- this.titleText.setText(theme.fg("accent", theme.bold(this.mode === "comment" ? "Optional comment" : "Question")));
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) : "");
868
879
  this.questionText.setText(theme.fg("text", theme.bold(this.question)));
869
880
  if (this.contextComponent && this.context) {
870
881
  if (this.contextComponent instanceof Markdown) (this.contextComponent as Markdown).setText(`**Context:**\n${this.context}`);
@@ -985,8 +996,13 @@ class AskComponent extends Container {
985
996
  const editor = this.ensureEditor();
986
997
  this.setEditorText(this.freeformDraft);
987
998
  (editor as any).focused = this._focused;
988
- this.modeContainer.addChild(new Text(this.theme.fg("accent", this.theme.bold("Custom answer")), 1, 0));
989
- this.modeContainer.addChild(new Spacer(1));
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
+ }
990
1006
  this.modeContainer.addChild(editor);
991
1007
  this.updateHelpText();
992
1008
  this.invalidate();
@@ -1165,7 +1181,7 @@ async function askQuestionBlocking(
1165
1181
  const factory = (tui: TUI, theme: Theme, keybindings: KeybindingsManager, done: (result: AskResponse | null) => void) => {
1166
1182
  if (params.signal) params.signal.addEventListener("abort", () => done(null), { once: true });
1167
1183
  if (params.timeout && params.timeout > 0) setTimeout(() => done(null), params.timeout);
1168
- 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);
1169
1185
  };
1170
1186
 
1171
1187
  const overlayToggle = shortcuts.overlayToggle;
@@ -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
- const question = `Reply to "${discussion.title}":`;
86
- // Same fix as the live discuss tool: the title alone isn't the actual question -- show
87
- // the most recent round's real content as context, not a bare title prompt.
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 context = transcript.rounds.at(-1)?.content?.trim() || undefined;
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, context, options: pending.pendingOptions.map((title) => ({ title })), allowMultiple: pending.pendingOptionsMode === "multi" })
92
- : await askQuestion(commandCtx, { question, context });
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
- const question = `Reply to "${discussion.title}":`;
46
- // The discussion's title alone is often not the actual question -- a human staring at a bare
47
- // "Reply to '<title>':" prompt with no visible content has no way to tell what's being asked.
48
- // The just-recorded round's own content is the real question text; show it as context.
49
- const context = latestContent?.trim() || undefined;
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
- context,
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, context, onUpdate, signal });
61
+ return askQuestion(ctx, { question, subtitle, onUpdate, signal });
61
62
  }
62
63
 
63
64
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/papyrus",
3
- "version": "0.27.14",
3
+ "version": "0.27.15",
4
4
  "description": "Daemon-backed graph artifacts, evidence-bearing tasks, rules, skills, and native TUI workflows for Pi",
5
5
  "type": "module",
6
6
  "keywords": ["pi-package"],