Move sendInFlightRef.current and setSendPhase assignments inside the try block so that the finally block always resets them, preventing permanent send-lock if code between the assignments and the try block throws. Also removes the redundant setSendPhase('preparing-worktree') call that was already set identically before the try block.
Unified · Split
| @@ -1416,44 +1416,43 @@ export default function ChatView({ threadId }: ChatViewProps) { · 1415 unmodified lines | ||
| 1416 | 1416 | return; |
| 1417 | 1417 | } |
| 1418 | 1418 | |
| 1419 | − sendInFlightRef.current = true; | |
| 1420 | − setSendPhase(baseBranchForWorktree ? "preparing-worktree" : "sending-turn"); | |
| 1421 | − | |
| 1422 | − const composerImagesSnapshot = [...composerImages]; | |
| 1423 | − const messageIdForSend = newMessageId(); | |
| 1424 | − const messageCreatedAt = new Date().toISOString(); | |
| 1425 | − const optimisticAttachments = composerImagesSnapshot.map((image) => ({ | |
| 1426 | − type: "image" as const, | |
| 1427 | − id: image.id, | |
| 1428 | − name: image.name, | |
| 1429 | − mimeType: image.mimeType, | |
| 1430 | − sizeBytes: image.sizeBytes, | |
| 1431 | − previewUrl: image.previewUrl, | |
| 1432 | − })); | |
| 1433 | − setOptimisticUserMessages((existing) => [ | |
| 1434 | − ...existing, | |
| 1435 | − { | |
| 1436 | − id: messageIdForSend, | |
| 1437 | − role: "user", | |
| 1438 | − text: trimmed, | |
| 1439 | − ...(optimisticAttachments.length > 0 ? { attachments: optimisticAttachments } : {}), | |
| 1440 | − createdAt: messageCreatedAt, | |
| 1441 | − streaming: false, | |
| 1442 | − }, | |
| 1443 | − ]); | |
| 1444 | − | |
| 1445 | − setThreadError(threadIdForSend, null); | |
| 1446 | − promptRef.current = ""; | |
| 1447 | − setPrompt(""); | |
| 1448 | − setComposerImages([]); | |
| 1449 | − setComposerCursor(0); | |
| 1450 | − setComposerHighlightedItemId(null); | |
| 1451 | − | |
| 1452 | 1419 | let attemptedTurnStart = false; |
| 1453 | 1420 | try { |
| 1421 | + sendInFlightRef.current = true; | |
| 1422 | + setSendPhase(baseBranchForWorktree ? "preparing-worktree" : "sending-turn"); | |
| 1423 | + | |
| 1424 | + const composerImagesSnapshot = [...composerImages]; | |
| 1425 | + const messageIdForSend = newMessageId(); | |
| 1426 | + const messageCreatedAt = new Date().toISOString(); | |
| 1427 | + const optimisticAttachments = composerImagesSnapshot.map((image) => ({ | |
| 1428 | + type: "image" as const, | |
| 1429 | + id: image.id, | |
| 1430 | + name: image.name, | |
| 1431 | + mimeType: image.mimeType, | |
| 1432 | + sizeBytes: image.sizeBytes, | |
| 1433 | + previewUrl: image.previewUrl, | |
| 1434 | + })); | |
| 1435 | + setOptimisticUserMessages((existing) => [ | |
| 1436 | + ...existing, | |
| 1437 | + { | |
| 1438 | + id: messageIdForSend, | |
| 1439 | + role: "user", | |
| 1440 | + text: trimmed, | |
| 1441 | + ...(optimisticAttachments.length > 0 ? { attachments: optimisticAttachments } : {}), | |
| 1442 | + createdAt: messageCreatedAt, | |
| 1443 | + streaming: false, | |
| 1444 | + }, | |
| 1445 | + ]); | |
| 1446 | + | |
| 1447 | + setThreadError(threadIdForSend, null); | |
| 1448 | + promptRef.current = ""; | |
| 1449 | + setPrompt(""); | |
| 1450 | + setComposerImages([]); | |
| 1451 | + setComposerCursor(0); | |
| 1452 | + setComposerHighlightedItemId(null); | |
| 1453 | + | |
| 1454 | 1454 | // On first message: lock in branch + create worktree if needed. |
| 1455 | 1455 | if (baseBranchForWorktree) { |
| 1456 | − setSendPhase("preparing-worktree"); | |
| 1457 | 1456 | const newBranch = `codething/${crypto.randomUUID().slice(0, 8)}`; |
| 1458 | 1457 | const result = await createWorktreeMutation.mutateAsync({ |
| 1459 | 1458 | cwd: activeProject.cwd, |