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
33import type { Project, Thread } from "../types";
44import {
55 buildBrowseGroups,
6 buildCommandPaletteProjectMetadata,
67 buildProjectActionItems,
78 buildThreadActionItems,
89 buildLinkedThreadActionItems,
......@@ -55,6 +56,124 @@ describe("linked pull request thread navigation", () => {
5556 });
5657});
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
58177describe("reduceCommandPaletteUiState", () => {
59178 const closedState = { open: false, mode: "command", openIntent: null } as const;
60179
apps/web/src/components/CommandPalette.logic.ts+17
......@@ -1,6 +1,7 @@
11import { threadPullRequestSearchTerms } from "@t3tools/shared/threadPullRequests";
22import type { CommandPaletteLinkedThreads } from "../commandPaletteBus";
33import {
4 type EnvironmentId,
45 type FilesystemBrowseEntry,
56 type KeybindingCommand,
67 THREAD_JUMP_KEYBINDING_COMMANDS,
......@@ -185,6 +186,22 @@ export type CommandPaletteMode = "root" | "root-browse" | "submenu" | "submenu-b
185186// every other surface uses the real title, so overriding it desyncs the icon.
186187export 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
188205export function buildProjectActionItems(input: {
189206 projects: ReadonlyArray<CommandPaletteProject>;
190207 valuePrefix: string;
apps/web/src/components/CommandPalette.tsx+68-3
......@@ -126,6 +126,7 @@ import {
126126 ADDON_ICON_CLASS,
127127 browseInputEndPaddingClass,
128128 buildBrowseGroups,
129 buildCommandPaletteProjectMetadata,
129130 buildProjectActionItems,
130131 buildRootGroups,
131132 buildThreadActionItems,
......@@ -186,6 +187,38 @@ function projectFavicon(project: Project) {
186187 return <ProjectFavicon project={project} className="size-4" />;
187188}
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
189222function getEnvironmentBrowsePlatform(os: string | null | undefined): string {
190223 if (os === "windows") {
191224 return "Win32";
......@@ -1079,15 +1112,43 @@ function OpenCommandPaletteDialog(props: {
10791112 projects: pickerProjects,
10801113 valuePrefix: "project",
10811114 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 };
10831134 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 />
10851141 );
10861142 },
10871143 icon: projectFavicon,
10881144 runProject: openProjectFromSearch,
10891145 }),
1090 [openProjectFromSearch, pickerProjects, projectGroupByTargetKey],
1146 [
1147 openProjectFromSearch,
1148 pickerProjects,
1149 projectEnvironmentLocationById,
1150 projectGroupByTargetKey,
1151 ],
10911152 );
10921153
10931154 const projectThreadItems = useMemo(
......@@ -1176,6 +1237,9 @@ function OpenCommandPaletteDialog(props: {
11761237 <ThreadCommandSubtitle
11771238 project={projectByKey.get(`${thread.environmentId}:${thread.projectId}`) ?? null}
11781239 projectTitle={projectTitle ?? null}
1240 environmentLabel={
1241 projectEnvironmentLocationById.get(thread.environmentId)?.label ?? "Remote"
1242 }
11791243 branch={thread.branch}
11801244 worktreePath={thread.worktreePath}
11811245 isCurrent={thread.id === activeThreadId}
......@@ -1213,6 +1277,7 @@ function OpenCommandPaletteDialog(props: {
12131277 clientSettings.sidebarThreadSortOrder,
12141278 navigate,
12151279 projectByKey,
1280 projectEnvironmentLocationById,
12161281 projectTitleById,
12171282 providerEntryByEnvironmentAndInstanceId,
12181283 threadContentMatchByKey,
apps/web/src/components/ThreadCommandSubtitle.tsx+7
......@@ -36,6 +36,7 @@ function WorkspaceIcon(props: { variant: ThreadCommandSubtitleVariant; isWorktre
3636export function ThreadCommandSubtitle(props: {
3737 project: ProjectFaviconProject | null;
3838 projectTitle: string | null;
39 environmentLabel?: string | null;
3940 branch: string | null;
4041 worktreePath: string | null;
4142 isCurrent: boolean;
......@@ -69,6 +70,12 @@ export function ThreadCommandSubtitle(props: {
6970 <ProjectFavicon project={props.project} className="size-3 shrink-0" />
7071 ) : null}
7172 <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}
7279 </span>
7380 ) : null}
7481