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 @@...@@ -1,8 +1,9 @@
1import { describe, expect, it, vi } from "vite-plus/test";1import { describe, expect, it, vi } from "vite-plus/test";
2import { EnvironmentId, ProjectId, ProviderInstanceId, ThreadId } from "@t3tools/contracts";2import { EnvironmentId, ProjectId, ProviderInstanceId, ThreadId } from "@t3tools/contracts";
3import type { Thread } from "../types";3import type { Project, Thread } from "../types";
4import {4import {
5 buildBrowseGroups,5 buildBrowseGroups,
6 buildProjectActionItems,
6 buildThreadActionItems,7 buildThreadActionItems,
7 enumerateCommandPaletteItems,8 enumerateCommandPaletteItems,
8 filterPinnedBrowseEntries,9 filterPinnedBrowseEntries,
...@@ -112,6 +113,20 @@ describe("enumerateCommandPaletteItems", () => {...@@ -112,6 +113,20 @@ describe("enumerateCommandPaletteItems", () => {
112const LOCAL_ENVIRONMENT_ID = EnvironmentId.make("environment-local");113const LOCAL_ENVIRONMENT_ID = EnvironmentId.make("environment-local");
113const PROJECT_ID = ProjectId.make("project-1");114const 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
115function makeThread(overrides: Partial<Thread> = {}): Thread {130function makeThread(overrides: Partial<Thread> = {}): Thread {
116 return {131 return {
117 id: ThreadId.make("thread-1"),132 id: ThreadId.make("thread-1"),
...@@ -139,6 +154,28 @@ function makeThread(overrides: Partial<Thread> = {}): Thread {...@@ -139,6 +154,28 @@ function makeThread(overrides: Partial<Thread> = {}): Thread {
139 };154 };
140}155}
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
142describe("buildThreadActionItems", () => {179describe("buildThreadActionItems", () => {
143 it("orders threads by most recent activity and formats timestamps from updatedAt", () => {180 it("orders threads by most recent activity and formats timestamps from updatedAt", () => {
144 vi.useFakeTimers();181 vi.useFakeTimers();
apps/web/src/components/CommandPalette.logic.ts+18-7
...@@ -139,20 +139,31 @@ export function enumerateCommandPaletteItems(...@@ -139,20 +139,31 @@ export function enumerateCommandPaletteItems(
139139
140export type CommandPaletteMode = "root" | "root-browse" | "submenu" | "submenu-browse";140export 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
142export function buildProjectActionItems(input: {148export function buildProjectActionItems(input: {
143 projects: ReadonlyArray<Project>;149 projects: ReadonlyArray<CommandPaletteProject>;
144 valuePrefix: string;150 valuePrefix: string;
145 icon: (project: Project) => ReactNode;151 icon: (project: CommandPaletteProject) => ReactNode;
146 runProject: (project: Project) => Promise<void>;152 runProject: (project: CommandPaletteProject) => Promise<void>;
147 searchTerms?: (project: Project) => ReadonlyArray<string>;153 searchTerms?: (project: CommandPaletteProject) => ReadonlyArray<string>;
148 renderDescription?: (project: Project) => ReactNode;154 renderDescription?: (project: CommandPaletteProject) => ReactNode;
149 shortcutCommand?: KeybindingCommand;155 shortcutCommand?: KeybindingCommand;
150}): CommandPaletteActionItem[] {156}): CommandPaletteActionItem[] {
151 return input.projects.map((project) => ({157 return input.projects.map((project) => ({
152 kind: "action",158 kind: "action",
153 value: `${input.valuePrefix}:${project.environmentId}:${project.id}`,159 value: `${input.valuePrefix}:${project.environmentId}:${project.id}`,
154 searchTerms: [project.title, project.workspaceRoot, ...(input.searchTerms?.(project) ?? [])],160 searchTerms: [
155 title: project.title,161 project.displayName,
162 project.title,
163 project.workspaceRoot,
164 ...(input.searchTerms?.(project) ?? []),
165 ],
166 title: project.displayName,
156 description: input.renderDescription?.(project) ?? project.workspaceRoot,167 description: input.renderDescription?.(project) ?? project.workspaceRoot,
157 icon: input.icon(project),168 icon: input.icon(project),
158 ...(input.shortcutCommand !== undefined ? { shortcutCommand: input.shortcutCommand } : {}),169 ...(input.shortcutCommand !== undefined ? { shortcutCommand: input.shortcutCommand } : {}),
apps/web/src/components/CommandPalette.tsx+1-1
...@@ -794,7 +794,7 @@ function OpenCommandPaletteDialog(props: {...@@ -794,7 +794,7 @@ function OpenCommandPaletteDialog(props: {
794 () =>794 () =>
795 projectPickerEntries.map(({ group, targetProject }) => ({795 projectPickerEntries.map(({ group, targetProject }) => ({
796 ...targetProject,796 ...targetProject,
797 title: group.displayName,797 displayName: group.displayName,
798 })),798 })),
799 [projectPickerEntries],799 [projectPickerEntries],
800 );800 );