---
url: /wave-agent/specs/ui/ask-user-tool.md
description: '`AskUserQuestion` 结构化提问工具：单选/多选选项、多问题循环轮播、键盘可达与选项区折叠'
---

# 功能规格说明：AskUserQuestion 工具

**创建日期**：2026-01-19

## 用户场景与测试 *（必填）*

### 用户故事：澄清模糊指令（优先级：P1）

作为 AI agent，当我收到用户的模糊指令时，我希望使用结构化的多选界面提出澄清问题，以便我可以在正确理解用户意图的情况下继续。

**为什么是这个优先级**：这是工具的核心价值。它防止 agent 做出错误假设并提高交互质量。

**独立测试**：可以通过给 agent 一个模糊任务（如"重构这个函数"）并验证它使用 `AskUserQuestion` 工具询问期望的重构模式而不是猜测来测试。

**验收场景**：

1. **假设** agent 正在处理任务，**当**遇到模糊性时，**则**它必须调用 `AskUserQuestion` 工具并提供相关选项。
2. **假设**用户看到问题，**当**用户选择选项时，**则** agent 必须收到答案并基于该选择继续任务。

***

### 用户故事：在实现方案之间选择（优先级：P2）

作为 AI agent，当有多种有效方式实现功能时，我希望向用户展示这些选项，以便他们可以决定哪种方案最适合其需求。

**为什么是这个优先级**：使用户能够在不必自己编写代码的情况下做出架构或设计决策。

**独立测试**：可以通过要求 agent "添加认证" 并验证它询问是使用 JWT、OAuth2 还是基于 Session 的认证来测试。

**验收场景**：

1. **假设** agent 处于规划或实施阶段，**当**识别出多种方案时，**则**它应该使用 `AskUserQuestion` 让用户选择。
2. **假设**选项中包含推荐项，**则** agent 应该在标签中标记为 "(Recommended)"。

***

### 用户故事：在计划模式中收集需求（优先级：P2）

作为计划模式中的 AI agent，我希望在最终确定计划之前向用户询问缺失的需求，确保计划准确并获得批准。

**为什么是这个优先级**：确保规划阶段是交互式的并产生高质量计划。

**独立测试**：可以通过带着模糊请求进入计划模式并验证 agent 在调用 `ExitPlanMode` 之前提出问题来测试。

**验收场景**：

1. **假设** agent 处于计划模式，**当**需求缺失时，**则**它必须使用 `AskUserQuestion` 来收集。
2. **假设** agent 准备最终确定计划，**则**它不得将 `AskUserQuestion` 用于计划批准（必须使用 `ExitPlanMode` 代替）。

***

### 用户故事：选项组键盘可达（优先级：P1）

作为用户，当回答 agent 的提问时，我希望选项（单选/多选）逐个成为 Tab 可达的焦点目标、用空格选中、全部答完后用 Enter 提交，以便纯键盘用户无需鼠标即可选择任意选项并提交回答。

**为什么是这个优先级**：对齐 Claude IDE/桌面端的 AskUserQuestion 实际交互（`permissionRequestContainer` 选项每个 `tabIndex={0}`，选项只响应 Enter/Space，方向键不参与选项导航，输入框内按键 `stopPropagation` 保持文本编辑语义）。曾实现 roving tabindex（组内单一 Tab stop + 方向键移动），与 CC 不一致，且方向键导航与「其他」输入框内左右键编辑光标冲突（输入框里按左右键会跳走焦点）。改后选项间用 Tab 遍历，输入框内方向键回归光标语义。Enter 键分工按交互设计师裁决（2026-08-27 a4466e21）：Enter 仅用于全部题目答完后的提交、由空格负责选中选项——焦点在任意选项上时同样生效（答完按 Enter 即提交、未答完无动作），不把 Enter 当作选中键，避免与提交语义冲突（单选答完后重复按 Enter 若只做「选中」会变成无意义的 no-op，见场景 8/10 与「多问题循环轮播」）。

**独立测试**：触发 AskUserQuestion 弹窗，Tab 依次经过每个选项（含「其他」）、空格选中；在「其他」输入框内按方向键/空格确认光标移动与文本输入不受影响、不改变选中状态；Enter 仅用于提交（全部题目答完时提交，未答完无动作，见「多问题循环轮播」）。

**验收场景**：

1. **假设** 选项组渲染（单选或多选），**当** Tab 遍历弹窗时，**则** 每个选项（含「其他」）都是独立 Tab stop（`tabIndex={0}`），Tab 依次经过全部选项，不跳过任何一项。
2. **假设** 焦点在某个选项上，**当** 用户按方向键（↑/↓/←/→）时，**则** 焦点不移动（方向键不参与选项导航）。
3. **假设** 焦点在某个选项上，**当** 用户按空格时，**则** 单选=替换为该选项；多选=切换该选项的勾选状态；**当** 用户按 Enter 时，**则** Enter 仅用于提交（全部题目已答完时提交、未答完无动作），不改变选中状态（见场景 8/10 与「多问题循环轮播」）。
4. **假设** 「其他」选项被空格选中，**则** 输入框自动聚焦可直接输入（保留现有自动聚焦行为）；未选中时提示「输入自定义回答...」照常显示。
5. **假设** 焦点在「其他」输入框内，**当** 用户按方向键或空格时，**则** 均为输入框自身的文本编辑行为（光标移动/输入空格），不触发选项选择或任何弹窗级快捷键（输入框内按键不冒泡到弹窗，对齐 CC 的 `stopPropagation`）。
6. **假设** 焦点在选项或「其他」输入框上，**当** 用户按 Enter（非 Shift）且非输入法合成中时，**则** Enter 仅用于提交——全部题目已答完时提交，未答完时无动作，不负责切换题目、不改变选中状态（见「多问题循环轮播」场景 8/10）。
7. **假设** 用户回答完当前问题后切换到下一题（点击「下一个」或按 `→`），**当** 题目切换完成时，**则** 焦点落在新题目第一个选项（或当前已选中的选项）上，焦点不会因按钮状态变化而丢失到弹窗外。

***

### 用户故事：多问题循环轮播（优先级：P1）

作为用户，当 agent 在一次 AskUserQuestion 中提出多个问题时，我希望弹窗底部固定三个按钮（`[上一个] [下一个] [提交回答]`），题目可循环轮播切换，以便我随时前后浏览、修改任意一题答案，清楚还剩多少问题要回答，不会因未答完而困惑为什么无法提交。

**为什么是这个优先级**：固定三按钮 + 循环轮播让多问题问卷的浏览路径不受边界限制（首页可「上一个」回看末题、末题可「下一个」回到首题），进度与答题状态一目了然；替换之前「向导式」（第一题仅「下一个」、中间「上一个/下一个」、最后一题才出现「提交回答」）的阶段性按钮组合——向导式在中间题不显示「提交回答」，用户想提交时还得先走到最后一题，循环轮播固定三按钮把「提交」入口始终可见。

**独立测试**：触发包含 3 个问题的 AskUserQuestion，逐题观察底部三按钮与启用状态；首页点「上一个」验证跳到末题、末题点「下一个」验证跳回首题；答完部分题目验证进度条对应段着色、提交按钮保持禁用；全部答完后验证「提交回答」可用并回车提交。

**验收场景**：

1. **假设** 弹窗包含多个问题，**当** 展示任意一题时，**则** 底部导航固定显示「上一个」「下一个」「提交回答」三个按钮（「提交回答」为主按钮、最右侧），不随题目位置隐藏或新增按钮。
2. **假设** 用户在第一题，**当** 点击「上一个」时，**则** 循环跳转到最后一题，且各题已填答案保留。
3. **假设** 用户在最后一题，**当** 点击「下一个」时，**则** 循环跳转到第一题，且各题已填答案保留。
4. **假设** 用户处于任意一题，**当** 按 `←` 或 `→` 方向键时，**则** 循环切换上一题 / 下一题（与按钮行为一致）。
5. **假设** 题目进度条显示（每段代表一题），**当** 用户点击某一段时，**则** 直接定位到对应题目，不要求逐题翻页。
6. **假设** 用户已答完部分题目，**当** 查看进度条时，**则** 已答完的题目段标记为完成状态（着色），与当前浏览到第几题无关；回看已答题目时已答段保持完成色。
7. **假设** 弹窗存在未作答的题目，**当** 用户点击「提交回答」时，**则** 按钮保持禁用（不可点击）；「上一个」「下一个」不受作答状态影响，始终可点。
8. **假设** 全部题目均已回答，**当** 用户点击「提交回答」或按 Enter 时，**则** 一次性提交所有问题的答案并关闭弹窗。
9. **假设** 弹窗只有一个问题，**当** 查看底部导航时，**则** 只显示「提交回答」，无「上一个/下一个」按钮。
10. **假设** 用户未答完所有题目，**当** 按 Enter 时，**则** 无动作（Enter 仅用于全部答完后的提交，不负责切换题目）。
11. **假设** 主按钮「提交回答」可提交时，**则** 按钮上显示回车键提示标注（`⏎`），提示可用回车提交。
12. **假设** 弹窗显示底部导航按钮，**当** 用户按 Tab 遍历按钮，**则** 焦点按视觉从左到右顺序遍历：「上一个」→「下一个」→ 主按钮「提交回答」（主按钮最后）；Shift+Tab 反向（主按钮最先），Tab 陷阱仍限于弹窗内（与确认操作面板按钮布局一致，见 `confirm-ui.md`）。

***

### 用户故事：提问弹窗选项区可折叠（优先级：P1）

作为用户，当 AskUserQuestion 弹窗在窄窗口（小屏笔记本）里占住面板高度、把消息列表挤到几乎看不见时，我希望把选项区折叠起来、只留题干与底部按钮栏，以便我先回看消息列表再展开作答，而不必关闭弹窗（关闭等于跳过这次提问）。

**为什么是这个优先级**：桌面端弹窗高度上限为 `min(70vh, 560px)`，768px 高的窗口上可占面板高度约 70%，消息列表被挤到几乎不可见——这是用户实际反馈的问题；折叠是不动高度上限、不动布局的逃生口。范围**只限 AskUserQuestion**：折叠收起的是「选项交互区」，判断所需的上下文（题干 chip、题号进度、底部三按钮）留在原位，因此不会造成盲判；其余确认类型（Bash / Write / Edit / MCP / 计划审批）的卡体就是审批依据（命令行、diff、参数），收起它等于让用户在看不到内容的情况下批准，故不给折叠键。此范围与 Claude 一致——claude.ai 的确认卡是同一个壳（`.epitaxy-approval-card`），但折叠（`data-collapsed` + caret）只挂在 AskUserQuestion 卡上，通用工具审批卡没有折叠键。

**独立测试**：在窄窗口触发多题 AskUserQuestion，点卡头折叠键，验证选项区收起、题干与进度条、底部三按钮均在原位可点、消息列表完整可见；再点一次展开，验证已勾选选项、`其他` 输入内容、当前题号与选项区滚动位置均未丢失；对 Bash/Write/Edit 确认验证头部不出现折叠键。

**验收场景**：

1. **假设** AskUserQuestion 弹窗显示，**当** 用户查看弹窗头部尾部按钮组（与关闭键同一组），**则** 其中有一个折叠键，展开态下图标与 aria-label 表示「折叠」语义；**当** 确认类型为 Bash / Write / Edit / MCP / 计划审批时，**则** 头部不渲染该键。
2. **假设** 弹窗处于展开态，**当** 用户点击折叠键，**则** 选项区（全部选项与 `其他` 输入框）整体收起，题干 chip 与题号进度条仍可见，底部按钮栏（「上一个 / 下一个 / 提交回答」）保持原位可点，弹窗高度随之收缩、消息列表随之拿回空间。
3. **假设** 弹窗处于折叠态，**当** 用户再次点击折叠键（或 Tab 聚焦后按 Enter/空格），**则** 选项区恢复展开，折叠前已作出的选择不丢失：选项勾选、`其他` 输入内容、当前题号、以及折叠前选项区的滚动位置。
4. **假设** 弹窗处于折叠态，**当** 用户查看折叠键，**则** 其图标与 aria-label 表示「展开」语义，且该键在弹窗焦点圈循环覆盖范围内（见 `confirm-ui.md`「确认弹窗焦点圈」场景 2）。
5. **假设** 当前提问被解决后队列中的下一个确认出现，或弹窗因切换会话/会话重挂载，**当** 新确认显示时，**则** 折叠态重置为展开——折叠态随确认实例生灭，不跨确认、不跨会话记忆。
6. **假设** 弹窗处于折叠态，**当** 用户按 Esc 或点关闭键，**则** 行为与展开态完全一致（拒绝关闭）。
7. **假设** 弹窗处于折叠态，**当** 用户按 Tab 遍历，**则** 已收起的选项与 `其他` 输入框不在循环内，循环只覆盖标题行按钮与底部三按钮（与 `confirm-ui.md`「确认弹窗焦点圈」的「被折叠收起的元素不在循环内」一致）。

***

### 用户故事：题干超长时在题干区域内滚动（webview）（优先级：P2）

作为用户，当 agent 的问题描述（题干）字数很多、几乎占满确认弹窗时，我希望题干在它自己的区域内滚动，以便选项区仍然留在屏上、能直接看到并点选，而不必先滚过整段题干、或面对一片空的选项区。

**为什么是这个优先级**：题干此前没有高度上限，而弹窗的高度配额又全部由选项区承担 ⇒ 题干越长选项区越矮；字数极多时选项区被压成零高度，选项虽仍在 DOM 里，但既看不见也点不到（零高度的滚动容器无法交互），滚弹窗主体也救不回来。这直接违背下方边界情况的「选项列表固定在确认 UI 底部始终可见」。另两个备选口径已评估并否掉：让题干与选项整体一起滚（滚动容器上移到两者外层）只是把「看不见选项」换成「要滚过整段题干才见到第一个选项」，首屏仍然只有题干；截断/省略题干则让用户读不全判断依据。

**独立测试**：触发一条字数极多的题干 + 多个选项的 AskUserQuestion，在窄栏宽度、小屏笔记本高度、大窗口三档视口下验证：题干停在高度上限内并可滚到全文末尾、选项区至少露出一整行且可点选；多题场景切题后题干滚动位置回到顶部。

**验收场景**：

1. **假设** 题干内容不超过高度上限，**当** 弹窗显示时，**则** 题干不出现滚动条，弹窗布局与改动前一致。
2. **假设** 题干内容超过高度上限，**当** 弹窗显示时，**则** 题干在自身区域内纵向滚动（可滚到全文最后一行，不截断、不出现省略号），选项区不被压缩掉、仍至少露出一整行可点选的选项。
3. **假设** 题干与选项区都处于可滚动状态，**当** 用户在题干上滚动时，**则** 只有题干滚动、选项区滚动位置不变；反之滚选项区时题干不动。
4. **假设** 一次提问包含多个问题，**当** 用户切换到另一题（「上一个/下一个」或点进度条）时，**则** 新题目的题干从顶部开始显示，不沿用上一题的滚动位置。
5. **假设** 视口极小（题干上限与选项区所需高度之和超过弹窗可用高度），**当** 弹窗显示时，**则** 题干可被继续压缩以让出高度、选项区仍保证一行可见，超出部分由弹窗主体滚动承担——降级路径，任何情况下都不出现零高度、不可交互的选项区。

***

### 用户故事：选项缺 label 时返回输入错误而不是弹窗（优先级：P1）

作为用户，当模型漏写某个选项的 `label`（只写了 `description`）时，我希望这次调用被直接退回去让模型重试，以便我看到的是正常的提问卡片，而不是一个只有描述文字、点不出所以然的空白选项。

**为什么是这个优先级**：`label` 在声明里是必填，但模型仍会漏写；确认卡直接输出 `option.label`，漏字段就是一个没有标题的空白选项，用户既读不懂也没法判断该选什么，错误发生在离原因最远的一层。

**独立测试**：调用工具传 `options: [{ label: "A" }, { description: "只有描述" }]`，确认返回失败结果、错误文案点明 `questions[0].options[1]`，且确认弹窗未出现。

**验收场景**：

1. **假设** 某个选项缺 `label` 或 `label` 是空白字符串，**当** 工具执行时，**则** 返回输入校验错误（文案点明 `questions[i].options[j]` 与实收值，多个不合格项一次列全）且不弹出确认卡片，模型据该错误重试。

***

### 边界情况

* **用户提供自定义输入**："Other" 选项的自定义输入框在用户**选中 "Other" 后才出现**；未选中时不显示输入框。
  * 用户选中 "Other" 后，输入框出现，输入的文本作为该问题的答案返回。
  * 用户**选中 "Other"（键盘空格/Enter 或鼠标点击）后，输入框自动聚焦**，可直接开始输入；切换题目时，若新题目此前已选中 "Other"，不抢占输入框焦点（焦点落回选项组的第一个/已选中选项，用户需要时再 Tab 进入输入框）。
  * **未选中 "Other" 时**，选项下方以灰色显示「输入自定义回答...」提示（复用普通选项的 description 样式），说明该选项可输入自定义内容；选中后提示由输入框替代。
  * 用户取消选择 "Other"（单选改选其他选项 / 多选取消勾选）时，输入框隐藏，但**已输入的内容保留**；再次选中 "Other" 时内容恢复，不丢失。
  * 选中 "Other" 但未输入任何内容时，不算有效答案，提交/下一步仍被禁用。
  * 多选模式行为相同：勾选 "Other" 显示输入框，取消勾选隐藏输入框（内容保留）。
* **多个问题**：一次最多 4 个问题。弹窗一次只显示一个问题，底部固定「上一个/下一个/提交回答」三按钮（循环轮播：首页「上一个」跳末题、末题「下一个」回首题，`←`/`→` 方向键等效），顶部显示可点击的分段进度条（每段代表一题，已答段着色为完成状态，点击直接定位到对应题）；所有问题都有有效答案后「提交回答」才可用（按钮带 `⏎` 提示），一次性提交全部答案。单问题（仅 1 题）只显示「提交回答」。
* **键盘导航不改变答案**：Tab 聚焦弹窗内任意元素（包括自定义输入框）只应移动焦点，不得改变已选答案；选中/取消选项必须来自明确的用户操作（点击或空格），Enter 只负责全部题目答完后的提交。
* **多选答案**：多个选中选项如何返回？应该以逗号分隔的字符串或类似的结构化格式返回。
* **工具拒绝**：用户关掉提问弹窗（点右上角关闭键、按 Esc）时，提问工具收到拒绝**且本轮立即中止**——模型不会拿「用户拒绝了操作」当输入继续往下做事，会话回到空闲等用户重新发话。这条与 `confirm-ui.md`「基本工具确认」场景 2 / 5 是同一条语义，四端（桌面端 / VS Code / JetBrains / 远控页）必须一致：远控页曾经只回一条 `deny` 不中断，于是手机上关掉弹窗后模型继续、桌面端关掉就停。只有**带反馈的拒绝**（先给出说明再拒绝，例如计划模式的「不，现在开始实现」）才是「只拒绝该工具、模型按反馈继续或再次提问」。
* **选项与内容的关系**：选项列表固定在确认 UI 底部始终可见，方向键不参与选项导航（选项间用 Tab 遍历），与上方内容区的滚动完全无关（内容区超高时的 PgUp/PgDn 滚动见 `confirm-ui.md`「确认详情超高时滚动」）。题干本身超长时由题干区域自己滚动，选项区不被压缩到不可见（见「题干超长时在题干区域内滚动」）。
* **选项 Tab 遍历与选中状态**：所有选项（含「其他」）均为独立 Tab stop，方向键不改变焦点也不改变已选答案；「其他」选中后输入框自动聚焦；输入框内按键（方向键/空格/Enter 等，除 Meta/Ctrl 组合）不冒泡到弹窗，保持文本编辑语义。
* **折叠后还能作答吗？** 不能——折叠是「先回看消息列表」的暂存态，不是提交态：选项与 `其他` 输入框一并收起，作答前必须展开。折叠不改变任何提交校验（未答完的题目在展开后仍不可提交，见场景 7），也不代表跳过或拒绝。
* **为什么只有 AskUserQuestion 能折叠？** 折叠收起的是选项交互区，上下文（题干、题号进度）留在原位，不产生盲判风险；其余确认类型的卡体就是审批依据（命令行、diff、参数），收起它会让用户在看不到内容的情况下批准。所以折叠键只挂在提问弹窗上，不追求「所有确认类型都有」的统一。
* **折叠态会不会被误当成新确认？** 不会：折叠态下标题、题干与底部三按钮仍在原位，只有选项区收起；新确认（队列下一项）总是以展开态出现。

## 假设

* 渲染这些问题的 UI 组件已存在或将作为 `code` 包的一部分实现。
* `AskUserQuestion` 工具将被视为需要用户交互的"受限"工具，类似于 `Bash` 或 `Write`。
* "Other" 选项由 UI 自动处理，agent 不需要在工具的输入模式中明确定义。
