From 4a9fd6dca9581828060e29141250bfd88302761c Mon Sep 17 00:00:00 2001 From: Vitaliy Star Date: Thu, 28 May 2026 18:29:15 +0300 Subject: [PATCH] fix(tools): add all config properties to sessionQuestionSchema and push_question MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The config schema in start_session (sessionQuestionSchema) and push_question only declared question and context fields. When opencode parses tool args through Zod, any undeclared fields are stripped by looseObject, causing options, min, max, recommended, etc. to be silently dropped. This resulted in empty options in the UI for pick_one, pick_many, rank, and rate question types — the form card showed question text and context but no interactive choices. Fix: declare all config properties used by any question type as .any().optional() in both schemas. This preserves type safety (looseObject still rejects unknown top-level keys) while allowing all legitimate config fields to pass through Zod parsing. Same fix as vtemian/octto#28. --- src/tools/octto/factory.ts | 48 +++++++++++++++++++++++++++++++------- src/tools/octto/session.ts | 29 +++++++++++++++++++++++ 2 files changed, 68 insertions(+), 9 deletions(-) diff --git a/src/tools/octto/factory.ts b/src/tools/octto/factory.ts index e578857d..09ef8578 100644 --- a/src/tools/octto/factory.ts +++ b/src/tools/octto/factory.ts @@ -60,15 +60,50 @@ const QUESTION_TYPE_ENUM = [ "emoji_react", ] as const; +/** All config properties as .any().optional() so looseObject passes them through Zod parsing. */ +const pushQuestionConfigSchema = tool.schema + .looseObject({ + question: tool.schema.string().optional(), + context: tool.schema.string().optional(), + options: tool.schema.any().optional(), + min: tool.schema.any().optional(), + max: tool.schema.any().optional(), + step: tool.schema.any().optional(), + recommended: tool.schema.any().optional(), + allowOther: tool.schema.any().optional(), + allowFeedback: tool.schema.any().optional(), + allowCancel: tool.schema.any().optional(), + defaultValue: tool.schema.any().optional(), + labels: tool.schema.any().optional(), + emojis: tool.schema.any().optional(), + yesLabel: tool.schema.any().optional(), + noLabel: tool.schema.any().optional(), + before: tool.schema.any().optional(), + after: tool.schema.any().optional(), + filePath: tool.schema.any().optional(), + language: tool.schema.any().optional(), + content: tool.schema.any().optional(), + sections: tool.schema.any().optional(), + markdown: tool.schema.any().optional(), + placeholder: tool.schema.any().optional(), + multiline: tool.schema.any().optional(), + minLength: tool.schema.any().optional(), + maxLength: tool.schema.any().optional(), + accept: tool.schema.any().optional(), + multiple: tool.schema.any().optional(), + maxImages: tool.schema.any().optional(), + maxFiles: tool.schema.any().optional(), + maxSize: tool.schema.any().optional(), + }) + .describe("Question configuration (varies by type)"); + function executePushQuestion( sessions: SessionStore, args: { session_id: string; type: QuestionType; config: BaseConfig }, ): string { try { const pushed = sessions.pushQuestion(args.session_id, args.type, args.config); - return `Question pushed: ${pushed.question_id} -Type: ${args.type} -Use get_next_answer(session_id, block=true) to wait for the user's response.`; + return `Question pushed: ${pushed.question_id}\nType: ${args.type}\nUse get_next_answer(session_id, block=true) to wait for the user's response.`; } catch (error) { return `Failed to push question: ${extractErrorMessage(error)}`; } @@ -81,12 +116,7 @@ The question will appear in the browser for the user to answer.`, args: { session_id: tool.schema.string().describe("Session ID from start_session"), type: tool.schema.enum(QUESTION_TYPE_ENUM).describe("Question type"), - config: tool.schema - .looseObject({ - question: tool.schema.string().optional(), - context: tool.schema.string().optional(), - }) - .describe("Question configuration (varies by type)"), + config: pushQuestionConfigSchema, }, execute: async (args) => executePushQuestion(sessions, args), }); diff --git a/src/tools/octto/session.ts b/src/tools/octto/session.ts index e3806be0..426848f8 100644 --- a/src/tools/octto/session.ts +++ b/src/tools/octto/session.ts @@ -64,6 +64,35 @@ const sessionQuestionSchema = tool.schema .looseObject({ question: tool.schema.string().optional(), context: tool.schema.string().optional(), + options: tool.schema.any().optional(), + min: tool.schema.any().optional(), + max: tool.schema.any().optional(), + step: tool.schema.any().optional(), + recommended: tool.schema.any().optional(), + allowOther: tool.schema.any().optional(), + allowFeedback: tool.schema.any().optional(), + allowCancel: tool.schema.any().optional(), + defaultValue: tool.schema.any().optional(), + labels: tool.schema.any().optional(), + emojis: tool.schema.any().optional(), + yesLabel: tool.schema.any().optional(), + noLabel: tool.schema.any().optional(), + before: tool.schema.any().optional(), + after: tool.schema.any().optional(), + filePath: tool.schema.any().optional(), + language: tool.schema.any().optional(), + content: tool.schema.any().optional(), + sections: tool.schema.any().optional(), + markdown: tool.schema.any().optional(), + placeholder: tool.schema.any().optional(), + multiline: tool.schema.any().optional(), + minLength: tool.schema.any().optional(), + maxLength: tool.schema.any().optional(), + accept: tool.schema.any().optional(), + multiple: tool.schema.any().optional(), + maxImages: tool.schema.any().optional(), + maxFiles: tool.schema.any().optional(), + maxSize: tool.schema.any().optional(), }) .describe("Question config (varies by type)"), }),