From 63ce6194e92f3cf33b57abea4cb05cb6cfd4844b Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 30 Jul 2026 03:33:59 -0700 Subject: [PATCH] fix(web): keep worktree default when switching a draft's machine Switching the environment on a new-thread draft went through the projectChanged path in composerDraftStore, which reset envMode to "local" and startFromOrigin to false alongside the (correctly) cleared branch and worktree path. A user with defaultThreadEnvMode "worktree" who picked a different machine in the composer landed on "Current checkout" with whatever stale branch that machine had checked out. Branch and worktree path are machine-specific and still reset; env mode and start-from-origin are user intent and now carry across the switch. Co-Authored-By: Claude Fable 5 --- apps/web/src/composerDraftStore.test.ts | 12 ++++++---- apps/web/src/composerDraftStore.ts | 29 +++++++++--------------- apps/web/src/hooks/useHandleNewThread.ts | 3 +-- 3 files changed, 20 insertions(+), 24 deletions(-) diff --git a/apps/web/src/composerDraftStore.test.ts b/apps/web/src/composerDraftStore.test.ts index bc1b7107306..19822b8b7ee 100644 --- a/apps/web/src/composerDraftStore.test.ts +++ b/apps/web/src/composerDraftStore.test.ts @@ -1103,13 +1103,14 @@ describe("composerDraftStore project draft thread mapping", () => { }); }); - it("clears branch and worktree context when remapping a draft to another environment", () => { + it("clears branch and worktree but keeps env mode when remapping a draft to another environment", () => { const store = useComposerDraftStore.getState(); store.setProjectDraftThreadId(projectRef, draftId, { threadId, branch: "feature/local-only", worktreePath: "/tmp/local-worktree", envMode: "worktree", + startFromOrigin: true, }); store.setLogicalProjectDraftThreadId(scopedProjectKey(projectRef), remoteProjectRef, draftId, { @@ -1121,17 +1122,19 @@ describe("composerDraftStore project draft thread mapping", () => { projectId, branch: null, worktreePath: null, - envMode: "local", + envMode: "worktree", + startFromOrigin: true, }); }); - it("clears branch and worktree context when changing a draft thread project ref", () => { + it("clears branch and worktree but keeps env mode when changing a draft thread project ref", () => { const store = useComposerDraftStore.getState(); store.setProjectDraftThreadId(projectRef, draftId, { threadId, branch: "feature/local-only", worktreePath: "/tmp/local-worktree", envMode: "worktree", + startFromOrigin: true, }); store.setDraftThreadContext(draftId, { @@ -1143,7 +1146,8 @@ describe("composerDraftStore project draft thread mapping", () => { projectId, branch: null, worktreePath: null, - envMode: "local", + envMode: "worktree", + startFromOrigin: true, }); }); }); diff --git a/apps/web/src/composerDraftStore.ts b/apps/web/src/composerDraftStore.ts index 16c99abad8a..95dde6187c8 100644 --- a/apps/web/src/composerDraftStore.ts +++ b/apps/web/src/composerDraftStore.ts @@ -1340,6 +1340,10 @@ function createDraftThreadState( interactionMode?: ProviderInteractionMode; }, ): DraftThreadState { + // A project change (including switching environments within a logical + // project) invalidates machine-specific context: the branch may not exist + // there and the worktree path certainly doesn't. The user's *intent* — + // env mode and start-from-origin — is machine-independent and carries. const projectChanged = existingThread !== undefined && (existingThread.environmentId !== projectRef.environmentId || @@ -1358,9 +1362,7 @@ function createDraftThreadState( : (options.branch ?? null); const nextStartFromOrigin = options?.startFromOrigin === undefined - ? projectChanged - ? false - : (existingThread?.startFromOrigin ?? false) + ? (existingThread?.startFromOrigin ?? false) : options.startFromOrigin; return { threadId, @@ -1374,12 +1376,7 @@ function createDraftThreadState( branch: nextBranch, worktreePath: nextWorktreePath, envMode: - options?.envMode ?? - (nextWorktreePath - ? "worktree" - : projectChanged - ? "local" - : (existingThread?.envMode ?? "local")), + options?.envMode ?? (nextWorktreePath ? "worktree" : (existingThread?.envMode ?? "local")), startFromOrigin: nextStartFromOrigin, promotedTo: null, }; @@ -2343,6 +2340,9 @@ const composerDraftStore = create()( ) { return state; } + // Mirrors createDraftThreadState: a project/environment change + // drops machine-specific context (branch, worktree path) but + // keeps the user's env mode and start-from-origin intent. const projectChanged = nextProjectRef.environmentId !== existing.environmentId || nextProjectRef.projectId !== existing.projectId; @@ -2360,9 +2360,7 @@ const composerDraftStore = create()( : (options.branch ?? null); const nextStartFromOrigin = options.startFromOrigin === undefined - ? projectChanged - ? false - : existing.startFromOrigin + ? existing.startFromOrigin : options.startFromOrigin; const nextDraftThread: DraftThreadState = { threadId: existing.threadId, @@ -2378,12 +2376,7 @@ const composerDraftStore = create()( branch: nextBranch, worktreePath: nextWorktreePath, envMode: - options.envMode ?? - (nextWorktreePath - ? "worktree" - : projectChanged - ? "local" - : (existing.envMode ?? "local")), + options.envMode ?? (nextWorktreePath ? "worktree" : (existing.envMode ?? "local")), startFromOrigin: nextStartFromOrigin, promotedTo: existing.promotedTo ?? null, }; diff --git a/apps/web/src/hooks/useHandleNewThread.ts b/apps/web/src/hooks/useHandleNewThread.ts index 2479d6ba02f..74ad952e5a2 100644 --- a/apps/web/src/hooks/useHandleNewThread.ts +++ b/apps/web/src/hooks/useHandleNewThread.ts @@ -183,8 +183,7 @@ export function useNewThreadHandler() { // The workspace context must also ride along here: when projectRef // targets a different physical member of the logical project, // createDraftThreadState treats the remap as a project change and - // would otherwise wipe branch/worktree and force "local" mode, - // undoing the write above. + // would otherwise wipe branch/worktree, undoing the write above. setLogicalProjectDraftThreadId( logicalProjectKey, projectRef,