ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

qwen-code Web Shell 提问面板键盘交互设计:AskUserQuestion 的完整按键契约与无障碍实现

qwen-code Web Shell 提问面板键盘交互设计:AskUserQuestion 的完整按键契约与无障碍实现 qwen-code Web Shell 提问面板键盘交互设计AskUserQuestion 的完整按键契约与无障碍实现【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code导读qwen-code 的 Web Shell 在需要向用户收集决策信息时会通过ask_user_question权限请求渲染一个多步提问面板AskUserQuestion。本设计文档docs/design/2026-08-10-web-shell-ask-user-question-keyboard.md定义了该面板完整的键盘交互契约从选项导航、自定义回答输入到跨问题跳转、全局提交/取消以及无障碍语义与分栏面板焦点守卫。本文以该设计文档为骨架结合 AskUserQuestion.tsx 的实现与 AskUserQuestion.test.tsx 的 60 条用例逐条讲解按键契约背后的实现原理帮助你理解并复现这套可访问的键盘交互设计。问题背景键盘流在哪些场景会中断设计文档首先指出了既有实现的痛点Web Shell 提问浮层虽然支持在单个选项列表内做键盘导航但当用户在多个问题之间移动、输入自定义答案、或到达最终动作提交时键盘流会断裂此外回到一个已回答过的问题时焦点可能落在与已勾选答案不同的选项上。这些问题可归纳为三类交互缺口跨问题导航缺失多问题表单中仅靠上下键无法在问题间移动自定义输入无键盘出口进入 Other 自定义输入框后缺少明确的提交/退出路径状态-焦点不同步返回已答题时焦点没有恢复到该题的当前答案即“安全默认值”与真实答案不一致。解决方向不是简单加几个 keydown 监听而是建立一套完整、可验证的交互契约Interaction Contract并用组件测试逐条锁定行为。交互契约逐键语义详解设计文档给出了一条覆盖全生命周期的按键契约。下面结合源码 handleKeyDown 逐条展开。打开与初始聚焦Opening the topmost question focuses its current answer, or the first option when the question has not been visited.面板成为最顶层问题topmost时自动将焦点移到当前答案上若该问题尚未被访问过则聚焦第一个选项。实现位于聚焦副作用AskUserQuestion.tsxuseEffect(() { const wasActive prevKeyboardActiveRef.current; const prevRequestId prevRequestIdRef.current; prevKeyboardActiveRef.current keyboardActive; if (!keyboardActive || !current) return; const requestChanged request.id ! prevRequestId; if (requestChanged currentIdx ! 0) return; if (wasActive !requestChanged) return; prevRequestIdRef.current request.id; const idx selectedIdxRef.current ?? 0; if (idx current.options.length) customRef.current?.focus(); else optionRefs.current[idx]?.focus(); }, [current, currentIdx, keyboardActive, request.id]);这里用 ref 记录了上一请求的 request.id确保“新请求到达”和“同一请求内重新聚焦”两种情况能被区分新请求到达且当前在第 0 题时才拉取焦点同一请求下已处于激活状态则不重复抢焦点。选项移动Up/Down 与 j/kUp/Down and j/k move through options. In a single-select question, focus and the checked answer move together. Space toggles the focused multi-select option.Up/Down 与 j/k在选项间移动移动逻辑由 moveSelection 实现——它基于selectedIdxRef同步 ref计算(base delta total) % total形成首尾循环且利用 ref 保证连按方向键时在重渲染前就能正确前进对应测试advances on rapid repeated ArrowDown without a re-render in between。单选single-select遵循 radiogroup 契约焦点移动即答案提交aria-checked跟随焦点。核心是 selectIndex移动到普通选项时调用handleSelectOption提交答案移动到 Other 时清除已选普通答案。多选multi-select使用 toggle 按钮语义Space切换当前聚焦项的选中状态由 handleToggle 维护selectedMulti映射。额外支持Home/End 跳到首/末选项以及数字键 1-9 直接选择对应选项每个选项按钮通过aria-keyshortcuts暴露数字快捷键见渲染逻辑 AskUserQuestion.tsx。Enter前进与提交Enter advances to the next question. On the last question, it submits the current answers.Enter在非末题时进入下一题在末题时提交当前所有答案。源码中的 advanceQuestion 负责这一分支非末题调用selectQuestion(currentIdx 1)末题则调用handleSubmit。值得注意的是多选场景的细节若当前聚焦的是多选选项且尚未选中Enter会先选中该项再前进见 handleKeyDown避免用户按下 Enter 后该项未被记录。Previous / Next保留目标题状态Previous and Next move focus into the destination question, preserving its checked option or custom-answer trigger.向前/向后切题时焦点进入目标题并恢复该题已勾选的选项或自定义答案触发器而不是回到“安全默认值”。这是对最初“返回已答题焦点错位”问题的直接修复核心是 getSelectedIndexForQuestionconst getSelectedIndexForQuestion useCallback( (questionIdx: number): number | null { if (Object.hasOwn(selectedIdxByQuestionRef.current, questionIdx)) { return selectedIdxByQuestionRef.current[questionIdx] ?? null; } // 无记录时优先自定义答案 - 已勾选选项 - 第 0 个选项 ... }, [answers, customInputs, questions], );selectedIdxByQuestionRef按题目索引记录了每题的焦点位置切题时由 selectQuestion 读出并写回selectedIdxRef随后触发聚焦副作用把焦点落到对应选项或 Other 触发器上。对应测试包括restores focus to the checked answer when returning to a question、restores a custom answer and its focus when returning to a question。Left / Right任意非编辑控件的横移Left and Right perform the same navigation from any non-editable dialog control.横向箭头承担“上一题/下一题”职责并且从对话框内任何非编辑控件选项按钮、动作按钮等都能触发前提是当前聚焦元素不在可编辑目标输入框内。实现通过isEditableTarget(e.target)判断后在非选项目标和选项目标两条分支中都注册了ArrowLeft/ArrowRight处理AskUserQuestion.tsx。测试moves between questions with horizontal arrows from an action button专门覆盖了从动作按钮横移的场景。Command/CtrlEnter任意位置全局提交Command/CtrlEnter submits the current answers from anywhere in the dialog.无论焦点在选项、动作按钮还是自定义输入框Command/CtrlEnter都会提交当前全部答案。提交前必须满足所有问题均已作答allQuestionsAnswered否则不触发提交按钮上也用aria-keyshortcutsControlEnter MetaEnter向读屏器宣告该快捷键。快捷标签按平台显示⌘↵或Ctrl↵见 submitShortcutLabel。Escape编辑退出与请求取消的两段式语义Escape while editing a custom answer exits editing, preserves the text, and restores focus to the Other trigger. Escape elsewhere cancels the request, so pressing Escape a second time after leaving the input cancels.这是设计中最精巧的语义在自定义输入框内按 Escape 只退出编辑——保留已输入文本焦点回到 Other 触发器再次按 Escape 才取消整个请求。实现分两层输入框自身拦截 EscapehandleCustomInputKeyDownpreventDefaultstopPropagation设置focusCustomTriggerAfterEditRef后退出编辑态由副作用把焦点还给触发器面板层在非编辑目标上按 Escape 时调用handleCancel取消走reject_once/reject_always选项。测试exits custom-input editing on Escape, then cancels on a second Escape完整验证了两段式流程同时输入框对IME 组合输入isComposing/keyCode 229做了豁免避免中文等输入法在选词阶段被 Escape/Enter 打断。上下文快捷键提示Contextual HintA short contextual hint makes the available keys visible.面板底部常驻一行随状态变化的快捷键提示由 shortcutHint 计算逻辑 根据是否多选 / 是否末题 / 是否正在编辑自定义输入组合出不同文案i18n 文案位于 i18n.tsx状态文案英文单选、多题中间↑↓ select · Enter next单选、末题↑↓ select · Enter submit多选↑↓ move · Enter next/↑↓ move · Enter submit空 Other 触发器聚焦↑↓ select · Enter edit/↑↓ move · Enter edit输入框为空Type an answer · Esc stop editing输入框有内容Enter next/Enter submit · Esc stop editing若处于第 2 题及以后还会在前缀追加← previous提示用户可用左箭头返回。折叠态动作快捷键失效Action shortcuts are inactive while the dialog is collapsed.面板支持折叠collapse为一行标题栏。折叠状态下handleKeyDown只允许Escape取消与Command/CtrlEnter提交被preventDefault拦截其余动作快捷键全部静默AskUserQuestion.tsx同时快捷键提示区被隐藏。测试keeps the shortcut footer hidden while the dialog is collapsed与keeps action shortcuts inert while the dialog is collapsed分别锁定这两点。无障碍设计非模态多步表单The overlay is a non-modal multi-step form rather than a brief urgent alert, so it usesroledialogwithoutaria-modal.设计文档明确了无障碍定位这不是一个短暂的紧急弹窗alert而是非模态的多步表单因此面板根节点使用roledialog且不带aria-modal——非模态意味着用户仍可与页面其他部分交互无需强制焦点圈闭测试exposes a non-modal dialog of real buttons and focuses the first option同时断言role dialog且无aria-modal属性保留既有radiogroup与 toggle 按钮语义单选选项是roleradioaria-checked互斥语义多选选项是aria-pressed的 toggle 按钮见渲染逻辑 AskUserQuestion.tsx当前问题继续标注对话框及其选项组展开态下对话框用aria-labelledby同时引用工具名标题headingId与问题文本questionTextId避免读屏器丢失工具名上下文代码注释 AskUserQuestion.tsx选项组radiogroup/group用aria-labelledby{questionTextId}关联当前问题文本选项使用roving tabindex当前项tabIndex{0}其余-1把面板整体纳入 Tab 序列而非每个选项独立占位数字快捷键通过aria-keyshortcuts暴露提交按钮带aria-busy表达提交中的加载状态。焦点守卫split-view 分栏下的 keyboardActiveSplit-view panes keep their existingkeyboardActivefocus guard.当 Web Shell 处于split-view分栏模式时多个面板可能同时展示提问/审批组件。此时AskUserQuestion的keyboardActiveprop 传false禁止组件自动抢焦点避免一个面板的提问偷走用户正在操作的面板的焦点。关键设计点见 AskUserQuestion.tsx 的 prop 注释Whether this question should pull keyboard focus to its first option when it becomes the topmost one. Defaults to true. Split-view panes pass false so a question in one pane doesnt steal focus from the pane the user is in; like ToolApproval, keyboard handling is focus-scoped, so it stays operable once the user tabs/clicks into it.默认true单面板场景自动聚焦分栏场景下 ChatPane.tsx 对ToolApproval与AskUserQuestion均传keyboardActive{false}由于键盘处理是**焦点作用域focus-scoped**的用户 Tab 或点击进入某面板后该面板的提问依然完整可用——只是不主动抢焦点。测试does not steal focus when keyboardActive is false (split-view panes)验证了这一行为。影响范围与边界设计文档明确了本次改动的边界改动仅限Web Shell 提问组件本体、其样式、翻译文案与聚焦的组件测试权限载荷permission payload与 daemon 协议完全不变——提问数据仍通过PermissionRequest.rawInput.questions传入types.tsonConfirm(id, optionId, answers)的回调签名不变分栏面板保留各自的keyboardActive焦点守卫互不干扰。这保证了该交互升级对服务端协议与权限链路透明风险被严格限制在 UI 层。源码验证路径想深入验证上述契约可按以下路径继续探索交互契约实现AskUserQuestion.tsxhandleKeyDown、selectIndex、selectQuestion、advanceQuestion等核心回调行为契约测试60 条覆盖本文全部按键语义AskUserQuestion.test.tsxaccessibility 与 multiple questions 两大 describe 块快捷键提示文案i18n.tsxaskUser.shortcuts.*英文与中文两套分栏焦点守卫调用方ChatPane.tsx权限请求数据结构types.ts设计文档本身位于 docs/design/2026-08-10-web-shell-ask-user-question-keyboard.md可作为团队内部评审与后续迭代的基准契约。【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表