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导读AskUserQuestion 是 qwen-code Web Shell 中用于呈现 Agent 向用户提问如方案选择、参数确认的核心交互组件。当用户在面板中点击提交后权限请求可能仍处于异步处理中如何避免面板看似可用实则点击失效的假死状态如何在失败后优雅地解锁供用户重试是本次设计要解决的核心问题。本文将结合设计文档与源码实现完整剖析 AskUserQuestion 的 Promise 回调契约、提交锁定语义、失败解锁重试与缺省选项上报机制并给出可验证的源码路径与测试用例帮助读者理解并复现这套交互可靠性方案。背景提交后面板假死的两个隐患原设计文档 docs/design/2026-07-30-web-shell-ask-user-question-submit-retry.md 开篇指出两个问题失败后静默假死AskUserQuestion在用户点击某个决策选项后立即锁定但其回调并不暴露异步权限处理结果。一旦请求失败面板会停留在看似可用的状态却静默忽略用户的再次点击用户无法重试。缺省选项静默返回当权限载荷中不存在allow_once选项时提交路径会静默返回用户点击提交毫无反馈。这两个问题都源于回调不返回结果、错误无处上报的契约缺陷而非单纯的状态管理遗漏。设计方案Promise 化回调 显式状态机设计文档给出了五条明确的方案要点为AskUserQuestion提供返回 Promise 的确认回调以及由所属聊天界面chat surface注入的错误报告器请求在途in flight时禁用全部操作并显示提交指示器已成功接受的决策保持锁定直到权限事件移除面板这同时覆盖了已记录但尚未最终确定的共识投票consensus votes场景遭遇拒绝rejection或返回false时上报错误并解锁操作允许用户重试缺失allow_once选项时立即上报而非静默返回。这套方案的核心是把提交动作与提交结果解耦UI 只负责发起提交 → 等待结果 → 根据结果锁定或解锁结果的成败由宿主界面通过错误报告器反馈给用户。源码实现AskUserQuestion 组件剖析核心实现在 packages/web-shell/client/components/messages/AskUserQuestion.tsx。Promise 化回调契约组件通过 props 接收两个关键回调AskUserQuestion.tsx#L24-L42interface AskUserQuestionProps { request: PermissionRequest; onConfirm: ( id: string, selectedOption: string, answers?: Recordstring, string, ) Promiseboolean; onError: (error: unknown, fallback: string) void; variant?: inline | floating; customInputLabel?: string; keyboardActive?: boolean; }onConfirm返回Promisebooleantrue表示权限已被接受false表示未被接受reject表示提交链路出错onError(error, fallback)由宿主聊天界面注入用于在失败时向用户展示可读的错误信息。这两个签名正是对设计文档第一条promise-returning confirmation callback and an error reporter的直接落地。提交状态机submitting 与 submittedRef组件维护两组关键状态AskUserQuestion.tsx#L78-L80const [submitting, setSubmitting] useState(false); const submittedRef useRef(false); const submissionAttemptRef useRef(0);submitting驱动 UI禁用所有选项按钮与操作按钮并在提交按钮上渲染Spinner与提交中文案AskUserQuestion.tsx#L955-L965submittedRef是同步锁submitDecision入口处若已为true则直接返回防止重复提交AskUserQuestion.tsx#L159-L161submissionAttemptRef用于区分本次提交与过期提交避免旧请求的失败误伤新请求详见下文。核心提交流程submitDecisionsubmitDecision是整套锁定/解锁逻辑的中枢AskUserQuestion.tsx#L154-L178const submitDecision useCallback( async (optionId: string, submittedAnswers?: Recordstring, string): Promisevoid { if (submittedRef.current) return; submittedRef.current true; const attempt submissionAttemptRef.current; setSubmitting(true); try { const accepted await onConfirm(request.id, optionId, submittedAnswers); if (!accepted) throw new Error(t(askUser.submitFailed)); } catch (error) { if (submissionAttemptRef.current ! attempt) return; submittedRef.current false; setSubmitting(false); onError(error, t(askUser.submitFailed)); } }, [onConfirm, onError, request.id, t], );对照设计文档逐条验证在途锁定setSubmitting(true)使所有按钮进入disabled状态提交按钮显示Spinner与Submitting...文案且submittedRef同步置位杜绝连点成功保持锁定onConfirmresolve 为true时submittedRef与submitting均不重置——面板保持禁用等待权限事件如 daemon 端面板移除到来符合keep a successfully accepted decision locked失败解锁重试onConfirmreject 或 resolve 为false时进入 catch 分支重置submittedRef.current false与setSubmitting(false)并通过onError上报错误用户即可再次点击重试。防重复提交与过期失败保护submissionAttemptRef承担过期失败过滤每次提交都会submissionAttemptRef.current并记录本次attemptcatch 中若发现submissionAttemptRef.current ! attempt说明在本次提交失败返回前已有新请求发起了新的提交此时丢弃该过期错误AskUserQuestion.tsx#L170-L171。这一细节在测试用例ignores a stale failure after a new request starts submitting中有明确覆盖先发起一次挂起的提交随后切换到新 request组件通过request.id变化重置状态旧提交的 rejection 不应触发onError见 packages/web-shell/client/components/messages/AskUserQuestion.test.tsx#L821-L848。缺失 allow_once 选项的显式上报handleSubmit在真正提交前先校验载荷中是否存在allow_once选项AskUserQuestion.tsx#L180-L196const submitOption request.options.find((o) o.kind allow_once); if (!submitOption) { const message t(askUser.submitOptionUnavailable); onError(new Error(message), message); return; } void submitDecision(submitOption.id, result);若无allow_once选项立即通过onError上报Submit option is unavailable且不会调用onConfirm——这正是设计文档最后一条Report a missing allow_once option immediately instead of returning silently的实现。取消路径同理handleCancel查找reject_once或reject_always选项AskUserQuestion.tsx#L203-L210。新请求到达时的状态重置组件在useEffect中监听questions与request.id变化重置提交状态submittedRef.current false、setSubmitting(false)、索引与答案清空等AskUserQuestion.tsx#L94-L116。这保证面板复用于下一个权限请求时不会残留上一个请求的锁定状态。宿主界面集成ChatPane 与权限识别AskUserQuestion由聊天面板 packages/web-shell/client/components/ChatPane.tsx 渲染。面板先通过 packages/web-shell/client/utils/askUserPermission.ts 中的isAskUserPermission判定当前 pending 权限是否为 AskUserQuestion 提示判定依据rawInput.questions为数组且无 toolName 或 toolName 为ask_user_question据此将权限分流为普通工具审批ToolApproval与提问面板AskUserQuestion。宿主注入的回调如下ChatPane.tsx#L1206-L1210const handleAskUserConfirm useCallback( (id: string, selectedOption: string, answers?: Recordstring, string) actions.submitPermission(id, selectedOption, answers), [actions], );actions.submitPermission返回 Promise正好满足onConfirm的Promiseboolean契约提交结果成功/失败由 daemon 连接层决定。错误报告器则使用面板内的reportError优先转发给外层onError否则回退到console.errorChatPane.tsx#L550-L556。权限请求的数据结构定义于 packages/web-shell/client/adapters/types.tsPermissionOptionKind含allow_once等取值PermissionRequest携带id、options与rawInput内含questions数组。测试夹具中典型的options形如[{ id: submit, kind: allow_once }, { id: cancel, kind: reject_once }]其中allow_once即提交选项。测试验证四条关键行为的回归保障测试文件 packages/web-shell/client/components/messages/AskUserQuestion.test.tsx 以 jsdom 环境逐条锁定了本次设计的核心行为设计要点对应测试用例断言核心成功提交保持锁定keeps an accepted submission locked while awaiting resolution连点两次onConfirm仅调用一次提交按钮保持disabled且文案为 Submitting...resolve 后仍锁定拒绝后解锁重试reports a rejected submit and allows retrying首次reject触发onError且按钮恢复可用再次点击可提交onConfirm调用两次返回 false 视为失败reports an unaccepted submit and allows retryingresolve(false)同样触发onError与解锁重试过期失败被忽略ignores a stale failure after a new request starts submitting旧请求 reject 不触发onError新请求按钮保持禁用缺 allow_once 显式上报reports a missing submit option without calling onConfirm过滤掉allow_once后点击提交onConfirm未被调用onError收到 Submit option is unavailable此外还有一条失败后焦点恢复用例restores focus to the custom answer after Enter submission fails验证自定义输入Other提交失败后错误上报且焦点回到自定义答案触发器方便用户直接修改重试AskUserQuestion.test.tsx#L678-L703。端到端层面packages/web-shell/client/e2e/web-shell.question-message.spec.ts 还覆盖了 question message 在真实 Web Shell 会话中的交互链路。文案与多语言i18n 键错误与状态文案统一走 i18n中英文键值位于 packages/web-shell/client/i18n.tsxaskUser.submit/askUser.submittingSubmit/Submitting...中文提交/提交中...askUser.submitFailedFailed to submit answer中文提交回答失败——作为onConfirm返回false时抛出的默认错误与onError的 fallback 文案askUser.submitOptionUnavailableSubmit option is unavailable中文提交选项不可用——缺allow_once选项时的上报文案。总结一套可复用的提交-锁定-重试交互范式从设计文档到源码再到测试这套方案形成了一条完整的闭环契约层面onConfirm返回 Promise、onError注入错误报告器让 UI 能感知异步权限结果状态层面submittingUI 禁用 Spinner与submittedRef同步锁配合成功保持锁定、失败立即解锁边界处理submissionAttemptRef过滤过期失败allow_once缺失显式上报新请求到达自动重置质量保障单元测试逐条锁定锁定/重试/过期/缺省四种行为端到端测试验证真实会话交互。这套设计对同类异步审批类 UI工具审批、权限弹窗、共识投票面板具有直接的借鉴价值只要把点击动作与异步结果通过 Promise 契约显式连接起来配合错误上报与锁定/解锁状态机就能避免看似可用实则假死的经典交互陷阱。对 qwen-code 而言AskUserQuestion 面板也因此成为 Web Shell 中提问型交互见 docs/design/2026-09-09-web-shell-question-message.md的可靠前端载体。【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考