authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-07-30 03:48:33-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-30 03:48:33-07:00
logf0c6ba994670bbd9eeab97eab6701d5c3589178d
treee1f5c75380c74e109cee50e2591676c6a0a3c8ce
parent90f3913a81380af7aeafac62f8132aeb98345a12
signature Signed by PGP key B5690EEEBB952194

fix(web): keep worktree default when switching a draft's machine (#4964)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

3 files changed, 20 insertions(+), 24 deletions(-)

apps/web/src/composerDraftStore.test.ts+8-4
...@@ -1103,13 +1103,14 @@ describe("composerDraftStore project draft thread mapping", () => {...@@ -1103,13 +1103,14 @@ describe("composerDraftStore project draft thread mapping", () => {
1103 });1103 });
1104 });1104 });
11051105
1106 it("clears branch and worktree context when remapping a draft to another environment", () => {1106 it("clears branch and worktree but keeps env mode when remapping a draft to another environment", () => {
1107 const store = useComposerDraftStore.getState();1107 const store = useComposerDraftStore.getState();
1108 store.setProjectDraftThreadId(projectRef, draftId, {1108 store.setProjectDraftThreadId(projectRef, draftId, {
1109 threadId,1109 threadId,
1110 branch: "feature/local-only",1110 branch: "feature/local-only",
1111 worktreePath: "/tmp/local-worktree",1111 worktreePath: "/tmp/local-worktree",
1112 envMode: "worktree",1112 envMode: "worktree",
1113 startFromOrigin: true,
1113 });1114 });
11141115
1115 store.setLogicalProjectDraftThreadId(scopedProjectKey(projectRef), remoteProjectRef, draftId, {1116 store.setLogicalProjectDraftThreadId(scopedProjectKey(projectRef), remoteProjectRef, draftId, {
...@@ -1121,17 +1122,19 @@ describe("composerDraftStore project draft thread mapping", () => {...@@ -1121,17 +1122,19 @@ describe("composerDraftStore project draft thread mapping", () => {
1121 projectId,1122 projectId,
1122 branch: null,1123 branch: null,
1123 worktreePath: null,1124 worktreePath: null,
1124 envMode: "local",1125 envMode: "worktree",
1126 startFromOrigin: true,
1125 });1127 });
1126 });1128 });
11271129
1128 it("clears branch and worktree context when changing a draft thread project ref", () => {1130 it("clears branch and worktree but keeps env mode when changing a draft thread project ref", () => {
1129 const store = useComposerDraftStore.getState();1131 const store = useComposerDraftStore.getState();
1130 store.setProjectDraftThreadId(projectRef, draftId, {1132 store.setProjectDraftThreadId(projectRef, draftId, {
1131 threadId,1133 threadId,
1132 branch: "feature/local-only",1134 branch: "feature/local-only",
1133 worktreePath: "/tmp/local-worktree",1135 worktreePath: "/tmp/local-worktree",
1134 envMode: "worktree",1136 envMode: "worktree",
1137 startFromOrigin: true,
1135 });1138 });
11361139
1137 store.setDraftThreadContext(draftId, {1140 store.setDraftThreadContext(draftId, {
...@@ -1143,7 +1146,8 @@ describe("composerDraftStore project draft thread mapping", () => {...@@ -1143,7 +1146,8 @@ describe("composerDraftStore project draft thread mapping", () => {
1143 projectId,1146 projectId,
1144 branch: null,1147 branch: null,
1145 worktreePath: null,1148 worktreePath: null,
1146 envMode: "local",1149 envMode: "worktree",
1150 startFromOrigin: true,
1147 });1151 });
1148 });1152 });
1149});1153});
apps/web/src/composerDraftStore.ts+11-18
...@@ -1340,6 +1340,10 @@ function createDraftThreadState(...@@ -1340,6 +1340,10 @@ function createDraftThreadState(
1340 interactionMode?: ProviderInteractionMode;1340 interactionMode?: ProviderInteractionMode;
1341 },1341 },
1342): DraftThreadState {1342): DraftThreadState {
1343 // A project change (including switching environments within a logical
1344 // project) invalidates machine-specific context: the branch may not exist
1345 // there and the worktree path certainly doesn't. The user's *intent* —
1346 // env mode and start-from-origin — is machine-independent and carries.
1343 const projectChanged =1347 const projectChanged =
1344 existingThread !== undefined &&1348 existingThread !== undefined &&
1345 (existingThread.environmentId !== projectRef.environmentId ||1349 (existingThread.environmentId !== projectRef.environmentId ||
...@@ -1358,9 +1362,7 @@ function createDraftThreadState(...@@ -1358,9 +1362,7 @@ function createDraftThreadState(
1358 : (options.branch ?? null);1362 : (options.branch ?? null);
1359 const nextStartFromOrigin =1363 const nextStartFromOrigin =
1360 options?.startFromOrigin === undefined1364 options?.startFromOrigin === undefined
1361 ? projectChanged1365 ? (existingThread?.startFromOrigin ?? false)
1362 ? false
1363 : (existingThread?.startFromOrigin ?? false)
1364 : options.startFromOrigin;1366 : options.startFromOrigin;
1365 return {1367 return {
1366 threadId,1368 threadId,
...@@ -1374,12 +1376,7 @@ function createDraftThreadState(...@@ -1374,12 +1376,7 @@ function createDraftThreadState(
1374 branch: nextBranch,1376 branch: nextBranch,
1375 worktreePath: nextWorktreePath,1377 worktreePath: nextWorktreePath,
1376 envMode:1378 envMode:
1377 options?.envMode ??1379 options?.envMode ?? (nextWorktreePath ? "worktree" : (existingThread?.envMode ?? "local")),
1378 (nextWorktreePath
1379 ? "worktree"
1380 : projectChanged
1381 ? "local"
1382 : (existingThread?.envMode ?? "local")),
1383 startFromOrigin: nextStartFromOrigin,1380 startFromOrigin: nextStartFromOrigin,
1384 promotedTo: null,1381 promotedTo: null,
1385 };1382 };
...@@ -2343,6 +2340,9 @@ const composerDraftStore = create<ComposerDraftStoreState>()(...@@ -2343,6 +2340,9 @@ const composerDraftStore = create<ComposerDraftStoreState>()(
2343 ) {2340 ) {
2344 return state;2341 return state;
2345 }2342 }
2343 // Mirrors createDraftThreadState: a project/environment change
2344 // drops machine-specific context (branch, worktree path) but
2345 // keeps the user's env mode and start-from-origin intent.
2346 const projectChanged =2346 const projectChanged =
2347 nextProjectRef.environmentId !== existing.environmentId ||2347 nextProjectRef.environmentId !== existing.environmentId ||
2348 nextProjectRef.projectId !== existing.projectId;2348 nextProjectRef.projectId !== existing.projectId;
...@@ -2360,9 +2360,7 @@ const composerDraftStore = create<ComposerDraftStoreState>()(...@@ -2360,9 +2360,7 @@ const composerDraftStore = create<ComposerDraftStoreState>()(
2360 : (options.branch ?? null);2360 : (options.branch ?? null);
2361 const nextStartFromOrigin =2361 const nextStartFromOrigin =
2362 options.startFromOrigin === undefined2362 options.startFromOrigin === undefined
2363 ? projectChanged2363 ? existing.startFromOrigin
2364 ? false
2365 : existing.startFromOrigin
2366 : options.startFromOrigin;2364 : options.startFromOrigin;
2367 const nextDraftThread: DraftThreadState = {2365 const nextDraftThread: DraftThreadState = {
2368 threadId: existing.threadId,2366 threadId: existing.threadId,
...@@ -2378,12 +2376,7 @@ const composerDraftStore = create<ComposerDraftStoreState>()(...@@ -2378,12 +2376,7 @@ const composerDraftStore = create<ComposerDraftStoreState>()(
2378 branch: nextBranch,2376 branch: nextBranch,
2379 worktreePath: nextWorktreePath,2377 worktreePath: nextWorktreePath,
2380 envMode:2378 envMode:
2381 options.envMode ??2379 options.envMode ?? (nextWorktreePath ? "worktree" : (existing.envMode ?? "local")),
2382 (nextWorktreePath
2383 ? "worktree"
2384 : projectChanged
2385 ? "local"
2386 : (existing.envMode ?? "local")),
2387 startFromOrigin: nextStartFromOrigin,2380 startFromOrigin: nextStartFromOrigin,
2388 promotedTo: existing.promotedTo ?? null,2381 promotedTo: existing.promotedTo ?? null,
2389 };2382 };
apps/web/src/hooks/useHandleNewThread.ts+1-2
...@@ -183,8 +183,7 @@ export function useNewThreadHandler() {...@@ -183,8 +183,7 @@ export function useNewThreadHandler() {
183 // The workspace context must also ride along here: when projectRef183 // The workspace context must also ride along here: when projectRef
184 // targets a different physical member of the logical project,184 // targets a different physical member of the logical project,
185 // createDraftThreadState treats the remap as a project change and185 // createDraftThreadState treats the remap as a project change and
186 // would otherwise wipe branch/worktree and force "local" mode,186 // would otherwise wipe branch/worktree, undoing the write above.
187 // undoing the write above.
188 setLogicalProjectDraftThreadId(187 setLogicalProjectDraftThreadId(
189 logicalProjectKey,188 logicalProjectKey,
190 projectRef,189 projectRef,