qwen-code/packages/web-shell/client/utils/localCommandQueue.ts
Shaojin Wen 6b4a6295a2
Some checks failed
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
Sync cua-driver to Aliyun OSS / Mirror cua-driver binaries to Aliyun OSS (push) Has been cancelled
feat(web-shell): run read-only info commands immediately mid-turn (#8496)
* feat(web-shell): run read-only info commands immediately mid-turn

/stats, /about (/status) and /context were silently swallowed while a
turn was streaming, because their local user echo would act as a turn
boundary in applyTurnCollapse and split the active turn. Their output
is a status block, which is not a turn boundary and is not counted in
turn metrics, so only the echo needs to be skipped mid-turn.

Add appendLocalUserEchoIfIdle, which echoes when idle and skips the
echo while streaming without blocking the command, and switch these
three commands to it so their results render inline immediately even
during an active turn.

* fix(web-shell): keep streaming assistant block intact for mid-turn info commands

Address PR review. A status dispatch finalizes the active assistant
block by default, so running /stats, /about or /context mid-turn would
fragment the streaming answer around the status card and drop later
usage frames. Add an optional clearActiveText flag to the status event
and pass false from these three command dispatches, covered by
reducer-level tests.

Also collapse the echo gate into a single body (the new helper now
delegates to appendOrDeferLocalUserMessage), add App-level wiring tests
for the responding/idle behavior of /stats and /about, and surface
failed getStats via reportError instead of swallowing them.

* fix(web-shell): reset the echo user block when info output keeps streaming (#8496)

The clearActiveText: false opt-out skipped the whole clearActiveText call,
leaving the local command echo as the active user block indefinitely. A
peer client's prompt echo then merged into it, corrupting turn boundaries.
Keep the streaming assistant/thought block open on the opt-out path but
still drop the user pointer.

Also report /about load failures like /stats and /context already do, and
pin the new /context wiring plus the /stats failure path with tests.

* refactor(web-shell): centralize the mid-turn read-only status dispatch (#8496)

Address the round-3 review feedback on the mid-turn read-only commands:

- The read-only result dispatch (status block with clearActiveText:
  false plus the follow-resume) was copied verbatim at the /context,
  /stats, and /about sites, leaving the load-bearing flag enforced by
  convention at three places. Centralize it in one
  dispatchReadOnlyStatus callback next to echoLocalCommandIfIdle; the
  three .then bodies now call it with their serialized text.
- Pin the /about catch the way the sibling /stats catch is pinned:
  make collectSystemInfo throw once and assert the failure surfaces
  through console.error instead of becoming an unhandled rejection
  with zero user feedback.

* test: pin thought opt-out and serialized mid-turn status payloads (#8496)

* refactor(web-shell): consolidate read-only command echo into echoOrDeferLocalCommand (#8496)

* test(web-shell): cover /status routing and document the echo-suppression exception (#8496)

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-05 14:05:08 +00:00

64 lines
2.7 KiB
TypeScript

import type { PromptImage } from '../adapters/promptTypes';
/**
* Single choke point for echoing a local slash command into the transcript.
*
* Some local commands "echo": they append a local user message
* (`store.appendLocalUserMessage`) and render their result inline. If one runs
* while a turn is streaming, the injected user row acts as a turn boundary in
* `applyTurnCollapse` (a turn spans one user message up to the next) and splits
* the active turn into two — its tool/thinking/token counters are then computed
* per fragment and come out wrong.
*
* Routing every echo through this helper means a command can never append to the
* transcript mid-turn. While a turn is in flight the command is suppressed
* instead of being added to the daemon pending-prompt queue, because local
* commands must not be replayed as model-facing prompt text.
*
* The only call sites that should bypass this and append mid-stream are the
* deliberate "busy acknowledgement" paths (e.g. clearing a goal while a turn
* runs), which opt in by calling `append` directly. Read-only display
* commands (/stats, /about, /context) go through the same helper but ignore
* its suppression signal: they skip the echo mid-turn and still run
* immediately.
*/
export interface LocalEchoSink {
/** Append the command as a local user message (renders inline immediately). */
append: (text: string) => void;
}
/**
* Append a local command's echo, or suppress it if a turn is streaming.
*
* @returns `true` if the command was suppressed — the caller must stop and not
* run its inline side effects. `false` if it was appended and the caller
* should proceed. Read-only display commands are the deliberate exception:
* they ignore the signal and run mid-turn anyway (see the module docstring).
*/
export function appendOrDeferLocalUserMessage(
isStreaming: boolean,
text: string,
_images: PromptImage[] | undefined,
sink: LocalEchoSink,
): boolean {
if (isStreaming) {
return true;
}
sink.append(text);
return false;
}
/**
* Whether a queued prompt is a slash (`/…`) or shell (`!…`) command rather than
* model-facing prose.
*
* The queue's "insert" action injects the raw text into the running turn via
* `enqueueMidTurnMessage` — it is NOT re-dispatched as a command, so a command
* inserted this way reaches the model as the literal string "/context …" and
* never runs. Callers use this to disable "insert" for command entries that may
* still exist from daemon/custom command paths or from older sessions.
*/
export function isCommandPrompt(text: string): boolean {
const trimmed = text.trimStart();
return trimmed.startsWith('/') || trimmed.startsWith('!');
}