feat(ai-chat): expand chat question answers (#9310)

This commit is contained in:
centdix
2026-05-25 14:52:02 +00:00
committed by GitHub
parent 577a730e90
commit 3f219aed98
3 changed files with 123 additions and 6 deletions
@@ -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()
}
</script>
<div
@@ -81,5 +103,27 @@
</span>
</Button>
{/each}
<div class="flex min-w-0 gap-2 pt-1">
<TextInput
bind:value={customAnswer}
class="min-w-0 flex-1"
size="sm"
inputProps={{
'aria-label': 'Custom answer',
placeholder: 'Custom answer',
onkeydown: handleCustomAnswerKeydown
}}
/>
<Button
variant="default"
unifiedSize="sm"
disabled={!canSubmitCustomAnswer}
onClick={submitCustomAnswer}
btnClasses="shrink-0"
>
Send
</Button>
</div>
</div>
</div>
@@ -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', () => {
@@ -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)