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