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 | return; | 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 | let attemptedTurnStart = false; | 1419 | let attemptedTurnStart = false; |
| 1453 | try { | 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 | // On first message: lock in branch + create worktree if needed. | 1454 | // On first message: lock in branch + create worktree if needed. |
| 1455 | if (baseBranchForWorktree) { | 1455 | if (baseBranchForWorktree) { |
| 1456 | setSendPhase("preparing-worktree"); | ||
| 1457 | const newBranch = `codething/${crypto.randomUUID().slice(0, 8)}`; | 1456 | const newBranch = `codething/${crypto.randomUUID().slice(0, 8)}`; |
| 1458 | const result = await createWorktreeMutation.mutateAsync({ | 1457 | const result = await createWorktreeMutation.mutateAsync({ |
| 1459 | cwd: activeProject.cwd, | 1458 | cwd: activeProject.cwd, |