mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-27 18:04:37 +00:00
* fix(web-shell): prevent queued-prompt loss from drain race The auto-drain effect popped a queued prompt, called setQueuedPrompts, then submitted via setTimeout(0). Because the daemon flips streamingState asynchronously, the setState re-render could re-run the effect and pop a second prompt before the first registered as streaming — both submitted back-to-back and the first was lost. Arm an "awaiting turn start" gate synchronously at pop so the re-run is blocked until streamingState goes non-idle, released by a dedicated effect with a safety-net timer for a prompt that never streams (e.g. a queued slash command). Cleanup no longer cancels/re-queues the pending submit while the gate is armed. * feat(web-shell): friendlier Esc interruption + queued-prompt UX * refactor(web-shell): tidy Esc/queue code per review Behavior-preserving cleanups addressing review feedback on the Esc-interruption and queued-prompt changes: - Remove the now-dead queue.footer i18n key (EN + ZH) and the unreferenced .queuedHint CSS, orphaned when the Esc-clears-queue behavior was dropped. - Co-locate the queued-prompt styles in QueuedPromptDisplay.module.css instead of reaching into the parent App.module.css. - Make the Esc confirm-window constants the single source of truth: export them from escapeIntent.ts and drive the countdown-ring duration from one of them via a CSS custom property. - Nudge the queue-drain safety net with a dedicated tick counter instead of cloning queuedPrompts, so it no longer re-renders the composer for a no-op. - Drop a redundant !compact guard in StatusBar left over from flattening a ternary. - Document the pop/gate-arm ordering invariant in the drain effect.
77 lines
2.2 KiB
TypeScript
77 lines
2.2 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { decideEscapeIntent, type EscapeContext } from './escapeIntent';
|
|
|
|
const base: EscapeContext = {
|
|
blocked: false,
|
|
streaming: false,
|
|
hasInput: false,
|
|
armed: null,
|
|
};
|
|
|
|
describe('decideEscapeIntent', () => {
|
|
it('ignores Escape while blocked, even with a stream or input', () => {
|
|
expect(decideEscapeIntent({ ...base, blocked: true })).toEqual({
|
|
kind: 'ignore',
|
|
});
|
|
expect(
|
|
decideEscapeIntent({
|
|
...base,
|
|
blocked: true,
|
|
streaming: true,
|
|
hasInput: true,
|
|
armed: 'cancel',
|
|
}),
|
|
).toEqual({ kind: 'ignore' });
|
|
});
|
|
|
|
it('arms cancel on the first Esc while streaming', () => {
|
|
expect(decideEscapeIntent({ ...base, streaming: true })).toEqual({
|
|
kind: 'arm',
|
|
action: 'cancel',
|
|
});
|
|
});
|
|
|
|
it('confirms cancel on the second Esc while streaming', () => {
|
|
expect(
|
|
decideEscapeIntent({ ...base, streaming: true, armed: 'cancel' }),
|
|
).toEqual({ kind: 'cancel' });
|
|
});
|
|
|
|
it('re-arms cancel (not clear) when a clear-armed press lands while streaming', () => {
|
|
expect(
|
|
decideEscapeIntent({ ...base, streaming: true, armed: 'clear' }),
|
|
).toEqual({ kind: 'arm', action: 'cancel' });
|
|
});
|
|
|
|
it('prioritises streaming over composer text', () => {
|
|
expect(
|
|
decideEscapeIntent({ ...base, streaming: true, hasInput: true }),
|
|
).toEqual({ kind: 'arm', action: 'cancel' });
|
|
});
|
|
|
|
it('arms clear on the first Esc with text and no stream', () => {
|
|
expect(decideEscapeIntent({ ...base, hasInput: true })).toEqual({
|
|
kind: 'arm',
|
|
action: 'clear',
|
|
});
|
|
});
|
|
|
|
it('confirms clear on the second Esc with text', () => {
|
|
expect(
|
|
decideEscapeIntent({ ...base, hasInput: true, armed: 'clear' }),
|
|
).toEqual({ kind: 'clear' });
|
|
});
|
|
|
|
it('re-arms clear when a stale cancel-armed press lands with text', () => {
|
|
expect(
|
|
decideEscapeIntent({ ...base, hasInput: true, armed: 'cancel' }),
|
|
).toEqual({ kind: 'arm', action: 'clear' });
|
|
});
|
|
|
|
it('ignores Escape with no stream and no text', () => {
|
|
expect(decideEscapeIntent(base)).toEqual({ kind: 'ignore' });
|
|
expect(decideEscapeIntent({ ...base, armed: 'clear' })).toEqual({
|
|
kind: 'ignore',
|
|
});
|
|
});
|
|
});
|