From 3d14f01fd674ab2add062d3a302c30a6b457b791 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 10 Jun 2026 19:57:09 -0700 Subject: [PATCH] fix(desktop): debounce per-keystroke draft persistence writes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The salvaged draft-persistence effect wrote to localStorage on every keystroke — the composer's per-keystroke path was deliberately slimmed down previously, so debounce the write (400ms) and flush pending text on scope change/unmount so a fast session switch can't drop trailing keystrokes. Also add AUTHOR_MAP entry for the salvaged commit. --- apps/desktop/src/app/chat/composer/index.tsx | 33 +++++++++++++++++++- scripts/release.py | 1 + 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/app/chat/composer/index.tsx b/apps/desktop/src/app/chat/composer/index.tsx index 05fa4a451bc..5530ffb60ae 100644 --- a/apps/desktop/src/app/chat/composer/index.tsx +++ b/apps/desktop/src/app/chat/composer/index.tsx @@ -137,6 +137,10 @@ interface QueueEditState { const cloneAttachments = (attachments: ComposerAttachment[]) => attachments.map(a => ({ ...a })) +// How long the composer waits after the last keystroke before persisting the +// draft to localStorage. Scope-change/unmount flushes bypass the delay. +const DRAFT_PERSIST_DEBOUNCE_MS = 400 + export function ChatBar({ busy, cwd, @@ -180,6 +184,7 @@ export function ChatBar({ const draftRef = useRef(draft) const previousBusyRef = useRef(busy) const skipNextDraftPersistScopeRef = useRef(null) + const pendingDraftPersistRef = useRef<{ scope: string | null; value: string } | null>(null) const drainingQueueRef = useRef(false) const urlInputRef = useRef(null) @@ -1119,9 +1124,35 @@ export function ChatBar({ return } - writePersistedComposerDraft(draftPersistenceScope, draft) + // Debounce the localStorage write: the composer's per-keystroke path was + // deliberately slimmed down (see the draftRef sync comment above), so we + // don't touch storage on every keypress. The pending ref below is flushed + // on scope change / unmount so a fast session switch can't drop the + // trailing keystrokes. + pendingDraftPersistRef.current = { scope: draftPersistenceScope, value: draft } + + const handle = window.setTimeout(() => { + pendingDraftPersistRef.current = null + writePersistedComposerDraft(draftPersistenceScope, draft) + }, DRAFT_PERSIST_DEBOUNCE_MS) + + return () => window.clearTimeout(handle) }, [draft, draftPersistenceScope]) + // Flush any pending debounced draft write when leaving a session scope or + // unmounting, so the departing session's latest text is always persisted. + useEffect( + () => () => { + const pending = pendingDraftPersistRef.current + + if (pending) { + pendingDraftPersistRef.current = null + writePersistedComposerDraft(pending.scope, pending.value) + } + }, + [draftPersistenceScope] + ) + const beginQueuedEdit = (entry: QueuedPromptEntry) => { if (!activeQueueSessionKey || queueEdit) { return diff --git a/scripts/release.py b/scripts/release.py index 68ad134d6cd..10ed7b658a2 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -76,6 +76,7 @@ AUTHOR_MAP = { "290859878+synapsesx@users.noreply.github.com": "synapsesx", "dirtyren@users.noreply.github.com": "dirtyren", "mharris@parallel.ai": "NormallyGaussian", + "roger@roger.local": "mollusk", "ted.malone@outlook.com": "temalo", "adityamalik2833@gmail.com": "alarcritty", "islam666@users.noreply.github.com": "islam666",