Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions packages/web-shell/client/hooks/useQueuedPrompts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { describe, expect, it } from 'vitest';
import { mergeRestoredPromptText } from './useQueuedPrompts';

// Regression for #7128: restoration paths can fire more than once for the
// same prompt (failed submit + reconnect/refresh, queue clear racing an
// abort), and a user retrying an identical message restores identical text.
// Stacking those copies is what surfaced as "sent messages concatenated back
// into the input box after refresh".
describe('mergeRestoredPromptText', () => {
it('fills an empty editor with the restored text', () => {
expect(mergeRestoredPromptText('', 'hello')).toBe('hello');
expect(mergeRestoredPromptText(' ', 'hello')).toBe('hello');
});

it('prepends above a different draft the user is typing', () => {
expect(mergeRestoredPromptText('draft', 'restored')).toBe(
'restored\ndraft',
);
});

it('is a no-op when the same text was already restored', () => {
expect(mergeRestoredPromptText('hello', 'hello')).toBe('hello');
});

it('is a no-op when the text already sits at the top of the editor', () => {
expect(mergeRestoredPromptText('hello\ndraft', 'hello')).toBe(
'hello\ndraft',
);
});

it('stays idempotent across repeated restores of the same prompt', () => {
let editor = '';
for (let i = 0; i < 3; i++) {
editor = mergeRestoredPromptText(editor, '用python写一个hello world');
}
expect(editor).toBe('用python写一个hello world');
});

it('does not treat a same-prefix but different first line as a duplicate', () => {
expect(mergeRestoredPromptText('hello world\ndraft', 'hello')).toBe(
'hello\nhello world\ndraft',
);
});
});
21 changes: 19 additions & 2 deletions packages/web-shell/client/hooks/useQueuedPrompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,21 @@ interface UseQueuedPromptsArgs {

const MAX_COMPLETED_PROMPT_IDS = 100;

/**
* Merge a restored prompt's text into the editor content. Restoration paths
* (failed submits, failed mid-turn inserts, queue clears) prepend the prompt
* above whatever the user is currently typing — but several of them can fire
* for the same prompt across reconnects/refreshes, and a user retrying an
* identical message produces the same text twice. Stacking those copies is
* what #7128 reports as "inputs concatenated after refresh", so restoring
* text that is already present at the top of the editor is a no-op.
*/
export function mergeRestoredPromptText(current: string, text: string): string {
if (!current.trim()) return text;
if (current === text || current.startsWith(`${text}\n`)) return current;
return `${text}\n${current}`;
}

type RefreshPendingPromptsResult =
| 'refreshed'
| 'skipped'
Expand Down Expand Up @@ -286,8 +301,10 @@ export function useQueuedPrompts({
return;
}
const current = editorRef.current?.getText() ?? '';
const next = current.trim() ? `${text}\n${current}` : text;
editorRef.current?.setText(next);
const next = mergeRestoredPromptText(current, text);
if (next !== current) {
editorRef.current?.setText(next);
}
if (images && images.length > 0) {
editorRef.current?.restoreImages(images);
}
Comment on lines 308 to 310

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] restoreImages is outside the if (next !== current) dedup guard — images are unconditionally appended on every restore call via setPastedImages((prev) => [...prev, ...images]), while text dedup correctly skips setText. — Failure scenario: a prompt with attached images fails to submit, restore fires, then a reconnect/retry triggers a second restore of the same prompt → text stays correct (dedup works) but images silently double, producing an inconsistent editor state.

Suggested change
if (images && images.length > 0) {
editorRef.current?.restoreImages(images);
}
if (next !== current) {
editorRef.current?.setText(next);
if (images && images.length > 0) {
editorRef.current?.restoreImages(images);
}
}

— qwen3.7-max via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in follow-up PR #7169: restoreImages now runs only alongside an actual text change, so a deduplicated restore no longer doubles the attachments.

Expand Down
Loading