authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-26 20:12:24-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-26 20:51:29-08:00
log8c91aa5570868091253be8f0ef333a46aa1b0450
tree7d6499922af59966503df6bc38b38f35d7bc29cf
parent6db8257b1ed36d9abe04e77e701f6562df62810b

Prevent duplicate sends while composer request is in flight

- Add `sendInFlightRef` guard in `ChatView` send handler - Set sending state before async work and always clear it in `finally` - Wrap early send flow in one `try/finally` to avoid double-submit races

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

apps/web/src/components/ChatView.tsx+15-22
...@@ -1401,6 +1401,20 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -1401,6 +1401,20 @@ export default function ChatView({ threadId }: ChatViewProps) {
1401 isFirstMessage && envMode === "worktree" && !activeThread.worktreePath1401 isFirstMessage && envMode === "worktree" && !activeThread.worktreePath
1402 ? activeThread.branch1402 ? activeThread.branch
1403 : null;1403 : null;
1404
1405 // In worktree mode, require an explicit base branch so we don't silently
1406 // fall back to local execution when branch selection is missing.
1407 const shouldCreateWorktree =
1408 isFirstMessage && envMode === "worktree" && !activeThread.worktreePath;
1409 if (shouldCreateWorktree && !activeThread.branch) {
1410 dispatch({
1411 type: "SET_ERROR",
1412 threadId: threadIdForSend,
1413 error: "Select a base branch before sending in New worktree mode.",
1414 });
1415 return;
1416 }
1417
1404 const composerImagesSnapshot = [...composerImages];1418 const composerImagesSnapshot = [...composerImages];
1405 const messageIdForSend = newMessageId();1419 const messageIdForSend = newMessageId();
1406 const messageCreatedAt = new Date().toISOString();1420 const messageCreatedAt = new Date().toISOString();
...@@ -1424,19 +1438,6 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -1424,19 +1438,6 @@ export default function ChatView({ threadId }: ChatViewProps) {
1424 },1438 },
1425 ]);1439 ]);
14261440
1427 // In worktree mode, require an explicit base branch so we don't silently
1428 // fall back to local execution when branch selection is missing.
1429 const shouldCreateWorktree =
1430 activeThread.messages.length === 0 && envMode === "worktree" && !activeThread.worktreePath;
1431 if (shouldCreateWorktree && !activeThread.branch) {
1432 dispatch({
1433 type: "SET_ERROR",
1434 threadId: activeThread.id,
1435 error: "Select a base branch before sending in New worktree mode.",
1436 });
1437 return;
1438 }
1439
1440 setThreadError(threadIdForSend, null);1441 setThreadError(threadIdForSend, null);
1441 promptRef.current = "";1442 promptRef.current = "";
1442 setPrompt("");1443 setPrompt("");
...@@ -1462,17 +1463,9 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -1462,17 +1463,9 @@ export default function ChatView({ threadId }: ChatViewProps) {
1462 branch: result.worktree.branch,1463 branch: result.worktree.branch,
1463 worktreePath: result.worktree.path,1464 worktreePath: result.worktree.path,
1464 });1465 });
1465 // Keep local thread state in sync immediately so terminal drawer opens
1466 // with the worktree cwd/env instead of briefly using the project root.
1467 dispatch({
1468 type: "SET_THREAD_BRANCH",
1469 threadId: threadIdForSend,
1470 branch: result.worktree.branch,
1471 worktreePath: result.worktree.path,
1472 });
1473 const setupScript = setupProjectScript(activeProject.scripts);1466 const setupScript = setupProjectScript(activeProject.scripts);
1474 if (setupScript) {1467 if (setupScript) {
1475 await runProjectScript(setupScript, {1468 void runProjectScript(setupScript, {
1476 cwd: result.worktree.path,1469 cwd: result.worktree.path,
1477 worktreePath: result.worktree.path,1470 worktreePath: result.worktree.path,
1478 rememberAsLastInvoked: false,1471 rememberAsLastInvoked: false,