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