fix(cli): dispatch queued slash commands through the slash path#3523
Merged
Conversation
When the agent was responding and the user queued a message, the drain path joined all queued messages with `\n\n` and submitted them as one prompt. Any slash command in that blob (e.g. `/model`) no longer started with `/`, so it was sent to the model as plain text instead of opening the command's dialog. The mid-turn tool-result drain had the same problem: it drained the entire queue into the tool-result payload, so a slash command queued during tool execution was injected as context for the model rather than executed as a command. Queue draining now splits into segments — consecutive plain-text messages are still batched into one submission, while slash commands are submitted alone so their `/` prefix survives. The mid-turn drain only takes leading plain-text messages and leaves slash commands queued for the normal idle drain. The idle drain is gated on open dialogs so a queued `/model` does not cause the following queued prompt to be sent to the model while the picker is still open, and a re-entry lock plus a nonce close the race between state commits and the async dialog-open.
Contributor
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. |
wenshao
previously requested changes
Apr 24, 2026
Cancel's contract is now "abort and redirect" in both cancel paths: restore the most recent queued segment into the buffer for editing and drop the rest, so forgotten follow-ups cannot auto-submit once the turn settles. Previously the non-tool path left queued plain-text segments in place for the idle drain to fire, and the tool-executing path cleared only the buffer — both surprised users with belated message dispatches after they had already cancelled.
Idle drain now runs in two phases: drain all plain-text prompts into one turn (drainQueue), then pop slash commands one-by-one (popNextSegment). Mirrors the mid-turn behavior so queue handling is consistent across mid-turn and idle contexts. popAllMessages now drains the entire queue joined with \n\n for Ctrl+C cancel and ESC/Up edit-restore. Drop the unused options parameter from useMessageQueue and the extractFirstSegment helper.
Collaborator
Author
E2E verification12/12 scenarios pass against the rebuilt bundle (interactive tmux sessions).
Key C tests for the design change:
Unit tests: |
xaelistic
pushed a commit
to xaelistic/qwen-code
that referenced
this pull request
Jun 7, 2026
…LM#3523) * fix(cli): dispatch queued slash commands through the slash path When the agent was responding and the user queued a message, the drain path joined all queued messages with `\n\n` and submitted them as one prompt. Any slash command in that blob (e.g. `/model`) no longer started with `/`, so it was sent to the model as plain text instead of opening the command's dialog. The mid-turn tool-result drain had the same problem: it drained the entire queue into the tool-result payload, so a slash command queued during tool execution was injected as context for the model rather than executed as a command. Queue draining now splits into segments — consecutive plain-text messages are still batched into one submission, while slash commands are submitted alone so their `/` prefix survives. The mid-turn drain only takes leading plain-text messages and leaves slash commands queued for the normal idle drain. The idle drain is gated on open dialogs so a queued `/model` does not cause the following queued prompt to be sent to the model while the picker is still open, and a re-entry lock plus a nonce close the race between state commits and the async dialog-open. * fix(cli): defer queued slash commands until idle * fix(cli): drop queued messages on cancel instead of auto-submitting Cancel's contract is now "abort and redirect" in both cancel paths: restore the most recent queued segment into the buffer for editing and drop the rest, so forgotten follow-ups cannot auto-submit once the turn settles. Previously the non-tool path left queued plain-text segments in place for the idle drain to fire, and the tool-executing path cleared only the buffer — both surprised users with belated message dispatches after they had already cancelled. * refactor(cli): batch plain prompts in idle drain Idle drain now runs in two phases: drain all plain-text prompts into one turn (drainQueue), then pop slash commands one-by-one (popNextSegment). Mirrors the mid-turn behavior so queue handling is consistent across mid-turn and idle contexts. popAllMessages now drains the entire queue joined with \n\n for Ctrl+C cancel and ESC/Up edit-restore. Drop the unused options parameter from useMessageQueue and the extractFirstSegment helper. --------- Co-authored-by: 愚远 <zhenxing.tzx@alibaba-inc.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TLDR
Queued slash commands and prompts are dispatched in two consistent phases — one for mid-turn (during tool execution), one for idle. Plain-text prompts are batched together in both phases, while slash commands are deferred and routed through the normal slash path so dialogs open and handlers run. The cancel path drains the entire queue back into the buffer for editing.
Screenshots / Video Demo
N/A — TUI behavior change. Repro and verification are described in the test plan below; the visible difference is that slash commands queued during tool execution no longer get injected as plain-text follow-up context, and instead run when the app is idle again.
Dive Deeper
The message queue has three drain consumers, each with consistent rules:
Mid-turn drain (
useGeminiStream.ts→drainQueue). When a tool completes and the agent assembles the tool-result follow-up, every queued plain-text prompt is appended to the follow-up as[User message received during tool execution]: …. Slash commands are skipped — they are TUI control actions, not model input — and remain queued for the idle drain. Plain prompts queued AFTER a slash command are still drained mid-turn, hitching on the in-flight API call rather than waiting an extra round trip.Idle drain (
AppContainer.tsx) — two phases mirroring the mid-turn behavior:drainQueue()pulls every queued plain-text prompt and submits them joined with\n\nas a single new turn.popNextSegment()pops the next slash command and submits it through the normal slash path soisSlashCommandopens the right dialog or runs the handler.Cancel / edit-restore (
popAllMessages). Ctrl+C, ESC, and Up arrow at the top of the buffer all drain the entire queue joined with\n\ninto the input buffer for editing. Segment boundaries are intentionally collapsed because the user's next stop is the text editor, not the routing layer.A slash command typed while already idle still bypasses the queue and executes immediately — the change here is specifically about what happens after a command was queued during an active turn.
One intentional trade-off: in the idle drain, strict typing order is not preserved. Queued
[/model, hello]submitshelloto the OLD model first, then opens the picker. This favors fewer round trips and consistent queue handling across mid-turn and idle contexts over preserving the implicit "switch model before next prompt" intent of typing order.Reviewer Test Plan
Start an agent turn that takes a while (a long essay prompt, or a shell tool like
!sleep 30 && echo done). While it runs, queue the following messages and confirm the behavior after the agent finishes:hellothen/modelduring a normal model turn. Expected:hellois sent to the model; when the turn becomes idle, the model picker opens./modelthenhelloduring a normal model turn. Expected: when the turn becomes idle,hellois submitted FIRST (to the old model); after the model responds, the picker opens./modelduring a tool call. Expected:/modelis not injected as[User message received during tool execution]: /model; once the turn returns to idle, the model picker opens./modeland thenhelloduring a tool call. Expected:hellois appended to the tool-result follow-up as plain text and the model responds to it;/modelremains queued and opens the picker once the app returns to idle./modelthenhello, press Ctrl+C. Expected: the input buffer is restored with/model\n\nhello(the full queue joined). Queue is empty. Nothing auto-submits.hellothenworld(no slash). Expected: submitted as one turn whose body ishello\n\nworld.hello,/model,worldduring a normal model turn. Expected: when the turn becomes idle,hello\n\nworldis submitted as ONE batched turn (both prompts batched, even though/modelwas typed between them); after the model responds, the picker opens.Testing Matrix
Validation run:
Linked issues / bugs
N/A (reported directly).