1import type { EnvironmentId, EnvironmentMachineKind } from "@t3tools/contracts";
2
3import type { SidebarProjectSnapshot } from "../sidebarProjectGrouping";
4import type { Project } from "../types";
5import {
6 buildCommandPaletteProjectMetadata,
7 buildProjectActionItems,
8 type CommandPaletteItemPin,
9 type CommandPaletteProject,
10 ITEM_ICON_CLASS,
11} from "./CommandPalette.logic";
12import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon";
13import { ProjectFavicon } from "./ProjectFavicon";
14import { COMMAND_PALETTE_META_ICON_CLASS, CommandPaletteMetaDot } from "./ThreadCommandSubtitle";
15
16export interface CommandPaletteProjectLocation {
17 readonly kind: "local" | "remote";
18 readonly label: string;
19 readonly machine: EnvironmentMachineKind;
20}
21
22const UNKNOWN_PROJECT_LOCATION: CommandPaletteProjectLocation = {
23 kind: "remote",
24 label: "Remote",
25 machine: "server",
26};
27
28interface ProjectItemsInput {
29 readonly projects: ReadonlyArray<CommandPaletteProject>;
30 readonly projectGroupByTargetKey: ReadonlyMap<string, SidebarProjectSnapshot>;
31 readonly locationByEnvironmentId: ReadonlyMap<EnvironmentId, CommandPaletteProjectLocation>;
32 readonly runProject: (project: CommandPaletteProject) => Promise<void>;
33 readonly pin?: (project: CommandPaletteProject) => CommandPaletteItemPin | null;
34}
35
36function projectFaviconIcon(project: Project) {
37 return <ProjectFavicon project={project} className={ITEM_ICON_CLASS} />;
38}
39
40/** Projects as root search results; a grouped project matches on every member. */
41export function buildProjectSearchItems(input: ProjectItemsInput) {
42 const membersOf = (project: CommandPaletteProject) =>
43 input.projectGroupByTargetKey.get(`${project.environmentId}:${project.id}`)?.memberProjects ?? [
44 project,
45 ];
46 return buildProjectActionItems({
47 projects: input.projects,
48 valuePrefix: "project",
49 searchTerms: (project) =>
50 buildCommandPaletteProjectMetadata({
51 projects: membersOf(project),
52 locationByEnvironmentId: input.locationByEnvironmentId,
53 }).searchTerms,
54 renderDescription: (project) => {
55 const members = membersOf(project);
56 const metadata = buildCommandPaletteProjectMetadata({
57 projects: members,
58 locationByEnvironmentId: input.locationByEnvironmentId,
59 });
60 return members.length > 1 ? (
61 <span className="truncate">{metadata.environmentLabels.join(" · ")}</span>
62 ) : (
63 <ProjectLocationDescription
64 location={input.locationByEnvironmentId.get(project.environmentId)}
65 workspaceRoot={project.workspaceRoot}
66 />
67 );
68 },
69 icon: projectFaviconIcon,
70 runProject: input.runProject,
71 ...(input.pin ? { pin: input.pin } : {}),
72 });
73}
74
75/** Projects as targets of the "New thread in..." submenu. */
76export function buildNewThreadInProjectItems(
77 input: ProjectItemsInput & { readonly currentProjectKey: string | null },
78) {
79 return buildProjectActionItems({
80 projects: input.projects,
81 valuePrefix: "new-thread-in",
82 searchTerms: (project) => {
83 const group = input.projectGroupByTargetKey.get(`${project.environmentId}:${project.id}`);
84 const location = input.locationByEnvironmentId.get(project.environmentId);
85 return [
86 ...(group?.memberProjects.flatMap((member) => [member.title, member.workspaceRoot]) ?? []),
87 ...(location ? [location.label] : []),
88 ];
89 },
90 renderDescription: (project) => (
91 <ProjectLocationDescription
92 location={input.locationByEnvironmentId.get(project.environmentId)}
93 workspaceRoot={project.workspaceRoot}
94 current={input.currentProjectKey === `${project.environmentId}:${project.id}`}
95 />
96 ),
97 icon: projectFaviconIcon,
98 runProject: input.runProject,
99 ...(input.pin ? { pin: input.pin } : {}),
100 });
101}
102
103function ProjectLocationDescription(props: {
104 readonly location: CommandPaletteProjectLocation | undefined;
105 readonly workspaceRoot: string;
106 readonly current?: boolean;
107}) {
108 const location = props.location ?? UNKNOWN_PROJECT_LOCATION;
109 return (
110 <span className="flex min-w-0 items-center gap-1">
111 {props.current ? (
112 <>
113 <span className="shrink-0">Current</span>
114 <CommandPaletteMetaDot />
115 </>
116 ) : null}
117 <span className="inline-flex min-w-0 items-center gap-1">
118 {location.kind === "remote" ? (
119 <EnvironmentMachineIcon
120 aria-hidden
121 kind={location.machine}
122 className={COMMAND_PALETTE_META_ICON_CLASS}
123 />
124 ) : null}
125 <span className="truncate">{location.label}</span>
126 </span>
127 <CommandPaletteMetaDot />
128 <span className="min-w-0 flex-1 truncate">{props.workspaceRoot}</span>
129 </span>
130 );
131}