1import type { EnvironmentProject } from "@t3tools/client-runtime/state/shell";
2import { repositoryGroupingKeyOf, type EnvironmentId } from "@t3tools/contracts";
3
4import { scopedProjectKey } from "../../lib/scopedEntities";
5import type { HomeProjectScope } from "../home/homeThreadList";
6
7type DraftProjectSelectionResolution =
8 | { readonly kind: "preserve" }
9 | { readonly kind: "select"; readonly project: EnvironmentProject }
10 | { readonly kind: "pick" };
11
12export function getProjectScopeSelectionTarget(
13 scope: HomeProjectScope,
14 preferredEnvironmentId: EnvironmentId | null,
15): EnvironmentProject {
16 return (
17 scope.projects.find((project) => project.environmentId === preferredEnvironmentId) ??
18 scope.representative
19 );
20}
21
22export function filterProjectScopes(
23 scopes: ReadonlyArray<HomeProjectScope>,
24 searchText: string,
25): ReadonlyArray<HomeProjectScope> {
26 const query = searchText.trim().toLowerCase();
27 if (!query) return scopes;
28 return scopes.filter(
29 (scope) =>
30 scope.title.toLowerCase().includes(query) ||
31 scope.projects.some(
32 (project) =>
33 project.title.toLowerCase().includes(query) ||
34 project.workspaceRoot.toLowerCase().includes(query),
35 ),
36 );
37}
38
39function getOnlySelectableProject(
40 projectScopes: ReadonlyArray<HomeProjectScope>,
41): EnvironmentProject | null {
42 const onlyScope = projectScopes.length === 1 ? projectScopes[0] : null;
43 return onlyScope?.representative ?? null;
44}
45
46/**
47 * Picks the project on a target environment that corresponds to the project
48 * currently selected in the new-task flow, so switching computers follows the
49 * same repo. Repository identity is preferred; projects without one (e.g. not
50 * yet indexed) fall back to workspace basename, then title. When nothing
51 * matches, the first project on the target stands in — the same fallback the
52 * render path applies when no key is selected — so the draft always has a
53 * concrete key to carry over to.
54 */
55export function resolveEnvironmentProjectMatch(
56 projectsOnTarget: ReadonlyArray<EnvironmentProject>,
57 selectedProject: EnvironmentProject | null,
58): EnvironmentProject | null {
59 const repositoryKey = selectedProject?.repositoryIdentity
60 ? repositoryGroupingKeyOf(selectedProject.repositoryIdentity)
61 : null;
62 // `|| null` (not `??`): a pending-task placeholder project can have an empty
63 // workspaceRoot, and an "" basename would match nothing meaningful.
64 const workspaceBasename = selectedProject?.workspaceRoot.split("/").at(-1) || null;
65 // The weaker signals only apply where identity is unknown on at least one
66 // side; two known, different repositories never match on a shared basename
67 // or title (mirrors the environment list filter in the new-task flow).
68 const isKnownMismatch = (project: EnvironmentProject) => {
69 const projectKey = project.repositoryIdentity
70 ? repositoryGroupingKeyOf(project.repositoryIdentity)
71 : null;
72 return repositoryKey !== null && projectKey !== null && projectKey !== repositoryKey;
73 };
74 return (
75 (repositoryKey !== null
76 ? projectsOnTarget.find(
77 (project) =>
78 (project.repositoryIdentity
79 ? repositoryGroupingKeyOf(project.repositoryIdentity)
80 : null) === repositoryKey,
81 )
82 : undefined) ??
83 (workspaceBasename !== null
84 ? projectsOnTarget.find(
85 (project) =>
86 !isKnownMismatch(project) &&
87 project.workspaceRoot.split("/").at(-1) === workspaceBasename,
88 )
89 : undefined) ??
90 (selectedProject !== null
91 ? projectsOnTarget.find(
92 (project) => !isKnownMismatch(project) && project.title === selectedProject.title,
93 )
94 : undefined) ??
95 projectsOnTarget[0] ??
96 null
97 );
98}
99
100export function resolveDraftProjectSelection(
101 selectedProjectKey: string | null,
102 projects: ReadonlyArray<EnvironmentProject>,
103 projectScopes: ReadonlyArray<HomeProjectScope>,
104): DraftProjectSelectionResolution {
105 const hasExplicitProjectSelection =
106 selectedProjectKey !== null &&
107 projects.some(
108 (project) => scopedProjectKey(project.environmentId, project.id) === selectedProjectKey,
109 );
110 if (hasExplicitProjectSelection) {
111 return { kind: "preserve" };
112 }
113
114 const onlyProject = getOnlySelectableProject(projectScopes);
115 return onlyProject ? { kind: "select", project: onlyProject } : { kind: "pick" };
116}