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
11 changes: 1 addition & 10 deletions apps/mobile/src/features/threads/NewTaskDraftScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ import { resolveDraftProjectSelection } from "./new-task-project-selection";
import {
resolveNewTaskBranchLabel,
resolveNewTaskWorkspaceLabel,
resolveNewTaskWorkspaceSelection,
} from "./new-task-context-presentation";
import { useIncomingShare } from "../sharing/IncomingShareProvider";
import { selectIncomingShareAttachmentsForServer } from "../sharing/incoming-share-model";
Expand Down Expand Up @@ -808,15 +807,7 @@ export function NewTaskDraftScreen(props: {
draft.modelSelection ?? null,
selectedProject.id,
) ?? flow.selectedModel;
const workspaceSelection = resolveNewTaskWorkspaceSelection({
selection: draft.workspaceSelection,
fallback: {
mode: flow.workspaceMode,
branch: flow.selectedBranchName,
worktreePath: flow.selectedWorktreePath,
},
isGitRepo: flow.isGitRepo,
});
const workspaceSelection = flow.resolveWorkspaceSelection(draft.workspaceSelection);
const workspaceMode = workspaceSelection.mode;
const selectedBranchName = workspaceSelection.branch;
const initialMessageText = draft.text.trim();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, it } from "vite-plus/test";

import {
fillNewTaskWorkspaceBranch,
resolveNewTaskBranchLabel,
resolveNewTaskBranchWorktreePath,
resolveNewTaskLocalWorkspaceSelection,
Expand Down Expand Up @@ -105,3 +106,66 @@ describe("resolveNewTaskBranchWorktreePath", () => {
).toBeNull();
});
});

describe("fillNewTaskWorkspaceBranch", () => {
const branches = [
{ name: "origin/main", current: false, isDefault: true, isRemote: true },
{ name: "feature", current: true, isDefault: false, worktreePath: "/repo-feature" },
];
const base = {
branches,
projectCwd: "/repo",
worktreeDefaultSettled: true,
};

it("bases a new worktree on the default branch, even when only the remote has it", () => {
expect(
fillNewTaskWorkspaceBranch({
...base,
selection: { mode: "worktree", branch: null, worktreePath: null },
}),
).toEqual({ mode: "worktree", branch: "origin/main", worktreePath: null });
});

it("waits for the workspace default to settle before picking a base", () => {
const selection = { mode: "worktree", branch: null, worktreePath: null } as const;
expect(fillNewTaskWorkspaceBranch({ ...base, selection, worktreeDefaultSettled: false })).toBe(
selection,
);
});

it("follows the current checkout only for a local selection the user just chose", () => {
const selection = { mode: "local", branch: null, worktreePath: null } as const;
expect(fillNewTaskWorkspaceBranch({ ...base, selection })).toBe(selection);
expect(
fillNewTaskWorkspaceBranch({
...base,
selection: { ...selection, followsCurrentCheckout: true },
}),
).toEqual({
mode: "local",
branch: "feature",
worktreePath: "/repo-feature",
followsCurrentCheckout: true,
});
});

it("fills the current branch once refs arrive after local mode was chosen", () => {
const selection = {
mode: "local",
branch: null,
worktreePath: null,
followsCurrentCheckout: true,
} as const;
expect(fillNewTaskWorkspaceBranch({ ...base, branches: [], selection })).toBe(selection);
expect(fillNewTaskWorkspaceBranch({ ...base, selection })).toMatchObject({
branch: "feature",
worktreePath: "/repo-feature",
});
});

it("keeps an explicit branch", () => {
const selection = { mode: "worktree", branch: "release", worktreePath: null } as const;
expect(fillNewTaskWorkspaceBranch({ ...base, selection })).toBe(selection);
});
});
68 changes: 68 additions & 0 deletions apps/mobile/src/features/threads/new-task-context-presentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,71 @@ export function filterNewTaskBranches<T extends { readonly name: string }>(
? branches
: branches.filter((branch) => branch.name.toLowerCase().includes(query));
}

/**
* A worktree with no chosen base starts from the default branch, and a local
* selection the user just switched to follows the current checkout until refs
* load. Both are derived, never written back, so a late ref list or t3.json
* default still wins.
*/
export function fillNewTaskWorkspaceBranch(input: {
readonly selection: ComposerDraftWorkspaceSelection;
readonly branches: ReadonlyArray<{
readonly name: string;
readonly current: boolean;
readonly isDefault: boolean;
readonly isRemote?: boolean;
readonly worktreePath?: string | null;
}>;
readonly projectCwd: string | null;
readonly worktreeDefaultSettled: boolean;
}): ComposerDraftWorkspaceSelection {
const { selection } = input;
if (selection.branch !== null) return selection;
if (selection.mode === "worktree") {
// The default may only exist as origin/<default>, so search remote refs too.
const base =
input.branches.find((branch) => branch.isDefault) ??
input.branches.find((branch) => !branch.isRemote && branch.current) ??
null;
return input.worktreeDefaultSettled && base
? { ...selection, branch: base.name, worktreePath: null }
: selection;
}
if (!selection.followsCurrentCheckout || input.projectCwd === null) return selection;
const local = resolveNewTaskLocalWorkspaceSelection({
branches: input.branches.filter((branch) => !branch.isRemote),
projectCwd: input.projectCwd,
});
return local.awaitsCurrentBranch
? selection
: { ...selection, branch: local.branch, worktreePath: local.worktreePath };
}

/**
* The workspace a new-task draft starts in: its own selection, else the
* project's default mode, with the branch filled in as above. The new-task
* flow, its send path and the home pending-drafts list all resolve through
* this, so what each shows is what is sent.
*/
export function resolveNewTaskDraftWorkspace(input: {
readonly selection: ComposerDraftWorkspaceSelection | undefined;
readonly canChooseWorkspace: boolean;
readonly defaultMode: WorkspaceMode;
readonly defaultModeSettled: boolean;
readonly isGitRepo: boolean;
readonly branches: Parameters<typeof fillNewTaskWorkspaceBranch>[0]["branches"];
readonly projectCwd: string | null;
}): ComposerDraftWorkspaceSelection {
const selection = input.canChooseWorkspace ? input.selection : undefined;
return fillNewTaskWorkspaceBranch({
selection: resolveNewTaskWorkspaceSelection({
selection,
fallback: { mode: input.defaultMode, branch: null, worktreePath: null },
isGitRepo: input.isGitRepo,
}),
branches: input.branches,
projectCwd: input.projectCwd,
worktreeDefaultSettled: selection?.mode !== undefined || input.defaultModeSettled,
});
}
Loading
Loading