feat(nav): show question mark icon when AskUserQuestion is pending - #1541
Conversation
limityan
left a comment
There was a problem hiding this comment.
整体结论:建议 Request changes。当前实现存在一个会漏报真实等待状态的状态模型问题,以及两项影响该状态可感知性的无障碍问题。
1. [P2] 应按状态机跟踪的 turn 判断 AskUserQuestion
问题
SessionsSection.tsx:235-236 和 :962-964 都检查 dialogTurns 的最后一项。但生产逻辑明确允许较新的 turn 已由 composer 预先追加,而 currentDialogTurnId 仍指向正在执行的旧 turn;现有 agentCompanionActivity.ts 也已经通过 currentDialogTurnId 解析 tracked turn。
风险
当 turn A 正等待 AskUserQuestion、turn B 已排队追加时,两处判断都会检查 turn B 并返回 false,导航继续显示 Loader。非活跃会话因此无法提示用户需要回答,直接违背本 PR 的目标。
建议
让订阅 fingerprint 与渲染共用同一个 tracked-turn resolver:优先按 currentDialogTurnId 找 turn,缺失时再回退最后一项,并确保 tracked id/revision 的变化能触发订阅刷新。增加 focused regression test:turn A pending Ask、turn B queued,断言问号出现,并在等待结束后移除。
2. [P2] 等待输入状态需要可被读屏实时感知
问题
SessionsSection.tsx:1063-1069 只给异步插入的 SVG 设置了 aria-label。当焦点停留在聊天区或编辑器时,Loader 替换为问号不会自动被辅助技术播报。
风险
读屏用户无法知道后台会话已经阻塞并需要输入。该变化属于需要在不移动焦点的情况下可感知的状态消息。
建议
使用集中、去重的 aria-live=polite / role=status 区域播报“会话 X 需要你的输入”,并处理等待结束状态;图标本身可保持装饰性,避免多个会话行重复播报。参考 WCAG 4.1.3:https://www.w3.org/WAI/WCAG22/Understanding/status-messages.html
3. [P2] Light 主题下问号图标对比度低于 3:1
问题
SessionsSection.scss:201-204 使用 --color-warning。在内置 Light 主题下,#c08c42 对导航背景 #f3f3f5 的对比度只有约 2.68:1;pulse 的 0.85 透明度低点约 2.28:1,hover/active soft 背景下最低约 2.06:1。
风险
这个 14px 细线图标是区分“仍在运行”和“等待用户输入”的关键状态图形,低于有意义 UI 状态图形的 3:1 阈值。当前 theme color audit 只检查 token 治理,不检查实际对比度。
建议
使用经过 Light/Dark 全主题验证的高对比前景,或将 warning 与 --color-text-primary 混合并提供足够对比的底色/边框;动画全过程也应保持 3:1。参考 WCAG 1.4.11:https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html
验证说明
- 审查 head:
81b54d6245d8586fddfd31172a11675868d00b5d - GitHub 当前 4 个 CI job 均通过。
- 本地
pnpm run type-check:web、15 个 focused tests、pnpm run theme:color-audit:all与git diff --check均通过。 - 现有测试没有覆盖多 turn 状态机与 SessionsSection 导航渲染,上述问题不会被当前绿色 CI 捕获。
- 修复后请按仓库规则 rebase 到最新 main。
limityan
left a comment
There was a problem hiding this comment.
复核结论:tracked-turn 选择和主题对比度已修复,但新的 aria-live 实现仍有 3 个 P2 问题,暂不建议合并。
1. [P2] 多个 SessionsSection 会重复播报同一个全局事件
问题
SessionsSection.tsx:257-266 在每个组件实例中遍历全部 flowChatState.sessions,并在 :973-975 各自创建 live region。SessionsSection 会按工作区多实例挂载,折叠只改变样式和 isVisible,不会卸载组件;当前 effect 也没有按工作区、远程连接或可见性过滤。
风险
任一会话等待输入时,多个工作区的 live region 会重复播报。隐藏的 transient/subagent 会话也可能产生“有会话需要输入”,但当前列表中没有可操作的会话行。
建议
把 live region 上移到唯一的 Nav owner,由一个集中状态源去重;如果必须保留在 SessionsSection,至少复用列表的工作区/远程连接/transient/subagent 过滤,并受 isVisible 控制和正确重置 scope。
2. [P2] 连续等待和部分解除会丢失或保留错误公告
问题
SessionsSection.tsx:268-280 只处理 addedCount 和“全部集合清零”。A 已等待后 B 再等待时,两次都设置相同单数文案;React 会跳过相同字符串状态更新,live region 没有 DOM 变化。A、B 同时等待后仅 A 解除时,代码也不会更新公告。
风险
第二个等待事件可能完全不被播报;部分解除后 live region 仍可能保留“2 个会话需要输入”等过期内容。
建议
显式计算 added/removed IDs,以会话名称和当前总数生成每次可区分的消息,或使用带事件序号的集中公告队列。增加连续单新增、部分解除、等待会话交换的组件级测试。
3. [P2] 读屏用户仍无法定位具体等待会话
问题
SessionsSection.tsx:1098-1106 把 CircleHelp 设为 aria-hidden 本身没有问题,但会话行没有对应的 sr-only 状态或 aria-describedby;全局公告也只有数量,不包含会话标题。
风险
用户即使听到“有会话需要输入”,随后遍历会话列表也无法判断是哪一个会话需要操作,状态也无法被重新查询。
建议
为每个等待行增加本地化的隐藏状态文本,并与行的可访问名称/描述关联;live region 消息应包含发生变化的会话标题。
已确认修复与验证
- tracked-turn resolver 已覆盖 turn A 等待、turn B queued 的场景。
- Light 主题对比度修复后约为 8.23:1,pulse 最低点约 6.33:1。
pnpm run type-check:web、i18n audit、theme audit、diff check 通过。- focused tests 22/22;Web 全量 tests 1515/1515;lint 0 errors。
- 审查 head:
3048e5a6d0404f7658d20d7c6bff9435c92e6ab7。 - 该 head 的 GitHub CI run 当前为
action_required,没有 job 实际运行,不能视为 CI 已通过。 - 分支落后当前 main 8 个提交,虚拟合并无冲突;修复后请按仓库规则 rebase。
limityan
left a comment
There was a problem hiding this comment.
再次复核最新提交 9ca611f 后,仍有一项需要修改的问题:
[P2] 最终 merge 恢复了分区级 aria-live,导致重复播报
问题
NavPanel 已经挂载单一的全局 AskUserAnnouncer,但最终 merge 又在 SessionsSection 中恢复了 liveAnnouncement、prevWaitingIdsRef、对应 effect,以及局部的 role=status aria-live=polite。这部分旧逻辑在 19bae86 中已经删除,却被 9ca611f 重新合入。
风险
每个已挂载的 SessionsSection 都会扫描全部 session,而不是只处理所属 workspace。任一 AskUserQuestion 进入或退出等待态时,全局 announcer 与多个 workspace announcer 会同时播报;多 workspace 或多会话切换时,还可能播报不同的数量信息。这会让屏幕阅读器收到重复或相互冲突的状态提示,违背本次修复的 single-owner 目标。
建议
- 删除 SessionsSection 中的 liveAnnouncement、prevWaitingIdsRef、对应 effect 和局部 live region,只保留全局 AskUserAnnouncer。
- 增加最终组件树的集成测试:同时挂载多个 SessionsSection,断言整个 NavPanel 只有一个 AskUser live region,并且一次状态变化只更新一次消息。
另外,当前净变更约 +610/-55,但 PR 包含 7 个提交及 2 个 merge commit,并存在重复 patch。请按仓库约定基于最新 main rebase,并压缩到最多 2 个提交。此次 live-region 回归正是交叉 merge 拓扑带回旧实现造成的。
本地验证:Web 全量测试 250 文件 / 1538 项通过;type-check:web、i18n:audit、theme:color-audit:all、git diff --check 均通过。GitHub CI 当前也已全部通过,但现有测试仅覆盖消息计算函数,没有覆盖上述最终组件集成回归。
9ca611f to
19bae86
Compare
5cdb083 to
9146c96
Compare
limityan
left a comment
There was a problem hiding this comment.
再次复核最新 head 9146c96 后,提交已压缩为 1 个 commit,上轮的重复提交/merge 拓扑问题已经修复。但仍有一项新增能力覆盖缺口,暂不建议合并。
[P2] 新增的 AskUser 状态播报没有覆盖存量 Toolbar Mode
问题
本 PR 新增的 AskUserAnnouncer 只挂载在 NavPanel。存量 AppLayout 在 Toolbar Mode 下会提前返回,只渲染 ToolbarMode,整个 WorkspaceBody/NavPanel 子树都会卸载。
这不是本 PR 改坏了 Toolbar Mode:Toolbar Mode 的布局分支是历史代码。准确的问题是,本 PR 新增“单实例 AskUser 状态播报”时没有覆盖这个既有、仍可创建/切换/运行 FlowChat 会话的宿主。Toolbar Mode 当前只把 AskUserQuestion 当作普通 processing/streaming 状态,也没有对应的 waiting live region。
风险
在 Toolbar Mode,特别是折叠状态下,当前或后台会话进入 AskUserQuestion 等待态时,读屏用户不会收到“需要输入”或“等待已解除”的状态播报,界面仍可能只表现为 processing/stop,容易让用户一直等待。
建议
将 AskUserAnnouncer 提升到 AppLayout 模式分支之外的共同常驻 owner,例如 ToolbarModeProvider 内、LazyAppLayout 外;或者让 ToolbarMode 复用相同的 AskUser waiting 状态源。补充一个集成测试:切换到 Toolbar Mode 后触发和解除 AskUserQuestion,断言仍只有一个 live region 且消息正确更新。
已扩大检查存量宿主:正常 ChatPane、FloatingMiniChat、AssistantConfig、ACP、远程/Peer 与折叠导航均和 NavPanel 同根挂载;Agent Companion 有自己的 aria-live/activity bridge。除 Toolbar Mode 外,未发现第二个同类高置信遗漏。
合并准备
提交数量现已符合要求,但 9146c96 的父提交仍是旧基线 e9d94f1,当前 GCWing/main 为 71ceb38,分支落后 13 个提交。请按仓库约定 rebase 最新 main 后再更新 PR。
验证
- 审查 head:9146c96bfde8b4641420f582b2104d000753470e
- Web 全量测试:249 files / 1532 tests 通过
- type-check:web、i18n:audit、git diff --check 通过
- 本轮 force-push 后 GitHub 尚无 CI check 结果
Replace the spinning Loader2 icon with a pulsing CircleHelp icon in the left nav session list when a running session has a pending AskUserQuestion tool call, so users can distinguish 'waiting for your input' from 'agent is working'.
- Extract findPendingAskUserQuestion into a shared util (askUserQuestionState.ts) with a boolean wrapper hasPendingAskUserQuestion
- Add hasPendingAskUserQuestion to the SessionsSection selector string so the nav row re-renders when the pending status changes (fixes active session not refreshing)
- Render CircleHelp (is-ask-user class, warning color + pulse animation) instead of Loader2 when isRunning && isWaitingForUserAnswer
- Add prefers-reduced-motion fallback and hover-scale exclusions for the new icon class
fix(nav): use tracked turn, aria-live, and contrast fix for AskUserQuestion icon
Address three P2 review findings:
1. Use state-machine tracked turn (currentDialogTurnId) instead of last
dialogTurn to detect pending AskUserQuestion. When a newer turn is queued
while the tracked turn still has a pending question, the last-turn check
would miss it. Added resolveTrackedTurn helper and included tracked turn
ID in the selector fingerprint.
2. Add aria-live=polite status region so screen readers announce when
sessions start/stop waiting for user input without focus movement. The
CircleHelp icon is now decorative (aria-hidden); announcement is
centralized and deduplicated.
3. Fix Light-theme contrast: replace pure --color-warning with
color-mix(text-primary 70%, warning 30%) to exceed the 3:1 non-text
contrast threshold. Raised pulse minimum opacity from 0.85 to 0.9.
Added regression tests for tracked turn resolution with queued turns.
fix(a11y): single-owner aria-live, distinguishable messages, per-row status
Address three P2 accessibility review findings:
1. Move aria-live region from per-workspace SessionsSection (multi-
instance) to a single AskUserAnnouncer component rendered once in
NavPanel. Eliminates duplicate announcements across workspace instances
and filters out transient/subagent sessions.
2. Rewrite announcement logic: explicitly compute added/removed session
IDs with titles. Single add announces session name; multi-add
announces count; partial resolution announces resolved name + remaining
count; all-resolved announces clearance. Uses clear-then-rAF DOM
manipulation to force re-announcement even with identical text.
Added computeAnnouncementMessage pure function with 7 tests covering
consecutive add, partial resolution, swap, and no-change scenarios.
3. Add per-row sr-only status text ('Needs your input') inside each
waiting session row so screen-reader users can identify which session
needs action when navigating the list. CircleHelp icon stays aria-hidden.
Added i18n keys (ariaNeedsInputWithName, ariaInputResolvedRemaining) to
en-US, zh-CN, zh-TW locales. Code by AI.
The AskUserAnnouncer was mounted only in NavPanel, which is unmounted in Toolbar Mode (AppLayout returns early with only <ToolbarMode />). Toolbar Mode still runs FlowChat sessions and can receive AskUserQuestion calls, so the live region must persist across both modes. Move <AskUserAnnouncer /> from NavPanel to App.tsx, inside ToolbarModeProvider as a sibling of LazyAppLayout. This makes it a single-instance persistent sibling that stays mounted in both normal and Toolbar Mode layouts. Also rebased on origin/main (71ceb38) to pick up 13 missing commits.
9146c96 to
628589b
Compare
limityan
left a comment
There was a problem hiding this comment.
再次复核当前 head 628589b8,此前阻塞问题已解决:全局 AskUserAnnouncer 在普通布局和 Toolbar Mode 中均保持单实例挂载,companion pet 窗口没有重复实例;等待状态识别、订阅清理和现有渲染路径未发现新的正确性问题。聚焦测试、类型检查、Web 构建、i18n/theme 审计均已通过,因此批准本次变更。
以下为非阻塞的后续优化建议:
aria-live当前为每次状态变化单独注册requestAnimationFrame。极短时间内连续出现多个等待事件时,浏览器或辅助技术可能合并同一帧内的更新;后续可考虑按帧合并或串行播报,并补一个可控 rAF 的组件级测试。AskUserAnnouncer注释仍写着 “Rendered once in NavPanel”,可改为实际的App.tsx全局挂载位置;未使用的ariaNeedsInput翻译键也可一并清理。- 建议更新 PR 描述,使其覆盖全局 announcer、i18n、Toolbar Mode 支持和扩展后的验证范围,避免描述与最终 diff 不一致。
Summary
在左侧导航面板的会话列表中,当运行中的会话 Agent 调用
AskUserQuestion等待用户回答时,将旋转加载图标替换为脉动问号图标(CircleHelp),使用户能直观区分"Agent 正在工作"与"Agent 正在等待你的输入"两种状态。Type and Areas
Type:
UI
Areas:
Web UI(桌面前端导航面板)
Motivation / Impact
问题:当会话 Agent 调用
AskUserQuestion时,会话仍处于PROCESSING状态,左侧面板显示与普通运行时一致的旋转Loader2图标,用户无法区分 Agent 是在自主执行还是在等待回答。根因:
TOOL_CONFIRMATION_NEEDED等)已定义但从未在生产代码中派发,processingPhase不会变为TOOL_CONFIRMING。needsUserAttention === 'ask_user'仅对非活跃会话设置(handleConfirmationNeeded中有sessionId !== activeSessionId守卫)。attentionKind推导有!isRunning守卫,运行态时抑制了 attention 标志。pending_confirmation时选择器输出不变,flowChatState不刷新,导航行不重渲染。解决方案:复用 companion-pet 中已有的
findPendingAskUserQuestion(直接扫描对话轮次中的非终态AskUserQuestion工具项),将其提取为共享 util 并添加布尔包装函数。将该检测结果加入选择器字符串以触发重渲染,在图标槽中用CircleHelp替代Loader2。影响:用户在左侧面板即可看到问号图标,无需切换到会话内容区即可知晓某会话正在等待输入。对非活跃会话同样有效。
Verification
pnpm run type-check:webpnpm --dir src/web-ui run test:run src/flow_chat/utils/agentCompanionActivity.test.tsReviewer Notes
改动文件:
findPendingAskUserQuestion、TRANSIENT_TURN_STATUSES及新增hasPendingAskUserQuestionhasPendingAskUserQuestion;渲染CircleHelp替代Loader2is-ask-user样式 +bitfun-nav-ask-user-pulse关键帧 +prefers-reduced-motion回退架构说明:
findPendingAskUserQuestion直接扫描对话轮次中的工具项,不依赖未接线的状态机事件或仅对非活跃会话设置的needsUserAttention标志,因此对活跃和非活跃会话均可靠。--color-warning为已有 token,无新增颜色。兼容性风险:低。仅影响导航面板图标渲染,不改变状态机、Store 或后端逻辑。
agentCompanionActivity.ts为纯重构(函数来源迁移),测试全部通过。代码由AI辅助生成。UI Comparison
befor:


after: