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 {...@@ -145,15 +145,7 @@ import {
145 DialogTitle,145 DialogTitle,
146} from "./ui/dialog";146} from "./ui/dialog";
147import { Input } from "./ui/input";147import { Input } from "./ui/input";
148import {148import { Menu, MenuGroup, MenuPopup, MenuRadioGroup, MenuRadioItem, MenuTrigger } from "./ui/menu";
149 Menu,
150 MenuGroup,
151 MenuPopup,
152 MenuRadioGroup,
153 MenuRadioItem,
154 MenuSeparator,
155 MenuTrigger,
156} from "./ui/menu";
157import {149import {
158 NumberField,150 NumberField,
159 NumberFieldDecrement,151 NumberFieldDecrement,
...@@ -2601,20 +2593,16 @@ type SortableProjectHandleProps = Pick<...@@ -2601,20 +2593,16 @@ type SortableProjectHandleProps = Pick<
2601function ProjectSortMenu({2593function ProjectSortMenu({
2602 projectSortOrder,2594 projectSortOrder,
2603 threadSortOrder,2595 threadSortOrder,
2604 projectGroupingMode,
2605 threadPreviewCount,2596 threadPreviewCount,
2606 onProjectSortOrderChange,2597 onProjectSortOrderChange,
2607 onThreadSortOrderChange,2598 onThreadSortOrderChange,
2608 onProjectGroupingModeChange,
2609 onThreadPreviewCountChange,2599 onThreadPreviewCountChange,
2610}: {2600}: {
2611 projectSortOrder: SidebarProjectSortOrder;2601 projectSortOrder: SidebarProjectSortOrder;
2612 threadSortOrder: SidebarThreadSortOrder;2602 threadSortOrder: SidebarThreadSortOrder;
2613 projectGroupingMode: SidebarProjectGroupingMode;
2614 threadPreviewCount: SidebarThreadPreviewCount;2603 threadPreviewCount: SidebarThreadPreviewCount;
2615 onProjectSortOrderChange: (sortOrder: SidebarProjectSortOrder) => void;2604 onProjectSortOrderChange: (sortOrder: SidebarProjectSortOrder) => void;
2616 onThreadSortOrderChange: (sortOrder: SidebarThreadSortOrder) => void;2605 onThreadSortOrderChange: (sortOrder: SidebarThreadSortOrder) => void;
2617 onProjectGroupingModeChange: (mode: SidebarProjectGroupingMode) => void;
2618 onThreadPreviewCountChange: (count: SidebarThreadPreviewCount) => void;2606 onThreadPreviewCountChange: (count: SidebarThreadPreviewCount) => void;
2619}) {2607}) {
2620 const handleThreadPreviewCountChange = useCallback(2608 const handleThreadPreviewCountChange = useCallback(
...@@ -2718,30 +2706,6 @@ function ProjectSortMenu({...@@ -2718,30 +2706,6 @@ function ProjectSortMenu({
2718 </NumberField>2706 </NumberField>
2719 </div>2707 </div>
2720 </MenuGroup>2708 </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>
2745 </MenuPopup>2709 </MenuPopup>
2746 </Menu>2710 </Menu>
2747 );2711 );
...@@ -2792,7 +2756,6 @@ interface SidebarProjectsContentProps {...@@ -2792,7 +2756,6 @@ interface SidebarProjectsContentProps {
2792 handleDesktopUpdateButtonClick: () => void;2756 handleDesktopUpdateButtonClick: () => void;
2793 projectSortOrder: SidebarProjectSortOrder;2757 projectSortOrder: SidebarProjectSortOrder;
2794 threadSortOrder: SidebarThreadSortOrder;2758 threadSortOrder: SidebarThreadSortOrder;
2795 projectGroupingMode: SidebarProjectGroupingMode;
2796 threadPreviewCount: SidebarThreadPreviewCount;2759 threadPreviewCount: SidebarThreadPreviewCount;
2797 updateSettings: ReturnType<typeof useUpdateClientSettings>;2760 updateSettings: ReturnType<typeof useUpdateClientSettings>;
2798 openAddProject: () => void;2761 openAddProject: () => void;
...@@ -2833,7 +2796,6 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(...@@ -2833,7 +2796,6 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
2833 handleDesktopUpdateButtonClick,2796 handleDesktopUpdateButtonClick,
2834 projectSortOrder,2797 projectSortOrder,
2835 threadSortOrder,2798 threadSortOrder,
2836 projectGroupingMode,
2837 threadPreviewCount,2799 threadPreviewCount,
2838 updateSettings,2800 updateSettings,
2839 openAddProject,2801 openAddProject,
...@@ -2875,12 +2837,6 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(...@@ -2875,12 +2837,6 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
2875 },2837 },
2876 [updateSettings],2838 [updateSettings],
2877 );2839 );
2878 const handleProjectGroupingModeChange = useCallback(
2879 (groupingMode: SidebarProjectGroupingMode) => {
2880 updateSettings({ sidebarProjectGroupingMode: groupingMode });
2881 },
2882 [updateSettings],
2883 );
2884 const handleThreadPreviewCountChange = useCallback(2840 const handleThreadPreviewCountChange = useCallback(
2885 (count: SidebarThreadPreviewCount) => {2841 (count: SidebarThreadPreviewCount) => {
2886 updateSettings({ sidebarThreadPreviewCount: count });2842 updateSettings({ sidebarThreadPreviewCount: count });
...@@ -2944,11 +2900,9 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(...@@ -2944,11 +2900,9 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
2944 <ProjectSortMenu2900 <ProjectSortMenu
2945 projectSortOrder={projectSortOrder}2901 projectSortOrder={projectSortOrder}
2946 threadSortOrder={threadSortOrder}2902 threadSortOrder={threadSortOrder}
2947 projectGroupingMode={projectGroupingMode}
2948 threadPreviewCount={threadPreviewCount}2903 threadPreviewCount={threadPreviewCount}
2949 onProjectSortOrderChange={handleProjectSortOrderChange}2904 onProjectSortOrderChange={handleProjectSortOrderChange}
2950 onThreadSortOrderChange={handleThreadSortOrderChange}2905 onThreadSortOrderChange={handleThreadSortOrderChange}
2951 onProjectGroupingModeChange={handleProjectGroupingModeChange}
2952 onThreadPreviewCountChange={handleThreadPreviewCountChange}2906 onThreadPreviewCountChange={handleThreadPreviewCountChange}
2953 />2907 />
2954 <Tooltip>2908 <Tooltip>
...@@ -3064,7 +3018,6 @@ export default function Sidebar() {...@@ -3064,7 +3018,6 @@ export default function Sidebar() {
3064 const isOnSettings = pathname.startsWith("/settings");3018 const isOnSettings = pathname.startsWith("/settings");
3065 const sidebarThreadSortOrder = useClientSettings((s) => s.sidebarThreadSortOrder);3019 const sidebarThreadSortOrder = useClientSettings((s) => s.sidebarThreadSortOrder);
3066 const sidebarProjectSortOrder = useClientSettings((s) => s.sidebarProjectSortOrder);3020 const sidebarProjectSortOrder = useClientSettings((s) => s.sidebarProjectSortOrder);
3067 const sidebarProjectGroupingMode = useClientSettings((s) => s.sidebarProjectGroupingMode);
3068 const projectGroupingSettings = useClientSettings(selectProjectGroupingSettings);3021 const projectGroupingSettings = useClientSettings(selectProjectGroupingSettings);
3069 const sidebarThreadPreviewCount = useClientSettings((s) => s.sidebarThreadPreviewCount);3022 const sidebarThreadPreviewCount = useClientSettings((s) => s.sidebarThreadPreviewCount);
3070 const updateSettings = useUpdateClientSettings();3023 const updateSettings = useUpdateClientSettings();
...@@ -3674,7 +3627,6 @@ export default function Sidebar() {...@@ -3674,7 +3627,6 @@ export default function Sidebar() {
3674 handleDesktopUpdateButtonClick={handleDesktopUpdateButtonClick}3627 handleDesktopUpdateButtonClick={handleDesktopUpdateButtonClick}
3675 projectSortOrder={sidebarProjectSortOrder}3628 projectSortOrder={sidebarProjectSortOrder}
3676 threadSortOrder={sidebarThreadSortOrder}3629 threadSortOrder={sidebarThreadSortOrder}
3677 projectGroupingMode={sidebarProjectGroupingMode}
3678 threadPreviewCount={sidebarThreadPreviewCount}3630 threadPreviewCount={sidebarThreadPreviewCount}
3679 updateSettings={updateSettings}3631 updateSettings={updateSettings}
3680 openAddProject={openAddProjectCommandPalette}3632 openAddProject={openAddProjectCommandPalette}
apps/web/src/components/settings/SettingsPanels.logic.test.ts+17
...@@ -8,8 +8,25 @@ import { describe, expect, it } from "vite-plus/test";...@@ -8,8 +8,25 @@ import { describe, expect, it } from "vite-plus/test";
8import {8import {
9 buildProviderInstanceUpdatePatch,9 buildProviderInstanceUpdatePatch,
10 formatDiagnosticsDescription,10 formatDiagnosticsDescription,
11 isProjectGroupingEnabled,
12 projectGroupingModeFromToggle,
11} from "./SettingsPanels.logic";13} 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
13describe("formatDiagnosticsDescription", () => {30describe("formatDiagnosticsDescription", () => {
14 it("collapses trace and metric URLs that share the same OTEL base path", () => {31 it("collapses trace and metric URLs that share the same OTEL base path", () => {
15 expect(32 expect(
apps/web/src/components/settings/SettingsPanels.logic.ts+34
...@@ -3,10 +3,44 @@ import type {...@@ -3,10 +3,44 @@ import type {
3 ProviderInstanceConfig,3 ProviderInstanceConfig,
4 ProviderInstanceId,4 ProviderInstanceId,
5 ServerSettings,5 ServerSettings,
6 SidebarProjectGroupingMode,
6 UnifiedSettings,7 UnifiedSettings,
7} from "@t3tools/contracts";8} from "@t3tools/contracts";
8import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings";9import { 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
10function collapseOtelSignalsUrl(input: {44function collapseOtelSignalsUrl(input: {
11 readonly tracesUrl: string;45 readonly tracesUrl: string;
12 readonly metricsUrl: string;46 readonly metricsUrl: string;
apps/web/src/components/settings/SettingsPanels.tsx+50
...@@ -11,6 +11,7 @@ import {...@@ -11,6 +11,7 @@ import {
11 type ProviderInstanceConfig,11 type ProviderInstanceConfig,
12 type ProviderInstanceId,12 type ProviderInstanceId,
13 type ScopedThreadRef,13 type ScopedThreadRef,
14 type SidebarProjectGroupingMode,
14} from "@t3tools/contracts";15} from "@t3tools/contracts";
15import { scopeThreadRef } from "@t3tools/client-runtime/environment";16import { scopeThreadRef } from "@t3tools/client-runtime/environment";
16import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors";17import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors";
...@@ -83,6 +84,10 @@ import { DRIVER_OPTIONS, getDriverOption } from "./providerDriverMeta";...@@ -83,6 +84,10 @@ import { DRIVER_OPTIONS, getDriverOption } from "./providerDriverMeta";
83import {84import {
84 buildProviderInstanceUpdatePatch,85 buildProviderInstanceUpdatePatch,
85 formatDiagnosticsDescription,86 formatDiagnosticsDescription,
87 isProjectGroupingEnabled,
88 projectGroupingModeFromToggle,
89 readLastEnabledProjectGroupingMode,
90 rememberEnabledProjectGroupingMode,
86} from "./SettingsPanels.logic";91} from "./SettingsPanels.logic";
87import {92import {
88 SettingResetButton,93 SettingResetButton,
...@@ -402,6 +407,10 @@ export function useSettingsRestore(onRestored?: () => void) {...@@ -402,6 +407,10 @@ export function useSettingsRestore(onRestored?: () => void) {
402 ...(settings.sidebarThreadPreviewCount !== DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount407 ...(settings.sidebarThreadPreviewCount !== DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount
403 ? ["Visible threads"]408 ? ["Visible threads"]
404 : []),409 : []),
410 ...(settings.sidebarProjectGroupingMode !==
411 DEFAULT_UNIFIED_SETTINGS.sidebarProjectGroupingMode
412 ? ["Project Grouping"]
413 : []),
405 ...(settings.wordWrap !== DEFAULT_UNIFIED_SETTINGS.wordWrap ? ["Word wrap"] : []),414 ...(settings.wordWrap !== DEFAULT_UNIFIED_SETTINGS.wordWrap ? ["Word wrap"] : []),
406 ...(settings.diffIgnoreWhitespace !== DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace415 ...(settings.diffIgnoreWhitespace !== DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace
407 ? ["Diff whitespace changes"]416 ? ["Diff whitespace changes"]
...@@ -451,6 +460,7 @@ export function useSettingsRestore(onRestored?: () => void) {...@@ -451,6 +460,7 @@ export function useSettingsRestore(onRestored?: () => void) {
451 settings.automaticGitFetchInterval,460 settings.automaticGitFetchInterval,
452 settings.enableAssistantStreaming,461 settings.enableAssistantStreaming,
453 settings.enableProviderUpdateChecks,462 settings.enableProviderUpdateChecks,
463 settings.sidebarProjectGroupingMode,
454 settings.sidebarThreadPreviewCount,464 settings.sidebarThreadPreviewCount,
455 settings.timestampFormat,465 settings.timestampFormat,
456 settings.wordWrap,466 settings.wordWrap,
...@@ -475,6 +485,7 @@ export function useSettingsRestore(onRestored?: () => void) {...@@ -475,6 +485,7 @@ export function useSettingsRestore(onRestored?: () => void) {
475 diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace,485 diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace,
476 glassOpacity: DEFAULT_UNIFIED_SETTINGS.glassOpacity,486 glassOpacity: DEFAULT_UNIFIED_SETTINGS.glassOpacity,
477 sidebarThreadPreviewCount: DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount,487 sidebarThreadPreviewCount: DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount,
488 sidebarProjectGroupingMode: DEFAULT_UNIFIED_SETTINGS.sidebarProjectGroupingMode,
478 autoOpenPlanSidebar: DEFAULT_UNIFIED_SETTINGS.autoOpenPlanSidebar,489 autoOpenPlanSidebar: DEFAULT_UNIFIED_SETTINGS.autoOpenPlanSidebar,
479 enableAssistantStreaming: DEFAULT_UNIFIED_SETTINGS.enableAssistantStreaming,490 enableAssistantStreaming: DEFAULT_UNIFIED_SETTINGS.enableAssistantStreaming,
480 enableProviderUpdateChecks: DEFAULT_UNIFIED_SETTINGS.enableProviderUpdateChecks,491 enableProviderUpdateChecks: DEFAULT_UNIFIED_SETTINGS.enableProviderUpdateChecks,
...@@ -499,6 +510,9 @@ export function GeneralSettingsPanel() {...@@ -499,6 +510,9 @@ export function GeneralSettingsPanel() {
499 const { theme, setTheme } = useTheme();510 const { theme, setTheme } = useTheme();
500 const settings = usePrimarySettings();511 const settings = usePrimarySettings();
501 const updateSettings = useUpdatePrimarySettings();512 const updateSettings = useUpdatePrimarySettings();
513 const lastEnabledProjectGroupingMode = useRef<SidebarProjectGroupingMode>(
514 readLastEnabledProjectGroupingMode(),
515 );
502 const observability = useAtomValue(primaryServerObservabilityAtom);516 const observability = useAtomValue(primaryServerObservabilityAtom);
503 const serverProviders = useAtomValue(primaryServerProvidersAtom);517 const serverProviders = useAtomValue(primaryServerProvidersAtom);
504 const glassOpacityRatio =518 const glassOpacityRatio =
...@@ -620,6 +634,42 @@ export function GeneralSettingsPanel() {...@@ -620,6 +634,42 @@ export function GeneralSettingsPanel() {
620 }634 }
621 />635 />
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
623 <SettingsRow673 <SettingsRow
624 title="Time format"674 title="Time format"
625 description="System default follows your browser or OS clock preference."675 description="System default follows your browser or OS clock preference."