feat(cli): support same-turn message steering - #7090
Conversation
E2E Test ReportEnvironment: macOS, built bundle, isolated HOME and runtime directories, tmux, qwen3.7-plus.
The steer path intentionally does not interrupt tokens already streaming; it takes effect at the next model sampling boundary. Additional validation:
|
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
|
Thanks for the PR! Template looks good ✓ — all required sections present with before/after evidence and bilingual summary. Problem: the inconsistency between text-streaming queue behavior and tool-execution queue behavior is real — plain-text Enter during streaming becomes a separate prompt while tool-time input stays in the same turn. Before/after evidence provided, verified on macOS with the real bundled TUI. No linked issue, but the problem is directly observable. Direction: steer-and-defer for in-flight interactions is a well-established pattern in interactive CLI tools. No direct CHANGELOG reference, but the area (interactive UX, turn management) is clearly within scope. Aligned. Size: 162 production lines in core ( Approach: scope feels right for the feature. Three steer consumption points (post-response, post-Stop-hook, post-next-speaker-check) cover the main sampling boundaries cleanly. The transactional accept/restore settlement is a solid way to handle cancellation and delivery-failure edge cases. No obvious scope creep. Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ — 所有必填部分齐全,包含 before/after 证据和双语摘要。 问题:文本流式响应期间和工具执行期间的排队行为不一致是真实存在的——流式响应期间的纯文本 Enter 会变成独立提示,而工具执行期间的输入留在同一回合。提供了 before/after 证据,已在 macOS 真实 TUI 上验证。无关联 issue,但问题可直接观察。 方向:交互式 CLI 工具中 steer-and-defer 是成熟的交互模式。CHANGELOG 无直接参考,但交互 UX 和回合管理明显在范围内。对齐。 规模:核心 ( 方案:范围与功能匹配。三个 steer 消费点(响应后、Stop hook 后、next-speaker 检查后)干净地覆盖了主要采样边界。事务性 accept/restore 结算很好地处理了取消和投递失败边界。无明显范围膨胀。 进入代码审查 🔍 — Qwen Code · qwen3.7-max Reviewed at |
Code ReviewIndependent proposal: add a PR's approach: matches this closely. The
No critical issues found. No AGENTS.md violations. The code is focused, well-structured, and the test coverage is thorough — especially the accept/restore edge cases (cancellation, budget exhaustion, hook blocking, history rejection). TestingUnit tests: all pass
Typecheck: clean ( Build: Real-scenario (tmux): the TUI starts and renders correctly, but full steer/queue testing requires live model streaming — not feasible in this headless CI environment without API credentials. The unit tests cover all the steer consumption paths, accept/restore settlement, and cancellation edge cases comprehensively. The author reports verification on macOS with the real bundled TUI. 中文说明代码审查独立方案: 在核心中添加 PR 方案: 与独立方案高度一致。 未发现关键问题,未发现 AGENTS.md 违规。代码聚焦、结构清晰,测试覆盖充分。 测试
— Qwen Code · qwen3.7-max Reviewed at |
|
Confidence: 4/5 — clean across every stage, ships a well-designed UX improvement with thorough test coverage. This is a solid PR. The problem is real and well-documented — Enter and tool-time queuing had inconsistent turn semantics, which is confusing when you're typing while the model streams. The solution is clean: a The only minor reservation is that the full steer/queue behavior couldn't be verified end-to-end in this headless CI environment — it needs live model streaming. But the unit test coverage is comprehensive (822 tests across core and CLI, all passing), the build and typecheck are clean, and the author verified the real TUI interaction on macOS. The accept/restore settlement pattern is well-tested across normal delivery, cancellation, hook blocking, budget exhaustion, and delivery failure. Approving. ✅ 中文说明置信度: 4/5 — 各阶段均通过,以充分的测试覆盖交付了一个设计良好的 UX 改进。 这是一个扎实的 PR。问题是真实且有文档支持的——Enter 和工具执行期间的排队有不同的回合语义,在模型流式输出时输入会令人困惑。方案干净: 唯一的轻微保留是完整的 steer/queue 行为无法在当前 headless CI 环境中端到端验证——需要实时模型流式响应。但单元测试覆盖充分(跨 core 和 CLI 共 822 个测试,全部通过),构建和类型检查均 clean,作者已在 macOS 上验证了真实 TUI 交互。 批准 ✅ — Qwen Code · qwen3.7-max Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. Not reviewed: chunk 3 — launched with a prompt that is not the one the CLI built. Not reviewed: chunk 4 — launched with a prompt that is not the one the CLI built. Not reviewed: chunk 5 — launched with a prompt that is not the one the CLI built. Not reviewed: chunk 2 — launched with a prompt that is not the one the CLI built. Not reviewed: chunk 6 — launched with a prompt that is not the one the CLI built. Not reviewed: chunk 1 — launched with a prompt that is not the one the CLI built. Not reviewed: verification — the review posts findings, but no verifier ran (Step 4 builds its prompt with agent-prompt --role verify; none was recorded, so the findings were not verified).
— qwen3.7-max via Qwen Code /review
| if (options?.deferUntilIdle) { | ||
| addMessage(submittedValue, true); | ||
| return; | ||
| } |
There was a problem hiding this comment.
[Suggestion] The deferUntilIdle check (Ctrl+Q) is placed after the sub-agent routing guard, so Ctrl+Q on a sub-agent tab silently delivers the message to the sub-agent immediately instead of deferring it for the main agent's next turn.
Failure scenario: The user is viewing a sub-agent tab while the main agent is streaming. The Footer shows "Enter to steer · Ctrl+Q to queue". The user presses Ctrl+Q expecting to queue for the main agent's next turn. Instead, handleFinalSubmit hits the agentViewState.activeView !== 'main' branch first (line ~2156), calls agent.interactiveAgent.enqueueMessage(submittedValue.trim()), and returns — the { deferUntilIdle: true } option is silently discarded and the message lands in the sub-agent's queue.
| if (options?.deferUntilIdle) { | |
| addMessage(submittedValue, true); | |
| return; | |
| } | |
| if (options?.deferUntilIdle) { | |
| addMessage(submittedValue, true); | |
| return; | |
| } | |
| // Route to active in-process agent if viewing a sub-agent tab. | |
| if (agentViewState.activeView !== 'main') { | |
| const agent = agentViewState.agents.get(agentViewState.activeView); | |
| if (agent) { | |
| agent.interactiveAgent.enqueueMessage(submittedValue.trim()); | |
| return; | |
| } | |
| } |
— qwen3.7-max via Qwen Code /review
✅ Local real-build verification — PR #7090 (same-turn message steering)Verified the PR end-to-end on the real built bundle (not just unit mocks) at head How it was verified
1)
|
| Check | Result |
|---|---|
npm ci + npm run build — PR bundle |
✅ exit 0 |
npm ci + npm run build — main baseline (A/B) |
✅ exit 0 |
tsc --noEmit — packages/core + packages/cli |
✅ clean |
core · client.test.ts (incl. 9 new steer-boundary tests) |
✅ 255 passed |
cli · useGeminiStream + AppContainer |
✅ 281 passed |
cli · useMessageQueue, InputPrompt, Footer, QueuedMessageDisplay, keyMatchers |
✅ 286 passed |
| Unit / component total | ✅ 822 passed · 0 failed |
The 18 new tests exercise every boundary: UserPromptSubmit bypass for steer, consume-before-Stop-hook, input queued during a blocking Stop hook, input queued during next-speaker classification, no-drain-without-budget, and the transactional accept/restore for cancellation, delivery failure and hook blocking. The risk areas the PR calls out (cancellation, budget exhaustion, delivery failure) are covered by these unit tests and the accept/restore E2E above.
Minor, non-blocking observation
On the text-streaming Enter-steer path, the steered message's ● notification is written to history by the transactional accept(), which fires after the steer response finishes. So it renders just below the model's steer response rather than at the point it was consumed (visible in screenshot 1, panel 3). This is a small chronological-ordering nuance, not a correctness issue — the content, turn membership and settlement are all correct.
Verdict
CI is green (Test (ubuntu, Node 22.x) ✅, web-shell E2E ✅); the only thing gating merge is REVIEW_REQUIRED. Behavior matches the PR description exactly, the new semantics are cleanly A/B-distinguishable from main, and there are no regressions in the touched suites. Looks good to merge from a verification standpoint.
Verified locally via isolated worktrees + real PTY/xterm E2E against a mock OpenAI SSE endpoint. Screenshots are faithful renders of the actual @xterm/headless buffer captured during each run.
中文版本(点击展开)
✅ 本地真实构建验证 —— PR #7090(同回合消息 steering)
在真实构建产物(不仅是单测 mock)上端到端验证了本 PR,head 052f5927,环境 macOS / Node v22.23.1,使用隔离 worktree。同时对 main(b9e5629d,本 PR 的合并基)做了 A/B 对比,以证明行为确实发生了改变。Reviewer 测试计划中的 5 个场景全部复现;构建、测试、类型检查全绿。
验证方式
- 在 PR head 上隔离
git worktree→ 干净的npm ci+npm run build(真实 bundle,已确认 PR 代码进入dist/)。为 A/B 在main上再建一个 worktree 同样构建。 - 单测 / 组件测试: 在 PR worktree 中运行真实
vitest。 - E2E: 通过 PTY(
@lydell/node-pty+@xterm/headless)驱动构建后的 CLI(node packages/cli/dist/index.js),后端为一个模拟 OpenAI 兼容 SSE 服务,它慢速流式输出,从而能在模型响应过程中输入消息。该 mock 记录每一次/chat/completions请求,提供回合归属的 wire 级视角。
1) 流式中按 Enter → 引导同一逻辑回合
在流式过程中输入消息并按 Enter。新的底栏提示 Enter to steer · Ctrl+Q to queue 出现;消息进入队列,随后在下一个模型采样边界、同一回合内被消费,并以 ● 通知记录。Wire 级证据:steer 请求(req#2)复用同一对话历史并原样追加输入文本,无合成前缀/包装,中间没有新回合/分类器请求(见英文版 req#1/req#2)。
2) A/B —— 相同的流式中 Enter:main(前)对比本 PR(后)
相同 PTY 脚本、相同 mock,仅 bundle 不同。在 main 上该消息在响应结束后变成一个独立的 > 用户回合(且没有 steer 底栏提示);在本 PR 上它引导同一回合。
3) Ctrl+Q → 明确延迟到全新回合
新增的 QUEUE_MESSAGE 绑定将消息标记为 deferUntilIdle。它在回合中的采样边界被跳过,仅在响应结束后作为新的 > 用户回合投递 —— 即改动前的“排队到下一回合”语义,现在放到了专用按键上。
4) ↑ 将排队内容恢复到编辑器
在流式过程中排队一条消息,投递前按 ↑ —— 内容被恢复到输入框(accept/restore 结算保护用户原始输入)。
5) @ 附件 steer 内联解析,无合成包装
在流式中输入 @note.txt summarize it + Enter。steer 请求携带用户原文以及解析后的文件正文,采用标准 @ 框架(而非旧的 prefixMidTurnUserMessageParts 包装,见英文版 parts 结构)。
构建 / 测试 / 静态检查结果
| 检查 | 结果 |
|---|---|
npm ci + npm run build —— PR bundle |
✅ exit 0 |
npm ci + npm run build —— main 基线(A/B) |
✅ exit 0 |
tsc --noEmit —— packages/core + packages/cli |
✅ 无错误 |
core · client.test.ts(含 9 个新 steer 边界测试) |
✅ 255 通过 |
cli · useGeminiStream + AppContainer |
✅ 281 通过 |
cli · useMessageQueue、InputPrompt、Footer、QueuedMessageDisplay、keyMatchers |
✅ 286 通过 |
| 单测 / 组件合计 | ✅ 822 通过 · 0 失败 |
新增的 18 个测试覆盖了每个边界:steer 绕过 UserPromptSubmit、在 Stop hook 前消费、阻断式 Stop hook 期间排队的输入、next-speaker 分类期间排队的输入、无预算不 drain,以及取消 / 投递失败 / hook 阻断下的事务性 accept/restore。PR 自述的风险点(取消、预算耗尽、投递失败)均由这些单测和上面的 accept/restore E2E 覆盖。
小的、非阻塞的观察
在文本流式 Enter-steer 路径上,steer 消息的 ● 通知由事务性 accept() 写入历史,而它在 steer 响应结束后才触发。因此它渲染在模型 steer 响应的下方,而非在其被消费的位置(见截图 1 第 3 栏)。这只是一个轻微的时间顺序细节,并非正确性问题 —— 内容、回合归属与结算都正确。
结论
CI 全绿(Test (ubuntu, Node 22.x) ✅、web-shell E2E ✅);唯一阻塞合并的是 REVIEW_REQUIRED。行为与 PR 描述完全一致,新语义与 main 可清晰 A/B 区分,受影响的测试套件无回归。从验证角度看,建议合并。
QwenLM#7219) PR QwenLM#7090 added a `StreamingState.Responding` branch in the Footer's `leftBottomContent` priority chain that takes precedence over the `AutoAcceptIndicator`, causing the approval mode name text to disappear while the agent is responding. Users switching modes with Shift+Tab during streaming only saw the input prompt border color change. Include the `AutoAcceptIndicator` in the streaming branch so both the steering hint and the mode name are visible at the same time. Fixes QwenLM#7217 Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>





What this PR does
This PR makes messages entered while the model is responding steer the current logical turn at the next model sampling boundary. It adds an explicit Ctrl+Q path for deferring a message to the next fresh turn, keeps messages entered during tool execution in the same logical turn after tool results arrive, and preserves queued input when delivery is cancelled or cannot be accepted. The TUI now exposes the two behaviors in its footer, help, shortcut list, queued-message hint, and English, Simplified Chinese, and Traditional Chinese translations.
Why it's needed
Previously, plain-text input submitted while a response was streaming could wait until the response fully completed and then become a separate prompt, while Ctrl+Q had no dedicated defer behavior. That made steering inconsistent across text streaming and tool execution. The new behavior gives Enter and Ctrl+Q distinct, predictable semantics while preserving user-authored content, including resolved @ attachments, without synthetic prompt prefixes.
Reviewer Test Plan
How to verify
Evidence (Before & After)
A real bundled TUI session on macOS verified plain-text steering, Ctrl+Q deferral, steering during tool execution, and Up-based queue restoration.
Tested on
Environment (optional)
Built bundle running in an isolated HOME, temporary runtime directory, and tmux session with the qwen3.7-plus model.
Risk & Scope
Linked Issues
None.
中文说明
这个 PR 做了什么
这个 PR 让模型响应过程中通过 Enter 提交的消息在下一个模型采样边界引导当前逻辑回合。它新增了通过 Ctrl+Q 将消息明确延迟到下一个全新回合的路径,让工具执行期间输入的消息在工具结果返回后继续留在同一逻辑回合,并在投递被取消或无法接受时保留排队输入。TUI 的底栏、帮助、快捷键列表、排队消息提示,以及英文、简体中文和繁体中文翻译现在都会展示这两种行为。
为什么需要它
此前,在流式响应过程中提交的纯文本输入可能会一直等待响应完全结束,随后成为一个独立的新提示,而 Ctrl+Q 没有专门的延迟行为。这使文本流式响应和工具执行期间的 steer 语义不一致。新行为让 Enter 和 Ctrl+Q 具有明确且可预测的不同语义,同时保留用户原始输入内容,包括解析后的 @ 附件,不添加合成的提示前缀。
Reviewer 测试计划
如何验证
证据(Before & After)
已在 macOS 上使用真实打包 TUI 会话验证纯文本 steer、Ctrl+Q 延迟、工具执行期间 steer 和基于 Up 的队列恢复。
测试平台
环境(可选)
在隔离的 HOME、临时运行目录和 tmux 会话中运行构建后的 bundle,使用 qwen3.7-plus 模型。
风险与范围
关联 Issue
无。