fix(harness): gate widget round-trip via native ctx.ui.input (no sendRaw); no-limbo

This commit is contained in:
2026-06-27 20:02:19 +02:00
parent c55df2ad0f
commit 8c4a1798da
2 changed files with 43 additions and 37 deletions
+18 -37
View File
@@ -133,46 +133,30 @@ function advanceIfReady(ctx, session) {
}
}
// --- widget emission + wait (NEW: replaces ctx.ui.* blocking primitives) -------
// --- widget emission + wait — usa l'API UI NATIVA di Pi (ctx.ui.input) --------
//
// emitAndWait(ctx, descriptor) sends a ui_request widget and awaits the correlated
// ui_response by id. This is the correlation-by-id layer the reference implementation never had (its
// native TUI primitives handled it implicitly). The descriptor is built by the pure
// ./gate/builders.js (L1-tested).
// emitAndWait(ctx, descriptor) sends the widget-descriptor as JSON in the `title`
// of ctx.ui.input and awaits the response as a parsed string value. No ctx.sendRaw,
// no pi.on("extension_ui_response"): Pi routes the response via pendingExtensionRequests.
//
// The no-limbo invariant: a control response of "cancel"/undefined is NEVER accepted
// as a final answer -- the widget is re-presented. Real escapes (Back/Exit/Other) are
// always present as selectable options in the descriptor (reserved field), so cancel
// has no legitimate meaning.
let _pending = new Map(); // id -> {resolve, reject}
async function emitAndWait(ctx, descriptor) {
// The no-limbo invariant: undefined/null/invalid-JSON/control:"cancel" are NEVER
// accepted as a final answer — the widget is re-presented. Real escapes (Back/Exit/Other)
// are always present as selectable options in the descriptor, so cancel has no
// legitimate meaning.
export async function emitAndWait(ctx, descriptor) {
for (;;) {
const promise = new Promise((resolve, reject) => {
_pending.set(descriptor.id, { resolve, reject });
});
ctx.sendRaw({ type: "extension_ui_request", ui_request: descriptor });
const resp = await promise;
// no-limbo: a cancel/undefined response re-presents the same widget
if (resp && resp.control !== "cancel" && resp.id === descriptor.id) {
return resp;
}
if (ctx.hasUI) {
await ctx.ui.notify(
"Esc non chiude il gate: usa Torna indietro / Esci / Altro dalle opzioni.",
"warning",
);
}
const value = await ctx.ui.input(JSON.stringify(descriptor), "");
if (value === undefined || value === null) { await reLoop(ctx); continue; }
let resp;
try { resp = JSON.parse(value); } catch { await reLoop(ctx); continue; }
if (resp && resp.control !== "cancel" && resp.id === descriptor.id) return resp;
await reLoop(ctx);
}
}
// The runtime invokes this when a ui_response arrives (RPC reply or SSE from FE).
function handleUiResponse(event) {
const slot = _pending.get(event.id);
if (slot) {
_pending.delete(event.id);
slot.resolve(event);
}
async function reLoop(ctx) {
if (ctx.hasUI) await ctx.ui.notify(
"Esc non chiude il gate: usa Torna indietro / Esci / Altro dalle opzioni.", "warning");
}
// --- the extension ------------------------------------------------------------
@@ -284,9 +268,6 @@ export default function (pi) {
}
});
// 6) UI_RESPONSE dispatch: correlate an incoming response to its awaiting widget.
pi.on("extension_ui_response", handleUiResponse);
// --- the four reviewer tools (widget-descriptor emit + await) ---------------
pi.registerTool({