@danypops/papyrus 0.27.8 → 0.27.10
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.
|
@@ -35,27 +35,6 @@ import {
|
|
|
35
35
|
wrapTextWithAnsi,
|
|
36
36
|
} from "@earendil-works/pi-tui";
|
|
37
37
|
import { renderSingleSelectRows, type AskOption } from "./discuss-ask-layout.ts";
|
|
38
|
-
import { callService } from "./service-client.ts";
|
|
39
|
-
|
|
40
|
-
/** Temporary RCA instrumentation for the live-observed duplicate-picker defect. Fire-and-forget: never lets a logging failure affect the actual ask. Remove once root-caused. */
|
|
41
|
-
const DIAG_SOURCE = "papyrus-discuss-ask-diag";
|
|
42
|
-
let diagSequence = 0;
|
|
43
|
-
function diag(message: string, fields?: Record<string, unknown>): void {
|
|
44
|
-
void callService("logs.append", {
|
|
45
|
-
source_id: DIAG_SOURCE,
|
|
46
|
-
source_label: "Discuss live-ask RCA",
|
|
47
|
-
level: "info",
|
|
48
|
-
message,
|
|
49
|
-
operation_id: `diag-${Date.now()}-${++diagSequence}`,
|
|
50
|
-
...(fields ? { fields } : {}),
|
|
51
|
-
}).catch((error) => {
|
|
52
|
-
void callService("logs.append", {
|
|
53
|
-
source_id: DIAG_SOURCE, source_label: "Discuss live-ask RCA", level: "error",
|
|
54
|
-
message: `diag() itself failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
55
|
-
operation_id: `diag-error-${Date.now()}-${++diagSequence}`,
|
|
56
|
-
}).catch(() => {});
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
38
|
|
|
60
39
|
/** See pi-ask-user's identical safeMarkdownTheme() comment: a broken theme Proxy throws only on
|
|
61
40
|
* property access, not construction, so a bare try/catch around getMarkdownTheme() alone would
|
|
@@ -943,7 +922,7 @@ class AskComponent extends Container {
|
|
|
943
922
|
if (this.singleSelectList) return this.singleSelectList;
|
|
944
923
|
const list = new WrappedSingleSelectList(this.options, this.allowFreeform, this.allowComment, this.theme, this.keybindings, this.shortcuts.commentToggle);
|
|
945
924
|
list.onSubmit = (result) => this.handleSelectionSubmit([result], list.isCommentEnabled());
|
|
946
|
-
list.onCancel = () =>
|
|
925
|
+
list.onCancel = () => this.onDone(null);
|
|
947
926
|
list.onEnterFreeform = () => this.showFreeformMode();
|
|
948
927
|
this.singleSelectList = list;
|
|
949
928
|
return list;
|
|
@@ -1099,9 +1078,7 @@ async function askViaDialogs(
|
|
|
1099
1078
|
|
|
1100
1079
|
const selectOptions = options.map((o) => o.title);
|
|
1101
1080
|
if (allowFreeform) selectOptions.push(FREEFORM_SENTINEL);
|
|
1102
|
-
diag("askViaDialogs: calling ui.select", { dialogOpts: dialogOpts === undefined ? null : JSON.stringify(dialogOpts), promptPreview: prompt.slice(0, 80) });
|
|
1103
1081
|
const selected = (await ui.select(prompt, selectOptions, dialogOpts)) as string | undefined;
|
|
1104
|
-
diag("askViaDialogs: ui.select resolved", { selected: selected ?? null });
|
|
1105
1082
|
if (isCancelledInput(selected)) return null;
|
|
1106
1083
|
|
|
1107
1084
|
if (selected === FREEFORM_SENTINEL) {
|
|
@@ -1149,11 +1126,10 @@ const pendingByKey = new Map<string, Promise<AskAnswer | undefined>>();
|
|
|
1149
1126
|
* contexts all resolve to undefined.
|
|
1150
1127
|
*/
|
|
1151
1128
|
export async function askQuestion(ctx: ExtensionContext, params: AskQuestionParams): Promise<AskAnswer | undefined> {
|
|
1152
|
-
diag("askQuestion() called", { question: params.question, key: params.key, hasUI: ctx.hasUI, mode: ctx.mode, optionCount: params.options?.length ?? 0, alreadyPendingForKey: params.key !== undefined && pendingByKey.has(params.key) });
|
|
1153
1129
|
if (!ctx.hasUI || !ctx.ui) return undefined;
|
|
1154
1130
|
if (params.key !== undefined) {
|
|
1155
1131
|
const existing = pendingByKey.get(params.key);
|
|
1156
|
-
if (existing)
|
|
1132
|
+
if (existing) return existing;
|
|
1157
1133
|
}
|
|
1158
1134
|
const promise = askQuestionUnguarded(ctx, params);
|
|
1159
1135
|
if (params.key !== undefined) {
|
|
@@ -1166,6 +1142,23 @@ export async function askQuestion(ctx: ExtensionContext, params: AskQuestionPara
|
|
|
1166
1142
|
return promise;
|
|
1167
1143
|
}
|
|
1168
1144
|
|
|
1145
|
+
/**
|
|
1146
|
+
* Sent at least this often while blocked on the human -- well under any 8-second window a
|
|
1147
|
+
* watchdog upstream might require to consider a tool call still alive. A single upfront ping
|
|
1148
|
+
* (this view's original heartbeat) only covers the first ~8s; for genuinely long human response
|
|
1149
|
+
* times (the whole point of a live ask) it goes silent again after that, indistinguishable from
|
|
1150
|
+
* dead. A live-observed bug traced back to exactly this: cancellation always cited "idle timeout,
|
|
1151
|
+
* no interaction within 8000ms" regardless of whether the real wait was 5 seconds or 18 minutes
|
|
1152
|
+
* -- a fixed re-check window, not a measure of total elapsed time -- meaning periodic liveness,
|
|
1153
|
+
* not a one-time ping, is what's required here.
|
|
1154
|
+
*/
|
|
1155
|
+
let liveAskHeartbeatIntervalMs = 4_000;
|
|
1156
|
+
|
|
1157
|
+
/** Test seam: real tiny interval, not a faked global timer -- exercises the periodic heartbeat deterministically and fast. */
|
|
1158
|
+
export function setLiveAskHeartbeatIntervalMsForTests(ms: number): void {
|
|
1159
|
+
liveAskHeartbeatIntervalMs = ms;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1169
1162
|
async function askQuestionUnguarded(ctx: ExtensionContext, params: AskQuestionParams): Promise<AskAnswer | undefined> {
|
|
1170
1163
|
const options = params.options ?? [];
|
|
1171
1164
|
const allowMultiple = params.allowMultiple ?? false;
|
|
@@ -1176,12 +1169,15 @@ async function askQuestionUnguarded(ctx: ExtensionContext, params: AskQuestionPa
|
|
|
1176
1169
|
const displayMode: AskDisplayMode = params.displayMode ?? envDisplayMode ?? "overlay";
|
|
1177
1170
|
const normalizedContext = params.context?.trim() || undefined;
|
|
1178
1171
|
|
|
1179
|
-
params.onUpdate?.({ content: [{ type: "text", text: "Waiting for human input..." }], details: undefined });
|
|
1172
|
+
const sendHeartbeat = () => params.onUpdate?.({ content: [{ type: "text", text: "Waiting for human input..." }], details: undefined });
|
|
1173
|
+
sendHeartbeat();
|
|
1174
|
+
const heartbeatTimer = params.onUpdate ? setInterval(sendHeartbeat, liveAskHeartbeatIntervalMs) : undefined;
|
|
1180
1175
|
livePendingCount += 1;
|
|
1181
1176
|
try {
|
|
1182
1177
|
return await askQuestionBlocking(ctx, params, options, allowMultiple, allowFreeform, allowComment, displayMode, normalizedContext);
|
|
1183
1178
|
} finally {
|
|
1184
1179
|
livePendingCount -= 1;
|
|
1180
|
+
if (heartbeatTimer !== undefined) clearInterval(heartbeatTimer);
|
|
1185
1181
|
}
|
|
1186
1182
|
}
|
|
1187
1183
|
|
|
@@ -1212,10 +1208,9 @@ async function askQuestionBlocking(
|
|
|
1212
1208
|
let hasAnnouncedHide = false;
|
|
1213
1209
|
let response: AskResponse | null;
|
|
1214
1210
|
try {
|
|
1215
|
-
diag("factory about to construct AskComponent", { hasSignal: params.signal !== undefined, signalAlreadyAborted: params.signal?.aborted ?? null, hasExplicitTimeout: params.timeout !== undefined });
|
|
1216
1211
|
const factory = (tui: TUI, theme: Theme, keybindings: KeybindingsManager, done: (result: AskResponse | null) => void) => {
|
|
1217
|
-
if (params.signal) params.signal.addEventListener("abort", () =>
|
|
1218
|
-
if (params.timeout && params.timeout > 0) setTimeout(() =>
|
|
1212
|
+
if (params.signal) params.signal.addEventListener("abort", () => done(null), { once: true });
|
|
1213
|
+
if (params.timeout && params.timeout > 0) setTimeout(() => done(null), params.timeout);
|
|
1219
1214
|
return new AskComponent(params.question, normalizedContext, options, allowMultiple, allowFreeform, allowComment, displayMode, tui, theme, keybindings, shortcuts, done);
|
|
1220
1215
|
};
|
|
1221
1216
|
|
|
@@ -1230,12 +1225,8 @@ async function askQuestionBlocking(
|
|
|
1230
1225
|
});
|
|
1231
1226
|
}
|
|
1232
1227
|
|
|
1233
|
-
diag("calling ctx.ui.custom()", { displayMode });
|
|
1234
1228
|
const customResult = await ctx.ui.custom<AskResponse | null>(factory, buildCustomUIOptions(displayMode, (handle) => { overlayHandle = handle; }));
|
|
1235
|
-
diag("ctx.ui.custom() resolved", { wasUndefined: customResult === undefined, result: customResult === undefined ? undefined : JSON.stringify(customResult) });
|
|
1236
|
-
if (customResult === undefined) diag("falling back to askViaDialogs -- about to call ctx.ui.select with dialogOpts", { explicitTimeout: params.timeout ?? null });
|
|
1237
1229
|
response = customResult !== undefined ? customResult : await askViaDialogs(ctx.ui, params.question, normalizedContext, options, allowMultiple, allowFreeform, allowComment, params.timeout);
|
|
1238
|
-
diag("askQuestionBlocking resolved", { response: response === null ? null : JSON.stringify(response) });
|
|
1239
1230
|
} finally {
|
|
1240
1231
|
removeOverlayInputListener?.();
|
|
1241
1232
|
}
|
package/package.json
CHANGED