authorgravatar for justin@cyber-lane.comJustin Nel <justin@cyber-lane.com> 2026-09-11 08:50:28+09:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-10 23:50:28+00:00
logc52b8d96e4b34201f19b5e5bb12c6b2a77bfaa9a
treef7525127ad6ec525c67907ba11057f824aaf0dce
parent2afa02a280acb0db4868d36409ba9bf7eae286c1
signature Signed by PGP key B5690EEEBB952194

feat(command-palette): show environments in search results (#10722)


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
3import type { Project, Thread } from "../types";3import type { Project, Thread } from "../types";
4import {4import {
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});
5758
59describe("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
58describe("reduceCommandPaletteUiState", () => {177describe("reduceCommandPaletteUiState", () => {
59 const closedState = { open: false, mode: "command", openIntent: null } as const;178 const closedState = { open: false, mode: "command", openIntent: null } as const;
60179
apps/web/src/components/CommandPalette.logic.ts+17
...@@ -1,6 +1,7 @@...@@ -1,6 +1,7 @@
1import { threadPullRequestSearchTerms } from "@t3tools/shared/threadPullRequests";1import { threadPullRequestSearchTerms } from "@t3tools/shared/threadPullRequests";
2import type { CommandPaletteLinkedThreads } from "../commandPaletteBus";2import type { CommandPaletteLinkedThreads } from "../commandPaletteBus";
3import {3import {
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.
186export type CommandPaletteProject = Project & { readonly displayName: string };187export type CommandPaletteProject = Project & { readonly displayName: string };
187188
189export 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
188export function buildProjectActionItems(input: {205export 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}
188189
190function 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
189function getEnvironmentBrowsePlatform(os: string | null | undefined): string {222function 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 );
10921153
1093 const projectThreadItems = useMemo(1154 const projectThreadItems = useMemo(
...@@ -1176,6 +1237,9 @@ function OpenCommandPaletteDialog(props: {...@@ -1176,6 +1237,9 @@ function OpenCommandPaletteDialog(props: {
1176 <ThreadCommandSubtitle1237 <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
36export function ThreadCommandSubtitle(props: {36export 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}
7481