| 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,6 +3,7 @@ import { EnvironmentId, ProjectId, ProviderInstanceId, ThreadId } from "@t3tools |
| 3 | import type { Project, Thread } from "../types"; | 3 | import type { Project, Thread } from "../types"; |
| 4 | import { | 4 | import { |
| 5 | buildBrowseGroups, | 5 | buildBrowseGroups, |
| 6 | buildCommandPaletteProjectMetadata, | ||
| 6 | buildProjectActionItems, | 7 | buildProjectActionItems, |
| 7 | buildThreadActionItems, | 8 | buildThreadActionItems, |
| 8 | buildLinkedThreadActionItems, | 9 | buildLinkedThreadActionItems, |
| ... | @@ -55,6 +56,124 @@ describe("linked pull request thread navigation", () => { | ... | @@ -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 | describe("reduceCommandPaletteUiState", () => { | 177 | describe("reduceCommandPaletteUiState", () => { |
| 59 | const closedState = { open: false, mode: "command", openIntent: null } as const; | 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,6 +1,7 @@ |
| 1 | import { threadPullRequestSearchTerms } from "@t3tools/shared/threadPullRequests"; | 1 | import { threadPullRequestSearchTerms } from "@t3tools/shared/threadPullRequests"; |
| 2 | import type { CommandPaletteLinkedThreads } from "../commandPaletteBus"; | 2 | import type { CommandPaletteLinkedThreads } from "../commandPaletteBus"; |
| 3 | import { | 3 | import { |
| 4 | type EnvironmentId, | ||
| 4 | type FilesystemBrowseEntry, | 5 | type FilesystemBrowseEntry, |
| 5 | type KeybindingCommand, | 6 | type KeybindingCommand, |
| 6 | THREAD_JUMP_KEYBINDING_COMMANDS, | 7 | THREAD_JUMP_KEYBINDING_COMMANDS, |
| ... | @@ -185,6 +186,22 @@ export type CommandPaletteMode = "root" | "root-browse" | "submenu" | "submenu-b | ... | @@ -185,6 +186,22 @@ export type CommandPaletteMode = "root" | "root-browse" | "submenu" | "submenu-b |
| 185 | // every other surface uses the real title, so overriding it desyncs the icon. | 186 | // every other surface uses the real title, so overriding it desyncs the icon. |
| 186 | export type CommandPaletteProject = Project & { readonly displayName: string }; | 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 | export function buildProjectActionItems(input: { | 205 | export function buildProjectActionItems(input: { |
| 189 | projects: ReadonlyArray<CommandPaletteProject>; | 206 | projects: ReadonlyArray<CommandPaletteProject>; |
| 190 | valuePrefix: string; | 207 | valuePrefix: string; |
apps/web/src/components/CommandPalette.tsx+68-3| ... | @@ -126,6 +126,7 @@ import { | ... | @@ -126,6 +126,7 @@ import { |
| 126 | ADDON_ICON_CLASS, | 126 | ADDON_ICON_CLASS, |
| 127 | browseInputEndPaddingClass, | 127 | browseInputEndPaddingClass, |
| 128 | buildBrowseGroups, | 128 | buildBrowseGroups, |
| 129 | buildCommandPaletteProjectMetadata, | ||
| 129 | buildProjectActionItems, | 130 | buildProjectActionItems, |
| 130 | buildRootGroups, | 131 | buildRootGroups, |
| 131 | buildThreadActionItems, | 132 | buildThreadActionItems, |
| ... | @@ -186,6 +187,38 @@ function projectFavicon(project: Project) { | ... | @@ -186,6 +187,38 @@ function projectFavicon(project: Project) { |
| 186 | return <ProjectFavicon project={project} className="size-4" />; | 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 | function getEnvironmentBrowsePlatform(os: string | null | undefined): string { | 222 | function getEnvironmentBrowsePlatform(os: string | null | undefined): string { |
| 190 | if (os === "windows") { | 223 | if (os === "windows") { |
| 191 | return "Win32"; | 224 | return "Win32"; |
| ... | @@ -1079,15 +1112,43 @@ function OpenCommandPaletteDialog(props: { | ... | @@ -1079,15 +1112,43 @@ function OpenCommandPaletteDialog(props: { |
| 1079 | projects: pickerProjects, | 1112 | projects: pickerProjects, |
| 1080 | valuePrefix: "project", | 1113 | valuePrefix: "project", |
| 1081 | searchTerms: (project) => { | 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 | return ( | 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 | icon: projectFavicon, | 1143 | icon: projectFavicon, |
| 1088 | runProject: openProjectFromSearch, | 1144 | runProject: openProjectFromSearch, |
| 1089 | }), | 1145 | }), |
| 1090 | [openProjectFromSearch, pickerProjects, projectGroupByTargetKey], | 1146 | [ |
| 1147 | openProjectFromSearch, | ||
| 1148 | pickerProjects, | ||
| 1149 | projectEnvironmentLocationById, | ||
| 1150 | projectGroupByTargetKey, | ||
| 1151 | ], | ||
| 1091 | ); | 1152 | ); |
| 1092 | 1153 | ||
| 1093 | const projectThreadItems = useMemo( | 1154 | const projectThreadItems = useMemo( |
| ... | @@ -1176,6 +1237,9 @@ function OpenCommandPaletteDialog(props: { | ... | @@ -1176,6 +1237,9 @@ function OpenCommandPaletteDialog(props: { |
| 1176 | <ThreadCommandSubtitle | 1237 | <ThreadCommandSubtitle |
| 1177 | project={projectByKey.get(`${thread.environmentId}:${thread.projectId}`) ?? null} | 1238 | project={projectByKey.get(`${thread.environmentId}:${thread.projectId}`) ?? null} |
| 1178 | projectTitle={projectTitle ?? null} | 1239 | projectTitle={projectTitle ?? null} |
| 1240 | environmentLabel={ | ||
| 1241 | projectEnvironmentLocationById.get(thread.environmentId)?.label ?? "Remote" | ||
| 1242 | } | ||
| 1179 | branch={thread.branch} | 1243 | branch={thread.branch} |
| 1180 | worktreePath={thread.worktreePath} | 1244 | worktreePath={thread.worktreePath} |
| 1181 | isCurrent={thread.id === activeThreadId} | 1245 | isCurrent={thread.id === activeThreadId} |
| ... | @@ -1213,6 +1277,7 @@ function OpenCommandPaletteDialog(props: { | ... | @@ -1213,6 +1277,7 @@ function OpenCommandPaletteDialog(props: { |
| 1213 | clientSettings.sidebarThreadSortOrder, | 1277 | clientSettings.sidebarThreadSortOrder, |
| 1214 | navigate, | 1278 | navigate, |
| 1215 | projectByKey, | 1279 | projectByKey, |
| 1280 | projectEnvironmentLocationById, | ||
| 1216 | projectTitleById, | 1281 | projectTitleById, |
| 1217 | providerEntryByEnvironmentAndInstanceId, | 1282 | providerEntryByEnvironmentAndInstanceId, |
| 1218 | threadContentMatchByKey, | 1283 | threadContentMatchByKey, |
apps/web/src/components/ThreadCommandSubtitle.tsx+7| ... | @@ -36,6 +36,7 @@ function WorkspaceIcon(props: { variant: ThreadCommandSubtitleVariant; isWorktre | ... | @@ -36,6 +36,7 @@ function WorkspaceIcon(props: { variant: ThreadCommandSubtitleVariant; isWorktre |
| 36 | export function ThreadCommandSubtitle(props: { | 36 | export function ThreadCommandSubtitle(props: { |
| 37 | project: ProjectFaviconProject | null; | 37 | project: ProjectFaviconProject | null; |
| 38 | projectTitle: string | null; | 38 | projectTitle: string | null; |
| 39 | environmentLabel?: string | null; | ||
| 39 | branch: string | null; | 40 | branch: string | null; |
| 40 | worktreePath: string | null; | 41 | worktreePath: string | null; |
| 41 | isCurrent: boolean; | 42 | isCurrent: boolean; |
| ... | @@ -69,6 +70,12 @@ export function ThreadCommandSubtitle(props: { | ... | @@ -69,6 +70,12 @@ export function ThreadCommandSubtitle(props: { |
| 69 | <ProjectFavicon project={props.project} className="size-3 shrink-0" /> | 70 | <ProjectFavicon project={props.project} className="size-3 shrink-0" /> |
| 70 | ) : null} | 71 | ) : null} |
| 71 | <span className="min-w-0 truncate">{projectLabel}</span> | 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 | </span> | 79 | </span> |
| 73 | ) : null} | 80 | ) : null} |
| 74 | 81 |