Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions apps/mobile/src/features/threads/new-task-flow-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -560,11 +560,14 @@ export function NewTaskFlowProvider(
replaceComposerDraftAttachments(selectedProjectDraftKey, []);
};

const carryDraftContentTo = (project: EnvironmentProject): string => {
const carryDraftContentTo = (
project: EnvironmentProject,
options?: Parameters<typeof retargetNewTaskDraft>[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);
Expand All @@ -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,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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", () => {
Expand All @@ -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"),
Expand All @@ -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);
});
});

Expand Down
13 changes: 13 additions & 0 deletions apps/mobile/src/features/threads/new-task-project-selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<EnvironmentProject>,
Expand Down
20 changes: 18 additions & 2 deletions apps/mobile/src/state/use-composer-drafts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -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");
Expand All @@ -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 () => {
Expand Down
9 changes: 8 additions & 1 deletion apps/mobile/src/state/use-composer-drafts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -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 &&
Expand All @@ -1988,6 +1994,7 @@ export function retargetNewTaskDraft(
...current,
[draftKey]: {
...retained,
...(keptWorkspaceSelection ? { workspaceSelection: keptWorkspaceSelection } : {}),
attachments,
project: {
environmentId: project.environmentId,
Expand Down
Loading