@danypops/papyrus 0.27.9 → 0.27.11
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
|
|
@@ -84,19 +63,10 @@ export interface AskQuestionParams {
|
|
|
84
63
|
displayMode?: AskDisplayMode;
|
|
85
64
|
timeout?: number;
|
|
86
65
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
|
|
91
|
-
key?: string;
|
|
92
|
-
/**
|
|
93
|
-
* Streamed once before blocking on the human. A live ask can legitimately sit pending far
|
|
94
|
-
* longer than a typical tool call (real human response time, not milliseconds) -- without any
|
|
95
|
-
* progress signal, a tool call sitting silent that long looks indistinguishable from a dead
|
|
96
|
-
* one to anything upstream watching for stalled calls. pi-ask-user's own original code (the
|
|
97
|
-
* prior art this view is adapted from) sent exactly this same heartbeat before presenting its
|
|
98
|
-
* UI; dropping it during the port was the regression that let two independent executions of
|
|
99
|
-
* the same live ask run concurrently, each opening its own picker for the same question.
|
|
66
|
+
* Streamed once before blocking on the human, matching pi-ask-user's own original code (the
|
|
67
|
+
* prior art this view is adapted from) -- gives the tool call's progress UI something to show
|
|
68
|
+
* during a wait that legitimately runs far longer than a typical tool call (real human response
|
|
69
|
+
* time, not milliseconds).
|
|
100
70
|
*/
|
|
101
71
|
onUpdate?: AgentToolUpdateCallback;
|
|
102
72
|
/**
|
|
@@ -943,7 +913,7 @@ class AskComponent extends Container {
|
|
|
943
913
|
if (this.singleSelectList) return this.singleSelectList;
|
|
944
914
|
const list = new WrappedSingleSelectList(this.options, this.allowFreeform, this.allowComment, this.theme, this.keybindings, this.shortcuts.commentToggle);
|
|
945
915
|
list.onSubmit = (result) => this.handleSelectionSubmit([result], list.isCommentEnabled());
|
|
946
|
-
list.onCancel = () =>
|
|
916
|
+
list.onCancel = () => this.onDone(null);
|
|
947
917
|
list.onEnterFreeform = () => this.showFreeformMode();
|
|
948
918
|
this.singleSelectList = list;
|
|
949
919
|
return list;
|
|
@@ -1099,9 +1069,7 @@ async function askViaDialogs(
|
|
|
1099
1069
|
|
|
1100
1070
|
const selectOptions = options.map((o) => o.title);
|
|
1101
1071
|
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
1072
|
const selected = (await ui.select(prompt, selectOptions, dialogOpts)) as string | undefined;
|
|
1104
|
-
diag("askViaDialogs: ui.select resolved", { selected: selected ?? null });
|
|
1105
1073
|
if (isCancelledInput(selected)) return null;
|
|
1106
1074
|
|
|
1107
1075
|
if (selected === FREEFORM_SENTINEL) {
|
|
@@ -1122,9 +1090,8 @@ async function askViaDialogs(
|
|
|
1122
1090
|
* driver (extension/src/index.ts's driveActiveTasks, on agent_settled) queue a "continue the
|
|
1123
1091
|
* active task" nudge as a `deliverAs: "nextTurn"` message while this exact live ask is still
|
|
1124
1092
|
* awaiting an answer -- starting a second, concurrent turn that reasons about the very Discussion
|
|
1125
|
-
* this call is already resolving, independently of it.
|
|
1126
|
-
*
|
|
1127
|
-
* to exactly this race. driveActiveTasks checks isLiveAskPending() and skips queuing while true.
|
|
1093
|
+
* this call is already resolving, independently of it. driveActiveTasks checks isLiveAskPending()
|
|
1094
|
+
* and skips queuing while true.
|
|
1128
1095
|
*/
|
|
1129
1096
|
let livePendingCount = 0;
|
|
1130
1097
|
|
|
@@ -1132,16 +1099,6 @@ export function isLiveAskPending(): boolean {
|
|
|
1132
1099
|
return livePendingCount > 0;
|
|
1133
1100
|
}
|
|
1134
1101
|
|
|
1135
|
-
/**
|
|
1136
|
-
* Keyed reentrancy join: whatever the exact external cause (an upstream retry, a duplicate turn,
|
|
1137
|
-
* anything outside code this package controls -- verified Pi's own ctx.ui.custom() is a clean,
|
|
1138
|
-
* single-shot, well-guarded call with no retry/timeout logic of its own), a second concurrent
|
|
1139
|
-
* askQuestion() call for the SAME key must never open a second picker for the same question. It
|
|
1140
|
-
* joins the already-in-flight promise instead. Keyed by the target Discussion's id (stable across
|
|
1141
|
-
* a genuine duplicate call, unlike a fresh toolCallId each retry might mint).
|
|
1142
|
-
*/
|
|
1143
|
-
const pendingByKey = new Map<string, Promise<AskAnswer | undefined>>();
|
|
1144
|
-
|
|
1145
1102
|
/**
|
|
1146
1103
|
* Discuss's live:true synchronous ask -- interactive AskComponent when a real TUI is available,
|
|
1147
1104
|
* dialog fallback (ctx.ui.select/input) in RPC/headless mode, no-op undefined without any
|
|
@@ -1149,21 +1106,8 @@ const pendingByKey = new Map<string, Promise<AskAnswer | undefined>>();
|
|
|
1149
1106
|
* contexts all resolve to undefined.
|
|
1150
1107
|
*/
|
|
1151
1108
|
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
1109
|
if (!ctx.hasUI || !ctx.ui) return undefined;
|
|
1154
|
-
|
|
1155
|
-
const existing = pendingByKey.get(params.key);
|
|
1156
|
-
if (existing) { diag("joining an already in-flight ask for this key", { key: params.key }); return existing; }
|
|
1157
|
-
}
|
|
1158
|
-
const promise = askQuestionUnguarded(ctx, params);
|
|
1159
|
-
if (params.key !== undefined) {
|
|
1160
|
-
const key = params.key;
|
|
1161
|
-
pendingByKey.set(key, promise);
|
|
1162
|
-
void promise.finally(() => {
|
|
1163
|
-
if (pendingByKey.get(key) === promise) pendingByKey.delete(key);
|
|
1164
|
-
});
|
|
1165
|
-
}
|
|
1166
|
-
return promise;
|
|
1110
|
+
return askQuestionUnguarded(ctx, params);
|
|
1167
1111
|
}
|
|
1168
1112
|
|
|
1169
1113
|
async function askQuestionUnguarded(ctx: ExtensionContext, params: AskQuestionParams): Promise<AskAnswer | undefined> {
|
|
@@ -1212,15 +1156,9 @@ async function askQuestionBlocking(
|
|
|
1212
1156
|
let hasAnnouncedHide = false;
|
|
1213
1157
|
let response: AskResponse | null;
|
|
1214
1158
|
try {
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
const done = (result: AskResponse | null) => {
|
|
1219
|
-
diag("done() invoked -- generic catch-all", { elapsedMs: Date.now() - startedAt, result: result === null ? null : JSON.stringify(result), stack: new Error().stack?.split("\n").slice(1, 6).join(" | ") });
|
|
1220
|
-
realDone(result);
|
|
1221
|
-
};
|
|
1222
|
-
if (params.signal) params.signal.addEventListener("abort", () => { diag("tool call signal fired abort -- resolving null"); done(null); }, { once: true });
|
|
1223
|
-
if (params.timeout && params.timeout > 0) setTimeout(() => { diag("explicit params.timeout expired -- resolving null", { timeout: params.timeout }); done(null); }, params.timeout);
|
|
1159
|
+
const factory = (tui: TUI, theme: Theme, keybindings: KeybindingsManager, done: (result: AskResponse | null) => void) => {
|
|
1160
|
+
if (params.signal) params.signal.addEventListener("abort", () => done(null), { once: true });
|
|
1161
|
+
if (params.timeout && params.timeout > 0) setTimeout(() => done(null), params.timeout);
|
|
1224
1162
|
return new AskComponent(params.question, normalizedContext, options, allowMultiple, allowFreeform, allowComment, displayMode, tui, theme, keybindings, shortcuts, done);
|
|
1225
1163
|
};
|
|
1226
1164
|
|
|
@@ -1235,12 +1173,8 @@ async function askQuestionBlocking(
|
|
|
1235
1173
|
});
|
|
1236
1174
|
}
|
|
1237
1175
|
|
|
1238
|
-
diag("calling ctx.ui.custom()", { displayMode });
|
|
1239
1176
|
const customResult = await ctx.ui.custom<AskResponse | null>(factory, buildCustomUIOptions(displayMode, (handle) => { overlayHandle = handle; }));
|
|
1240
|
-
diag("ctx.ui.custom() resolved", { wasUndefined: customResult === undefined, result: customResult === undefined ? undefined : JSON.stringify(customResult) });
|
|
1241
|
-
if (customResult === undefined) diag("falling back to askViaDialogs -- about to call ctx.ui.select with dialogOpts", { explicitTimeout: params.timeout ?? null });
|
|
1242
1177
|
response = customResult !== undefined ? customResult : await askViaDialogs(ctx.ui, params.question, normalizedContext, options, allowMultiple, allowFreeform, allowComment, params.timeout);
|
|
1243
|
-
diag("askQuestionBlocking resolved", { response: response === null ? null : JSON.stringify(response) });
|
|
1244
1178
|
} finally {
|
|
1245
1179
|
removeOverlayInputListener?.();
|
|
1246
1180
|
}
|
package/extension/src/discuss.ts
CHANGED
|
@@ -84,8 +84,8 @@ export async function showDiscussions(ctx: ExtensionCommandContext): Promise<voi
|
|
|
84
84
|
const pending = (() => { try { return readDiscussionExtra(discussion.extra); } catch { return undefined; } })();
|
|
85
85
|
const question = `Reply to "${discussion.title}":`;
|
|
86
86
|
const answer = pending?.pendingOptions && pending.pendingOptions.length > 0 && pending.pendingOptionsMode
|
|
87
|
-
? await askQuestion(commandCtx, { question, options: pending.pendingOptions.map((title) => ({ title })), allowMultiple: pending.pendingOptionsMode === "multi"
|
|
88
|
-
: await askQuestion(commandCtx, { question
|
|
87
|
+
? await askQuestion(commandCtx, { question, options: pending.pendingOptions.map((title) => ({ title })), allowMultiple: pending.pendingOptionsMode === "multi" })
|
|
88
|
+
: await askQuestion(commandCtx, { question });
|
|
89
89
|
if (!answer) return; // canceled
|
|
90
90
|
await callService("discuss.reply", { id: discussion.id, actor: ACTOR, content: answer.content, ...(answer.selected ? { selected: answer.selected } : {}), source: SOURCE });
|
|
91
91
|
commandCtx.ui.notify(answer.selected ? `Selected: ${answer.selected.join(", ")}` : "Reply added.", "info");
|
|
@@ -50,10 +50,9 @@ async function liveAnswer(ctx: ExtensionContext, discussion: Artifact, onUpdate:
|
|
|
50
50
|
allowMultiple: pending.pendingOptionsMode === "multi",
|
|
51
51
|
onUpdate,
|
|
52
52
|
signal,
|
|
53
|
-
key: discussion.id,
|
|
54
53
|
});
|
|
55
54
|
}
|
|
56
|
-
return askQuestion(ctx, { question, onUpdate, signal
|
|
55
|
+
return askQuestion(ctx, { question, onUpdate, signal });
|
|
57
56
|
}
|
|
58
57
|
|
|
59
58
|
/**
|
package/package.json
CHANGED