authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-10-06 17:17:32-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-06 17:17:32-07:00
log517188b3caa135bc09ec0a4f93474ef17c8b5c4f
tree168e75832316b9dc2ee7a16801527485f349aee2
parent4b5c60485366421794787aee7513913f92eced29
signature Signed by PGP key B5690EEEBB952194

fix(web): show "No project" near the top of the new thread picker (#16628)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

1 files changed, 90 insertions(+), 107 deletions(-)

apps/web/src/components/CommandPalette.tsx+90-107
......@@ -163,6 +163,7 @@ import {
163163 findHighlightedCommandPaletteItem,
164164 type CommandPaletteActionItem,
165165 type CommandPaletteOpenIntent,
166 type CommandPaletteProject,
166167 type CommandPaletteSubmenuItem,
167168 type CommandPaletteView,
168169 filterCommandPaletteGroups,
......@@ -1309,89 +1310,94 @@ function OpenCommandPaletteDialog(props: {
13091310 ],
13101311 );
13111312
1312 const projectThreadItems = useMemo(
1313 () =>
1314 enumerateCommandPaletteItems([
1315 ...buildProjectActionItems({
1316 // The no-project home shows once, as the "No project" item below.
1317 projects: pickerProjects.filter(
1318 (project) => !isScratchProject(project, scratchWorkspaceRootFor(project.environmentId)),
1319 ),
1320 valuePrefix: "new-thread-in",
1321 searchTerms: (project) => {
1322 const group = projectGroupByTargetKey.get(`${project.environmentId}:${project.id}`);
1323 const location = projectEnvironmentLocationById.get(project.environmentId);
1324 return [
1325 ...(group?.memberProjects.flatMap((member) => [member.title, member.workspaceRoot]) ??
1326 []),
1327 ...(location ? [location.label] : []),
1328 ];
1329 },
1330 renderDescription: (project) => {
1331 const location = projectEnvironmentLocationById.get(project.environmentId) ?? {
1332 kind: "remote",
1333 label: "Remote",
1334 machine: "server" as const,
1335 };
1336 return (
1337 <span className="flex min-w-0 items-center gap-1">
1338 <span className="inline-flex min-w-0 items-center gap-1">
1339 {location.kind === "remote" ? (
1340 <EnvironmentMachineIcon
1341 aria-hidden
1342 kind={location.machine}
1343 className={COMMAND_PALETTE_META_ICON_CLASS}
1344 />
1345 ) : null}
1346 <span className="truncate">{location.label}</span>
1347 </span>
1348 <CommandPaletteMetaDot />
1349 <span className="truncate">{project.workspaceRoot}</span>
1313 const projectThreadItems = useMemo(() => {
1314 const isScratch = (project: CommandPaletteProject) =>
1315 isScratchProject(project, scratchWorkspaceRootFor(project.environmentId));
1316 const projectItems = enumerateCommandPaletteItems(
1317 buildProjectActionItems({
1318 // The no-project home shows once, as the "No project" item below.
1319 projects: pickerProjects.filter((project) => !isScratch(project)),
1320 valuePrefix: "new-thread-in",
1321 searchTerms: (project) => {
1322 const group = projectGroupByTargetKey.get(`${project.environmentId}:${project.id}`);
1323 const location = projectEnvironmentLocationById.get(project.environmentId);
1324 return [
1325 ...(group?.memberProjects.flatMap((member) => [member.title, member.workspaceRoot]) ??
1326 []),
1327 ...(location ? [location.label] : []),
1328 ];
1329 },
1330 renderDescription: (project) => {
1331 const location = projectEnvironmentLocationById.get(project.environmentId) ?? {
1332 kind: "remote",
1333 label: "Remote",
1334 machine: "server" as const,
1335 };
1336 return (
1337 <span className="flex min-w-0 items-center gap-1">
1338 <span className="inline-flex min-w-0 items-center gap-1">
1339 {location.kind === "remote" ? (
1340 <EnvironmentMachineIcon
1341 aria-hidden
1342 kind={location.machine}
1343 className={COMMAND_PALETTE_META_ICON_CLASS}
1344 />
1345 ) : null}
1346 <span className="truncate">{location.label}</span>
13501347 </span>
1348 <CommandPaletteMetaDot />
1349 <span className="truncate">{project.workspaceRoot}</span>
1350 </span>
1351 );
1352 },
1353 icon: projectFaviconIcon,
1354 runProject: async (project) => {
1355 const group = projectGroupByTargetKey.get(`${project.environmentId}:${project.id}`);
1356 const contextualRefBelongsToGroup =
1357 contextualProjectRef !== null &&
1358 group?.memberProjectRefs.some(
1359 (projectRef) =>
1360 projectRef.environmentId === contextualProjectRef.environmentId &&
1361 projectRef.projectId === contextualProjectRef.projectId,
13511362 );
1352 },
1353 icon: projectFaviconIcon,
1354 runProject: async (project) => {
1355 const group = projectGroupByTargetKey.get(`${project.environmentId}:${project.id}`);
1356 const contextualRefBelongsToGroup =
1357 contextualProjectRef !== null &&
1358 group?.memberProjectRefs.some(
1359 (projectRef) =>
1360 projectRef.environmentId === contextualProjectRef.environmentId &&
1361 projectRef.projectId === contextualProjectRef.projectId,
1362 );
1363 await handleNewThread(
1364 contextualRefBelongsToGroup
1365 ? contextualProjectRef
1366 : scopeProjectRef(project.environmentId, project.id),
1367 );
1368 },
1369 }),
1370 ...(scratchTargetEnvironmentId === null
1371 ? []
1372 : [
1373 {
1374 kind: "action" as const,
1375 value: "new-thread-in:no-project",
1376 searchTerms: ["no project", "without project", "none"],
1377 title: "No project",
1378 icon: <MessageSquareDashedIcon className={ITEM_ICON_CLASS} />,
1379 shortcutCommand: "chat.newWithoutProject" as const,
1380 run: () => startScratchThread(scratchTargetEnvironmentId),
1381 },
1382 ]),
1383 ]),
1384 [
1385 contextualProjectRef,
1386 handleNewThread,
1387 pickerProjects,
1388 projectEnvironmentLocationById,
1389 projectGroupByTargetKey,
1390 scratchTargetEnvironmentId,
1391 scratchWorkspaceRootFor,
1392 startScratchThread,
1393 ],
1394 );
1363 await handleNewThread(
1364 contextualRefBelongsToGroup
1365 ? contextualProjectRef
1366 : scopeProjectRef(project.environmentId, project.id),
1367 );
1368 },
1369 }),
1370 );
1371 if (scratchTargetEnvironmentId === null) return projectItems;
1372
1373 // "No project" goes right after the current project: visible without
1374 // scrolling past every project, while Enter still starts in the current
1375 // one. When the current thread has no project, it is the current entry and
1376 // goes first. It keeps its own shortcut, so the projects' mod+1..9 hold.
1377 const noProjectIndex = pickerProjects[0] !== undefined && isScratch(pickerProjects[0]) ? 0 : 1;
1378 return [
1379 ...projectItems.slice(0, noProjectIndex),
1380 {
1381 kind: "action" as const,
1382 value: "new-thread-in:no-project",
1383 searchTerms: ["no project", "without project", "none"],
1384 title: "No project",
1385 icon: <MessageSquareDashedIcon className={ITEM_ICON_CLASS} />,
1386 shortcutCommand: "chat.newWithoutProject" as const,
1387 run: () => startScratchThread(scratchTargetEnvironmentId),
1388 },
1389 ...projectItems.slice(noProjectIndex),
1390 ];
1391 }, [
1392 contextualProjectRef,
1393 handleNewThread,
1394 pickerProjects,
1395 projectEnvironmentLocationById,
1396 projectGroupByTargetKey,
1397 scratchTargetEnvironmentId,
1398 scratchWorkspaceRootFor,
1399 startScratchThread,
1400 ]);
13951401
13961402 const allThreadItems = useMemo(
13971403 () =>
......@@ -1848,35 +1854,12 @@ function OpenCommandPaletteDialog(props: {
18481854 setNewProjectFlow(null);
18491855 setViewStack([]);
18501856 setQuery("");
1851 const currentPrefix =
1852 currentProjectEnvironmentId && currentProjectId
1853 ? `new-thread-in:${currentProjectEnvironmentId}:${currentProjectId}`
1854 : null;
1855 const prioritized = currentPrefix
1856 ? [
1857 ...projectThreadItems.filter((item) => item.value === currentPrefix),
1858 ...projectThreadItems.filter((item) => item.value !== currentPrefix),
1859 ]
1860 : projectThreadItems;
1857 // projectThreadItems already lists the current project first.
18611858 pushPaletteView({
18621859 addonIcon: <SquarePenIcon className={ADDON_ICON_CLASS} />,
1863 groups: [
1864 {
1865 value: "projects",
1866 label: "Projects",
1867 items: enumerateCommandPaletteItems(prioritized),
1868 },
1869 ],
1860 groups: [{ value: "projects", label: "Projects", items: projectThreadItems }],
18701861 });
1871 }, [
1872 clearOpenIntent,
1873 browseNavigation,
1874 currentProjectEnvironmentId,
1875 currentProjectId,
1876 openIntent,
1877 projectThreadItems,
1878 pushPaletteView,
1879 ]);
1862 }, [clearOpenIntent, browseNavigation, openIntent, projectThreadItems, pushPaletteView]);
18801863
18811864 const actionItems: Array<CommandPaletteActionItem | CommandPaletteSubmenuItem> = [];
18821865