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) {...@@ -368,6 +368,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
368 const textareaRef = useRef<HTMLTextAreaElement>(null);368 const textareaRef = useRef<HTMLTextAreaElement>(null);
369 const composerCommandInputRef = useRef<HTMLInputElement>(null);369 const composerCommandInputRef = useRef<HTMLInputElement>(null);
370 const composerImagesRef = useRef<ComposerImageAttachment[]>([]);370 const composerImagesRef = useRef<ComposerImageAttachment[]>([]);
371 const sendInFlightRef = useRef(false);
371 const dragDepthRef = useRef(0);372 const dragDepthRef = useRef(0);
372 const terminalOpenByThreadRef = useRef<Record<string, boolean>>({});373 const terminalOpenByThreadRef = useRef<Record<string, boolean>>({});
373374
...@@ -1391,7 +1392,7 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -1391,7 +1392,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
1391 const onSend = async (e: React.SubmitEvent | React.KeyboardEvent) => {1392 const onSend = async (e: React.SubmitEvent | React.KeyboardEvent) => {
1392 e.preventDefault();1393 e.preventDefault();
1393 const api = readNativeApi();1394 const api = readNativeApi();
1394 if (!api || !activeThread || isSendBusy || isConnecting) return;1395 if (!api || !activeThread || isSendBusy || isConnecting || sendInFlightRef.current) return;
1395 const trimmed = prompt.trim();1396 const trimmed = prompt.trim();
1396 if (!trimmed && composerImages.length === 0) return;1397 if (!trimmed && composerImages.length === 0) return;
1397 if (!activeProject) return;1398 if (!activeProject) return;
...@@ -1415,6 +1416,9 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -1415,6 +1416,9 @@ export default function ChatView({ threadId }: ChatViewProps) {
1415 return;1416 return;
1416 }1417 }
14171418
1419 sendInFlightRef.current = true;
1420 setSendPhase(baseBranchForWorktree ? "preparing-worktree" : "sending-turn");
1421
1418 const composerImagesSnapshot = [...composerImages];1422 const composerImagesSnapshot = [...composerImages];
1419 const messageIdForSend = newMessageId();1423 const messageIdForSend = newMessageId();
1420 const messageCreatedAt = new Date().toISOString();1424 const messageCreatedAt = new Date().toISOString();
...@@ -1463,9 +1467,17 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -1463,9 +1467,17 @@ export default function ChatView({ threadId }: ChatViewProps) {
1463 branch: result.worktree.branch,1467 branch: result.worktree.branch,
1464 worktreePath: result.worktree.path,1468 worktreePath: result.worktree.path,
1465 });1469 });
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 });
1466 const setupScript = setupProjectScript(activeProject.scripts);1478 const setupScript = setupProjectScript(activeProject.scripts);
1467 if (setupScript) {1479 if (setupScript) {
1468 void runProjectScript(setupScript, {1480 await runProjectScript(setupScript, {
1469 cwd: result.worktree.path,1481 cwd: result.worktree.path,
1470 worktreePath: result.worktree.path,1482 worktreePath: result.worktree.path,
1471 rememberAsLastInvoked: false,1483 rememberAsLastInvoked: false,
...@@ -1544,6 +1556,7 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -1544,6 +1556,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
1544 err instanceof Error ? err.message : "Failed to send message.",1556 err instanceof Error ? err.message : "Failed to send message.",
1545 );1557 );
1546 } finally {1558 } finally {
1559 sendInFlightRef.current = false;
1547 setSendPhase("idle");1560 setSendPhase("idle");
1548 }1561 }
1549 };1562 };