authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-09-08 03:08:53-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-08 03:08:53-07:00
log82451eeb7277b0b7fb5a7cb53156c83f948f32c1
tree04282ea510f41494a4a699ff5a70924bea9eb463
parent83b865fec704b177804aa6b9b695aefea4ccd04a
signature Signed by PGP key B5690EEEBB952194

fix(web): show the same project icon in the command palette as everywhere else (#10712)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

3 files changed, 57 insertions(+), 9 deletions(-)

apps/web/src/components/CommandPalette.logic.test.ts+38-1
......@@ -1,8 +1,9 @@
11import { describe, expect, it, vi } from "vite-plus/test";
22import { EnvironmentId, ProjectId, ProviderInstanceId, ThreadId } from "@t3tools/contracts";
3import type { Thread } from "../types";
3import type { Project, Thread } from "../types";
44import {
55 buildBrowseGroups,
6 buildProjectActionItems,
67 buildThreadActionItems,
78 enumerateCommandPaletteItems,
89 filterPinnedBrowseEntries,
......@@ -112,6 +113,20 @@ describe("enumerateCommandPaletteItems", () => {
112113const LOCAL_ENVIRONMENT_ID = EnvironmentId.make("environment-local");
113114const PROJECT_ID = ProjectId.make("project-1");
114115
116function makeProject(overrides: Partial<Project> = {}): Project {
117 return {
118 id: PROJECT_ID,
119 environmentId: LOCAL_ENVIRONMENT_ID,
120 title: "Project",
121 workspaceRoot: "/workspace/project",
122 defaultModelSelection: null,
123 scripts: [],
124 createdAt: "2026-03-01T00:00:00.000Z",
125 updatedAt: "2026-03-01T00:00:00.000Z",
126 ...overrides,
127 };
128}
129
115130function makeThread(overrides: Partial<Thread> = {}): Thread {
116131 return {
117132 id: ThreadId.make("thread-1"),
......@@ -139,6 +154,28 @@ function makeThread(overrides: Partial<Thread> = {}): Thread {
139154 };
140155}
141156
157describe("buildProjectActionItems", () => {
158 it("shows the grouped display name but keeps the real title for icons", () => {
159 const project = makeProject({ title: "fleet", workspaceRoot: "/Users/theo/Code/p/fleet" });
160 const iconTitles: string[] = [];
161 const [item] = buildProjectActionItems({
162 projects: [{ ...project, displayName: "t3dotgg/fleet" }],
163 valuePrefix: "project",
164 icon: (candidate) => {
165 iconTitles.push(candidate.title);
166 return null;
167 },
168 runProject: async () => undefined,
169 });
170
171 expect(item?.title).toBe("t3dotgg/fleet");
172 expect(item?.searchTerms).toEqual(
173 expect.arrayContaining(["t3dotgg/fleet", "fleet", "/Users/theo/Code/p/fleet"]),
174 );
175 expect(iconTitles).toEqual(["fleet"]);
176 });
177});
178
142179describe("buildThreadActionItems", () => {
143180 it("orders threads by most recent activity and formats timestamps from updatedAt", () => {
144181 vi.useFakeTimers();
apps/web/src/components/CommandPalette.logic.ts+18-7
......@@ -139,20 +139,31 @@ export function enumerateCommandPaletteItems(
139139
140140export type CommandPaletteMode = "root" | "root-browse" | "submenu" | "submenu-browse";
141141
142// A project as the palette shows it. `displayName` is the grouped label (for
143// example "owner/repo" when projects are merged across machines). Keep `title`
144// as the real project title: the automatic project icon is derived from it, and
145// every other surface uses the real title, so overriding it desyncs the icon.
146export type CommandPaletteProject = Project & { readonly displayName: string };
147
142148export function buildProjectActionItems(input: {
143 projects: ReadonlyArray<Project>;
149 projects: ReadonlyArray<CommandPaletteProject>;
144150 valuePrefix: string;
145 icon: (project: Project) => ReactNode;
146 runProject: (project: Project) => Promise<void>;
147 searchTerms?: (project: Project) => ReadonlyArray<string>;
148 renderDescription?: (project: Project) => ReactNode;
151 icon: (project: CommandPaletteProject) => ReactNode;
152 runProject: (project: CommandPaletteProject) => Promise<void>;
153 searchTerms?: (project: CommandPaletteProject) => ReadonlyArray<string>;
154 renderDescription?: (project: CommandPaletteProject) => ReactNode;
149155 shortcutCommand?: KeybindingCommand;
150156}): CommandPaletteActionItem[] {
151157 return input.projects.map((project) => ({
152158 kind: "action",
153159 value: `${input.valuePrefix}:${project.environmentId}:${project.id}`,
154 searchTerms: [project.title, project.workspaceRoot, ...(input.searchTerms?.(project) ?? [])],
155 title: project.title,
160 searchTerms: [
161 project.displayName,
162 project.title,
163 project.workspaceRoot,
164 ...(input.searchTerms?.(project) ?? []),
165 ],
166 title: project.displayName,
156167 description: input.renderDescription?.(project) ?? project.workspaceRoot,
157168 icon: input.icon(project),
158169 ...(input.shortcutCommand !== undefined ? { shortcutCommand: input.shortcutCommand } : {}),
apps/web/src/components/CommandPalette.tsx+1-1
......@@ -794,7 +794,7 @@ function OpenCommandPaletteDialog(props: {
794794 () =>
795795 projectPickerEntries.map(({ group, targetProject }) => ({
796796 ...targetProject,
797 title: group.displayName,
797 displayName: group.displayName,
798798 })),
799799 [projectPickerEntries],
800800 );