authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-07-23 18:11:48+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-23 16:11:48+00:00
log9d9208cec5ac6a3ddb61647644da2b99eee59bff
tree68255f7b5ca7e0009130e9a78604811d16f63e0c
parent57100fba854fb1214ae7a413111efae14443d445
signature Signed by PGP key B5690EEEBB952194

[codex] Group projects in new-thread pickers (#4312)

Co-authored-by: codex <codex@users.noreply.github.com>

5 files changed, 268 insertions(+), 53 deletions(-)

apps/web/src/components/CommandPalette.logic.ts+2-1
......@@ -111,12 +111,13 @@ export function buildProjectActionItems(input: {
111111 valuePrefix: string;
112112 icon: (project: Project) => ReactNode;
113113 runProject: (project: Project) => Promise<void>;
114 searchTerms?: (project: Project) => ReadonlyArray<string>;
114115 shortcutCommand?: KeybindingCommand;
115116}): CommandPaletteActionItem[] {
116117 return input.projects.map((project) => ({
117118 kind: "action",
118119 value: `${input.valuePrefix}:${project.environmentId}:${project.id}`,
119 searchTerms: [project.title, project.workspaceRoot],
120 searchTerms: [project.title, project.workspaceRoot, ...(input.searchTerms?.(project) ?? [])],
120121 title: project.title,
121122 description: project.workspaceRoot,
122123 icon: input.icon(project),
apps/web/src/components/CommandPalette.tsx+166-18
......@@ -56,9 +56,10 @@ import { useEnvironmentQuery } from "../state/query";
5656import { sourceControlEnvironment } from "../state/sourceControl";
5757import { useAtomCommand } from "../state/use-atom-command";
5858import { useAtomQueryRunner } from "../state/use-atom-query-runner";
59import { useEnvironments, usePrimaryEnvironment } from "../state/environments";
59import { useEnvironments, usePrimaryEnvironmentId } from "../state/environments";
6060import { useProjects, useThreadShells } from "../state/entities";
6161import {
62 resolveThreadActionProjectRef,
6263 startNewThreadInProjectFromContext,
6364 startNewThreadFromContext,
6465} from "../lib/chatThreadActions";
......@@ -79,7 +80,7 @@ import {
7980} from "../lib/projectPaths";
8081import { onOpenCommandPalette } from "../commandPaletteBus";
8182import { isTerminalFocused } from "../lib/terminalFocus";
82import { getLatestThreadForProject } from "../lib/threadSort";
83import { getLatestThreadForProject, sortThreads } from "../lib/threadSort";
8384import { cn, isMacPlatform, isWindowsPlatform, newProjectId } from "../lib/utils";
8485import { selectThreadTerminalUiState, useTerminalUiStateStore } from "../terminalUiStateStore";
8586import { buildThreadRouteParams, resolveThreadRouteTarget } from "../threadRoutes";
......@@ -106,6 +107,7 @@ import {
106107 ITEM_ICON_CLASS,
107108 RECENT_THREAD_LIMIT,
108109} from "./CommandPalette.logic";
110import { orderItemsByPreferredIds, sortLogicalProjectsForSidebar } from "./Sidebar.logic";
109111import { resolveEnvironmentOptionLabel } from "./BranchToolbar.logic";
110112import { CommandPaletteResults } from "./CommandPaletteResults";
111113import { AzureDevOpsIcon, BitbucketIcon, GitHubIcon, GitLabIcon } from "./Icons";
......@@ -128,6 +130,12 @@ import { stackedThreadToast, toastManager } from "./ui/toast";
128130import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
129131import { ComposerHandleContext, useComposerHandleContext } from "../composerHandleContext";
130132import type { ChatComposerHandle } from "./chat/ChatComposer";
133import { getProjectOrderKey, selectProjectGroupingSettings } from "../logicalProject";
134import { legacyProjectCwdPreferenceKey, useUiStateStore } from "../uiStateStore";
135import {
136 buildSidebarProjectPickerEntries,
137 buildSidebarProjectSnapshots,
138} from "../sidebarProjectGrouping";
131139
132140const EMPTY_BROWSE_ENTRIES: FilesystemBrowseResult["entries"] = [];
133141
......@@ -487,10 +495,11 @@ function OpenCommandPaletteDialog(props: {
487495 });
488496 const { environments } = useEnvironments();
489497 const desktopLocalBootstraps = useDesktopLocalBootstraps();
490 const primaryEnvironment = usePrimaryEnvironment();
498 const primaryEnvironmentId = usePrimaryEnvironmentId();
491499 const { activeDraftThread, activeThread, defaultProjectRef, handleNewThread } =
492500 useHandleNewThread();
493501 const projects = useProjects();
502 const projectOrder = useUiStateStore((store) => store.projectOrder);
494503 const threads = useThreadShells();
495504 const keybindings = useAtomValue(primaryServerKeybindingsAtom);
496505 const providers = useAtomValue(primaryServerProvidersAtom);
......@@ -504,7 +513,93 @@ function OpenCommandPaletteDialog(props: {
504513 const [addProjectCloneFlow, setAddProjectCloneFlow] = useState<AddProjectCloneFlow | null>(null);
505514 const [isRemoteProjectLookingUp, setIsRemoteProjectLookingUp] = useState(false);
506515 const [isRemoteProjectCloning, setIsRemoteProjectCloning] = useState(false);
507 const primaryEnvironmentId = primaryEnvironment?.environmentId ?? null;
516 const projectGroupingSettings = useMemo(
517 () => selectProjectGroupingSettings(clientSettings),
518 [clientSettings],
519 );
520
521 const environmentLabelById = useMemo(
522 () =>
523 new Map(
524 environments.map((environment) => [environment.environmentId, environment.label] as const),
525 ),
526 [environments],
527 );
528 const orderedProjects = useMemo(
529 () =>
530 orderItemsByPreferredIds({
531 items: projects,
532 preferredIds: projectOrder,
533 getId: getProjectOrderKey,
534 getPreferenceIds: (project) => [
535 getProjectOrderKey(project),
536 legacyProjectCwdPreferenceKey(project.workspaceRoot),
537 ],
538 }),
539 [projectOrder, projects],
540 );
541 const unsortedProjectGroups = useMemo(
542 () =>
543 buildSidebarProjectSnapshots({
544 projects: clientSettings.sidebarProjectSortOrder === "manual" ? orderedProjects : projects,
545 settings: projectGroupingSettings,
546 primaryEnvironmentId,
547 resolveEnvironmentLabel: (environmentId) => environmentLabelById.get(environmentId) ?? null,
548 }),
549 [
550 clientSettings.sidebarProjectSortOrder,
551 environmentLabelById,
552 orderedProjects,
553 primaryEnvironmentId,
554 projectGroupingSettings,
555 projects,
556 ],
557 );
558 const projectGroups = useMemo(
559 () =>
560 sortLogicalProjectsForSidebar(
561 unsortedProjectGroups,
562 threads,
563 clientSettings.sidebarProjectSortOrder,
564 ),
565 [clientSettings.sidebarProjectSortOrder, threads, unsortedProjectGroups],
566 );
567 const contextualProjectRef = useMemo(
568 () =>
569 resolveThreadActionProjectRef({
570 activeDraftThread,
571 activeThread: activeThread ?? undefined,
572 defaultProjectRef,
573 handleNewThread,
574 }),
575 [activeDraftThread, activeThread, defaultProjectRef, handleNewThread],
576 );
577 const projectPickerEntries = useMemo(
578 () =>
579 buildSidebarProjectPickerEntries({
580 groups: projectGroups,
581 preferredProjectRef: contextualProjectRef,
582 }),
583 [contextualProjectRef, projectGroups],
584 );
585 const pickerProjects = useMemo(
586 () =>
587 projectPickerEntries.map(({ group, targetProject }) => ({
588 ...targetProject,
589 title: group.displayName,
590 })),
591 [projectPickerEntries],
592 );
593 const projectGroupByTargetKey = useMemo(
594 () =>
595 new Map(
596 projectPickerEntries.map(({ group, targetProject }) => [
597 `${targetProject.environmentId}:${targetProject.id}`,
598 group,
599 ]),
600 ),
601 [projectPickerEntries],
602 );
508603
509604 const addProjectEnvironmentOptions = useMemo(() => {
510605 const options = environments.map((environment): AddProjectEnvironmentOption => {
......@@ -648,11 +743,28 @@ function OpenCommandPaletteDialog(props: {
648743
649744 const openProjectFromSearch = useMemo(
650745 () => async (project: (typeof projects)[number]) => {
651 const latestThread = getLatestThreadForProject(
652 threads.filter((thread) => thread.environmentId === project.environmentId),
653 project.id,
654 clientSettings.sidebarThreadSortOrder,
655 );
746 const group = projectGroupByTargetKey.get(`${project.environmentId}:${project.id}`);
747 const groupedProjectKeys = group
748 ? new Set(
749 group.memberProjectRefs.map(
750 (projectRef) => `${projectRef.environmentId}:${projectRef.projectId}`,
751 ),
752 )
753 : null;
754 const latestThread = groupedProjectKeys
755 ? (sortThreads(
756 threads.filter(
757 (thread) =>
758 thread.archivedAt === null &&
759 groupedProjectKeys.has(`${thread.environmentId}:${thread.projectId}`),
760 ),
761 clientSettings.sidebarThreadSortOrder,
762 )[0] ?? null)
763 : getLatestThreadForProject(
764 threads.filter((thread) => thread.environmentId === project.environmentId),
765 project.id,
766 clientSettings.sidebarThreadSortOrder,
767 );
656768 if (latestThread) {
657769 await navigate({
658770 to: "/$environmentId/$threadId",
......@@ -665,14 +777,26 @@ function OpenCommandPaletteDialog(props: {
665777
666778 await handleNewThread(scopeProjectRef(project.environmentId, project.id));
667779 },
668 [handleNewThread, navigate, clientSettings.sidebarThreadSortOrder, threads],
780 [
781 clientSettings.sidebarThreadSortOrder,
782 handleNewThread,
783 navigate,
784 projectGroupByTargetKey,
785 threads,
786 ],
669787 );
670788
671789 const projectSearchItems = useMemo(
672790 () =>
673791 buildProjectActionItems({
674 projects,
792 projects: pickerProjects,
675793 valuePrefix: "project",
794 searchTerms: (project) => {
795 const group = projectGroupByTargetKey.get(`${project.environmentId}:${project.id}`);
796 return (
797 group?.memberProjects.flatMap((member) => [member.title, member.workspaceRoot]) ?? []
798 );
799 },
676800 icon: (project) => (
677801 <ProjectFavicon
678802 environmentId={project.environmentId}
......@@ -682,15 +806,21 @@ function OpenCommandPaletteDialog(props: {
682806 ),
683807 runProject: openProjectFromSearch,
684808 }),
685 [openProjectFromSearch, projects],
809 [openProjectFromSearch, pickerProjects, projectGroupByTargetKey],
686810 );
687811
688812 const projectThreadItems = useMemo(
689813 () =>
690814 enumerateCommandPaletteItems(
691815 buildProjectActionItems({
692 projects,
816 projects: pickerProjects,
693817 valuePrefix: "new-thread-in",
818 searchTerms: (project) => {
819 const group = projectGroupByTargetKey.get(`${project.environmentId}:${project.id}`);
820 return (
821 group?.memberProjects.flatMap((member) => [member.title, member.workspaceRoot]) ?? []
822 );
823 },
694824 icon: (project) => (
695825 <ProjectFavicon
696826 environmentId={project.environmentId}
......@@ -699,6 +829,14 @@ function OpenCommandPaletteDialog(props: {
699829 />
700830 ),
701831 runProject: async (project) => {
832 const group = projectGroupByTargetKey.get(`${project.environmentId}:${project.id}`);
833 const contextualRefBelongsToGroup =
834 contextualProjectRef !== null &&
835 group?.memberProjectRefs.some(
836 (projectRef) =>
837 projectRef.environmentId === contextualProjectRef.environmentId &&
838 projectRef.projectId === contextualProjectRef.projectId,
839 );
702840 await startNewThreadInProjectFromContext(
703841 {
704842 activeDraftThread,
......@@ -706,12 +844,22 @@ function OpenCommandPaletteDialog(props: {
706844 defaultProjectRef,
707845 handleNewThread,
708846 },
709 scopeProjectRef(project.environmentId, project.id),
847 contextualRefBelongsToGroup
848 ? contextualProjectRef
849 : scopeProjectRef(project.environmentId, project.id),
710850 );
711851 },
712852 }),
713853 ),
714 [activeDraftThread, activeThread, defaultProjectRef, handleNewThread, projects],
854 [
855 activeDraftThread,
856 activeThread,
857 contextualProjectRef,
858 defaultProjectRef,
859 handleNewThread,
860 pickerProjects,
861 projectGroupByTargetKey,
862 ],
715863 );
716864
717865 const allThreadItems = useMemo(
......@@ -1018,9 +1166,9 @@ function OpenCommandPaletteDialog(props: {
10181166 const actionItems: Array<CommandPaletteActionItem | CommandPaletteSubmenuItem> = [];
10191167
10201168 if (projects.length > 0) {
1021 const activeProjectTitle = currentProjectId
1022 ? (projectTitleById.get(currentProjectId) ?? null)
1023 : null;
1169 const activeProjectTitle =
1170 projectPickerEntries.find((entry) => entry.isPreferred)?.group.displayName ??
1171 (currentProjectId ? (projectTitleById.get(currentProjectId) ?? null) : null);
10241172
10251173 if (activeProjectTitle) {
10261174 actionItems.push({
apps/web/src/components/SidebarV2.tsx+14-7
......@@ -598,7 +598,7 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {
598598 type="button"
599599 aria-label="Un-settle thread"
600600 onClick={handleUnsettleClick}
601 className="absolute inset-y-0 right-0 inline-flex cursor-pointer items-center gap-1 rounded-md border border-sidebar-border bg-sidebar-row-hover px-2 text-xs text-muted-foreground opacity-0 transition-opacity hover:text-foreground focus-visible:opacity-100 group-hover/v2-row:opacity-100 dark:border-transparent dark:inset-ring-1 dark:inset-ring-white/5"
601 className="absolute inset-y-0 right-0 inline-flex cursor-pointer items-center gap-1 rounded-md bg-transparent px-2 text-xs text-muted-foreground opacity-0 transition-opacity hover:text-foreground focus-visible:opacity-100 group-hover/v2-row:opacity-100"
602602 >
603603 <Undo2Icon className="size-3" />
604604 </button>
......@@ -607,7 +607,7 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {
607607 type="button"
608608 aria-label="Settle thread"
609609 onClick={handleSettleClick}
610 className="absolute inset-y-0 right-0 inline-flex cursor-pointer items-center gap-1 rounded-md border border-sidebar-border bg-sidebar-row-hover px-2 text-xs text-muted-foreground opacity-0 transition-opacity hover:text-foreground focus-visible:opacity-100 group-hover/v2-row:opacity-100 dark:border-transparent dark:inset-ring-1 dark:inset-ring-white/5"
610 className="absolute inset-y-0 right-0 inline-flex cursor-pointer items-center gap-1 rounded-md bg-transparent px-2 text-xs text-muted-foreground opacity-0 transition-opacity hover:text-foreground focus-visible:opacity-100 group-hover/v2-row:opacity-100"
611611 >
612612 <CheckIcon className="size-3" />
613613 </button>
......@@ -684,7 +684,7 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {
684684 type="button"
685685 aria-label="Settle thread"
686686 onClick={handleSettleClick}
687 className="absolute inset-y-0 right-0 inline-flex cursor-pointer items-center gap-1 rounded-md border border-sidebar-border bg-sidebar-row-hover px-2 text-xs text-muted-foreground opacity-0 transition-opacity hover:text-foreground focus-visible:opacity-100 group-hover/v2-row:opacity-100 dark:border-transparent dark:inset-ring-1 dark:inset-ring-white/5"
687 className="absolute inset-y-0 right-0 inline-flex cursor-pointer items-center gap-1 rounded-md bg-transparent px-2 text-xs text-muted-foreground opacity-0 transition-opacity hover:text-foreground focus-visible:opacity-100 group-hover/v2-row:opacity-100"
688688 >
689689 <CheckIcon className="size-3" />
690690 Settle
......@@ -874,10 +874,16 @@ export default function SidebarV2() {
874874 ),
875875 [projects],
876876 );
877 const projectTitleByKey = useMemo(
877 const projectDisplayNameByKey = useMemo(
878878 () =>
879 new Map(projects.map((project) => [`${project.environmentId}:${project.id}`, project.title])),
880 [projects],
879 new Map(
880 projectGroups.flatMap((group) =>
881 group.memberProjects.map(
882 (project) => [`${project.environmentId}:${project.id}`, group.displayName] as const,
883 ),
884 ),
885 ),
886 [projectGroups],
881887 );
882888
883889 // now is quantized to the minute so effectiveSettled memoization doesn't
......@@ -1876,7 +1882,8 @@ export default function SidebarV2() {
18761882 projectCwdByKey.get(`${thread.environmentId}:${thread.projectId}`) ?? null
18771883 }
18781884 projectTitle={
1879 projectTitleByKey.get(`${thread.environmentId}:${thread.projectId}`) ?? null
1885 projectDisplayNameByKey.get(`${thread.environmentId}:${thread.projectId}`) ??
1886 null
18801887 }
18811888 providerEntryByInstanceId={providerEntryByInstanceId}
18821889 onThreadClick={handleThreadClick}
apps/web/src/components/chat/DraftHeroHeadline.tsx+67-23
......@@ -5,8 +5,15 @@ import { useCallback, useMemo } from "react";
55
66import { openCommandPalette } from "~/commandPaletteBus";
77import { useNewThreadHandler } from "~/hooks/useHandleNewThread";
8import { useClientSettings } from "~/hooks/useSettings";
9import { selectProjectGroupingSettings } from "~/logicalProject";
10import {
11 buildSidebarProjectPickerEntries,
12 buildSidebarProjectSnapshots,
13} from "~/sidebarProjectGrouping";
814import { useProjects, useThreadShells } from "~/state/entities";
9import { sortScopedProjectsForSidebar } from "../Sidebar.logic";
15import { useEnvironments, usePrimaryEnvironmentId } from "~/state/environments";
16import { sortLogicalProjectsForSidebar } from "../Sidebar.logic";
1017import {
1118 Menu,
1219 MenuItem,
......@@ -28,29 +35,66 @@ export function DraftHeroHeadline({
2835}: DraftHeroHeadlineProps) {
2936 const projects = useProjects();
3037 const threads = useThreadShells();
38 const { environments } = useEnvironments();
39 const primaryEnvironmentId = usePrimaryEnvironmentId();
40 const projectGroupingSettings = useClientSettings(selectProjectGroupingSettings);
41 const projectSortOrder = useClientSettings((settings) => settings.sidebarProjectSortOrder);
3142 const handleNewThread = useNewThreadHandler();
3243 const openAddProject = useCallback(() => openCommandPalette({ open: "add-project" }), []);
3344
34 const orderedProjects = useMemo(
35 () => sortScopedProjectsForSidebar(projects, threads, "updated_at"),
36 [projects, threads],
37 );
38 const projectByKey = useMemo(
45 const environmentLabelById = useMemo(
3946 () =>
4047 new Map(
41 orderedProjects.map(
42 (project) =>
43 [
44 scopedProjectKey(scopeProjectRef(project.environmentId, project.id)),
45 project,
46 ] as const,
47 ),
48 environments.map((environment) => [environment.environmentId, environment.label] as const),
49 ),
50 [environments],
51 );
52 const projectGroups = useMemo(
53 () =>
54 sortLogicalProjectsForSidebar(
55 buildSidebarProjectSnapshots({
56 projects,
57 settings: projectGroupingSettings,
58 primaryEnvironmentId,
59 resolveEnvironmentLabel: (environmentId) =>
60 environmentLabelById.get(environmentId) ?? null,
61 }),
62 threads,
63 projectSortOrder,
4864 ),
49 [orderedProjects],
65 [
66 environmentLabelById,
67 primaryEnvironmentId,
68 projectGroupingSettings,
69 projectSortOrder,
70 projects,
71 threads,
72 ],
73 );
74 const projectPickerEntries = useMemo(
75 () =>
76 buildSidebarProjectPickerEntries({
77 groups: projectGroups,
78 preferredProjectRef: activeProjectRef,
79 }),
80 [activeProjectRef, projectGroups],
81 );
82 const projectEntryByKey = useMemo(
83 () => new Map(projectPickerEntries.map((entry) => [entry.group.projectKey, entry] as const)),
84 [projectPickerEntries],
5085 );
51 const activeProjectKey = activeProjectRef === null ? "" : scopedProjectKey(activeProjectRef);
86 const activeProjectGroup =
87 activeProjectRef === null
88 ? null
89 : (projectGroups.find((group) =>
90 group.memberProjectRefs.some(
91 (projectRef) => scopedProjectKey(projectRef) === scopedProjectKey(activeProjectRef),
92 ),
93 ) ?? null);
94 const activeProjectKey = activeProjectGroup?.projectKey ?? "";
95 const activeProjectDisplayName = activeProjectGroup?.displayName ?? activeProjectTitle;
5296 const hasResolvedProject = activeProjectTitle !== null;
53 const canChooseProject = orderedProjects.length > 0;
97 const canChooseProject = projectPickerEntries.length > 0;
5498 const shouldShowProjectMenu = canChooseProject;
5599
56100 const projectSelector = shouldShowProjectMenu ? (
......@@ -59,26 +103,26 @@ export function DraftHeroHeadline({
59103 aria-label={hasResolvedProject ? "Change project" : "Choose a project"}
60104 className="pointer-events-auto inline cursor-pointer border-current border-b border-dotted text-foreground underline-offset-8 transition-opacity hover:opacity-75 focus-visible:rounded-sm focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
61105 >
62 {activeProjectTitle ?? "Choose a project"}
106 {activeProjectDisplayName ?? "Choose a project"}
63107 </MenuTrigger>
64108 <MenuPopup align="center" className="max-h-80 w-64 overflow-y-auto">
65109 <MenuRadioGroup
66110 value={activeProjectKey}
67111 onValueChange={(value) => {
68 const project = projectByKey.get(value as string);
69 if (!project || value === activeProjectKey) {
112 const entry = projectEntryByKey.get(value as string);
113 if (!entry || value === activeProjectKey) {
70114 return;
71115 }
116 const project = entry.targetProject;
72117 void handleNewThread(scopeProjectRef(project.environmentId, project.id), {
73118 replace: true,
74119 });
75120 }}
76121 >
77 {orderedProjects.map((project) => {
78 const key = scopedProjectKey(scopeProjectRef(project.environmentId, project.id));
122 {projectPickerEntries.map(({ group }) => {
79123 return (
80 <MenuRadioItem key={key} value={key} closeOnClick>
81 <span className="min-w-0 truncate">{project.title}</span>
124 <MenuRadioItem key={group.projectKey} value={group.projectKey} closeOnClick>
125 <span className="min-w-0 truncate">{group.displayName}</span>
82126 </MenuRadioItem>
83127 );
84128 })}
apps/web/src/routes/_chat.tsx+19-4
......@@ -1,11 +1,14 @@
11import { Outlet, createFileRoute, redirect } from "@tanstack/react-router";
22import { useAtomValue } from "@effect/atom-react";
3import { useEffect } from "react";
3import { useEffect, useMemo } from "react";
44
55import { isCommandPaletteOpen } from "../commandPaletteBus";
66import { useClientSettings } from "../hooks/useSettings";
77import { openCommandPalette } from "../commandPaletteBus";
88import { useProjects } from "../state/entities";
9import { usePrimaryEnvironmentId } from "../state/environments";
10import { selectProjectGroupingSettings } from "../logicalProject";
11import { buildSidebarProjectSnapshots } from "../sidebarProjectGrouping";
912import { dispatchPreviewAction } from "../components/preview/previewActionBus";
1013import { useHandleNewThread } from "../hooks/useHandleNewThread";
1114import {
......@@ -29,7 +32,19 @@ function ChatRouteGlobalShortcuts() {
2932 useHandleNewThread();
3033 const keybindings = useAtomValue(primaryServerKeybindingsAtom);
3134 const sidebarV2Enabled = useClientSettings((settings) => settings.sidebarV2Enabled);
32 const projectCount = useProjects().length;
35 const projectGroupingSettings = useClientSettings(selectProjectGroupingSettings);
36 const projects = useProjects();
37 const primaryEnvironmentId = usePrimaryEnvironmentId();
38 const projectGroupCount = useMemo(
39 () =>
40 buildSidebarProjectSnapshots({
41 projects,
42 settings: projectGroupingSettings,
43 primaryEnvironmentId,
44 resolveEnvironmentLabel: () => null,
45 }).length,
46 [primaryEnvironmentId, projectGroupingSettings, projects],
47 );
3348 const terminalOpen = useTerminalUiStateStore((state) =>
3449 routeThreadRef
3550 ? selectThreadTerminalUiState(state.terminalUiStateByThreadKey, routeThreadRef).terminalOpen
......@@ -83,7 +98,7 @@ function ChatRouteGlobalShortcuts() {
8398 // Sidebar v2 routes creation through the command palette whenever
8499 // there is a real choice to make; v1 (and single-project setups)
85100 // keep the immediate contextual create.
86 if (sidebarV2Enabled && projectCount > 1) {
101 if (sidebarV2Enabled && projectGroupCount > 1) {
87102 openCommandPalette({ open: "new-thread-in" });
88103 return;
89104 }
......@@ -152,7 +167,7 @@ function ChatRouteGlobalShortcuts() {
152167 keybindings,
153168 defaultProjectRef,
154169 previewOpen,
155 projectCount,
170 projectGroupCount,
156171 routeThreadRef,
157172 selectedThreadKeysSize,
158173 sidebarV2Enabled,