mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-21 06:54:12 +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.
34 lines
1.1 KiB
TypeScript
34 lines
1.1 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { canDrainQueue, type QueueDrainGate } from './queueDrain';
|
|
|
|
// A gate where every condition is satisfied — the next prompt should drain.
|
|
const ready: QueueDrainGate = {
|
|
draining: false,
|
|
awaitingTurnStart: false,
|
|
connected: true,
|
|
streaming: false,
|
|
interactionBlocked: false,
|
|
pendingApproval: false,
|
|
queueLength: 1,
|
|
};
|
|
|
|
describe('canDrainQueue', () => {
|
|
it('drains when every condition is satisfied', () => {
|
|
expect(canDrainQueue(ready)).toBe(true);
|
|
});
|
|
|
|
it('holds the queue when it is empty', () => {
|
|
expect(canDrainQueue({ ...ready, queueLength: 0 })).toBe(false);
|
|
});
|
|
|
|
it.each([
|
|
['a drain is already in flight', { draining: true }],
|
|
['waiting for the prior turn to start', { awaitingTurnStart: true }],
|
|
['the connection is down', { connected: false }],
|
|
['a turn is streaming', { streaming: true }],
|
|
['interaction is blocked', { interactionBlocked: true }],
|
|
['a tool approval is pending', { pendingApproval: true }],
|
|
] as const)('holds the queue when %s', (_label, override) => {
|
|
expect(canDrainQueue({ ...ready, ...override })).toBe(false);
|
|
});
|
|
});
|