authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-07-23 18:06:33+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-23 18:06:33+02:00
log7609495bf70a51dd4d8608e3f71da636a7348aa6
treee734167ddb9dbac57fe0dc59ade5032142475765
parentb41e89eba9cd232cc3257b400fc30972a9b53438
signature Signed by PGP key B5690EEEBB952194

[codex] Move project grouping to General settings (#4313)

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

4 files changed, 102 insertions(+), 49 deletions(-)

apps/web/src/components/Sidebar.tsx+1-49
......@@ -145,15 +145,7 @@ import {
145145 DialogTitle,
146146} from "./ui/dialog";
147147import { Input } from "./ui/input";
148import {
149 Menu,
150 MenuGroup,
151 MenuPopup,
152 MenuRadioGroup,
153 MenuRadioItem,
154 MenuSeparator,
155 MenuTrigger,
156} from "./ui/menu";
148import { Menu, MenuGroup, MenuPopup, MenuRadioGroup, MenuRadioItem, MenuTrigger } from "./ui/menu";
157149import {
158150 NumberField,
159151 NumberFieldDecrement,
......@@ -2601,20 +2593,16 @@ type SortableProjectHandleProps = Pick<
26012593function ProjectSortMenu({
26022594 projectSortOrder,
26032595 threadSortOrder,
2604 projectGroupingMode,
26052596 threadPreviewCount,
26062597 onProjectSortOrderChange,
26072598 onThreadSortOrderChange,
2608 onProjectGroupingModeChange,
26092599 onThreadPreviewCountChange,
26102600}: {
26112601 projectSortOrder: SidebarProjectSortOrder;
26122602 threadSortOrder: SidebarThreadSortOrder;
2613 projectGroupingMode: SidebarProjectGroupingMode;
26142603 threadPreviewCount: SidebarThreadPreviewCount;
26152604 onProjectSortOrderChange: (sortOrder: SidebarProjectSortOrder) => void;
26162605 onThreadSortOrderChange: (sortOrder: SidebarThreadSortOrder) => void;
2617 onProjectGroupingModeChange: (mode: SidebarProjectGroupingMode) => void;
26182606 onThreadPreviewCountChange: (count: SidebarThreadPreviewCount) => void;
26192607}) {
26202608 const handleThreadPreviewCountChange = useCallback(
......@@ -2718,30 +2706,6 @@ function ProjectSortMenu({
27182706 </NumberField>
27192707 </div>
27202708 </MenuGroup>
2721 <MenuSeparator />
2722 <MenuGroup>
2723 <div className="px-2 pt-2 pb-1 font-medium text-muted-foreground sm:text-xs">
2724 Group projects
2725 </div>
2726 <MenuRadioGroup
2727 value={projectGroupingMode}
2728 onValueChange={(value) => {
2729 if (value === "repository" || value === "repository_path" || value === "separate") {
2730 onProjectGroupingModeChange(value);
2731 }
2732 }}
2733 >
2734 {(
2735 Object.entries(PROJECT_GROUPING_MODE_LABELS) as Array<
2736 [SidebarProjectGroupingMode, string]
2737 >
2738 ).map(([value, label]) => (
2739 <MenuRadioItem key={value} value={value} className="min-h-7 py-1 sm:text-xs">
2740 {label}
2741 </MenuRadioItem>
2742 ))}
2743 </MenuRadioGroup>
2744 </MenuGroup>
27452709 </MenuPopup>
27462710 </Menu>
27472711 );
......@@ -2792,7 +2756,6 @@ interface SidebarProjectsContentProps {
27922756 handleDesktopUpdateButtonClick: () => void;
27932757 projectSortOrder: SidebarProjectSortOrder;
27942758 threadSortOrder: SidebarThreadSortOrder;
2795 projectGroupingMode: SidebarProjectGroupingMode;
27962759 threadPreviewCount: SidebarThreadPreviewCount;
27972760 updateSettings: ReturnType<typeof useUpdateClientSettings>;
27982761 openAddProject: () => void;
......@@ -2833,7 +2796,6 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
28332796 handleDesktopUpdateButtonClick,
28342797 projectSortOrder,
28352798 threadSortOrder,
2836 projectGroupingMode,
28372799 threadPreviewCount,
28382800 updateSettings,
28392801 openAddProject,
......@@ -2875,12 +2837,6 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
28752837 },
28762838 [updateSettings],
28772839 );
2878 const handleProjectGroupingModeChange = useCallback(
2879 (groupingMode: SidebarProjectGroupingMode) => {
2880 updateSettings({ sidebarProjectGroupingMode: groupingMode });
2881 },
2882 [updateSettings],
2883 );
28842840 const handleThreadPreviewCountChange = useCallback(
28852841 (count: SidebarThreadPreviewCount) => {
28862842 updateSettings({ sidebarThreadPreviewCount: count });
......@@ -2944,11 +2900,9 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
29442900 <ProjectSortMenu
29452901 projectSortOrder={projectSortOrder}
29462902 threadSortOrder={threadSortOrder}
2947 projectGroupingMode={projectGroupingMode}
29482903 threadPreviewCount={threadPreviewCount}
29492904 onProjectSortOrderChange={handleProjectSortOrderChange}
29502905 onThreadSortOrderChange={handleThreadSortOrderChange}
2951 onProjectGroupingModeChange={handleProjectGroupingModeChange}
29522906 onThreadPreviewCountChange={handleThreadPreviewCountChange}
29532907 />
29542908 <Tooltip>
......@@ -3064,7 +3018,6 @@ export default function Sidebar() {
30643018 const isOnSettings = pathname.startsWith("/settings");
30653019 const sidebarThreadSortOrder = useClientSettings((s) => s.sidebarThreadSortOrder);
30663020 const sidebarProjectSortOrder = useClientSettings((s) => s.sidebarProjectSortOrder);
3067 const sidebarProjectGroupingMode = useClientSettings((s) => s.sidebarProjectGroupingMode);
30683021 const projectGroupingSettings = useClientSettings(selectProjectGroupingSettings);
30693022 const sidebarThreadPreviewCount = useClientSettings((s) => s.sidebarThreadPreviewCount);
30703023 const updateSettings = useUpdateClientSettings();
......@@ -3674,7 +3627,6 @@ export default function Sidebar() {
36743627 handleDesktopUpdateButtonClick={handleDesktopUpdateButtonClick}
36753628 projectSortOrder={sidebarProjectSortOrder}
36763629 threadSortOrder={sidebarThreadSortOrder}
3677 projectGroupingMode={sidebarProjectGroupingMode}
36783630 threadPreviewCount={sidebarThreadPreviewCount}
36793631 updateSettings={updateSettings}
36803632 openAddProject={openAddProjectCommandPalette}
apps/web/src/components/settings/SettingsPanels.logic.test.ts+17
......@@ -8,8 +8,25 @@ import { describe, expect, it } from "vite-plus/test";
88import {
99 buildProviderInstanceUpdatePatch,
1010 formatDiagnosticsDescription,
11 isProjectGroupingEnabled,
12 projectGroupingModeFromToggle,
1113} from "./SettingsPanels.logic";
1214
15describe("project grouping toggle", () => {
16 it("enables repository grouping and disables into separate projects", () => {
17 expect(isProjectGroupingEnabled("repository")).toBe(true);
18 expect(isProjectGroupingEnabled("repository_path")).toBe(true);
19 expect(isProjectGroupingEnabled("separate")).toBe(false);
20 expect(projectGroupingModeFromToggle(true)).toBe("repository");
21 expect(projectGroupingModeFromToggle(false)).toBe("separate");
22 });
23
24 it("restores repository path grouping when the toggle is cycled", () => {
25 expect(projectGroupingModeFromToggle(false, "repository_path")).toBe("separate");
26 expect(projectGroupingModeFromToggle(true, "repository_path")).toBe("repository_path");
27 });
28});
29
1330describe("formatDiagnosticsDescription", () => {
1431 it("collapses trace and metric URLs that share the same OTEL base path", () => {
1532 expect(
apps/web/src/components/settings/SettingsPanels.logic.ts+34
......@@ -3,10 +3,44 @@ import type {
33 ProviderInstanceConfig,
44 ProviderInstanceId,
55 ServerSettings,
6 SidebarProjectGroupingMode,
67 UnifiedSettings,
78} from "@t3tools/contracts";
89import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings";
910
11export function isProjectGroupingEnabled(mode: SidebarProjectGroupingMode): boolean {
12 return mode !== "separate";
13}
14
15export function projectGroupingModeFromToggle(
16 enabled: boolean,
17 lastEnabledMode: SidebarProjectGroupingMode = "repository",
18): SidebarProjectGroupingMode {
19 if (!enabled) return "separate";
20 return lastEnabledMode === "repository_path" ? "repository_path" : "repository";
21}
22
23const LAST_ENABLED_PROJECT_GROUPING_MODE_KEY = "t3code:last-enabled-project-grouping-mode";
24
25export function readLastEnabledProjectGroupingMode(): SidebarProjectGroupingMode {
26 try {
27 return localStorage.getItem(LAST_ENABLED_PROJECT_GROUPING_MODE_KEY) === "repository_path"
28 ? "repository_path"
29 : "repository";
30 } catch {
31 return "repository";
32 }
33}
34
35export function rememberEnabledProjectGroupingMode(mode: SidebarProjectGroupingMode): void {
36 if (mode === "separate") return;
37 try {
38 localStorage.setItem(LAST_ENABLED_PROJECT_GROUPING_MODE_KEY, mode);
39 } catch {
40 // Storage can be unavailable in restricted browser contexts.
41 }
42}
43
1044function collapseOtelSignalsUrl(input: {
1145 readonly tracesUrl: string;
1246 readonly metricsUrl: string;
apps/web/src/components/settings/SettingsPanels.tsx+50
......@@ -11,6 +11,7 @@ import {
1111 type ProviderInstanceConfig,
1212 type ProviderInstanceId,
1313 type ScopedThreadRef,
14 type SidebarProjectGroupingMode,
1415} from "@t3tools/contracts";
1516import { scopeThreadRef } from "@t3tools/client-runtime/environment";
1617import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors";
......@@ -83,6 +84,10 @@ import { DRIVER_OPTIONS, getDriverOption } from "./providerDriverMeta";
8384import {
8485 buildProviderInstanceUpdatePatch,
8586 formatDiagnosticsDescription,
87 isProjectGroupingEnabled,
88 projectGroupingModeFromToggle,
89 readLastEnabledProjectGroupingMode,
90 rememberEnabledProjectGroupingMode,
8691} from "./SettingsPanels.logic";
8792import {
8893 SettingResetButton,
......@@ -402,6 +407,10 @@ export function useSettingsRestore(onRestored?: () => void) {
402407 ...(settings.sidebarThreadPreviewCount !== DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount
403408 ? ["Visible threads"]
404409 : []),
410 ...(settings.sidebarProjectGroupingMode !==
411 DEFAULT_UNIFIED_SETTINGS.sidebarProjectGroupingMode
412 ? ["Project Grouping"]
413 : []),
405414 ...(settings.wordWrap !== DEFAULT_UNIFIED_SETTINGS.wordWrap ? ["Word wrap"] : []),
406415 ...(settings.diffIgnoreWhitespace !== DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace
407416 ? ["Diff whitespace changes"]
......@@ -451,6 +460,7 @@ export function useSettingsRestore(onRestored?: () => void) {
451460 settings.automaticGitFetchInterval,
452461 settings.enableAssistantStreaming,
453462 settings.enableProviderUpdateChecks,
463 settings.sidebarProjectGroupingMode,
454464 settings.sidebarThreadPreviewCount,
455465 settings.timestampFormat,
456466 settings.wordWrap,
......@@ -475,6 +485,7 @@ export function useSettingsRestore(onRestored?: () => void) {
475485 diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace,
476486 glassOpacity: DEFAULT_UNIFIED_SETTINGS.glassOpacity,
477487 sidebarThreadPreviewCount: DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount,
488 sidebarProjectGroupingMode: DEFAULT_UNIFIED_SETTINGS.sidebarProjectGroupingMode,
478489 autoOpenPlanSidebar: DEFAULT_UNIFIED_SETTINGS.autoOpenPlanSidebar,
479490 enableAssistantStreaming: DEFAULT_UNIFIED_SETTINGS.enableAssistantStreaming,
480491 enableProviderUpdateChecks: DEFAULT_UNIFIED_SETTINGS.enableProviderUpdateChecks,
......@@ -499,6 +510,9 @@ export function GeneralSettingsPanel() {
499510 const { theme, setTheme } = useTheme();
500511 const settings = usePrimarySettings();
501512 const updateSettings = useUpdatePrimarySettings();
513 const lastEnabledProjectGroupingMode = useRef<SidebarProjectGroupingMode>(
514 readLastEnabledProjectGroupingMode(),
515 );
502516 const observability = useAtomValue(primaryServerObservabilityAtom);
503517 const serverProviders = useAtomValue(primaryServerProvidersAtom);
504518 const glassOpacityRatio =
......@@ -620,6 +634,42 @@ export function GeneralSettingsPanel() {
620634 }
621635 />
622636
637 <SettingsRow
638 title="Project Grouping"
639 description="Combine matching repositories across environments."
640 resetAction={
641 settings.sidebarProjectGroupingMode !==
642 DEFAULT_UNIFIED_SETTINGS.sidebarProjectGroupingMode ? (
643 <SettingResetButton
644 label="project grouping"
645 onClick={() =>
646 updateSettings({
647 sidebarProjectGroupingMode: DEFAULT_UNIFIED_SETTINGS.sidebarProjectGroupingMode,
648 })
649 }
650 />
651 ) : null
652 }
653 control={
654 <Switch
655 checked={isProjectGroupingEnabled(settings.sidebarProjectGroupingMode)}
656 onCheckedChange={(checked) => {
657 if (!checked && settings.sidebarProjectGroupingMode !== "separate") {
658 lastEnabledProjectGroupingMode.current = settings.sidebarProjectGroupingMode;
659 rememberEnabledProjectGroupingMode(settings.sidebarProjectGroupingMode);
660 }
661 updateSettings({
662 sidebarProjectGroupingMode: projectGroupingModeFromToggle(
663 checked,
664 lastEnabledProjectGroupingMode.current,
665 ),
666 });
667 }}
668 aria-label="Project Grouping"
669 />
670 }
671 />
672
623673 <SettingsRow
624674 title="Time format"
625675 description="System default follows your browser or OS clock preference."