authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-26 20:49:17-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-26 20:51:29-08:00
logbb2164c9459eeeff603aebd540701918e0d9286f
treef8569ae7cf014bd42df4df6340cdc4e5ff058487
parent8c91aa5570868091253be8f0ef333a46aa1b0450

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

1 files changed, 15 insertions(+), 2 deletions(-)

apps/web/src/components/ChatView.tsx+15-2
......@@ -368,6 +368,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
368368 const textareaRef = useRef<HTMLTextAreaElement>(null);
369369 const composerCommandInputRef = useRef<HTMLInputElement>(null);
370370 const composerImagesRef = useRef<ComposerImageAttachment[]>([]);
371 const sendInFlightRef = useRef(false);
371372 const dragDepthRef = useRef(0);
372373 const terminalOpenByThreadRef = useRef<Record<string, boolean>>({});
373374
......@@ -1391,7 +1392,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
13911392 const onSend = async (e: React.SubmitEvent | React.KeyboardEvent) => {
13921393 e.preventDefault();
13931394 const api = readNativeApi();
1394 if (!api || !activeThread || isSendBusy || isConnecting) return;
1395 if (!api || !activeThread || isSendBusy || isConnecting || sendInFlightRef.current) return;
13951396 const trimmed = prompt.trim();
13961397 if (!trimmed && composerImages.length === 0) return;
13971398 if (!activeProject) return;
......@@ -1415,6 +1416,9 @@ export default function ChatView({ threadId }: ChatViewProps) {
14151416 return;
14161417 }
14171418
1419 sendInFlightRef.current = true;
1420 setSendPhase(baseBranchForWorktree ? "preparing-worktree" : "sending-turn");
1421
14181422 const composerImagesSnapshot = [...composerImages];
14191423 const messageIdForSend = newMessageId();
14201424 const messageCreatedAt = new Date().toISOString();
......@@ -1463,9 +1467,17 @@ export default function ChatView({ threadId }: ChatViewProps) {
14631467 branch: result.worktree.branch,
14641468 worktreePath: result.worktree.path,
14651469 });
1470 // Keep local thread state in sync immediately so terminal drawer opens
1471 // with the worktree cwd/env instead of briefly using the project root.
1472 dispatch({
1473 type: "SET_THREAD_BRANCH",
1474 threadId: threadIdForSend,
1475 branch: result.worktree.branch,
1476 worktreePath: result.worktree.path,
1477 });
14661478 const setupScript = setupProjectScript(activeProject.scripts);
14671479 if (setupScript) {
1468 void runProjectScript(setupScript, {
1480 await runProjectScript(setupScript, {
14691481 cwd: result.worktree.path,
14701482 worktreePath: result.worktree.path,
14711483 rememberAsLastInvoked: false,
......@@ -1544,6 +1556,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
15441556 err instanceof Error ? err.message : "Failed to send message.",
15451557 );
15461558 } finally {
1559 sendInFlightRef.current = false;
15471560 setSendPhase("idle");
15481561 }
15491562 };