From cdaec3b7c8146764c13788ed5e073856030c5fdb Mon Sep 17 00:00:00 2001 From: ciregenz Date: Thu, 11 Jun 2026 03:52:42 -0700 Subject: [PATCH] [eric] chat: prefill caret parks at end (not pos 0) + skip the background reconcile-fetch while streaming so a mid-stream reopen stops wiping the user bubble --- .../src/app/pages/AgentChat/AgentChat.tsx | 10 +++++++++- .../src/app/pages/AgentChat/ChatInput.tsx | 19 +++++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/pages/AgentChat/AgentChat.tsx b/frontend/src/app/pages/AgentChat/AgentChat.tsx index b6592d93..a0441991 100644 --- a/frontend/src/app/pages/AgentChat/AgentChat.tsx +++ b/frontend/src/app/pages/AgentChat/AgentChat.tsx @@ -344,7 +344,15 @@ const AgentChat: React.FC = ({ sessionId: sessionIdProp, onClose // let the fetch reconcile in the background; awaiting serialized a slow // round trip in front of the live stream on every reopen. const warm = !!store.getState().agents.sessions[id]?.messages?.length; - if (warm) { + // Mid-stream reopen: the server snapshot predates the just-sent user turn + // (not persisted until the turn ends), and the WS echo already cleared the + // bubble's optimistic flag, so the background reconcile would DROP the user + // message until the stream finishes. The warm store + the kept-alive socket + // are authoritative here, so skip the fetch; stream-end persists everything. + const streamingNow = !!store.getState().streaming.bySession[id]; + if (warm && streamingNow) { + // no fetch: don't let a stale snapshot wipe the in-flight turn + } else if (warm) { dispatch(fetchSession(id)); } else { try { diff --git a/frontend/src/app/pages/AgentChat/ChatInput.tsx b/frontend/src/app/pages/AgentChat/ChatInput.tsx index 3b215deb..3f111815 100644 --- a/frontend/src/app/pages/AgentChat/ChatInput.tsx +++ b/frontend/src/app/pages/AgentChat/ChatInput.tsx @@ -70,8 +70,23 @@ const ChatInput = forwardRef(({ onSend, disabled, mode, if (!prefillPrompt || prefilledRef.current === prefillPrompt) return; const editor = editorRef.current; if (!editor) return; - if (editor.tagName === 'TEXTAREA') (editor as unknown as HTMLTextAreaElement).value = prefillPrompt; - else editor.textContent = prefillPrompt; + if (editor.tagName === 'TEXTAREA') { + const ta = editor as unknown as HTMLTextAreaElement; + ta.value = prefillPrompt; + ta.setSelectionRange(prefillPrompt.length, prefillPrompt.length); + } else { + editor.textContent = prefillPrompt; + // Park the caret AFTER the seeded text, not at position 0 (the default for a + // freshly-set textContent), so the user types/sends from the end. + const sel = window.getSelection(); + if (sel) { + const range = document.createRange(); + range.selectNodeContents(editor); + range.collapse(false); + sel.removeAllRanges(); + sel.addRange(range); + } + } setHasContent(true); prefilledRef.current = prefillPrompt; editor.style.opacity = '0.5';