feat: support ask flow in RPC clients
CI / validate (push) Failing after 13s

This commit is contained in:
jb
2026-08-22 21:55:08 -04:00
parent 49482b7e5d
commit 80da307828
5 changed files with 346 additions and 14 deletions
+109 -11
View File
@@ -56,6 +56,31 @@ function makeCtx(hasUi: boolean, mode = hasUi ? "tui" : "print"): unknown {
return { [HAS_UI]: hasUi, mode };
}
function makeRpcCtx(responses: Array<string | undefined>) {
const requests: Array<{
title: string;
options?: string[];
placeholder?: string;
}> = [];
return {
ctx: {
[HAS_UI]: true,
mode: "rpc",
ui: {
select(title: string, options: string[]) {
requests.push({ title, options });
return Promise.resolve(responses.shift());
},
input(title: string, placeholder?: string) {
requests.push({ title, placeholder });
return Promise.resolve(responses.shift());
},
},
},
requests,
};
}
function sampleParams(): AskParams {
return {
title: "Clarify next step",
@@ -155,23 +180,96 @@ test("ask tool returns pending questions in non-interactive mode", async () => {
assert.match(result.content[0].text, CUSTOM_OPTION_RE);
});
test("ask tool does not open custom UI outside TUI mode", async () => {
test("ask tool uses standard dialogs in RPC mode", async () => {
const { tool } = registerMockTool();
let customOpened = false;
const { ctx, requests } = makeRpcCtx(["1. Speed"]);
const result = await tool.execute("call-1", sampleParams(), undefined, noop, {
[HAS_UI]: true,
mode: "rpc",
ui: {
custom() {
customOpened = true;
},
const result = await tool.execute(
"call-1",
sampleParams(),
undefined,
noop,
ctx
);
assert.deepEqual(requests, [
{
title: "Goal: What should I optimize for?",
options: ["1. Speed", "2. Safety", "3. Type your own"],
},
]);
assert.equal(result.details.cancelled, false);
assert.deepEqual(result.details.answers.goal, {
indices: [0],
labels: ["Speed"],
values: ["speed"],
});
});
test("ask tool collects a custom answer through an RPC input dialog", async () => {
const { tool } = registerMockTool();
const { ctx, requests } = makeRpcCtx(["3. Type your own", "Balance both"]);
const result = await tool.execute(
"call-1",
sampleParams(),
undefined,
noop,
ctx
);
assert.deepEqual(requests[1], {
title: "Goal: What should I optimize for?",
placeholder: "Type your answer",
});
assert.deepEqual(result.details.answers.goal, {
indices: [],
labels: [],
values: [],
customText: "Balance both",
});
});
test("ask tool collects multiple selections through RPC dialogs", async () => {
const { tool } = registerMockTool();
const params = sampleParams();
params.questions[0].type = "multi";
const { ctx, requests } = makeRpcCtx([
"1. [ ] Speed",
"2. [ ] Safety",
"3. Done",
]);
const result = await tool.execute("call-1", params, undefined, noop, ctx);
assert.equal(requests.length, 3);
assert.deepEqual(requests[1]?.options, [
"1. [x] Speed",
"2. [ ] Safety",
"3. Done",
"4. Type your own",
]);
assert.deepEqual(result.details.answers.goal, {
indices: [0, 1],
labels: ["Speed", "Safety"],
values: ["speed", "safety"],
});
});
test("ask tool returns a cancelled RPC result when a dialog is dismissed", async () => {
const { tool } = registerMockTool();
const { ctx } = makeRpcCtx([undefined]);
const result = await tool.execute(
"call-1",
sampleParams(),
undefined,
noop,
ctx
);
assert.equal(customOpened, false);
assert.equal(result.details.cancelled, true);
assert.match(result.content[0].text, NON_INTERACTIVE_MESSAGE_RE);
assert.deepEqual(result.details.answers, {});
});
test("ask tool includes custom answer fallback for preview questions", async () => {