| author | |
| committer | |
| log | 5f7ec73a22b28e2775987b03a744f75e5e151152 |
| tree | 5b1ea0b52b882e2e979d857de56e9e88afc24b9b |
| parent | 569fea87a90f1d9a5d4d9b6c7c1c59801324a561 |
| signature | Signed by PGP key B5690EEEBB952194 |
5 files changed, 128 insertions(+), 2 deletions(-)
apps/web/src/components/ChatView.browser.tsx+92| ... | @@ -3581,6 +3581,98 @@ describe("ChatView timeline estimator parity (full app)", () => { | ... | @@ -3581,6 +3581,98 @@ describe("ChatView timeline estimator parity (full app)", () => { |
| 3581 | } | 3581 | } |
| 3582 | }); | 3582 | }); |
| 3583 | 3583 | ||
| 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 | |||
| 3584 | it("snapshots sticky codex settings into a new draft thread", async () => { | 3676 | it("snapshots sticky codex settings into a new draft thread", async () => { |
| 3585 | useComposerDraftStore.setState({ | 3677 | useComposerDraftStore.setState({ |
| 3586 | stickyModelSelectionByProvider: { | 3678 | stickyModelSelectionByProvider: { |
apps/web/src/components/Sidebar.logic.test.ts+22| ... | @@ -184,6 +184,28 @@ describe("resolveSidebarNewThreadEnvMode", () => { | ... | @@ -184,6 +184,28 @@ describe("resolveSidebarNewThreadEnvMode", () => { |
| 184 | }); | 184 | }); |
| 185 | 185 | ||
| 186 | describe("resolveSidebarNewThreadSeedContext", () => { | 186 | 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 | |||
| 187 | it("inherits the active server thread context when creating a new thread in the same project", () => { | 209 | it("inherits the active server thread context when creating a new thread in the same project", () => { |
| 188 | expect( | 210 | expect( |
| 189 | resolveSidebarNewThreadSeedContext({ | 211 | resolveSidebarNewThreadSeedContext({ |
apps/web/src/components/Sidebar.logic.ts+6| ... | @@ -186,6 +186,12 @@ export function resolveSidebarNewThreadSeedContext(input: { | ... | @@ -186,6 +186,12 @@ export function resolveSidebarNewThreadSeedContext(input: { |
| 186 | worktreePath?: string | null; | 186 | worktreePath?: string | null; |
| 187 | envMode: SidebarNewThreadEnvMode; | 187 | envMode: SidebarNewThreadEnvMode; |
| 188 | } { | 188 | } { |
| 189 | if (input.defaultEnvMode === "worktree") { | ||
| 190 | return { | ||
| 191 | envMode: "worktree", | ||
| 192 | }; | ||
| 193 | } | ||
| 194 | |||
| 189 | if (input.activeDraftThread?.projectId === input.projectId) { | 195 | if (input.activeDraftThread?.projectId === input.projectId) { |
| 190 | return { | 196 | return { |
| 191 | branch: input.activeDraftThread.branch, | 197 | branch: input.activeDraftThread.branch, |
apps/web/src/composerDraftStore.ts+6-1| ... | @@ -1881,13 +1881,18 @@ const composerDraftStore = create<ComposerDraftStoreState>()( | ... | @@ -1881,13 +1881,18 @@ const composerDraftStore = create<ComposerDraftStoreState>()( |
| 1881 | [draftId]: nextDraftThread, | 1881 | [draftId]: nextDraftThread, |
| 1882 | }; | 1882 | }; |
| 1883 | let nextDraftsByThreadKey = state.draftsByThreadKey; | 1883 | let nextDraftsByThreadKey = state.draftsByThreadKey; |
| 1884 | const previousDraftThread = | ||
| 1885 | previousThreadKeyForLogicalProject === undefined | ||
| 1886 | ? undefined | ||
| 1887 | : nextDraftThreadsByThreadKey[previousThreadKeyForLogicalProject]; | ||
| 1884 | if ( | 1888 | if ( |
| 1885 | previousThreadKeyForLogicalProject && | 1889 | previousThreadKeyForLogicalProject && |
| 1886 | previousThreadKeyForLogicalProject !== draftId && | 1890 | previousThreadKeyForLogicalProject !== draftId && |
| 1887 | !isComposerThreadKeyInUse( | 1891 | !isComposerThreadKeyInUse( |
| 1888 | nextLogicalProjectDraftThreadKeyByLogicalProjectKey, | 1892 | nextLogicalProjectDraftThreadKeyByLogicalProjectKey, |
| 1889 | previousThreadKeyForLogicalProject, | 1893 | previousThreadKeyForLogicalProject, |
| 1890 | ) | 1894 | ) && |
| 1895 | !isDraftThreadPromoting(previousDraftThread) | ||
| 1891 | ) { | 1896 | ) { |
| 1892 | delete nextDraftThreadsByThreadKey[previousThreadKeyForLogicalProject]; | 1897 | delete nextDraftThreadsByThreadKey[previousThreadKeyForLogicalProject]; |
| 1893 | if (state.draftsByThreadKey[previousThreadKeyForLogicalProject] !== undefined) { | 1898 | if (state.draftsByThreadKey[previousThreadKeyForLogicalProject] !== undefined) { |
apps/web/src/hooks/useHandleNewThread.ts+2-1| ... | @@ -87,7 +87,8 @@ function useNewThreadState() { | ... | @@ -87,7 +87,8 @@ function useNewThreadState() { |
| 87 | if ( | 87 | if ( |
| 88 | latestActiveDraftThread && | 88 | latestActiveDraftThread && |
| 89 | currentRouteTarget?.kind === "draft" && | 89 | currentRouteTarget?.kind === "draft" && |
| 90 | latestActiveDraftThread.logicalProjectKey === logicalProjectKey | 90 | latestActiveDraftThread.logicalProjectKey === logicalProjectKey && |
| 91 | latestActiveDraftThread.promotedTo == null | ||
| 91 | ) { | 92 | ) { |
| 92 | if (hasBranchOption || hasWorktreePathOption || hasEnvModeOption) { | 93 | if (hasBranchOption || hasWorktreePathOption || hasEnvModeOption) { |
| 93 | setDraftThreadContext(currentRouteTarget.draftId, { | 94 | setDraftThreadContext(currentRouteTarget.draftId, { |