From 32e5fd9cc3bd3b28012b0ba0897fe456edd266f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E8=B4=B5=E8=90=8D0668001030?= Date: Sun, 21 Jun 2026 14:38:40 +0800 Subject: [PATCH] fix(control-ui): keep new draft through delayed replay --- ui/src/ui/views/chat.test.ts | 53 ++++++++++++++++++++++++++++++++++++ ui/src/ui/views/chat.ts | 17 +----------- 2 files changed, 54 insertions(+), 16 deletions(-) diff --git a/ui/src/ui/views/chat.test.ts b/ui/src/ui/views/chat.test.ts index 0c24b9f8aff2..f0da355f4996 100644 --- a/ui/src/ui/views/chat.test.ts +++ b/ui/src/ui/views/chat.test.ts @@ -2009,6 +2009,59 @@ describe("chat slash menu accessibility", () => { expect(onDraftChange).toHaveBeenCalledTimes(1); }); + it("keeps a new same-session draft when a delayed stale replay arrives", () => { + let draft = ""; + const container = document.createElement("div"); + const onDraftChange = vi.fn((next: string) => { + draft = next; + }); + const onSend = vi.fn(() => { + draft = ""; + }); + const renderWithDraft = () => { + render( + renderChat(createChatProps({ draft, getDraft: () => draft, onDraftChange, onSend })), + container, + ); + }; + + renderWithDraft(); + inputDraft(container, "submitted message"); + container.querySelector(".chat-send-btn")!.click(); + + const textarea = container.querySelector("textarea"); + expect(textarea?.value).toBe(""); + + textarea!.dispatchEvent( + new InputEvent("beforeinput", { + bubbles: true, + data: "new draft", + inputType: "insertText", + }), + ); + textarea!.value = "new draft"; + textarea!.dispatchEvent( + new InputEvent("input", { + bubbles: true, + data: "new draft", + inputType: "insertText", + }), + ); + expect(textarea?.value).toBe("new draft"); + + textarea!.value = "submitted message"; + textarea!.dispatchEvent( + new InputEvent("input", { + bubbles: true, + data: "submitted message", + inputType: "insertText", + }), + ); + + expect(textarea?.value).toBe("new draft"); + expect(onDraftChange).toHaveBeenCalledTimes(1); + }); + it("does not apply a stale submitted draft replay to another session", () => { const drafts: Record = { "stale-replay-a": "", diff --git a/ui/src/ui/views/chat.ts b/ui/src/ui/views/chat.ts index 40a9e9c329f5..83fc59708efe 100644 --- a/ui/src/ui/views/chat.ts +++ b/ui/src/ui/views/chat.ts @@ -509,7 +509,6 @@ function renderRealtimeTalkConversation(props: ChatProps) { type PendingClearedSubmittedDraft = { key: string; value: string; - inputVersion: number; }; interface ChatEphemeralState { @@ -526,7 +525,6 @@ interface ChatEphemeralState { pinnedExpanded: boolean; composerComposing: boolean; composerInputIntentKey: string | null; - composerInputVersions: Map; pendingClearedSubmittedDraft: PendingClearedSubmittedDraft | null; historyRenderSessionKey: string | null; historyRenderMessagesRef: unknown[] | null; @@ -556,7 +554,6 @@ function createChatEphemeralState(): ChatEphemeralState { pinnedExpanded: false, composerComposing: false, composerInputIntentKey: null, - composerInputVersions: new Map(), pendingClearedSubmittedDraft: null, historyRenderSessionKey: null, historyRenderMessagesRef: null, @@ -614,12 +611,7 @@ function commitComposerDraft(props: ChatProps, value: string): void { props.onDraftChange(value); } -function currentComposerInputVersion(key: string): number { - return vs.composerInputVersions.get(key) ?? 0; -} - function markComposerInputIntent(key: string): void { - vs.composerInputVersions.set(key, currentComposerInputVersion(key) + 1); vs.composerInputIntentKey = key; } @@ -651,12 +643,7 @@ function suppressStaleSubmittedDraftReplay( if (!pending) { return false; } - if ( - target.value !== pending.value || - pending.inputVersion !== currentComposerInputVersion(pending.key) || - hasInputIntent || - isExplicitComposerInsertion(event) - ) { + if (target.value !== pending.value || hasInputIntent || isExplicitComposerInsertion(event)) { return false; } @@ -2338,7 +2325,6 @@ export function renderChat(props: ChatProps) { vs.pendingClearedSubmittedDraft = { key: mirrorKey, value: submittedDraft, - inputVersion: currentComposerInputVersion(mirrorKey), }; } else { clearPendingClearedSubmittedDraft(mirrorKey); @@ -2513,7 +2499,6 @@ export function renderChat(props: ChatProps) { if (suppressStaleSubmittedDraftReplay(target, e, draftMirror, hasInputIntent)) { return; } - clearPendingClearedSubmittedDraft(mirrorKey); syncComposerValue(target); }; const handleCompositionEnd = (e: CompositionEvent) => {