From bb2164c9459eeeff603aebd540701918e0d9286f Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 26 Feb 2026 20:49:17 -0800 Subject: [PATCH] Prevent duplicate sends and sync worktree branch state - Guard `onSend` with an in-flight ref to block double submits - Set send phase before worktree prep and always clear in-flight state in `finally` - Sync thread branch/worktree path immediately and await setup script execution --- apps/web/src/components/ChatView.tsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 501742c7ec8c181e2b834908d9ea2f5fc66797e0..64ac9e1bd3e70151893bd672c5e4b83810f39038 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -368,6 +368,7 @@ export default function ChatView({ threadId }: ChatViewProps) { const textareaRef = useRef(null); const composerCommandInputRef = useRef(null); const composerImagesRef = useRef([]); + const sendInFlightRef = useRef(false); const dragDepthRef = useRef(0); const terminalOpenByThreadRef = useRef>({}); @@ -1391,7 +1392,7 @@ export default function ChatView({ threadId }: ChatViewProps) { const onSend = async (e: React.SubmitEvent | React.KeyboardEvent) => { e.preventDefault(); const api = readNativeApi(); - if (!api || !activeThread || isSendBusy || isConnecting) return; + if (!api || !activeThread || isSendBusy || isConnecting || sendInFlightRef.current) return; const trimmed = prompt.trim(); if (!trimmed && composerImages.length === 0) return; if (!activeProject) return; @@ -1415,6 +1416,9 @@ export default function ChatView({ threadId }: ChatViewProps) { return; } + sendInFlightRef.current = true; + setSendPhase(baseBranchForWorktree ? "preparing-worktree" : "sending-turn"); + const composerImagesSnapshot = [...composerImages]; const messageIdForSend = newMessageId(); const messageCreatedAt = new Date().toISOString(); @@ -1463,9 +1467,17 @@ export default function ChatView({ threadId }: ChatViewProps) { branch: result.worktree.branch, worktreePath: result.worktree.path, }); + // Keep local thread state in sync immediately so terminal drawer opens + // with the worktree cwd/env instead of briefly using the project root. + dispatch({ + type: "SET_THREAD_BRANCH", + threadId: threadIdForSend, + branch: result.worktree.branch, + worktreePath: result.worktree.path, + }); const setupScript = setupProjectScript(activeProject.scripts); if (setupScript) { - void runProjectScript(setupScript, { + await runProjectScript(setupScript, { cwd: result.worktree.path, worktreePath: result.worktree.path, rememberAsLastInvoked: false, @@ -1544,6 +1556,7 @@ export default function ChatView({ threadId }: ChatViewProps) { err instanceof Error ? err.message : "Failed to send message.", ); } finally { + sendInFlightRef.current = false; setSendPhase("idle"); } }; -- 2.54.0