@danypops/papyrus 0.29.1 → 0.29.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.
@@ -1126,6 +1126,53 @@ export function isLiveAskPending(): boolean {
1126
1126
  return livePendingCount > 0;
1127
1127
  }
1128
1128
 
1129
+ const DISCUSS_TYPING_COURTESY_POLL_MS = 400;
1130
+ const DISCUSS_TYPING_COURTESY_DEFAULT_MAX_WAIT_MS = 15_000;
1131
+
1132
+ function resolveTypingCourtesyMaxWaitMs(): number {
1133
+ const raw = process.env["PAPYRUS_DISCUSS_TYPING_COURTESY_MS"];
1134
+ if (raw === undefined) return DISCUSS_TYPING_COURTESY_DEFAULT_MAX_WAIT_MS;
1135
+ const parsed = Number(raw);
1136
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : DISCUSS_TYPING_COURTESY_DEFAULT_MAX_WAIT_MS;
1137
+ }
1138
+
1139
+ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
1140
+ return new Promise((resolve) => {
1141
+ if (signal?.aborted) { resolve(); return; }
1142
+ const timer = setTimeout(resolve, ms);
1143
+ signal?.addEventListener("abort", () => { clearTimeout(timer); resolve(); }, { once: true });
1144
+ });
1145
+ }
1146
+
1147
+ /**
1148
+ * Whether there is a genuine editor draft to wait out right now -- a plain synchronous check so
1149
+ * the common case (no draft) never forces the caller through an extra microtask. Deliberately not
1150
+ * folded into waitForEditorCourtesy itself: an unconditional `await` there -- even one that
1151
+ * resolves immediately -- still yields once, which is enough to let a signal aborted synchronously
1152
+ * right after invoking askQuestion race past the abort listener registered deeper in
1153
+ * askQuestionBlocking and get missed entirely.
1154
+ */
1155
+ export function hasEditorCourtesyDraft(ctx: ExtensionContext): boolean {
1156
+ return typeof ctx.ui.getEditorText === "function" && ctx.ui.getEditorText().length > 0;
1157
+ }
1158
+
1159
+ /**
1160
+ * Waits for a non-empty editor draft to clear before popping the live ask over it. Bounded
1161
+ * (PAPYRUS_DISCUSS_TYPING_COURTESY_MS, default 15s, 0 disables): a draft left sitting unattended
1162
+ * must not withhold the question indefinitely, only a genuinely in-progress reply gets the
1163
+ * courtesy. Only call when hasEditorCourtesyDraft(ctx) is already true.
1164
+ */
1165
+ export async function waitForEditorCourtesy(ctx: ExtensionContext, params: Pick<AskQuestionParams, "onUpdate" | "signal">): Promise<void> {
1166
+ const maxWaitMs = resolveTypingCourtesyMaxWaitMs();
1167
+ if (maxWaitMs <= 0) return;
1168
+ const deadline = Date.now() + maxWaitMs;
1169
+ params.onUpdate?.({ content: [{ type: "text", text: "Waiting for you to finish typing before asking..." }], details: undefined });
1170
+ while (Date.now() < deadline && !params.signal?.aborted) {
1171
+ await sleep(DISCUSS_TYPING_COURTESY_POLL_MS, params.signal);
1172
+ if (typeof ctx.ui.getEditorText !== "function" || ctx.ui.getEditorText().length === 0) return;
1173
+ }
1174
+ }
1175
+
1129
1176
  /**
1130
1177
  * Discuss's live:true synchronous ask -- interactive AskComponent when a real TUI is available,
1131
1178
  * dialog fallback (ctx.ui.select/input) in RPC/headless mode, no-op undefined without any
@@ -1147,9 +1194,12 @@ async function askQuestionUnguarded(ctx: ExtensionContext, params: AskQuestionPa
1147
1194
  const displayMode: AskDisplayMode = params.displayMode ?? envDisplayMode ?? "overlay";
1148
1195
  const normalizedContext = params.context?.trim() || undefined;
1149
1196
 
1150
- params.onUpdate?.({ content: [{ type: "text", text: "Waiting for human input..." }], details: undefined });
1151
1197
  livePendingCount += 1;
1152
1198
  try {
1199
+ // Only actually awaits (yielding a microtask) when there's a real draft to wait out --
1200
+ // see hasEditorCourtesyDraft's own comment for why the common case must stay synchronous.
1201
+ if (hasEditorCourtesyDraft(ctx)) await waitForEditorCourtesy(ctx, params);
1202
+ params.onUpdate?.({ content: [{ type: "text", text: "Waiting for human input..." }], details: undefined });
1153
1203
  return await askQuestionBlocking(ctx, params, options, allowMultiple, allowFreeform, allowComment, displayMode, normalizedContext);
1154
1204
  } finally {
1155
1205
  livePendingCount -= 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/papyrus",
3
- "version": "0.29.1",
3
+ "version": "0.29.2",
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"],