---
url: /wave-agent/specs/ui/confirm-ui.md
description: 工具权限确认弹窗：按钮布局与滚动、确认队列、持久权限模式与 Bypass 切换、模态焦点圈
---

# 功能规格说明：确认 UI

**创建日期**：2026-04-06

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

### 用户故事：基本工具确认（优先级：P1）

作为用户，我希望在 agent 执行敏感操作之前被提示确认，以便我可以审查并批准或拒绝每个操作。

**为什么是这个优先级**：这是保护用户免受 agent 意外或潜在有害操作的核心功能。

**独立测试**：可以通过触发受限工具（Bash、Write、Edit）、验证确认 UI 出现并显示正确详情以及选择不同选项来全面测试。

**验收场景**：

1. **假设** agent 尝试运行 `npm install`，**当**确认 UI 出现时，**则**显示工具名称 "Bash" 和命令详情，并提供允许、自动接受或提供替代输入的选项。
2. **假设**确认已显示，**当**用户按下 ESC 或点弹窗右上角的关闭键（未填写任何反馈）时，**则**操作被取消、agent 被通知拒绝，**并且本轮立即中止**——会话回到空闲，模型不会带着「用户拒绝了」这条结果继续往下做。**带反馈的拒绝是另一条语义**：用户在「提供反馈」里输入文本后发送，则只拒绝这一次工具调用、模型收到这段反馈后继续。判据是「有没有给出反馈」，不是「点了哪个键」。**依据**：Claude Code 的取消（Esc / Ctrl+C / 无反馈关闭）走 `cancelAndAbort` 直接 `abortController.abort()` 结束本轮，只有带 feedback 的拒绝才把拒绝结果交回模型继续；其 VS Code 插件的取消帧同样带 `interrupt: true`，中断由 CLI 侧执行。
3. **假设**确认已显示，**当**用户在替代选项上聚焦时输入文本，**则**文本被捕获为 agent 的反馈。
4. **假设** agent 尝试 `Write`/`Edit` 工作目录之外的文件，**当**确认 UI 出现时（交互式 CLI 或三端 GUI），**则**在允许与自动接受之外额外提供"允许本会话编辑 `<目录名>/`"选项，选择它把该目录加入当前会话的安全区域且不写任何配置文件；目标文件在安全区域内时不出现该选项。
5. **假设** 同一次确认在**任一端**被关闭（桌面端 / VS Code / JetBrains / 远控页，含 `AskUserQuestion` 提问弹窗），**当**用户点关闭键或按 Esc 而未填反馈时，**则**四端结果一致：本轮被中止、会话回到空闲，工具块上留下一条拒绝记录。远控页（`RemoteControlCliHost` 与 daemon 的 `AgentBridge` 两个宿主）此前只回一条 `permissionResponse {behavior:"deny"}`、不中断，于是手机上关掉提问弹窗后模型继续往下做、桌面端关掉就停——这是必须消除的端间偏差，不是「远端少一个可选能力」。

***

### 用户故事：持久权限模式（优先级：P1）

作为用户，我希望为特定操作设置持久权限，这样我就不必反复批准相同的操作。

**为什么是这个优先级**：通过减少受信任操作的重复确认来提高工作流效率。

**独立测试**：可以通过为命令选择"不再询问"，然后再次触发相同命令并验证不出现确认来测试。

**验收场景**：

1. **假设** agent 尝试运行 `mkdir -p src/components`，**当**用户选择"是，不再询问：mkdir"时，**则**后续 mkdir 命令在此会话中不再提示确认。
2. **假设** agent 尝试运行带有建议前缀的 Bash 命令，**当**用户选择带有前缀的自动接受选项时，**则**所有匹配该前缀的命令被自动批准。

***

### 用户故事：向用户提问流程（优先级：P2）

作为用户，我希望通过交互式 UI 回答 agent 的澄清问题，以便我可以在 agent 需要更多上下文时提供指导。

**为什么是这个优先级**：这使 agent 能够为复杂或模糊的任务收集必要信息。

**独立测试**：可以通过触发 AskUserQuestion 工具并验证多问题流程、单选/多选选项和"Other"文本输入来测试。

**验收场景**：

1. **假设** agent 提出带有多选选项的问题，**当**问题 UI 出现时，**则**用户可以使用方向键导航、空格键选择选项（多选），Enter 提交。
2. **假设**多问题流程，**当**用户回答第一个问题并按下 Enter 时，**则** UI 过渡到下一个问题，同时保留之前的答案。
3. **假设**问题选择了"Other"，**当**用户输入自定义文本时，**则**文本被捕获并提交为答案。

***

### 用户故事：计划模式审批（优先级：P2）

作为用户，我希望在 agent 进行更改之前审查并批准执行计划，以便我可以确保计划符合我的意图。

**为什么是这个优先级**：这在提交实施之前为 agent 在计划模式中的操作提供控制。

**独立测试**：可以通过完成计划模式会话并验证退出计划确认出现并显示计划内容和审批选项来测试。

**验收场景**：

1. **假设** agent 已完成规划，**当** exit_plan_mode 工具被触发时，**则**确认 UI 显示计划内容（以 Markdown 渲染，与助手消息同一渲染器：列表为 `•`/序号、表格为单线框表格、粗体/斜体/链接按样式呈现、标题保留彩色 `#` 前缀、代码块带围栏与高亮——裸 Markdown 语法如 `- 项`、`**粗体**`、`| 表格 |` 不再原样出现），并提供手动批准（切换到默认模式）、自动接受编辑（切换到 acceptEdits 模式）或 bypass permissions（切换到 bypassPermissions 模式）的选项。
2. **假设**退出计划确认已显示，**当**用户选择自动接受编辑选项时，**则**权限模式更改为 acceptEdits，对话上下文保持不变。
3. **假设**退出计划确认已显示，**当**用户选择 bypass permissions 选项时，**则**计划被批准执行，且权限模式更改为 bypassPermissions（CLI 与插件端 webview 均提供该选项）。

***

### 用户故事：确认操作面板按钮布局（优先级：P1）

作为用户，当确认弹窗显示时，我希望操作按钮按主次从右到左横向排列（最重要的操作在最右侧），且 Tab 顺序与视觉顺序一致、从左到右（最次按钮 → 次按钮 → 主按钮，即主按钮最后），以便我不用寻找就能把主操作放在同一位置、Tab 遍历方向与眼睛移动方向一致（安全侧操作先可达、批准类操作收尾确认）。

**为什么是这个优先级**：按钮是确认弹窗的核心操作区，布局规则影响每次确认的操作效率与键盘可达性；视觉上与主流桌面交互（macOS/Windows 对话框主按钮在右）一致，键盘顺序对齐 Claude 的确认 UI——claude.ai 工具确认卡与桌面原生授权弹窗均为「次按钮（Deny/Cancel）先 Tab、主按钮（Allow once/Allow）最后、默认焦点落在次按钮（安全侧）」，DOM 顺序=视觉顺序，无方向反转。

**独立测试**：触发 Bash/Write/Edit 权限确认，验证按钮横向排列且主按钮「批准并继续」在最右侧；按 Tab 验证焦点顺序与视觉一致、主按钮最后（最次 → 次 → 主）；进入反馈流程验证「取消/发送反馈」布局；验证各按钮启用/禁用状态。

**验收场景**：

1. **假设** 确认弹窗显示（Bash/Write/Edit/MCP/计划审批），**当** 用户查看操作区，**则** 按钮横向排列（非纵向堆叠），主按钮「批准并继续」位于最右侧，次要操作依次向左，如 `[提供反馈] [自动接受/跳过确认] [批准并继续]`（最次、次、主）；越界 `Write`/`Edit` 的「是，且允许本会话编辑 `<目录名>/`」（见 `docs/specs/core/tool-permission-system.md`）同属次要操作。
2. **假设** 确认弹窗显示，**当** 用户按 Tab 遍历按钮，**则** 焦点按视觉从左到右顺序遍历：`[提供反馈]` → `[自动接受/跳过确认]`（越界时含「是，且允许本会话编辑 `<目录名>/`」）→ 主按钮「批准并继续」（主按钮最后）；Shift+Tab 反向（主按钮最先），Tab 陷阱仍限于弹窗内。
3. **假设** 主按钮「批准并继续」显示，**当** 用户查看其状态，**则** 始终可点击（不因权限模式变化禁用）。
4. **假设** 「自动接受/跳过确认」类按钮显示，**当** 当前权限模式允许自动接受时，**则** 按钮可用；**当** 权限模式不允许时，**则** 按钮禁用（不可点击）。
5. **假设** 「提供反馈」按钮显示，**当** 用户查看其状态，**则** 始终可点击。
6. **假设** 用户点击「提供反馈」进入反馈流程，**当** 查看反馈区按钮，**则** 显示 `[取消] [发送反馈]`（「发送反馈」为主按钮、最右），Tab 顺序与视觉一致：「取消」最先、「发送反馈」最后。
7. **假设** 确认弹窗打开，**当** 用户按 Esc，**则** 弹窗关闭并视为拒绝（现有行为保留）。
8. **假设** 弹窗宽度不足以容纳全部操作按钮，**当** 用户查看操作区，**则** 放不下的按钮整体换到下一行显示（保持右对齐与主次视觉顺序、Tab 顺序仍与视觉一致），按钮文本保持完整可读；仅当连单个按钮都放不下时才以省略号截断该按钮文本。

***

### 用户故事：确认详情超高时滚动（优先级：P1）

作为用户，当确认详情（diff、plan 或工具输入）超过终端可视高度时，我希望内容区可以独立滚动浏览全文，同时选项列表固定在底部始终可见，以便我可以先完整审查内容再做出批准决定。

**为什么是这个优先级**：详情不可浏览意味着用户必须在信息不完整时做批准决定，属于核心可用性。布局与 Claude Code 对齐：plan/详情内容区与选项区物理分离——内容区独立滚动（PgUp/PgDn），选项列表固定底部，↑↓ 只用于选项导航，与内容滚动完全无关。

**独立测试**：触发带超长 plan（30 行以上）与超宽行（单行长于终端宽度、折行后占多行）的 `ExitPlanMode`，或大 diff / 超宽代码行的 `Edit`/`Write` 确认，用 PgUp/PgDn 或 Ctrl+u/Ctrl+d 浏览全文，验证内容区滚动而选项列表位置不变、`↑/↓ N more` 的 N 与折行后的实际行数一致、且详情区渲染高度始终不超过终端预留高度。

**验收场景**：

1. **假设** 确认详情内容超过可视高度，**当** 确认 UI 出现时，**则** 内容区以固定高度渲染并裁剪超出部分（内容行不预折行，折行由渲染层按终端宽度完成），显示首屏（固定头部：Tool 名、动作描述、warning），并在底部显示向下滚动指示（如 `↓ N more`，N 取 `实测内容行数 - 可视行数`）。
2. **假设** 用户按 PgDn，**则** 内容区下滚一页（可视行数），底部指示更新，顶部出现向上滚动指示（如 `↑ N more`）。
3. **假设** 用户反复按 PgDn，**则** 可以浏览到内容最后一行；再按 PgUp 可逐页返回，选中项（选项列表）不因滚动而改变。
4. **假设** 用户按 Ctrl+d / Ctrl+u，**则** 内容区分别下/上滚半页（可视行数的一半，向上取整），滚动指示同步更新；Ctrl+u/d 为无 PgUp/PgDn 键的小键盘/笔记本键盘提供替代滚动方式（与 Claude Code 的 modal pager 键一致）。
5. **假设** 内容区可滚动（存在滚动指示），**则** 内容区底部显示滚动快捷键提示（如 `PgUp/PgDn page • Ctrl+u/d half page`，dim 样式），让用户发现可用按键；内容不超高时不显示。
6. **假设** 内容不超过可视高度，**则** 全部内容一次性显示，不出现滚动指示与快捷键提示。
7. **假设** 选项列表在内容区下方，**则** 无论内容区滚动到哪一页，选项列表固定在底部始终可见，↑↓/Tab/Enter 只操作选项。
8. **假设** 出现新的确认（如确认队列下一项），**则** 内容区滚动位置重置到顶部。
9. **假设** 确认详情是 `ExitPlanMode` 的计划内容，**当** 内容区渲染时，**则** plan 先按 Markdown 渲染、再以渲染结果参与滚动：可视行数、`↑/↓ N more` 指示器与翻页步进都以渲染后的实测行数（含折行）计算，PgUp/PgDn/Ctrl+u/d 的翻页语义与上述场景完全一致（Markdown 渲染只改变行的内容与行数，不改变滚动机制）。
10. **假设** 某内容行（diff 代码行、工具输入 JSON、plan 段落）宽于内容区可用宽度，**当** 内容区渲染时，**则** 该行由渲染层按可用宽度折行、占多个终端行，内容区仍固定高度并裁剪（高度预算不因隐形折行被撑破）；滚动指示器与翻页步进以折行后的实测行数计算，折行后半段落在裁剪边界外时继续滚动即可看到（内容不丢失）。终端宽度变化时重新测量行数并把滚动位置夹紧到新的范围。

***

### 用户故事：确认弹窗主体滚动与按钮栏固定（webview）（优先级：P1）

作为用户，当确认弹窗（webview：桌面端/VS Code/JetBrains）内容较多超出可视高度时，我希望弹窗主体可独立纵向滚动，操作按钮栏固定不随内容滚动（始终可见可点），以便内容超长（长 diff、长命令、长计划）时无需滚动到弹窗底部才能点「批准并继续」。

**为什么是这个优先级**：webview 确认弹窗当前无最大高度约束、不独立滚动，内容超高时整个弹窗随外层滚动、按钮可能滚出可视区，用户必须先滚到底才能操作；与 CLI 侧「确认详情超高时滚动」对齐（内容区滚动、选项区固定底部）。

**独立测试**：在 webview 触发一个带超长 diff 或超长命令参数的确认（内容高度超过弹窗可视高度），验证弹窗主体纵向滚动、按钮栏固定可见；滚动主体到任意位置时按钮栏位置不变。

**验收场景**：

1. **假设** 确认弹窗内容（diff/命令/参数/计划）超高，**当** 弹窗显示时，**则** 弹窗主体出现纵向滚动（内容可滚动浏览全文），操作按钮栏（「批准并继续」等）固定在弹窗底部始终可见可点，不随内容滚动。
2. **假设** 弹窗内容不高，**当** 弹窗显示时，**则** 不出现滚动条，按钮栏在内容下方正常显示。
3. **假设** 弹窗主体处于滚动状态，**当** 用户滚动主体浏览内容时，**则** 按钮栏位置保持不变，↑↓/Tab/Enter 只操作选项与按钮，与内容滚动互不干扰。

***

### 用户故事：确认队列（优先级：P2）

作为用户，我希望确认按队列一次处理一个，以便我可以一次专注于一个决策而不会被淹没。

**为什么是这个优先级**：这确保当多个操作同时需要确认时提供可管理的用户体验。

**独立测试**：可以通过快速触发多个受限操作并验证确认按顺序出现而非同时出现来测试。

**验收场景**：

1. **假设**三个工具等待确认，**当**第一个确认被解决时，**则**第二个确认立即出现。
2. **假设**存在确认队列，**当**用户按下 ESC 取消当前确认时，**则**仅取消当前确认，队列中的下一个出现。

***

### 用户故事：从 Bash 确认切换到 Bypass Permissions 模式（优先级：P2）

作为用户，我希望在 Bash 命令确认对话框中直接开启 bypass permissions 模式，以便在信任当前任务流程时无需先解决确认再按 Shift+Tab 切换模式。CLI 和 IDE（VS Code / JetBrains webview）都必须支持。

**为什么是这个优先级**：这是便利性改进；用户已可通过 Shift+Tab（CLI）或状态栏模式切换（IDE）随时切换模式，此选项只是减少操作步骤。

**独立测试**：触发一个需要确认的 Bash 命令，选择 bypass 选项，验证命令被执行且后续工具调用不再提示确认。

**验收场景**：

1. **假设** agent 尝试运行需要确认的 Bash 命令，**当**确认 UI 出现时，**则**CLI 显示 "Yes, and bypass permissions" 选项，IDE 显示对应的 bypass permissions 按钮。
2. **假设**确认 UI 已显示，**当**用户选择 bypass permissions 选项时，**则**当前命令被允许执行，且权限模式切换为 bypassPermissions。
3. **假设**权限模式已切换为 bypassPermissions，**当**后续工具调用发生时，**则**不再显示确认 UI。

***

### 用户故事：确认弹窗焦点圈（模态焦点 trap）（优先级：P1）

作为用户，当确认弹窗（工具确认 / AskUserQuestion / 计划审批）打开时，我希望 Tab/Shift+Tab 循环被限制在弹窗内部，关闭后焦点归还打开前的位置，以便键盘焦点不会在审批期间漏出弹窗、落到背景消息列表里数百个元素上。

**为什么是这个优先级**：当前确认弹窗只处理了 Escape/Enter 快捷键，没有焦点 trap——Tab 会从弹窗漏出进入背景消息列表，与消息列表焦点圈（见 `message-rendering-system.md`）叠加后仍可能走很远。模态对话框内循环焦点是 WCAG/ARIA APG 对 modal dialog 的标准要求。

**独立测试**：打开任意确认弹窗，验证打开瞬间焦点保持在原位置不移动；按 Tab 进入弹窗后循环 Tab 数轮，验证焦点始终在弹窗内部不落入背景；Escape 关闭后验证焦点归还触发元素（或主界面默认位置）。

**验收场景**：

1. **假设** 确认弹窗打开，**当** 弹窗挂载时，**则** 焦点保持原位不移动——弹窗可能出现在用户正在输入或浏览的任何时候，打开动作本身不打断用户；键盘用户按 Tab 进入弹窗（焦点圈把第一下 Tab 拉入循环）或用鼠标点击交互。
2. **假设** 焦点在弹窗内最后一个可聚焦元素上，**当** 用户按 Tab 时，**则** 焦点循环回弹窗内第一个可聚焦元素；在第一个元素上按 Shift+Tab 反向循环到最后一个。循环覆盖弹窗内全部可聚焦元素（选项组、上一题/下一题、自定义输入框、提交按钮、关闭按钮；AskUserQuestion 另有折叠键，见 `ask-user-tool.md`「提问弹窗选项区可折叠」），任一元素都不会漏在循环外；被折叠收起的元素不在循环内。
3. **假设** 确认弹窗打开期间，**当** 用户连续按 Tab 时，**则** 焦点始终不落入背景消息列表、输入框或侧边栏。
4. **假设** 确认被解决（允许/拒绝/Escape 关闭），**当** 弹窗卸载时，**则** 焦点归还打开弹窗前的位置（触发元素）；若触发元素已不可见或卸载，则焦点移到主界面默认位置（如消息输入框）。
5. **假设** AskUserQuestion 分页切换（上一题/下一题），**当** 用户点击导航按钮时，**则** 题目切换完成后焦点移到新题第一个（或已选中）选项上，不丢失到弹窗外（与 `ask-user-tool.md`「选项组键盘可达」场景 7 一致）。
6. **假设** 确认类型为按钮式选项（Bash/Write/Edit/MCP/计划审批等，选项为 2~4 个平铺按钮），**当** 弹窗打开时，**则** 这些按钮在 trap 循环内按序 Tab 可达，无需额外的组内 roving（数量少，Tab 一步即到；AskUserQuestion 选项同样逐个 Tab 可达，见 `ask-user-tool.md`）。

***

### 边界情况

* **确认 UI 超出终端高度怎么办？** 确认详情区高度约束为 `终端行数 - 预留行数`（预留覆盖待处理工具块与底部选项选择器），保证 Ink 动态输出不超过终端高度，避免全屏清屏重绘（切换选项时闪烁）。头部（Tool 名、动作描述、warning）固定单行渲染；内容区是一个固定高度、裁剪溢出内容的盒子，内容整体按滚动偏移平移，因此无论内容行是否折行，帧高恒不超过该约束。
* **详情内容超高怎么办？** 内容区（diff / 工具输入 JSON / plan）以「内容 + 滚动偏移」的平移呈现，`scrollOffset` 状态 + PgUp/PgDn 翻页滚动、Ctrl+u/Ctrl+d 半页滚动（见「确认详情超高时滚动」），顶部/底部显示滚动指示；plan 先经 Markdown 渲染成 ANSI 文本行（与助手消息共用同一渲染器，因此 Markdown 标记不裸露），再参与滚动。滚动范围与指示器行数在渲染后按布局实测（布局按终端宽度折行，实测高度即折行后的可视行数），即 `maxOffset = max(0, 实测行数 - 可视行数)`——不预测折行行数，因此预测错误不会再撑破高度预算。
* **内容行宽于内容区怎么办？** 不预先折行：折行交给渲染层按可用宽度完成（中日韩宽字符按 2 列），行数在渲染后实测。折行不改变高度预算（内容区固定高度 + 裁剪），只改变滚动范围；一个内容行占多个可视行时，其后半段可能落在裁剪边界外，继续滚动即可看到——因此不再需要「内容行 = 一终端行」这个前提，diff 行也不必为折行复述行号/`+`/`-` 前缀。
* **plan 的 Markdown 渲染依赖终端宽度吗？** 依赖：表格列宽等按终端宽度计算，终端调整大小时 plan 行随宽度重新渲染（与「确认期间终端调整大小」同机制）；旧测量值在宽度变化后作废并重新测量，滚动位置随之夹紧到新范围。
* **选项列表会超高吗？** 选项列表固定在底部始终可见，不参与内容区滚动；↑↓ 只用于选项导航。
* **用户在确认期间导航离开怎么办？** 操作保持待处理状态，直到用户返回解决。
* **当一个确认活动时 agent 发送多个确认怎么办？** 新确认被排队并按顺序处理。
* **选择了"Other"但未输入文本怎么办？** 答案应为空，或 UI 应在提供文本之前阻止提交。
* **确认期间终端调整大小怎么办？** 详情区高度基于 `useWindowSize()`，终端调整大小时自动重新计算并适应新尺寸；内容行数随之重新测量，滚动位置夹紧到新范围（不越界、不留空白）。
* **弹窗内没有可聚焦元素怎么办？** 极端情况下（如弹窗内容为空），Tab 循环无元素可走，Escape 仍可关闭弹窗；关闭后焦点归还逻辑照常执行。
* **焦点 trap 与分页导航怎么配合？** 上一题/下一题按钮在 trap 循环内正常可达；切换题目时焦点移到新题第一个（或已选中）选项，避免「下一个」按钮禁用后焦点掉出弹窗。
