diff --git a/apps/mobile/src/features/threads/new-task-flow-provider.tsx b/apps/mobile/src/features/threads/new-task-flow-provider.tsx index d78dd73b18402..5852206095a8f 100644 --- a/apps/mobile/src/features/threads/new-task-flow-provider.tsx +++ b/apps/mobile/src/features/threads/new-task-flow-provider.tsx @@ -89,7 +89,7 @@ import { resolveNewTaskBranchWorktreePath, resolveNewTaskLocalWorkspaceSelection, } from "./new-task-context-presentation"; -import { resolveEnvironmentProjectMatch } from "./new-task-project-selection"; +import { isSameRepository, resolveEnvironmentProjectMatch } from "./new-task-project-selection"; import { useNewTaskProjectDefaults } from "./use-new-task-project-defaults"; import { resolveNewThreadRuntimeMode } from "./thread-settings-options"; import { resolveProjectThreadCreationBranch } from "./projectThreadCreationValidation"; @@ -560,11 +560,14 @@ export function NewTaskFlowProvider( replaceComposerDraftAttachments(selectedProjectDraftKey, []); }; - const carryDraftContentTo = (project: EnvironmentProject): string => { + const carryDraftContentTo = ( + project: EnvironmentProject, + options?: Parameters[2], + ): string => { const target = { environmentId: project.environmentId, projectId: project.id }; const current = readFlow().draftKey; if (current !== null && isNewTaskDraftKey(current)) { - retargetNewTaskDraft(current, target); + retargetNewTaskDraft(current, target, options); return current; } return createNewTaskDraft(target); @@ -584,7 +587,12 @@ export function NewTaskFlowProvider( selectedProject, ); updateFlow({ - draftKey: match ? carryDraftContentTo(match) : readFlow().draftKey, + // A name or title match may be another repository, which must not receive the PR checkout. + draftKey: match + ? carryDraftContentTo(match, { + keepPullRequestWorkspace: isSameRepository(match, selectedProject), + }) + : readFlow().draftKey, environmentId, projectKey: match ? scopedProjectKey(match.environmentId, match.id) : null, }); diff --git a/apps/mobile/src/features/threads/new-task-project-selection.test.ts b/apps/mobile/src/features/threads/new-task-project-selection.test.ts index 99cf9e7d37ef4..faf3862bfa578 100644 --- a/apps/mobile/src/features/threads/new-task-project-selection.test.ts +++ b/apps/mobile/src/features/threads/new-task-project-selection.test.ts @@ -5,6 +5,7 @@ import type { EnvironmentProject } from "@t3tools/client-runtime/state/shell"; import type { HomeProjectScope } from "../home/homeThreadList"; import { getProjectScopeSelectionTarget, + isSameRepository, newTaskDestination, resolveDraftProjectSelection, resolveEnvironmentProjectMatch, @@ -85,6 +86,7 @@ describe("resolveEnvironmentProjectMatch", () => { makeProject("t3code-clone", "server", { repositoryKey: "github.com/t3tools/t3code" }), ]; expect(resolveEnvironmentProjectMatch(target, selected)).toBe(target[1]); + expect(isSameRepository(target[1]!, selected)).toBe(true); }); it("falls back to workspace basename, then title, for unindexed projects", () => { @@ -94,6 +96,7 @@ describe("resolveEnvironmentProjectMatch", () => { makeProject("srv", "server", { workspaceRoot: "/home/me/t3code" }), ]; expect(resolveEnvironmentProjectMatch(byBasename, selected)).toBe(byBasename[1]); + expect(isSameRepository(byBasename[1]!, selected)).toBe(false); const byTitle = [ makeProject("other", "server"), @@ -115,6 +118,7 @@ describe("resolveEnvironmentProjectMatch", () => { const unindexed = makeProject("unindexed", "server", { workspaceRoot: "/srv/t3code" }); expect(resolveEnvironmentProjectMatch([fork, unindexed], selected)).toBe(unindexed); expect(resolveEnvironmentProjectMatch([fork], selected)).toBe(fork); + expect(isSameRepository(fork, selected)).toBe(false); }); }); diff --git a/apps/mobile/src/features/threads/new-task-project-selection.ts b/apps/mobile/src/features/threads/new-task-project-selection.ts index 93c8f1d5f51d6..f6259b298a94f 100644 --- a/apps/mobile/src/features/threads/new-task-project-selection.ts +++ b/apps/mobile/src/features/threads/new-task-project-selection.ts @@ -117,6 +117,19 @@ export function resolveEnvironmentProjectMatch( ); } +/** Both projects have a known repository identity, and it is the same one. */ +export function isSameRepository( + left: EnvironmentProject | null, + right: EnvironmentProject | null, +): boolean { + return ( + left?.repositoryIdentity != null && + right?.repositoryIdentity != null && + repositoryGroupingKeyOf(left.repositoryIdentity) === + repositoryGroupingKeyOf(right.repositoryIdentity) + ); +} + export function resolveDraftProjectSelection( selectedProjectKey: string | null, projects: ReadonlyArray, diff --git a/apps/mobile/src/state/use-composer-drafts.test.ts b/apps/mobile/src/state/use-composer-drafts.test.ts index f7fe9ebbcf57c..f25037f40f0b4 100644 --- a/apps/mobile/src/state/use-composer-drafts.test.ts +++ b/apps/mobile/src/state/use-composer-drafts.test.ts @@ -1901,7 +1901,7 @@ describe("mobile composer drafts", () => { expect(getComposerDraftSnapshot(second).text).toBe("second idea"); }); - it("retargets a new-task draft to another project without losing its text", () => { + it("retargets a new-task draft to another project, keeping only a pull request workspace", () => { const from = { environmentId: EnvironmentId.make("environment-1"), projectId: ProjectId.make("project-1"), @@ -1922,7 +1922,7 @@ describe("mobile composer drafts", () => { }); const createdAt = getComposerDraftSnapshot(key).project?.createdAt; - retargetNewTaskDraft(key, to); + retargetNewTaskDraft(key, to, { keepPullRequestWorkspace: true }); const moved = getComposerDraftSnapshot(key); expect(moved.text).toBe("moving house"); @@ -1932,6 +1932,22 @@ describe("mobile composer drafts", () => { expect(moved.project).toEqual({ ...to, createdAt }); expect(findNewTaskDraftKeys(appAtomRegistry.get(composerDraftsAtom), from)).toEqual([]); expect(findNewTaskDraftKeys(appAtomRegistry.get(composerDraftsAtom), to)).toEqual([key]); + + const pullRequestSelection = { + mode: "worktree" as const, + branch: "feature/pr-head", + worktreePath: "/old-machine/worktrees/pr-42", + startFromOrigin: false, + pullRequestReference: "https://github.com/acme/repo/pull/42", + }; + updateComposerDraftSettings(key, { workspaceSelection: pullRequestSelection }); + retargetNewTaskDraft(key, from, { keepPullRequestWorkspace: true }); + expect(getComposerDraftSnapshot(key).workspaceSelection).toEqual({ + ...pullRequestSelection, + worktreePath: null, + }); + retargetNewTaskDraft(key, to); + expect(getComposerDraftSnapshot(key).workspaceSelection).toBeUndefined(); }); it("retargets a saved new-task draft only after drafts load", async () => { diff --git a/apps/mobile/src/state/use-composer-drafts.ts b/apps/mobile/src/state/use-composer-drafts.ts index 2e4e53ef6dd45..7fb44be817f32 100644 --- a/apps/mobile/src/state/use-composer-drafts.ts +++ b/apps/mobile/src/state/use-composer-drafts.ts @@ -1965,6 +1965,7 @@ export function createNewTaskDraft(project: { export function retargetNewTaskDraft( draftKey: string, project: { readonly environmentId: EnvironmentId; readonly projectId: ProjectId }, + options?: { readonly keepPullRequestWorkspace?: boolean }, ): void { updateComposerDrafts((current) => { const existing = current[draftKey]; @@ -1976,7 +1977,12 @@ export function retargetNewTaskDraft( ) { return current; } - const { workspaceSelection: _workspaceSelection, ...retained } = normalizeDraft(existing); + const { workspaceSelection, ...retained } = normalizeDraft(existing); + // The PR checkout is recreated on the target machine, so only the old local path is dropped. + const keptWorkspaceSelection = + options?.keepPullRequestWorkspace && workspaceSelection?.pullRequestReference !== undefined + ? { ...workspaceSelection, worktreePath: null } + : undefined; // Keep bytes across environment moves but drop server-specific upload stamps. const attachments = retained.attachments.map((attachment) => attachment.uploadEnvironmentId !== undefined && @@ -1988,6 +1994,7 @@ export function retargetNewTaskDraft( ...current, [draftKey]: { ...retained, + ...(keptWorkspaceSelection ? { workspaceSelection: keptWorkspaceSelection } : {}), attachments, project: { environmentId: project.environmentId,