mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-15 19:54:54 +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.
61 lines
2 KiB
TypeScript
61 lines
2 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
import { act, type ReactNode } from 'react';
|
|
import { createRoot, type Root } from 'react-dom/client';
|
|
import { I18nProvider } from '../../i18n';
|
|
import { SystemMessage } from './SystemMessage';
|
|
|
|
(
|
|
globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
).IS_REACT_ACT_ENVIRONMENT = true;
|
|
|
|
const mounted: Array<{ root: Root; container: HTMLElement }> = [];
|
|
|
|
afterEach(() => {
|
|
for (const { root, container } of mounted.splice(0)) {
|
|
act(() => root.unmount());
|
|
container.remove();
|
|
}
|
|
});
|
|
|
|
function render(node: ReactNode): HTMLElement {
|
|
const container = document.createElement('div');
|
|
document.body.appendChild(container);
|
|
const root = createRoot(container);
|
|
act(() => {
|
|
root.render(<I18nProvider language="en">{node}</I18nProvider>);
|
|
});
|
|
mounted.push({ root, container });
|
|
return container;
|
|
}
|
|
|
|
describe('SystemMessage — prompt_cancelled marker', () => {
|
|
it('renders the user-cancelled marker as a status region', () => {
|
|
const container = render(
|
|
<SystemMessage content="" variant="info" source="prompt_cancelled" />,
|
|
);
|
|
const status = container.querySelector('[role="status"]');
|
|
expect(status).not.toBeNull();
|
|
expect(status?.textContent).toBe('You cancelled this request');
|
|
});
|
|
|
|
it('ignores message content when rendering the cancelled marker', () => {
|
|
const container = render(
|
|
<SystemMessage
|
|
content="raw daemon text that must not leak"
|
|
variant="info"
|
|
source="prompt_cancelled"
|
|
/>,
|
|
);
|
|
expect(container.textContent).toBe('You cancelled this request');
|
|
expect(container.textContent).not.toContain('raw daemon text');
|
|
});
|
|
|
|
it('renders a normal message without the status marker for other sources', () => {
|
|
const container = render(
|
|
<SystemMessage content="a plain note" variant="error" />,
|
|
);
|
|
expect(container.querySelector('[role="status"]')).toBeNull();
|
|
expect(container.textContent).toContain('a plain note');
|
|
});
|
|
});
|