| author | |
| committer | |
| log | c52b8d96e4b34201f19b5e5bb12c6b2a77bfaa9a |
| tree | f7525127ad6ec525c67907ba11057f824aaf0dce |
| parent | 2afa02a280acb0db4868d36409ba9bf7eae286c1 |
| signature | Signed by PGP key B5690EEEBB952194 |
4 files changed, 211 insertions(+), 3 deletions(-)
apps/web/src/components/CommandPalette.logic.test.ts+119| ... | ... | @@ -3,6 +3,7 @@ import { EnvironmentId, ProjectId, ProviderInstanceId, ThreadId } from "@t3tools |
| 3 | 3 | import type { Project, Thread } from "../types"; |
| 4 | 4 | import { |
| 5 | 5 | buildBrowseGroups, |
| 6 | buildCommandPaletteProjectMetadata, | |
| 6 | 7 | buildProjectActionItems, |
| 7 | 8 | buildThreadActionItems, |
| 8 | 9 | buildLinkedThreadActionItems, |
| ... | ... | @@ -55,6 +56,124 @@ describe("linked pull request thread navigation", () => { |
| 55 | 56 | }); |
| 56 | 57 | }); |
| 57 | 58 | |
| 59 | describe("buildCommandPaletteProjectMetadata", () => { | |
| 60 | const localEnvironmentId = EnvironmentId.make("environment-local"); | |
| 61 | const remoteEnvironmentId = EnvironmentId.make("environment-build-box"); | |
| 62 | const locations = new Map([ | |
| 63 | [localEnvironmentId, { kind: "local" as const, label: "Local", machine: "laptop" as const }], | |
| 64 | [ | |
| 65 | remoteEnvironmentId, | |
| 66 | { kind: "remote" as const, label: "Build box", machine: "server" as const }, | |
| 67 | ], | |
| 68 | ]); | |
| 69 | ||
| 70 | it("makes every member environment and path searchable", () => { | |
| 71 | const metadata = buildCommandPaletteProjectMetadata({ | |
| 72 | projects: [ | |
| 73 | { | |
| 74 | environmentId: localEnvironmentId, | |
| 75 | title: "T3 Code", | |
| 76 | workspaceRoot: "/Users/theo/Projects/t3code", | |
| 77 | }, | |
| 78 | { | |
| 79 | environmentId: remoteEnvironmentId, | |
| 80 | title: "t3code", | |
| 81 | workspaceRoot: "/srv/t3code", | |
| 82 | }, | |
| 83 | ], | |
| 84 | locationByEnvironmentId: locations, | |
| 85 | }); | |
| 86 | ||
| 87 | expect(metadata.searchTerms).toEqual([ | |
| 88 | "T3 Code", | |
| 89 | "/Users/theo/Projects/t3code", | |
| 90 | "Local", | |
| 91 | "t3code", | |
| 92 | "/srv/t3code", | |
| 93 | "Build box", | |
| 94 | ]); | |
| 95 | expect(metadata.environmentLabels).toEqual(["Local", "Build box"]); | |
| 96 | ||
| 97 | const [filteredGroup] = filterCommandPaletteGroups({ | |
| 98 | activeGroups: [], | |
| 99 | query: "build box", | |
| 100 | isInSubmenu: false, | |
| 101 | projectSearchItems: [ | |
| 102 | { | |
| 103 | kind: "action", | |
| 104 | value: "project:t3code", | |
| 105 | title: "T3 Code", | |
| 106 | searchTerms: metadata.searchTerms, | |
| 107 | icon: null, | |
| 108 | run: async () => undefined, | |
| 109 | }, | |
| 110 | ], | |
| 111 | threadSearchItems: [], | |
| 112 | }); | |
| 113 | expect(filteredGroup?.items).toHaveLength(1); | |
| 114 | }); | |
| 115 | ||
| 116 | it("deduplicates grouped checkouts by environment", () => { | |
| 117 | const metadata = buildCommandPaletteProjectMetadata({ | |
| 118 | projects: [ | |
| 119 | { | |
| 120 | environmentId: remoteEnvironmentId, | |
| 121 | title: "T3 Code", | |
| 122 | workspaceRoot: "/srv/t3code", | |
| 123 | }, | |
| 124 | { | |
| 125 | environmentId: remoteEnvironmentId, | |
| 126 | title: "T3 Code worktree", | |
| 127 | workspaceRoot: "/srv/t3code-feature", | |
| 128 | }, | |
| 129 | ], | |
| 130 | locationByEnvironmentId: locations, | |
| 131 | }); | |
| 132 | ||
| 133 | expect(metadata.environmentLabels).toEqual(["Build box"]); | |
| 134 | }); | |
| 135 | ||
| 136 | it("deduplicates distinct environments with the same label", () => { | |
| 137 | const secondRemoteEnvironmentId = EnvironmentId.make("environment-build-box-2"); | |
| 138 | const metadata = buildCommandPaletteProjectMetadata({ | |
| 139 | projects: [ | |
| 140 | { | |
| 141 | environmentId: remoteEnvironmentId, | |
| 142 | title: "T3 Code", | |
| 143 | workspaceRoot: "/srv/t3code", | |
| 144 | }, | |
| 145 | { | |
| 146 | environmentId: secondRemoteEnvironmentId, | |
| 147 | title: "T3 Code mirror", | |
| 148 | workspaceRoot: "/srv/mirror/t3code", | |
| 149 | }, | |
| 150 | ], | |
| 151 | locationByEnvironmentId: new Map([ | |
| 152 | [remoteEnvironmentId, { label: "Build box" }], | |
| 153 | [secondRemoteEnvironmentId, { label: "Build box" }], | |
| 154 | ]), | |
| 155 | }); | |
| 156 | ||
| 157 | expect(metadata.environmentLabels).toEqual(["Build box"]); | |
| 158 | }); | |
| 159 | ||
| 160 | it("uses a human-readable fallback when presentation data is unavailable", () => { | |
| 161 | const metadata = buildCommandPaletteProjectMetadata({ | |
| 162 | projects: [ | |
| 163 | { | |
| 164 | environmentId: remoteEnvironmentId, | |
| 165 | title: "T3 Code", | |
| 166 | workspaceRoot: "/srv/t3code", | |
| 167 | }, | |
| 168 | ], | |
| 169 | locationByEnvironmentId: new Map(), | |
| 170 | }); | |
| 171 | ||
| 172 | expect(metadata.searchTerms).toContain("Remote"); | |
| 173 | expect(metadata.environmentLabels).toEqual(["Remote"]); | |
| 174 | }); | |
| 175 | }); | |
| 176 | ||
| 58 | 177 | describe("reduceCommandPaletteUiState", () => { |
| 59 | 178 | const closedState = { open: false, mode: "command", openIntent: null } as const; |
| 60 | 179 |
apps/web/src/components/CommandPalette.logic.ts+17| ... | ... | @@ -1,6 +1,7 @@ |
| 1 | 1 | import { threadPullRequestSearchTerms } from "@t3tools/shared/threadPullRequests"; |
| 2 | 2 | import type { CommandPaletteLinkedThreads } from "../commandPaletteBus"; |
| 3 | 3 | import { |
| 4 | type EnvironmentId, | |
| 4 | 5 | type FilesystemBrowseEntry, |
| 5 | 6 | type KeybindingCommand, |
| 6 | 7 | THREAD_JUMP_KEYBINDING_COMMANDS, |
| ... | ... | @@ -185,6 +186,22 @@ export type CommandPaletteMode = "root" | "root-browse" | "submenu" | "submenu-b |
| 185 | 186 | // every other surface uses the real title, so overriding it desyncs the icon. |
| 186 | 187 | export type CommandPaletteProject = Project & { readonly displayName: string }; |
| 187 | 188 | |
| 189 | export function buildCommandPaletteProjectMetadata(input: { | |
| 190 | readonly projects: ReadonlyArray<Pick<Project, "environmentId" | "title" | "workspaceRoot">>; | |
| 191 | readonly locationByEnvironmentId: ReadonlyMap<EnvironmentId, { readonly label: string }>; | |
| 192 | }) { | |
| 193 | const searchTerms: string[] = []; | |
| 194 | const environmentLabels = new Set<string>(); | |
| 195 | ||
| 196 | for (const project of input.projects) { | |
| 197 | const label = input.locationByEnvironmentId.get(project.environmentId)?.label ?? "Remote"; | |
| 198 | searchTerms.push(project.title, project.workspaceRoot, label); | |
| 199 | environmentLabels.add(label); | |
| 200 | } | |
| 201 | ||
| 202 | return { searchTerms, environmentLabels: [...environmentLabels] }; | |
| 203 | } | |
| 204 | ||
| 188 | 205 | export function buildProjectActionItems(input: { |
| 189 | 206 | projects: ReadonlyArray<CommandPaletteProject>; |
| 190 | 207 | valuePrefix: string; |
apps/web/src/components/CommandPalette.tsx+68-3| ... | ... | @@ -126,6 +126,7 @@ import { |
| 126 | 126 | ADDON_ICON_CLASS, |
| 127 | 127 | browseInputEndPaddingClass, |
| 128 | 128 | buildBrowseGroups, |
| 129 | buildCommandPaletteProjectMetadata, | |
| 129 | 130 | buildProjectActionItems, |
| 130 | 131 | buildRootGroups, |
| 131 | 132 | buildThreadActionItems, |
| ... | ... | @@ -186,6 +187,38 @@ function projectFavicon(project: Project) { |
| 186 | 187 | return <ProjectFavicon project={project} className="size-4" />; |
| 187 | 188 | } |
| 188 | 189 | |
| 190 | function ProjectSearchDescription(props: { | |
| 191 | readonly environmentLabels: ReadonlyArray<string>; | |
| 192 | readonly grouped: boolean; | |
| 193 | readonly location: { | |
| 194 | readonly kind: "local" | "remote"; | |
| 195 | readonly label: string; | |
| 196 | readonly machine: EnvironmentMachineKind; | |
| 197 | }; | |
| 198 | readonly workspaceRoot: string; | |
| 199 | }) { | |
| 200 | if (!props.grouped) { | |
| 201 | return ( | |
| 202 | <span className="flex min-w-0 items-center gap-1"> | |
| 203 | <span className="inline-flex min-w-0 items-center gap-1"> | |
| 204 | {props.location.kind === "remote" ? ( | |
| 205 | <EnvironmentMachineIcon | |
| 206 | aria-hidden | |
| 207 | kind={props.location.machine} | |
| 208 | className={COMMAND_PALETTE_META_ICON_CLASS} | |
| 209 | /> | |
| 210 | ) : null} | |
| 211 | <span className="truncate">{props.location.label}</span> | |
| 212 | </span> | |
| 213 | <CommandPaletteMetaDot /> | |
| 214 | <span className="truncate">{props.workspaceRoot}</span> | |
| 215 | </span> | |
| 216 | ); | |
| 217 | } | |
| 218 | ||
| 219 | return <span className="truncate">{props.environmentLabels.join(" · ")}</span>; | |
| 220 | } | |
| 221 | ||
| 189 | 222 | function getEnvironmentBrowsePlatform(os: string | null | undefined): string { |
| 190 | 223 | if (os === "windows") { |
| 191 | 224 | return "Win32"; |
| ... | ... | @@ -1079,15 +1112,43 @@ function OpenCommandPaletteDialog(props: { |
| 1079 | 1112 | projects: pickerProjects, |
| 1080 | 1113 | valuePrefix: "project", |
| 1081 | 1114 | searchTerms: (project) => { |
| 1082 | const group = projectGroupByTargetKey.get(`${project.environmentId}:${project.id}`); | |
| 1115 | const members = projectGroupByTargetKey.get(`${project.environmentId}:${project.id}`) | |
| 1116 | ?.memberProjects ?? [project]; | |
| 1117 | return buildCommandPaletteProjectMetadata({ | |
| 1118 | projects: members, | |
| 1119 | locationByEnvironmentId: projectEnvironmentLocationById, | |
| 1120 | }).searchTerms; | |
| 1121 | }, | |
| 1122 | renderDescription: (project) => { | |
| 1123 | const members = projectGroupByTargetKey.get(`${project.environmentId}:${project.id}`) | |
| 1124 | ?.memberProjects ?? [project]; | |
| 1125 | const metadata = buildCommandPaletteProjectMetadata({ | |
| 1126 | projects: members, | |
| 1127 | locationByEnvironmentId: projectEnvironmentLocationById, | |
| 1128 | }); | |
| 1129 | const location = projectEnvironmentLocationById.get(project.environmentId) ?? { | |
| 1130 | kind: "remote" as const, | |
| 1131 | label: "Remote", | |
| 1132 | machine: "server" as const, | |
| 1133 | }; | |
| 1083 | 1134 | return ( |
| 1084 | group?.memberProjects.flatMap((member) => [member.title, member.workspaceRoot]) ?? [] | |
| 1135 | <ProjectSearchDescription | |
| 1136 | environmentLabels={metadata.environmentLabels} | |
| 1137 | grouped={members.length > 1} | |
| 1138 | location={location} | |
| 1139 | workspaceRoot={project.workspaceRoot} | |
| 1140 | /> | |
| 1085 | 1141 | ); |
| 1086 | 1142 | }, |
| 1087 | 1143 | icon: projectFavicon, |
| 1088 | 1144 | runProject: openProjectFromSearch, |
| 1089 | 1145 | }), |
| 1090 | [openProjectFromSearch, pickerProjects, projectGroupByTargetKey], | |
| 1146 | [ | |
| 1147 | openProjectFromSearch, | |
| 1148 | pickerProjects, | |
| 1149 | projectEnvironmentLocationById, | |
| 1150 | projectGroupByTargetKey, | |
| 1151 | ], | |
| 1091 | 1152 | ); |
| 1092 | 1153 | |
| 1093 | 1154 | const projectThreadItems = useMemo( |
| ... | ... | @@ -1176,6 +1237,9 @@ function OpenCommandPaletteDialog(props: { |
| 1176 | 1237 | <ThreadCommandSubtitle |
| 1177 | 1238 | project={projectByKey.get(`${thread.environmentId}:${thread.projectId}`) ?? null} |
| 1178 | 1239 | projectTitle={projectTitle ?? null} |
| 1240 | environmentLabel={ | |
| 1241 | projectEnvironmentLocationById.get(thread.environmentId)?.label ?? "Remote" | |
| 1242 | } | |
| 1179 | 1243 | branch={thread.branch} |
| 1180 | 1244 | worktreePath={thread.worktreePath} |
| 1181 | 1245 | isCurrent={thread.id === activeThreadId} |
| ... | ... | @@ -1213,6 +1277,7 @@ function OpenCommandPaletteDialog(props: { |
| 1213 | 1277 | clientSettings.sidebarThreadSortOrder, |
| 1214 | 1278 | navigate, |
| 1215 | 1279 | projectByKey, |
| 1280 | projectEnvironmentLocationById, | |
| 1216 | 1281 | projectTitleById, |
| 1217 | 1282 | providerEntryByEnvironmentAndInstanceId, |
| 1218 | 1283 | threadContentMatchByKey, |
apps/web/src/components/ThreadCommandSubtitle.tsx+7| ... | ... | @@ -36,6 +36,7 @@ function WorkspaceIcon(props: { variant: ThreadCommandSubtitleVariant; isWorktre |
| 36 | 36 | export function ThreadCommandSubtitle(props: { |
| 37 | 37 | project: ProjectFaviconProject | null; |
| 38 | 38 | projectTitle: string | null; |
| 39 | environmentLabel?: string | null; | |
| 39 | 40 | branch: string | null; |
| 40 | 41 | worktreePath: string | null; |
| 41 | 42 | isCurrent: boolean; |
| ... | ... | @@ -69,6 +70,12 @@ export function ThreadCommandSubtitle(props: { |
| 69 | 70 | <ProjectFavicon project={props.project} className="size-3 shrink-0" /> |
| 70 | 71 | ) : null} |
| 71 | 72 | <span className="min-w-0 truncate">{projectLabel}</span> |
| 73 | {props.environmentLabel ? ( | |
| 74 | <> | |
| 75 | <CommandPaletteMetaDot /> | |
| 76 | <span className="shrink-0">{props.environmentLabel}</span> | |
| 77 | </> | |
| 78 | ) : null} | |
| 72 | 79 | </span> |
| 73 | 80 | ) : null} |
| 74 | 81 |