diff --git a/apps/web/src/components/CommandPalette.logic.test.ts b/apps/web/src/components/CommandPalette.logic.test.ts index b82147b376c8492287a6ebc860e96f70bc656076..25896baf75559446ff255338c69bb07be0c8751f 100644 --- a/apps/web/src/components/CommandPalette.logic.test.ts +++ b/apps/web/src/components/CommandPalette.logic.test.ts @@ -3,6 +3,7 @@ import { EnvironmentId, ProjectId, ProviderInstanceId, ThreadId } from "@t3tools import type { Project, Thread } from "../types"; import { buildBrowseGroups, + buildCommandPaletteProjectMetadata, buildProjectActionItems, buildThreadActionItems, buildLinkedThreadActionItems, @@ -55,6 +56,124 @@ describe("linked pull request thread navigation", () => { }); }); +describe("buildCommandPaletteProjectMetadata", () => { + const localEnvironmentId = EnvironmentId.make("environment-local"); + const remoteEnvironmentId = EnvironmentId.make("environment-build-box"); + const locations = new Map([ + [localEnvironmentId, { kind: "local" as const, label: "Local", machine: "laptop" as const }], + [ + remoteEnvironmentId, + { kind: "remote" as const, label: "Build box", machine: "server" as const }, + ], + ]); + + it("makes every member environment and path searchable", () => { + const metadata = buildCommandPaletteProjectMetadata({ + projects: [ + { + environmentId: localEnvironmentId, + title: "T3 Code", + workspaceRoot: "/Users/theo/Projects/t3code", + }, + { + environmentId: remoteEnvironmentId, + title: "t3code", + workspaceRoot: "/srv/t3code", + }, + ], + locationByEnvironmentId: locations, + }); + + expect(metadata.searchTerms).toEqual([ + "T3 Code", + "/Users/theo/Projects/t3code", + "Local", + "t3code", + "/srv/t3code", + "Build box", + ]); + expect(metadata.environmentLabels).toEqual(["Local", "Build box"]); + + const [filteredGroup] = filterCommandPaletteGroups({ + activeGroups: [], + query: "build box", + isInSubmenu: false, + projectSearchItems: [ + { + kind: "action", + value: "project:t3code", + title: "T3 Code", + searchTerms: metadata.searchTerms, + icon: null, + run: async () => undefined, + }, + ], + threadSearchItems: [], + }); + expect(filteredGroup?.items).toHaveLength(1); + }); + + it("deduplicates grouped checkouts by environment", () => { + const metadata = buildCommandPaletteProjectMetadata({ + projects: [ + { + environmentId: remoteEnvironmentId, + title: "T3 Code", + workspaceRoot: "/srv/t3code", + }, + { + environmentId: remoteEnvironmentId, + title: "T3 Code worktree", + workspaceRoot: "/srv/t3code-feature", + }, + ], + locationByEnvironmentId: locations, + }); + + expect(metadata.environmentLabels).toEqual(["Build box"]); + }); + + it("deduplicates distinct environments with the same label", () => { + const secondRemoteEnvironmentId = EnvironmentId.make("environment-build-box-2"); + const metadata = buildCommandPaletteProjectMetadata({ + projects: [ + { + environmentId: remoteEnvironmentId, + title: "T3 Code", + workspaceRoot: "/srv/t3code", + }, + { + environmentId: secondRemoteEnvironmentId, + title: "T3 Code mirror", + workspaceRoot: "/srv/mirror/t3code", + }, + ], + locationByEnvironmentId: new Map([ + [remoteEnvironmentId, { label: "Build box" }], + [secondRemoteEnvironmentId, { label: "Build box" }], + ]), + }); + + expect(metadata.environmentLabels).toEqual(["Build box"]); + }); + + it("uses a human-readable fallback when presentation data is unavailable", () => { + const metadata = buildCommandPaletteProjectMetadata({ + projects: [ + { + environmentId: remoteEnvironmentId, + title: "T3 Code", + workspaceRoot: "/srv/t3code", + }, + ], + locationByEnvironmentId: new Map(), + }); + + expect(metadata.searchTerms).toContain("Remote"); + expect(metadata.environmentLabels).toEqual(["Remote"]); + }); +}); + describe("reduceCommandPaletteUiState", () => { const closedState = { open: false, mode: "command", openIntent: null } as const; diff --git a/apps/web/src/components/CommandPalette.logic.ts b/apps/web/src/components/CommandPalette.logic.ts index 95313ee940e68a7f5b1662fffab477d8133b9fa5..57c1711de158abeb04d3df568c4070f43ef19ce5 100644 --- a/apps/web/src/components/CommandPalette.logic.ts +++ b/apps/web/src/components/CommandPalette.logic.ts @@ -1,6 +1,7 @@ import { threadPullRequestSearchTerms } from "@t3tools/shared/threadPullRequests"; import type { CommandPaletteLinkedThreads } from "../commandPaletteBus"; import { + type EnvironmentId, type FilesystemBrowseEntry, type KeybindingCommand, THREAD_JUMP_KEYBINDING_COMMANDS, @@ -185,6 +186,22 @@ export type CommandPaletteMode = "root" | "root-browse" | "submenu" | "submenu-b // every other surface uses the real title, so overriding it desyncs the icon. export type CommandPaletteProject = Project & { readonly displayName: string }; +export function buildCommandPaletteProjectMetadata(input: { + readonly projects: ReadonlyArray>; + readonly locationByEnvironmentId: ReadonlyMap; +}) { + const searchTerms: string[] = []; + const environmentLabels = new Set(); + + for (const project of input.projects) { + const label = input.locationByEnvironmentId.get(project.environmentId)?.label ?? "Remote"; + searchTerms.push(project.title, project.workspaceRoot, label); + environmentLabels.add(label); + } + + return { searchTerms, environmentLabels: [...environmentLabels] }; +} + export function buildProjectActionItems(input: { projects: ReadonlyArray; valuePrefix: string; diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index c54f919125d37170981d362ca558ec3deea9517c..4a9877f87a027d614a965e86d36551a148b8c750 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -126,6 +126,7 @@ import { ADDON_ICON_CLASS, browseInputEndPaddingClass, buildBrowseGroups, + buildCommandPaletteProjectMetadata, buildProjectActionItems, buildRootGroups, buildThreadActionItems, @@ -186,6 +187,38 @@ function projectFavicon(project: Project) { return ; } +function ProjectSearchDescription(props: { + readonly environmentLabels: ReadonlyArray; + readonly grouped: boolean; + readonly location: { + readonly kind: "local" | "remote"; + readonly label: string; + readonly machine: EnvironmentMachineKind; + }; + readonly workspaceRoot: string; +}) { + if (!props.grouped) { + return ( + + + {props.location.kind === "remote" ? ( + + ) : null} + {props.location.label} + + + {props.workspaceRoot} + + ); + } + + return {props.environmentLabels.join(" ยท ")}; +} + function getEnvironmentBrowsePlatform(os: string | null | undefined): string { if (os === "windows") { return "Win32"; @@ -1079,15 +1112,43 @@ function OpenCommandPaletteDialog(props: { projects: pickerProjects, valuePrefix: "project", searchTerms: (project) => { - const group = projectGroupByTargetKey.get(`${project.environmentId}:${project.id}`); + const members = projectGroupByTargetKey.get(`${project.environmentId}:${project.id}`) + ?.memberProjects ?? [project]; + return buildCommandPaletteProjectMetadata({ + projects: members, + locationByEnvironmentId: projectEnvironmentLocationById, + }).searchTerms; + }, + renderDescription: (project) => { + const members = projectGroupByTargetKey.get(`${project.environmentId}:${project.id}`) + ?.memberProjects ?? [project]; + const metadata = buildCommandPaletteProjectMetadata({ + projects: members, + locationByEnvironmentId: projectEnvironmentLocationById, + }); + const location = projectEnvironmentLocationById.get(project.environmentId) ?? { + kind: "remote" as const, + label: "Remote", + machine: "server" as const, + }; return ( - group?.memberProjects.flatMap((member) => [member.title, member.workspaceRoot]) ?? [] + 1} + location={location} + workspaceRoot={project.workspaceRoot} + /> ); }, icon: projectFavicon, runProject: openProjectFromSearch, }), - [openProjectFromSearch, pickerProjects, projectGroupByTargetKey], + [ + openProjectFromSearch, + pickerProjects, + projectEnvironmentLocationById, + projectGroupByTargetKey, + ], ); const projectThreadItems = useMemo( @@ -1176,6 +1237,9 @@ function OpenCommandPaletteDialog(props: { ) : null} {projectLabel} + {props.environmentLabel ? ( + <> + + {props.environmentLabel} + + ) : null} ) : null}