qwen-code/packages/web-shell/client/utils/localCommandQueue.ts
ytahdn 1609bdaa32
feat(web-shell): queue prompts while turns are running (#6005)
* feat(web-shell): queue prompts while turns are running

* fix(web-shell): address pending prompt review feedback

* fix(web-shell): tighten queued prompt event handling

* fix(web-shell): avoid showing active prompt as queued

* fix(web-shell): address queued prompt review follow-ups

* fix(web-shell): address pending prompt review issues

* fix(web-shell): reconcile queued prompt actions from server

* fix(daemon): address pending prompt critical review

* test(webui): expect submit abort signal forwarding

* fix(web-shell): avoid duplicate queued prompt sync

* fix(web-shell): keep local slash commands out of queue

* fix(webui): avoid aborting prompt admission

* fix(daemon): avoid queued prompt cancel cascades

* fix(webui): avoid stale client id for queue cleanup

* test(webui): update stale session queue cleanup expectation

* fix(web-shell): preserve queue reconciliation identity

* fix(web-shell): guard queue clear session writes

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-06-30 14:51:28 +00:00

60 lines
2.4 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.
*/
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.
*/
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('!');
}