Skip to content

Require base branch selection before first send in worktree mode#99

Merged
juliusmarminge merged 5 commits intomainfrom
codething/7144546f
Feb 27, 2026
Merged

Require base branch selection before first send in worktree mode#99
juliusmarminge merged 5 commits intomainfrom
codething/7144546f

Conversation

@juliusmarminge
Copy link
Copy Markdown
Member

@juliusmarminge juliusmarminge commented Feb 27, 2026

Summary

  • enforce base branch selection before the first message when running in worktree mode
  • show a clear composer error instead of silently falling back when branch is missing
  • reuse a shouldCreateWorktree guard to simplify first-send worktree creation flow
  • immediately sync local thread branch/worktree state after creation so terminal context is correct
  • await setup script execution in the new worktree and prefer opening a new terminal
  • remove obsolete planning doc .plans/17-claude-code.md

Testing

  • Not run (tests not provided in this change set)
  • Manual check: first send in worktree mode without a selected base branch shows Select a base branch before sending in New worktree mode. and aborts send
  • Manual check: first send with a selected base branch creates worktree, updates thread state, and runs setup script in worktree cwd

Note

Medium Risk
Changes the core send/worktree-creation path in ChatView, which can block sending and alters sequencing around worktree setup and terminal context. Risk is moderate due to potential regressions in first-message and double-submit behavior.

Overview
Prevents the first message in New worktree mode from sending unless a base branch is selected, surfacing a clear SET_ERROR message instead of implicitly falling back.

Tightens the send lifecycle by adding a sendInFlightRef guard, setting send phase earlier, immediately syncing local thread branch/worktreePath after worktree creation, and awaiting the project setup script so terminal/worktree context is consistent before starting the turn.

Written by Cursor Bugbot for commit 7b0ae43. This will update automatically on new commits. Configure here.

Note

Require base branch selection before the first send in worktree mode and block concurrent sends in ChatView.onSend in ChatView.tsx

Add sendInFlightRef to gate concurrent sends, enforce base branch presence on the first worktree send with a thread error on missing branch, immediately dispatch SET_THREAD_BRANCH after worktree creation, await runProjectScript, and reset send phase and in-flight flag in finally.

📍Where to Start

Start with the onSend handler in ChatView.tsx.

Macroscope summarized 7b0ae43.

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling when creating new worktrees: requires a base branch in New worktree mode and shows precise error messages if missing or creation fails.
    • Ensured local thread state syncs immediately after worktree creation so branch, path, and titles update reliably.
    • Consistent error clearing and thread-context updates to prevent mismatched or stale thread state.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 27, 2026

Walkthrough

Refactors the worktree creation flow in ChatView to add explicit guard conditions, improved state handling, and synchronization. When in worktree mode with no messages, validates base branch selection before proceeding. Upon successful worktree creation, updates thread metadata and immediately syncs local state via dispatch.

Changes

Cohort / File(s) Summary
Worktree Creation Logic
apps/web/src/components/ChatView.tsx
Adds explicit guard condition checking for missing base branch in worktree mode; refactors worktree creation condition into named variable for clarity; synchronizes thread state by dispatching SET_THREAD_BRANCH with new branch and path immediately after creation; configures setup script execution with worktree path and terminal preferences; includes enhanced error handling with early returns.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: enforcing base branch selection before the first message in worktree mode, which is the primary objective of the PR.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codething/7144546f

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Stale threadId after await causes wrong-thread updates
    • Captured const threadId = activeThread.id before any await points in onSend and replaced all subsequent activeThread.id references with the captured threadId to prevent stale closure references after thread switches.
  • ✅ Fixed: Setup script failure does not abort send
    • Added throw error after the SET_ERROR dispatch in runProjectScript's catch block so errors propagate to the worktree creation try-catch which aborts the send, and updated fire-and-forget callers to use .catch(() => {}) to avoid unhandled rejections.

Create PR

Or push these changes by commenting:

@cursor push f793d6c97e
Preview (f793d6c97e)
diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx
--- a/apps/web/src/components/ChatView.tsx
+++ b/apps/web/src/components/ChatView.tsx
@@ -792,6 +792,7 @@
           threadId: activeThreadId,
           error: error instanceof Error ? error.message : `Failed to run script "${script.name}".`,
         });
+        throw error;
       }
     },
     [activeProject, activeThread, activeThreadId, dispatch, gitCwd],
@@ -1153,7 +1154,7 @@
       if (!script) return;
       event.preventDefault();
       event.stopPropagation();
-      void runProjectScript(script);
+      runProjectScript(script).catch(() => {});
     };
     window.addEventListener("keydown", handler);
     return () => window.removeEventListener("keydown", handler);
@@ -1336,6 +1337,7 @@
     if (!trimmed && composerImages.length === 0) return;
     if (!activeProject) return;
     const composerImagesSnapshot = [...composerImages];
+    const threadId = activeThread.id;
 
     const shouldCreateWorktree =
       activeThread.messages.length === 0 && envMode === "worktree" && !activeThread.worktreePath;
@@ -1345,7 +1347,7 @@
     if (shouldCreateWorktree && !activeThread.branch) {
       dispatch({
         type: "SET_ERROR",
-        threadId: activeThread.id,
+        threadId,
         error: "Select a base branch before sending in New worktree mode.",
       });
       return;
@@ -1363,7 +1365,7 @@
         await api.orchestration.dispatchCommand({
           type: "thread.meta.update",
           commandId: newCommandId(),
-          threadId: activeThread.id,
+          threadId,
           branch: result.worktree.branch,
           worktreePath: result.worktree.path,
         });
@@ -1371,7 +1373,7 @@
         // with the worktree cwd/env instead of briefly using the project root.
         dispatch({
           type: "SET_THREAD_BRANCH",
-          threadId: activeThread.id,
+          threadId,
           branch: result.worktree.branch,
           worktreePath: result.worktree.path,
         });
@@ -1387,7 +1389,7 @@
       } catch (err) {
         dispatch({
           type: "SET_ERROR",
-          threadId: activeThread.id,
+          threadId,
           error: err instanceof Error ? err.message : "Failed to create worktree",
         });
         return;
@@ -1406,13 +1408,13 @@
         await api.orchestration.dispatchCommand({
           type: "thread.meta.update",
           commandId: newCommandId(),
-          threadId: activeThread.id,
+          threadId,
           title,
         });
       }
     }
 
-    setThreadError(activeThread.id, null);
+    setThreadError(threadId, null);
     promptRef.current = "";
     setPrompt("");
     setComposerImages([]);
@@ -1443,7 +1445,7 @@
       await api.orchestration.dispatchCommand({
         type: "thread.turn.start",
         commandId: newCommandId(),
-        threadId: activeThread.id,
+        threadId,
         message: {
           messageId: newMessageId(),
           role: "user",
@@ -1456,7 +1458,7 @@
       });
     } catch (err) {
       setThreadError(
-        activeThread.id,
+        threadId,
         err instanceof Error ? err.message : "Failed to send message.",
       );
     } finally {
@@ -1713,7 +1715,7 @@
           gitCwd={gitCwd}
           diffOpen={diffOpen}
           onRunProjectScript={(script) => {
-            void runProjectScript(script);
+            runProjectScript(script).catch(() => {});
           }}
           onAddProjectScript={saveProjectScript}
           onUpdateProjectScript={updateProjectScript}

Comment thread apps/web/src/components/ChatView.tsx Outdated
Comment thread apps/web/src/components/ChatView.tsx Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/src/components/ChatView.tsx (1)

1340-1386: ⚠️ Potential issue | 🟠 Major

Prevent duplicate first-send while async worktree setup is in progress.

Line 1340 introduces extra async steps before send-state is locked, but setIsSending(true) is only set later (Line 1422). A quick second submit can re-enter onSend and issue duplicate non-idempotent calls (gitCreateWorktree / thread.turn.start).

🔧 Suggested fix
@@
   const composerImagesRef = useRef<ComposerImageAttachment[]>([]);
+  const sendInFlightRef = useRef(false);
@@
   const onSend = async (e: React.SubmitEvent | React.KeyboardEvent) => {
     e.preventDefault();
     const api = readNativeApi();
-    if (!api || !activeThread || isSending || isConnecting) return;
+    if (!api || !activeThread || isSending || isConnecting || sendInFlightRef.current) return;
@@
     const composerImagesSnapshot = [...composerImages];
+    sendInFlightRef.current = true;
+    setIsSending(true);
+    try {
@@
-    setIsSending(true);
-    try {
+    // existing send logic...
@@
     } finally {
+      sendInFlightRef.current = false;
       setIsSending(false);
     }
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/ChatView.tsx` around lines 1340 - 1386, When
shouldCreateWorktree is true, lock send-state immediately to prevent a second
concurrent onSend: at the start of the onSend path (inside the
shouldCreateWorktree branch, before awaiting
createWorktreeMutation.mutateAsync), call setIsSending(true) (and/or check an
existing isSending guard at the very top of onSend and return early if true).
Wrap the async worktree setup (createWorktreeMutation.mutateAsync,
api.orchestration.dispatchCommand, runProjectScript) in the existing
try/catch/finally and ensure setIsSending(false) is called in finally on error
or after completion only where appropriate so you don't leave state unlocked on
failures; also ensure any early returns (e.g., SET_ERROR dispatch) respect the
isSending guard.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@apps/web/src/components/ChatView.tsx`:
- Around line 1340-1386: When shouldCreateWorktree is true, lock send-state
immediately to prevent a second concurrent onSend: at the start of the onSend
path (inside the shouldCreateWorktree branch, before awaiting
createWorktreeMutation.mutateAsync), call setIsSending(true) (and/or check an
existing isSending guard at the very top of onSend and return early if true).
Wrap the async worktree setup (createWorktreeMutation.mutateAsync,
api.orchestration.dispatchCommand, runProjectScript) in the existing
try/catch/finally and ensure setIsSending(false) is called in finally on error
or after completion only where appropriate so you don't leave state unlocked on
failures; also ensure any early returns (e.g., SET_ERROR dispatch) respect the
isSending guard.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5ec8c9f and e7f2588.

📒 Files selected for processing (1)
  • apps/web/src/components/ChatView.tsx

@juliusmarminge juliusmarminge force-pushed the codething/7144546f branch 2 times, most recently from f81a72b to 451a2e7 Compare February 27, 2026 03:37
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Double-send race during worktree setup
    • Moved setIsSending(true) before the worktree creation block and added setIsSending(false) in the worktree catch path, closing the race window where a second send could pass the isSending guard during the awaited runProjectScript.

Create PR

Or push these changes by commenting:

@cursor push 88202b5fca
Preview (88202b5fca)
diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx
--- a/apps/web/src/components/ChatView.tsx
+++ b/apps/web/src/components/ChatView.tsx
@@ -1376,6 +1376,8 @@
       return;
     }
 
+    setIsSending(true);
+
     // On first message: lock in branch + create worktree if needed.
     if (shouldCreateWorktree && activeThread.branch) {
       try {
@@ -1410,6 +1412,7 @@
           });
         }
       } catch (err) {
+        setIsSending(false);
         dispatch({
           type: "SET_ERROR",
           threadId,
@@ -1444,7 +1447,6 @@
     setComposerCursor(0);
     setComposerHighlightedItemId(null);
 
-    setIsSending(true);
     try {
       const turnAttachments = await Promise.all(
         composerImagesSnapshot.map(

Comment thread apps/web/src/components/ChatView.tsx Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/web/src/components/ChatView.tsx (2)

1405-1409: ⚠️ Potential issue | 🟠 Major

Setup script execution doesn’t currently prefer a new terminal.

Given the new flow intent, Line 1405 should pass preferNewTerminal: true; otherwise setup may reuse the active terminal and mix output/state unexpectedly.

Proposed fix
         if (setupScript) {
           await runProjectScript(setupScript, {
             cwd: result.worktree.path,
             worktreePath: result.worktree.path,
+            preferNewTerminal: true,
             rememberAsLastInvoked: false,
           });
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/ChatView.tsx` around lines 1405 - 1409, The call to
runProjectScript in ChatView.tsx (the invocation that passes setupScript and
options) doesn't set preferNewTerminal so the setup may reuse an existing
terminal; update the options object passed to runProjectScript to include
preferNewTerminal: true so the setup script is launched in a new terminal
(locate the runProjectScript(...) call where setupScript is used and add the
preferNewTerminal property to the options).

1405-1409: ⚠️ Potential issue | 🟠 Major

Setup-script failures can be cleared immediately before the user sees them.

runProjectScript handles its own errors by dispatching SET_ERROR, but Line 1439 unconditionally clears thread errors afterward. This can hide setup failures and make first-send behavior look successful when setup actually failed.

Proposed fix
     const composerImagesSnapshot = [...composerImages];
+    setThreadError(threadId, null);

     const shouldCreateWorktree =
       activeThread.messages.length === 0 && envMode === "worktree" && !activeThread.worktreePath;

@@
-    setThreadError(threadId, null);
     promptRef.current = "";

Also applies to: 1439-1439

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/ChatView.tsx` around lines 1405 - 1409,
runProjectScript currently handles failures by dispatching SET_ERROR, but the
code following the await unconditionally clears thread errors which can hide
setup failures; modify the call site around runProjectScript to detect success
before clearing thread errors — either by changing runProjectScript to return a
success/failure value (or rethrow on failure) and then only perform the "clear
thread errors" action when that result indicates success, or wrap the await
runProjectScript(...) in a try/catch and only clear errors in the try path while
leaving the SET_ERROR state intact in the catch; reference runProjectScript and
the SET_ERROR dispatch when locating where to gate the clearing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@apps/web/src/components/ChatView.tsx`:
- Around line 1405-1409: The call to runProjectScript in ChatView.tsx (the
invocation that passes setupScript and options) doesn't set preferNewTerminal so
the setup may reuse an existing terminal; update the options object passed to
runProjectScript to include preferNewTerminal: true so the setup script is
launched in a new terminal (locate the runProjectScript(...) call where
setupScript is used and add the preferNewTerminal property to the options).
- Around line 1405-1409: runProjectScript currently handles failures by
dispatching SET_ERROR, but the code following the await unconditionally clears
thread errors which can hide setup failures; modify the call site around
runProjectScript to detect success before clearing thread errors — either by
changing runProjectScript to return a success/failure value (or rethrow on
failure) and then only perform the "clear thread errors" action when that result
indicates success, or wrap the await runProjectScript(...) in a try/catch and
only clear errors in the try path while leaving the SET_ERROR state intact in
the catch; reference runProjectScript and the SET_ERROR dispatch when locating
where to gate the clearing.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e7f2588 and 26224fd.

📒 Files selected for processing (1)
  • apps/web/src/components/ChatView.tsx

juliusmarminge and others added 4 commits February 26, 2026 20:43
- show an explicit error when New worktree mode has no base branch selected
- sync thread branch/worktree path immediately after creation
- run setup script in the new worktree terminal before continuing

Co-authored-by: codex <codex@users.noreply.github.com>
- Store `activeThread.id` once at send start and reuse it
- Prevents updates/errors from targeting the wrong thread during async worktree/send steps
- Remove `preferNewTerminal` when invoking `runProjectScript` in `ChatView`
- Lets setup scripts use default terminal behavior for worktree runs
- 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
Comment thread apps/web/src/components/ChatView.tsx
- 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
@juliusmarminge juliusmarminge merged commit bb2164c into main Feb 27, 2026
4 checks passed
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Send state set outside try-finally can lock permanently
    • Moved sendInFlightRef.current = true and setSendPhase(...) inside the try block so the finally block always resets them, preventing permanent send-lock if any code before the original try throws.
  • ✅ Fixed: Redundant setSendPhase("preparing-worktree") call inside try block
    • Removed the redundant setSendPhase("preparing-worktree") call inside the if (baseBranchForWorktree) block since the phase is already set to that value by the earlier conditional assignment.

Create PR

Or push these changes by commenting:

@cursor push ba4545ceb5
Preview (ba4545ceb5)
diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx
--- a/apps/web/src/components/ChatView.tsx
+++ b/apps/web/src/components/ChatView.tsx
@@ -1416,44 +1416,43 @@
       return;
     }
 
-    sendInFlightRef.current = true;
-    setSendPhase(baseBranchForWorktree ? "preparing-worktree" : "sending-turn");
+    let attemptedTurnStart = false;
+    try {
+      sendInFlightRef.current = true;
+      setSendPhase(baseBranchForWorktree ? "preparing-worktree" : "sending-turn");
 
-    const composerImagesSnapshot = [...composerImages];
-    const messageIdForSend = newMessageId();
-    const messageCreatedAt = new Date().toISOString();
-    const optimisticAttachments = composerImagesSnapshot.map((image) => ({
-      type: "image" as const,
-      id: image.id,
-      name: image.name,
-      mimeType: image.mimeType,
-      sizeBytes: image.sizeBytes,
-      previewUrl: image.previewUrl,
-    }));
-    setOptimisticUserMessages((existing) => [
-      ...existing,
-      {
-        id: messageIdForSend,
-        role: "user",
-        text: trimmed,
-        ...(optimisticAttachments.length > 0 ? { attachments: optimisticAttachments } : {}),
-        createdAt: messageCreatedAt,
-        streaming: false,
-      },
-    ]);
+      const composerImagesSnapshot = [...composerImages];
+      const messageIdForSend = newMessageId();
+      const messageCreatedAt = new Date().toISOString();
+      const optimisticAttachments = composerImagesSnapshot.map((image) => ({
+        type: "image" as const,
+        id: image.id,
+        name: image.name,
+        mimeType: image.mimeType,
+        sizeBytes: image.sizeBytes,
+        previewUrl: image.previewUrl,
+      }));
+      setOptimisticUserMessages((existing) => [
+        ...existing,
+        {
+          id: messageIdForSend,
+          role: "user",
+          text: trimmed,
+          ...(optimisticAttachments.length > 0 ? { attachments: optimisticAttachments } : {}),
+          createdAt: messageCreatedAt,
+          streaming: false,
+        },
+      ]);
 
-    setThreadError(threadIdForSend, null);
-    promptRef.current = "";
-    setPrompt("");
-    setComposerImages([]);
-    setComposerCursor(0);
-    setComposerHighlightedItemId(null);
+      setThreadError(threadIdForSend, null);
+      promptRef.current = "";
+      setPrompt("");
+      setComposerImages([]);
+      setComposerCursor(0);
+      setComposerHighlightedItemId(null);
 
-    let attemptedTurnStart = false;
-    try {
       // On first message: lock in branch + create worktree if needed.
       if (baseBranchForWorktree) {
-        setSendPhase("preparing-worktree");
         const newBranch = `codething/${crypto.randomUUID().slice(0, 8)}`;
         const result = await createWorktreeMutation.mutateAsync({
           cwd: activeProject.cwd,

}

sendInFlightRef.current = true;
setSendPhase(baseBranchForWorktree ? "preparing-worktree" : "sending-turn");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Send state set outside try-finally can lock permanently

Medium Severity

sendInFlightRef.current = true and setSendPhase(...) are set at lines before the try block, but only reset inside the finally block. If any code between these assignments and the try (lines 1422–1452) throws unexpectedly, the finally never runs, leaving sendInFlightRef.current stuck at true and sendPhase stuck at a non-idle value, permanently blocking all future sends for the session.

Additional Locations (1)

Fix in Cursor Fix in Web

});
const setupScript = setupProjectScript(activeProject.scripts);
if (setupScript) {
void runProjectScript(setupScript, {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant setSendPhase("preparing-worktree") call inside try block

Low Severity

setSendPhase("preparing-worktree") at line 1456 is redundant. Line 1420 already sets sendPhase to "preparing-worktree" when baseBranchForWorktree is truthy, which is exactly the condition guarding the block at line 1455. No code between these two calls changes the phase, so the second call is a no-op.

Additional Locations (1)

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant