Skip to content

Commit 5f7ec73

Browse files
Fix new-thread draft reuse for worktree defaults (#2003)
1 parent 569fea8 commit 5f7ec73

File tree

5 files changed

+128
-2
lines changed

5 files changed

+128
-2
lines changed

apps/web/src/components/ChatView.browser.tsx

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3581,6 +3581,98 @@ describe("ChatView timeline estimator parity (full app)", () => {
35813581
}
35823582
});
35833583

3584+
it("creates a fresh worktree draft from an existing worktree thread when the default mode is worktree", async () => {
3585+
const mounted = await mountChatView({
3586+
viewport: DEFAULT_VIEWPORT,
3587+
snapshot: {
3588+
...createSnapshotForTargetUser({
3589+
targetMessageId: "msg-user-new-thread-worktree-default-test" as MessageId,
3590+
targetText: "new thread worktree default test",
3591+
}),
3592+
threads: createSnapshotForTargetUser({
3593+
targetMessageId: "msg-user-new-thread-worktree-default-test" as MessageId,
3594+
targetText: "new thread worktree default test",
3595+
}).threads.map((thread) =>
3596+
thread.id === THREAD_ID
3597+
? Object.assign({}, thread, {
3598+
branch: "feature/existing",
3599+
worktreePath: "/repo/.t3/worktrees/existing",
3600+
})
3601+
: thread,
3602+
),
3603+
},
3604+
configureFixture: (nextFixture) => {
3605+
nextFixture.serverConfig = {
3606+
...nextFixture.serverConfig,
3607+
settings: {
3608+
...nextFixture.serverConfig.settings,
3609+
defaultThreadEnvMode: "worktree",
3610+
},
3611+
};
3612+
},
3613+
});
3614+
3615+
try {
3616+
const newThreadButton = page.getByTestId("new-thread-button");
3617+
await expect.element(newThreadButton).toBeInTheDocument();
3618+
3619+
await newThreadButton.click();
3620+
3621+
const newThreadPath = await waitForURL(
3622+
mounted.router,
3623+
(path) => UUID_ROUTE_RE.test(path),
3624+
"Route should change to a new draft thread.",
3625+
);
3626+
const newDraftId = draftIdFromPath(newThreadPath);
3627+
3628+
expect(useComposerDraftStore.getState().getDraftSession(newDraftId)).toMatchObject({
3629+
envMode: "worktree",
3630+
worktreePath: null,
3631+
});
3632+
} finally {
3633+
await mounted.cleanup();
3634+
}
3635+
});
3636+
3637+
it("creates a new draft instead of reusing a promoting draft thread", async () => {
3638+
const mounted = await mountChatView({
3639+
viewport: DEFAULT_VIEWPORT,
3640+
snapshot: createSnapshotForTargetUser({
3641+
targetMessageId: "msg-user-promoting-draft-new-thread-test" as MessageId,
3642+
targetText: "promoting draft new thread test",
3643+
}),
3644+
});
3645+
3646+
try {
3647+
const newThreadButton = page.getByTestId("new-thread-button");
3648+
await expect.element(newThreadButton).toBeInTheDocument();
3649+
3650+
await newThreadButton.click();
3651+
3652+
const firstDraftPath = await waitForURL(
3653+
mounted.router,
3654+
(path) => UUID_ROUTE_RE.test(path),
3655+
"Route should change to the first draft thread.",
3656+
);
3657+
const firstDraftId = draftIdFromPath(firstDraftPath);
3658+
const firstThreadId = draftThreadIdFor(firstDraftId);
3659+
3660+
await materializePromotedDraftThreadViaDomainEvent(firstThreadId);
3661+
expect(mounted.router.state.location.pathname).toBe(firstDraftPath);
3662+
3663+
await newThreadButton.click();
3664+
3665+
const secondDraftPath = await waitForURL(
3666+
mounted.router,
3667+
(path) => UUID_ROUTE_RE.test(path) && path !== firstDraftPath,
3668+
"Route should change to a second draft thread instead of reusing the promoting draft.",
3669+
);
3670+
expect(draftIdFromPath(secondDraftPath)).not.toBe(firstDraftId);
3671+
} finally {
3672+
await mounted.cleanup();
3673+
}
3674+
});
3675+
35843676
it("snapshots sticky codex settings into a new draft thread", async () => {
35853677
useComposerDraftStore.setState({
35863678
stickyModelSelectionByProvider: {

apps/web/src/components/Sidebar.logic.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,28 @@ describe("resolveSidebarNewThreadEnvMode", () => {
184184
});
185185

186186
describe("resolveSidebarNewThreadSeedContext", () => {
187+
it("prefers the default worktree mode over active thread context", () => {
188+
expect(
189+
resolveSidebarNewThreadSeedContext({
190+
projectId: "project-1",
191+
defaultEnvMode: "worktree",
192+
activeThread: {
193+
projectId: "project-1",
194+
branch: "feature/existing",
195+
worktreePath: "/repo/.t3/worktrees/existing",
196+
},
197+
activeDraftThread: {
198+
projectId: "project-1",
199+
branch: "feature/draft",
200+
worktreePath: "/repo/.t3/worktrees/draft",
201+
envMode: "worktree",
202+
},
203+
}),
204+
).toEqual({
205+
envMode: "worktree",
206+
});
207+
});
208+
187209
it("inherits the active server thread context when creating a new thread in the same project", () => {
188210
expect(
189211
resolveSidebarNewThreadSeedContext({

apps/web/src/components/Sidebar.logic.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,12 @@ export function resolveSidebarNewThreadSeedContext(input: {
186186
worktreePath?: string | null;
187187
envMode: SidebarNewThreadEnvMode;
188188
} {
189+
if (input.defaultEnvMode === "worktree") {
190+
return {
191+
envMode: "worktree",
192+
};
193+
}
194+
189195
if (input.activeDraftThread?.projectId === input.projectId) {
190196
return {
191197
branch: input.activeDraftThread.branch,

apps/web/src/composerDraftStore.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1881,13 +1881,18 @@ const composerDraftStore = create<ComposerDraftStoreState>()(
18811881
[draftId]: nextDraftThread,
18821882
};
18831883
let nextDraftsByThreadKey = state.draftsByThreadKey;
1884+
const previousDraftThread =
1885+
previousThreadKeyForLogicalProject === undefined
1886+
? undefined
1887+
: nextDraftThreadsByThreadKey[previousThreadKeyForLogicalProject];
18841888
if (
18851889
previousThreadKeyForLogicalProject &&
18861890
previousThreadKeyForLogicalProject !== draftId &&
18871891
!isComposerThreadKeyInUse(
18881892
nextLogicalProjectDraftThreadKeyByLogicalProjectKey,
18891893
previousThreadKeyForLogicalProject,
1890-
)
1894+
) &&
1895+
!isDraftThreadPromoting(previousDraftThread)
18911896
) {
18921897
delete nextDraftThreadsByThreadKey[previousThreadKeyForLogicalProject];
18931898
if (state.draftsByThreadKey[previousThreadKeyForLogicalProject] !== undefined) {

apps/web/src/hooks/useHandleNewThread.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ function useNewThreadState() {
8787
if (
8888
latestActiveDraftThread &&
8989
currentRouteTarget?.kind === "draft" &&
90-
latestActiveDraftThread.logicalProjectKey === logicalProjectKey
90+
latestActiveDraftThread.logicalProjectKey === logicalProjectKey &&
91+
latestActiveDraftThread.promotedTo == null
9192
) {
9293
if (hasBranchOption || hasWorktreePathOption || hasEnvModeOption) {
9394
setDraftThreadContext(currentRouteTarget.draftId, {

0 commit comments

Comments
 (0)