diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 4b4093e2120..431968cbaee 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -145,15 +145,7 @@ import { DialogTitle, } from "./ui/dialog"; import { Input } from "./ui/input"; -import { - Menu, - MenuGroup, - MenuPopup, - MenuRadioGroup, - MenuRadioItem, - MenuSeparator, - MenuTrigger, -} from "./ui/menu"; +import { Menu, MenuGroup, MenuPopup, MenuRadioGroup, MenuRadioItem, MenuTrigger } from "./ui/menu"; import { NumberField, NumberFieldDecrement, @@ -2601,20 +2593,16 @@ type SortableProjectHandleProps = Pick< function ProjectSortMenu({ projectSortOrder, threadSortOrder, - projectGroupingMode, threadPreviewCount, onProjectSortOrderChange, onThreadSortOrderChange, - onProjectGroupingModeChange, onThreadPreviewCountChange, }: { projectSortOrder: SidebarProjectSortOrder; threadSortOrder: SidebarThreadSortOrder; - projectGroupingMode: SidebarProjectGroupingMode; threadPreviewCount: SidebarThreadPreviewCount; onProjectSortOrderChange: (sortOrder: SidebarProjectSortOrder) => void; onThreadSortOrderChange: (sortOrder: SidebarThreadSortOrder) => void; - onProjectGroupingModeChange: (mode: SidebarProjectGroupingMode) => void; onThreadPreviewCountChange: (count: SidebarThreadPreviewCount) => void; }) { const handleThreadPreviewCountChange = useCallback( @@ -2718,30 +2706,6 @@ function ProjectSortMenu({ - - -
- Group projects -
- { - if (value === "repository" || value === "repository_path" || value === "separate") { - onProjectGroupingModeChange(value); - } - }} - > - {( - Object.entries(PROJECT_GROUPING_MODE_LABELS) as Array< - [SidebarProjectGroupingMode, string] - > - ).map(([value, label]) => ( - - {label} - - ))} - -
); @@ -2792,7 +2756,6 @@ interface SidebarProjectsContentProps { handleDesktopUpdateButtonClick: () => void; projectSortOrder: SidebarProjectSortOrder; threadSortOrder: SidebarThreadSortOrder; - projectGroupingMode: SidebarProjectGroupingMode; threadPreviewCount: SidebarThreadPreviewCount; updateSettings: ReturnType; openAddProject: () => void; @@ -2833,7 +2796,6 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent( handleDesktopUpdateButtonClick, projectSortOrder, threadSortOrder, - projectGroupingMode, threadPreviewCount, updateSettings, openAddProject, @@ -2875,12 +2837,6 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent( }, [updateSettings], ); - const handleProjectGroupingModeChange = useCallback( - (groupingMode: SidebarProjectGroupingMode) => { - updateSettings({ sidebarProjectGroupingMode: groupingMode }); - }, - [updateSettings], - ); const handleThreadPreviewCountChange = useCallback( (count: SidebarThreadPreviewCount) => { updateSettings({ sidebarThreadPreviewCount: count }); @@ -2944,11 +2900,9 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent( @@ -3064,7 +3018,6 @@ export default function Sidebar() { const isOnSettings = pathname.startsWith("/settings"); const sidebarThreadSortOrder = useClientSettings((s) => s.sidebarThreadSortOrder); const sidebarProjectSortOrder = useClientSettings((s) => s.sidebarProjectSortOrder); - const sidebarProjectGroupingMode = useClientSettings((s) => s.sidebarProjectGroupingMode); const projectGroupingSettings = useClientSettings(selectProjectGroupingSettings); const sidebarThreadPreviewCount = useClientSettings((s) => s.sidebarThreadPreviewCount); const updateSettings = useUpdateClientSettings(); @@ -3674,7 +3627,6 @@ export default function Sidebar() { handleDesktopUpdateButtonClick={handleDesktopUpdateButtonClick} projectSortOrder={sidebarProjectSortOrder} threadSortOrder={sidebarThreadSortOrder} - projectGroupingMode={sidebarProjectGroupingMode} threadPreviewCount={sidebarThreadPreviewCount} updateSettings={updateSettings} openAddProject={openAddProjectCommandPalette} diff --git a/apps/web/src/components/settings/SettingsPanels.logic.test.ts b/apps/web/src/components/settings/SettingsPanels.logic.test.ts index d783d16c7ad..077991f8de0 100644 --- a/apps/web/src/components/settings/SettingsPanels.logic.test.ts +++ b/apps/web/src/components/settings/SettingsPanels.logic.test.ts @@ -8,8 +8,25 @@ import { describe, expect, it } from "vite-plus/test"; import { buildProviderInstanceUpdatePatch, formatDiagnosticsDescription, + isProjectGroupingEnabled, + projectGroupingModeFromToggle, } from "./SettingsPanels.logic"; +describe("project grouping toggle", () => { + it("enables repository grouping and disables into separate projects", () => { + expect(isProjectGroupingEnabled("repository")).toBe(true); + expect(isProjectGroupingEnabled("repository_path")).toBe(true); + expect(isProjectGroupingEnabled("separate")).toBe(false); + expect(projectGroupingModeFromToggle(true)).toBe("repository"); + expect(projectGroupingModeFromToggle(false)).toBe("separate"); + }); + + it("restores repository path grouping when the toggle is cycled", () => { + expect(projectGroupingModeFromToggle(false, "repository_path")).toBe("separate"); + expect(projectGroupingModeFromToggle(true, "repository_path")).toBe("repository_path"); + }); +}); + describe("formatDiagnosticsDescription", () => { it("collapses trace and metric URLs that share the same OTEL base path", () => { expect( diff --git a/apps/web/src/components/settings/SettingsPanels.logic.ts b/apps/web/src/components/settings/SettingsPanels.logic.ts index 99d7052965a..51e318225ae 100644 --- a/apps/web/src/components/settings/SettingsPanels.logic.ts +++ b/apps/web/src/components/settings/SettingsPanels.logic.ts @@ -3,10 +3,44 @@ import type { ProviderInstanceConfig, ProviderInstanceId, ServerSettings, + SidebarProjectGroupingMode, UnifiedSettings, } from "@t3tools/contracts"; import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings"; +export function isProjectGroupingEnabled(mode: SidebarProjectGroupingMode): boolean { + return mode !== "separate"; +} + +export function projectGroupingModeFromToggle( + enabled: boolean, + lastEnabledMode: SidebarProjectGroupingMode = "repository", +): SidebarProjectGroupingMode { + if (!enabled) return "separate"; + return lastEnabledMode === "repository_path" ? "repository_path" : "repository"; +} + +const LAST_ENABLED_PROJECT_GROUPING_MODE_KEY = "t3code:last-enabled-project-grouping-mode"; + +export function readLastEnabledProjectGroupingMode(): SidebarProjectGroupingMode { + try { + return localStorage.getItem(LAST_ENABLED_PROJECT_GROUPING_MODE_KEY) === "repository_path" + ? "repository_path" + : "repository"; + } catch { + return "repository"; + } +} + +export function rememberEnabledProjectGroupingMode(mode: SidebarProjectGroupingMode): void { + if (mode === "separate") return; + try { + localStorage.setItem(LAST_ENABLED_PROJECT_GROUPING_MODE_KEY, mode); + } catch { + // Storage can be unavailable in restricted browser contexts. + } +} + function collapseOtelSignalsUrl(input: { readonly tracesUrl: string; readonly metricsUrl: string; diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index ebf4aed6234..03ac84405fa 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -11,6 +11,7 @@ import { type ProviderInstanceConfig, type ProviderInstanceId, type ScopedThreadRef, + type SidebarProjectGroupingMode, } from "@t3tools/contracts"; import { scopeThreadRef } from "@t3tools/client-runtime/environment"; import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors"; @@ -83,6 +84,10 @@ import { DRIVER_OPTIONS, getDriverOption } from "./providerDriverMeta"; import { buildProviderInstanceUpdatePatch, formatDiagnosticsDescription, + isProjectGroupingEnabled, + projectGroupingModeFromToggle, + readLastEnabledProjectGroupingMode, + rememberEnabledProjectGroupingMode, } from "./SettingsPanels.logic"; import { SettingResetButton, @@ -401,6 +406,10 @@ export function useSettingsRestore(onRestored?: () => void) { ...(settings.sidebarThreadPreviewCount !== DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount ? ["Visible threads"] : []), + ...(settings.sidebarProjectGroupingMode !== + DEFAULT_UNIFIED_SETTINGS.sidebarProjectGroupingMode + ? ["Project Grouping"] + : []), ...(settings.wordWrap !== DEFAULT_UNIFIED_SETTINGS.wordWrap ? ["Word wrap"] : []), ...(settings.diffIgnoreWhitespace !== DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace ? ["Diff whitespace changes"] @@ -450,6 +459,7 @@ export function useSettingsRestore(onRestored?: () => void) { settings.automaticGitFetchInterval, settings.enableAssistantStreaming, settings.enableProviderUpdateChecks, + settings.sidebarProjectGroupingMode, settings.sidebarThreadPreviewCount, settings.timestampFormat, settings.wordWrap, @@ -474,6 +484,7 @@ export function useSettingsRestore(onRestored?: () => void) { diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace, glassOpacity: DEFAULT_UNIFIED_SETTINGS.glassOpacity, sidebarThreadPreviewCount: DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount, + sidebarProjectGroupingMode: DEFAULT_UNIFIED_SETTINGS.sidebarProjectGroupingMode, autoOpenPlanSidebar: DEFAULT_UNIFIED_SETTINGS.autoOpenPlanSidebar, enableAssistantStreaming: DEFAULT_UNIFIED_SETTINGS.enableAssistantStreaming, enableProviderUpdateChecks: DEFAULT_UNIFIED_SETTINGS.enableProviderUpdateChecks, @@ -498,6 +509,9 @@ export function GeneralSettingsPanel() { const { theme, setTheme } = useTheme(); const settings = usePrimarySettings(); const updateSettings = useUpdatePrimarySettings(); + const lastEnabledProjectGroupingMode = useRef( + readLastEnabledProjectGroupingMode(), + ); const observability = useAtomValue(primaryServerObservabilityAtom); const serverProviders = useAtomValue(primaryServerProvidersAtom); const glassOpacityRatio = @@ -619,6 +633,42 @@ export function GeneralSettingsPanel() { } /> + + updateSettings({ + sidebarProjectGroupingMode: DEFAULT_UNIFIED_SETTINGS.sidebarProjectGroupingMode, + }) + } + /> + ) : null + } + control={ + { + if (!checked && settings.sidebarProjectGroupingMode !== "separate") { + lastEnabledProjectGroupingMode.current = settings.sidebarProjectGroupingMode; + rememberEnabledProjectGroupingMode(settings.sidebarProjectGroupingMode); + } + updateSettings({ + sidebarProjectGroupingMode: projectGroupingModeFromToggle( + checked, + lastEnabledProjectGroupingMode.current, + ), + }); + }} + aria-label="Project Grouping" + /> + } + /> +