From 3f219aed98d93158aefce01bb51ed12dcb4711a1 Mon Sep 17 00:00:00 2001
From: centdix <40307056+centdix@users.noreply.github.com>
Date: Mon, 25 May 2026 16:52:02 +0200
Subject: [PATCH] feat(ai-chat): expand chat question answers (#9310)
---
.../chat/AskUserQuestionDisplay.svelte | 44 +++++++++++
.../copilot/chat/global/core.test.ts | 75 ++++++++++++++++++-
.../components/copilot/chat/global/core.ts | 10 +--
3 files changed, 123 insertions(+), 6 deletions(-)
diff --git a/frontend/src/lib/components/copilot/chat/AskUserQuestionDisplay.svelte b/frontend/src/lib/components/copilot/chat/AskUserQuestionDisplay.svelte
index 29481a04f2..e59ed4b513 100644
--- a/frontend/src/lib/components/copilot/chat/AskUserQuestionDisplay.svelte
+++ b/frontend/src/lib/components/copilot/chat/AskUserQuestionDisplay.svelte
@@ -2,6 +2,7 @@
import { onMount, tick } from 'svelte'
import { CircleHelp } from 'lucide-svelte'
import Button from '$lib/components/common/button/Button.svelte'
+ import TextInput from '$lib/components/text_input/TextInput.svelte'
import { aiChatManager } from './AIChatManager.svelte'
import type { UserQuestionDisplay } from './shared'
@@ -13,6 +14,8 @@
let { toolCallId, userQuestion }: Props = $props()
let choiceButtons = $state<(HTMLButtonElement | undefined)[]>([])
+ let customAnswer = $state('')
+ let canSubmitCustomAnswer = $derived(customAnswer.trim().length > 0)
onMount(() => {
if (userQuestion.choices.length === 0) {
@@ -32,6 +35,15 @@
aiChatManager.handleUserQuestionAnswer(toolCallId, choice)
}
+ function submitCustomAnswer() {
+ const answer = customAnswer.trim()
+ if (!answer) {
+ return
+ }
+
+ aiChatManager.handleUserQuestionAnswer(toolCallId, answer)
+ }
+
function handleChoiceKeydown(event: KeyboardEvent, choice: string, index: number) {
if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {
event.preventDefault()
@@ -53,6 +65,16 @@
selectChoice(choice)
}
}
+
+ function handleCustomAnswerKeydown(event: KeyboardEvent) {
+ if (event.key !== 'Enter') {
+ return
+ }
+
+ event.preventDefault()
+ event.stopPropagation()
+ submitCustomAnswer()
+ }
{/each}
+
+
+
+
+
diff --git a/frontend/src/lib/components/copilot/chat/global/core.test.ts b/frontend/src/lib/components/copilot/chat/global/core.test.ts
index 7dce5eedad..6a7a9c8a9c 100644
--- a/frontend/src/lib/components/copilot/chat/global/core.test.ts
+++ b/frontend/src/lib/components/copilot/chat/global/core.test.ts
@@ -1104,7 +1104,7 @@ describe('global AI tools', () => {
expect(item.value.value).toBeUndefined()
})
- it('asks the user a multiple-choice question and returns the selected answer', async () => {
+ it('asks the user a question and returns the selected answer', async () => {
const callbacks: ToolCallbacks = {
setToolStatus: vi.fn(),
removeToolStatus: vi.fn(),
@@ -1138,6 +1138,79 @@ describe('global AI tools', () => {
})
)
})
+
+ it('allows up to ten proposed answers', async () => {
+ const choices = Array.from({ length: 10 }, (_, index) => `choice-${index + 1}`)
+ const callbacks: ToolCallbacks = {
+ setToolStatus: vi.fn(),
+ removeToolStatus: vi.fn(),
+ requestUserQuestion: vi.fn(async (_toolId, question) => question.choices[9])
+ }
+
+ const raw = await callGlobalTool(
+ 'askUserQuestion',
+ {
+ question: 'Which option should be used?',
+ choices
+ },
+ callbacks
+ )
+
+ expect(raw).toBe('choice-10')
+ expect(callbacks.requestUserQuestion).toHaveBeenCalledWith(
+ 'test-askUserQuestion',
+ expect.objectContaining({
+ choices
+ })
+ )
+ })
+
+ it('rejects more than ten proposed answers', async () => {
+ const callbacks: ToolCallbacks = {
+ setToolStatus: vi.fn(),
+ removeToolStatus: vi.fn(),
+ requestUserQuestion: vi.fn()
+ }
+
+ await expect(
+ callGlobalTool(
+ 'askUserQuestion',
+ {
+ question: 'Which option should be used?',
+ choices: Array.from({ length: 11 }, (_, index) => `choice-${index + 1}`)
+ },
+ callbacks
+ )
+ ).rejects.toThrow()
+ expect(callbacks.requestUserQuestion).not.toHaveBeenCalled()
+ })
+
+ it('returns a custom answer that is not one of the proposed answers', async () => {
+ const callbacks: ToolCallbacks = {
+ setToolStatus: vi.fn(),
+ removeToolStatus: vi.fn(),
+ requestUserQuestion: vi.fn(async () => 'use deno instead')
+ }
+
+ const raw = await callGlobalTool(
+ 'askUserQuestion',
+ {
+ question: 'Which script language should be used?',
+ choices: ['bun', 'python3']
+ },
+ callbacks
+ )
+
+ expect(raw).toBe('use deno instead')
+ expect(callbacks.setToolStatus).toHaveBeenLastCalledWith(
+ 'test-askUserQuestion',
+ expect.objectContaining({
+ content: 'User answered question: use deno instead',
+ result: 'use deno instead',
+ userQuestion: expect.objectContaining({ selectedChoice: 'use deno instead' })
+ })
+ )
+ })
})
describe('prepareGlobalSystemMessage', () => {
diff --git a/frontend/src/lib/components/copilot/chat/global/core.ts b/frontend/src/lib/components/copilot/chat/global/core.ts
index 11c94e677b..10d055938c 100644
--- a/frontend/src/lib/components/copilot/chat/global/core.ts
+++ b/frontend/src/lib/components/copilot/chat/global/core.ts
@@ -134,10 +134,10 @@ const askUserQuestionSchema = z.object({
.min(1)
.describe('The concise question to show to the user before continuing.'),
choices: z
- .array(z.string().min(1).describe('Short answer text shown to the user and returned as-is.'))
+ .array(z.string().min(1).describe('Proposed answer text shown to the user and returned as-is.'))
.min(2)
- .max(6)
- .describe('Two to six mutually exclusive answer strings.')
+ .max(10)
+ .describe('Two to ten mutually exclusive proposed answer strings.')
})
const listWorkspaceItemsSchema = z.object({
@@ -503,7 +503,7 @@ Rules:
- Variable values are never readable. For secrets, create a secret variable and reference it from resources as "$var:path/to/variable".
- Use search_resource_types before write_resource.
- Use get_instructions before writing scripts, flows, resources, or apps. For scripts, pass the target language.
-- Ask the user when a required decision is ambiguous.
+- When a required decision is ambiguous, use askUserQuestion with two to ten clear proposed answer strings instead of guessing. The user can also type a custom answer when none of the proposed answers fit.
- Keep context targeted.
Flows:
@@ -1275,7 +1275,7 @@ export const globalTools: Tool<{}>[] = [
def: createToolDef(
askUserQuestionSchema,
'askUserQuestion',
- 'Ask the user a multiple-choice question.'
+ 'Ask the user a question with proposed answers and wait for their selected or custom answer before continuing.'
),
fn: async ({ args, toolId, toolCallbacks }) => {
const parsed = askUserQuestionSchema.parse(args)