diff --git a/packages/pi-coding-agent/src/core/extensions/types.ts b/packages/pi-coding-agent/src/core/extensions/types.ts index 11bd86361..1efc7eda0 100644 --- a/packages/pi-coding-agent/src/core/extensions/types.ts +++ b/packages/pi-coding-agent/src/core/extensions/types.ts @@ -86,6 +86,8 @@ export interface ExtensionUIDialogOptions { signal?: AbortSignal; /** Timeout in milliseconds. Dialog auto-dismisses with live countdown display. */ timeout?: number; + /** When true, the user can select multiple options. The return type becomes `string[]`. */ + allowMultiple?: boolean; } /** Placement for extension widgets. */ @@ -105,8 +107,8 @@ export type TerminalInputHandler = (data: string) => { consume?: boolean; data?: * Each mode (interactive, RPC, print) provides its own implementation. */ export interface ExtensionUIContext { - /** Show a selector and return the user's choice. */ - select(title: string, options: string[], opts?: ExtensionUIDialogOptions): Promise; + /** Show a selector and return the user's choice. When `opts.allowMultiple` is true, returns an array. */ + select(title: string, options: string[], opts?: ExtensionUIDialogOptions): Promise; /** Show a confirmation dialog. */ confirm(title: string, message: string, opts?: ExtensionUIDialogOptions): Promise; diff --git a/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts b/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts index b38f43a12..8e859c3fe 100644 --- a/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +++ b/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts @@ -119,8 +119,8 @@ export async function runRpcMode(session: AgentSession): Promise { */ const createExtensionUIContext = (): ExtensionUIContext => ({ select: (title, options, opts) => - createDialogPromise(opts, undefined, { method: "select", title, options, timeout: opts?.timeout }, (r) => - "cancelled" in r && r.cancelled ? undefined : "value" in r ? r.value : undefined, + createDialogPromise(opts, undefined, { method: "select", title, options, timeout: opts?.timeout, allowMultiple: opts?.allowMultiple }, (r) => + "cancelled" in r && r.cancelled ? undefined : "values" in r ? r.values : "value" in r ? r.value : undefined, ), confirm: (title, message, opts) => diff --git a/packages/pi-coding-agent/src/modes/rpc/rpc-types.ts b/packages/pi-coding-agent/src/modes/rpc/rpc-types.ts index e2d15716c..b014640ad 100644 --- a/packages/pi-coding-agent/src/modes/rpc/rpc-types.ts +++ b/packages/pi-coding-agent/src/modes/rpc/rpc-types.ts @@ -210,7 +210,7 @@ export type RpcResponse = /** Emitted when an extension needs user input */ export type RpcExtensionUIRequest = - | { type: "extension_ui_request"; id: string; method: "select"; title: string; options: string[]; timeout?: number } + | { type: "extension_ui_request"; id: string; method: "select"; title: string; options: string[]; timeout?: number; allowMultiple?: boolean } | { type: "extension_ui_request"; id: string; method: "confirm"; title: string; message: string; timeout?: number } | { type: "extension_ui_request"; @@ -253,6 +253,7 @@ export type RpcExtensionUIRequest = /** Response to an extension UI request */ export type RpcExtensionUIResponse = | { type: "extension_ui_response"; id: string; value: string } + | { type: "extension_ui_response"; id: string; values: string[] } | { type: "extension_ui_response"; id: string; confirmed: boolean } | { type: "extension_ui_response"; id: string; cancelled: true }; diff --git a/src/resources/extensions/ask-user-questions.ts b/src/resources/extensions/ask-user-questions.ts index 3b218a90d..ecfdc6e1f 100644 --- a/src/resources/extensions/ask-user-questions.ts +++ b/src/resources/extensions/ask-user-questions.ts @@ -144,6 +144,46 @@ export default function AskUserQuestions(pi: ExtensionAPI) { // Delegate to shared interview UI const result = await showInterviewRound(params.questions, {}, ctx); + // RPC mode fallback: custom() returns undefined, so showInterviewRound + // may return undefined. Fall back to sequential ctx.ui.select() calls. + if (!result) { + const answers: Record = {}; + for (const q of params.questions) { + const options = q.options.map((o) => o.label); + if (!q.allowMultiple) { + options.push(OTHER_OPTION_LABEL); + } + const selected = await ctx.ui.select( + `${q.header}: ${q.question}`, + options, + { signal, ...(q.allowMultiple ? { allowMultiple: true } : {}) }, + ); + if (selected === undefined) { + return errorResult("ask_user_questions was cancelled", params.questions); + } + answers[q.id] = { + answers: Array.isArray(selected) ? selected : [selected], + }; + } + const roundResult: RoundResult = { + endInterview: false, + answers: Object.fromEntries( + Object.entries(answers).map(([id, a]) => [ + id, + { selected: a.answers.length === 1 ? a.answers[0] : a.answers, notes: "" }, + ]), + ), + }; + return { + content: [{ type: "text" as const, text: JSON.stringify({ answers }) }], + details: { + questions: params.questions, + response: roundResult, + cancelled: false, + } satisfies LocalResultDetails, + }; + } + // Check if cancelled (empty answers = user exited) const hasAnswers = Object.keys(result.answers).length > 0; if (!hasAnswers) { diff --git a/src/resources/extensions/search-the-web/command-search-provider.ts b/src/resources/extensions/search-the-web/command-search-provider.ts index 0f3ebb46d..59999ea60 100644 --- a/src/resources/extensions/search-the-web/command-search-provider.ts +++ b/src/resources/extensions/search-the-web/command-search-provider.ts @@ -81,7 +81,7 @@ export function registerSearchProviderCommand(pi: ExtensionAPI): void { return } - chosen = parseSelectChoice(result) + chosen = parseSelectChoice(Array.isArray(result) ? result[0] : result) } setSearchProviderPreference(chosen)