1import { sanitizeNewRefName } from "@t3tools/shared/git";
2import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients";
3import { resolveSendEnvMode } from "@t3tools/shared/projectSettings";
4import type { ComposerDraftWorkspaceSelection } from "../../state/use-composer-drafts";
5
6type WorkspaceMode = "local" | "worktree";
7
8export function resolveNewTaskWorkspaceSelection(input: {
9 readonly selection: ComposerDraftWorkspaceSelection | undefined;
10 readonly fallback: ComposerDraftWorkspaceSelection;
11 readonly isGitRepo: boolean;
12}): ComposerDraftWorkspaceSelection {
13 const selection = input.selection ?? input.fallback;
14 const mode = resolveSendEnvMode({ requestedEnvMode: selection.mode, isGitRepo: input.isGitRepo });
15 return input.isGitRepo ? selection : { mode, branch: null, worktreePath: null };
16}
17
18export function resolveNewTaskWorkspaceLabel(input: {
19 readonly workspaceMode: WorkspaceMode;
20 readonly worktreePath: string | null;
21}): "Current checkout" | "Current worktree" | "New worktree" {
22 if (input.workspaceMode === "worktree") {
23 return "New worktree";
24 }
25 return input.worktreePath ? "Current worktree" : "Current checkout";
26}
27
28export function resolveNewTaskBranchWorktreePath(input: {
29 readonly workspaceMode: WorkspaceMode;
30 readonly projectCwd: string;
31 readonly branchWorktreePath: string | null | undefined;
32}): string | null {
33 if (
34 input.workspaceMode === "worktree" ||
35 !input.branchWorktreePath ||
36 input.branchWorktreePath === input.projectCwd
37 ) {
38 return null;
39 }
40 return input.branchWorktreePath;
41}
42
43export function resolveNewTaskLocalWorkspaceSelection(input: {
44 readonly branches: ReadonlyArray<{
45 readonly name: string;
46 readonly current: boolean;
47 readonly worktreePath?: string | null;
48 }>;
49 readonly projectCwd: string;
50}): {
51 readonly branch: string | null;
52 readonly worktreePath: string | null;
53 readonly awaitsCurrentBranch: boolean;
54} {
55 const currentBranch = input.branches.find((branch) => branch.current) ?? null;
56 if (!currentBranch) {
57 return {
58 branch: null,
59 worktreePath: null,
60 awaitsCurrentBranch: true,
61 };
62 }
63
64 return {
65 branch: currentBranch.name,
66 worktreePath: resolveNewTaskBranchWorktreePath({
67 workspaceMode: "local",
68 projectCwd: input.projectCwd,
69 branchWorktreePath: currentBranch.worktreePath,
70 }),
71 awaitsCurrentBranch: false,
72 };
73}
74
75export function resolveNewTaskBranchLabel(input: {
76 readonly branchName: string | null;
77 readonly startFromOrigin: boolean;
78 readonly workspaceMode: WorkspaceMode;
79 readonly pullRequestReference?: string;
80}): string {
81 if (input.workspaceMode === "worktree" && input.pullRequestReference) {
82 const parsed = sourceControlClients.parseChangeRequestUrl(input.pullRequestReference);
83 if (parsed) return `From ${parsed.definition.changeRequest.shortLabel} #${parsed.link.number}`;
84 }
85 if (!input.branchName) {
86 return "Choose branch";
87 }
88
89 if (input.workspaceMode === "local") {
90 return input.branchName;
91 }
92
93 // Only the server can determine a local branch's configured remote.
94 return input.startFromOrigin ? `From ${input.branchName} (remote)` : `From ${input.branchName}`;
95}
96
97export function shouldCheckoutNewTaskBranch(input: {
98 readonly branchIsCurrent: boolean;
99 readonly branchWorktreePath: string | null | undefined;
100 readonly workspaceMode: WorkspaceMode;
101}): boolean {
102 return input.workspaceMode === "local" && !input.branchIsCurrent && !input.branchWorktreePath;
103}
104
105export function filterNewTaskBranches<T extends { readonly name: string }>(
106 branches: ReadonlyArray<T>,
107 rawQuery: string,
108): ReadonlyArray<T> {
109 const query = sanitizeNewRefName(rawQuery).toLowerCase();
110 return query.length === 0
111 ? branches
112 : branches.filter((branch) => branch.name.toLowerCase().includes(query));
113}
114
115/**
116 * A worktree with no chosen base starts from the default branch, and a local
117 * selection the user just switched to follows the current checkout until refs
118 * load. Both are derived, never written back, so a late ref list or t3.json
119 * default still wins.
120 */
121export function fillNewTaskWorkspaceBranch(input: {
122 readonly selection: ComposerDraftWorkspaceSelection;
123 readonly branches: ReadonlyArray<{
124 readonly name: string;
125 readonly current: boolean;
126 readonly isDefault: boolean;
127 readonly isRemote?: boolean;
128 readonly worktreePath?: string | null;
129 }>;
130 readonly projectCwd: string | null;
131 readonly worktreeDefaultSettled: boolean;
132}): ComposerDraftWorkspaceSelection {
133 const { selection } = input;
134 if (selection.branch !== null) return selection;
135 if (selection.mode === "worktree") {
136 // The default may only exist as origin/<default>, so search remote refs too.
137 const base =
138 input.branches.find((branch) => branch.isDefault) ??
139 input.branches.find((branch) => !branch.isRemote && branch.current) ??
140 null;
141 return input.worktreeDefaultSettled && base
142 ? { ...selection, branch: base.name, worktreePath: null }
143 : selection;
144 }
145 if (!selection.followsCurrentCheckout || input.projectCwd === null) return selection;
146 const local = resolveNewTaskLocalWorkspaceSelection({
147 branches: input.branches.filter((branch) => !branch.isRemote),
148 projectCwd: input.projectCwd,
149 });
150 return local.awaitsCurrentBranch
151 ? selection
152 : { ...selection, branch: local.branch, worktreePath: local.worktreePath };
153}
154
155/**
156 * The workspace a new-task draft starts in: its own selection, else the
157 * project's default mode, with the branch filled in as above. The new-task
158 * flow, its send path and the home pending-drafts list all resolve through
159 * this, so what each shows is what is sent.
160 */
161export function resolveNewTaskDraftWorkspace(input: {
162 readonly selection: ComposerDraftWorkspaceSelection | undefined;
163 readonly canChooseWorkspace: boolean;
164 readonly defaultMode: WorkspaceMode;
165 readonly defaultModeSettled: boolean;
166 readonly isGitRepo: boolean;
167 readonly branches: Parameters<typeof fillNewTaskWorkspaceBranch>[0]["branches"];
168 readonly projectCwd: string | null;
169}): ComposerDraftWorkspaceSelection {
170 const selection = input.canChooseWorkspace ? input.selection : undefined;
171 return fillNewTaskWorkspaceBranch({
172 selection: resolveNewTaskWorkspaceSelection({
173 selection,
174 fallback: { mode: input.defaultMode, branch: null, worktreePath: null },
175 isGitRepo: input.isGitRepo,
176 }),
177 branches: input.branches,
178 projectCwd: input.projectCwd,
179 worktreeDefaultSettled: selection?.mode !== undefined || input.defaultModeSettled,
180 });
181}