# 键盘、焦点与命中区技术方案:键盘走完主链路、触屏点得准 - 状态:已实施(2026-11-07,0.0.471,3 轮 code review 收敛,报告见 docs/code-review/2026-11-07-keyboard-focus-hit-area-review-round{1,2}.md;实现偏差:步骤 2 样式重置未采纳 width:100%/background:none 以满足本文「计算样式仅多 focus 轮廓」验收;步骤 5 的 restoreFocus 配套 searchJumpFocus 让位机制——跳转在途窗口 + 焦点停驻判定,覆盖 round1/round2 审查发现的两条联动抢焦路径) - 日期:2026-11-07 - 需求来源:`docs/proposals/2026-11-06-keyboard-focus-hit-area.md`(提案,含逐项代码证据;本文在其基础上完成定案与四处勘误,见 §3.2——其中勘误 1 把「无名称按钮」清单从 20+ 处收缩到 1 处真实缺口) - 规模:小(纯桌面端属性 / 样式 / 少量焦点逻辑,约 120 行改动 + 源码级回归测试;**零数据库迁移、零后端改动、零新依赖**) - 关联:`desktop/src/components/chat/{ChatInput,ChatRoundList,MessageBubble,QueuePanel}.vue`、`desktop/src/components/search/SessionSearchPopover.vue`、`desktop/src/components/common/TopNav.vue`、`desktop/src/style.css`、`AGENTS.md` ## 1. 需求背景 主链路功能面完整,交互可达性是短板:鼠标点得动的东西,键盘和触屏(安卓壳完全复用 desktop UI)未必。核验后的六组断点: 1. **发完消息光标丢了**:`ChatInput.vue` 的 `handleSend`(:1137-1171)在 `emit('send', …)`(:1167)后不把焦点还给编辑器;鼠标点「发送」按钮后焦点落在按钮上,按钮随后随状态切换消失,焦点掉到 body。`focusInput()`(:1215-1218,`props.disabled` 早退)早已存在——同文件 `insertText` / `addReplyQuote` 都在用,唯独发送链路没接。 2. **「已执行 N 个步骤」折叠块键盘完全无法展开**:`ChatRoundList.vue:29` 是 `
`——无 `role`、无 `tabindex`、无 `@keydown`;它是轮次步骤的唯一总入口。同项目 `RoundDirectory.vue:6-17` 的目录把手已经示范了完整写法(`type="button"` + `title` + `aria-label` + `:aria-expanded`)。 3. **复制按钮是同一行六个操作里唯一没有名称的**:`MessageBubble.vue:171-181` `.message-footer` 中 `.edit-btn`(:172,title「编辑消息」)、`.add-command-btn`(:175,title「添加到我的指令」)、`.dislike-btn`(:196,title「不满意此结果」)、`.fork-btn`(:233)、`.share-btn-icon`(:247)都有 title,唯独 `.copy-btn`(:178-181)既无 `title` 也无 `aria-label`——而复制是其中使用频率最高的。 4. **触屏命中区 24px**:全局令牌 `--aw-btn-height: 24px`(`style.css:138`)。安卓壳与触屏上「手指命中就是运气」。项目内已有成熟对策:`SessionSearchPopover.vue:391-399` 给 28px 的 `.search-toggle` 配了 `@media (pointer: coarse)` 的 44px 命中泡——但同一文件的 `.search-close`(32px,:469-481)和聊天区密集操作行都还没有。 5. **搜索跳转后焦点掉到 body**:`SessionSearchPopover.vue` `handleJump`(:324-349)第一步就 `isOpen.value = false`(:325),成功跳转后焦点无处安放,键盘用户接下来 Tab 从文档头走一遍。触发入口 `Ctrl/Cmd+K` 已在 `TopNav.vue:251-262` 实现,触发按钮 `.search-toggle` 常驻 TopNav。 6. **动态状态对读屏不播报**:排队数(`QueuePanel.vue:4`「待发送消息 (N)」)、执行中打字指示(`ChatPanel.vue:121-131`)都是纯视觉变化。项目已有 `role="status"`/`role="alert"` 惯例(`ChatPanel.vue:84` 历史加载、`ConnectionBanner.vue:2` 断线、`ExecutionErrorBanner.vue:2` 错误),这几个动态态没铺。 ## 2. 需求描述 ### 2.1 目标(全部要做) | # | 优先级 | 目标 | 一句话验收 | |---|---|---|---| | 1 | P1 | 发送后焦点回归输入框:`ChatInput.handleSend` 在 `emit('send')` 后补 `focusInput()`(单点覆盖主聊天 / 边路 / 队列全部发送路径,见 §3.2 勘误 3) | 连发三句话不需要每次点输入框 | | 2 | P1 | `.steps-summary` 键盘可达:`div` → `