| author | |
| committer | |
| log | 9a91177c3ecadaaa088d00e467491de4be9d7ce3 |
| tree | d1bccce18a0b261f8d46a7e04bf38d9196195ac1 |
| parent | 720490adc9b8f3676c614f3611d8c7fd9f4dd368 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>4 files changed, 52 insertions(+), 40 deletions(-)
apps/web/src/components/AppSidebarLayout.tsx+2| ... | @@ -35,6 +35,7 @@ import LegacyThreadSidebar from "./LegacySidebar"; | ... | @@ -35,6 +35,7 @@ import LegacyThreadSidebar from "./LegacySidebar"; |
| 35 | import ThreadSidebar from "./Sidebar"; | 35 | import ThreadSidebar from "./Sidebar"; |
| 36 | import { SettingsSidebarNav } from "./settings/SettingsSidebarNav"; | 36 | import { SettingsSidebarNav } from "./settings/SettingsSidebarNav"; |
| 37 | import { SidebarChromeHeader } from "./sidebar/SidebarChrome"; | 37 | import { SidebarChromeHeader } from "./sidebar/SidebarChrome"; |
| 38 | import { MainAppLocationTracker } from "./sidebar/mainAppLocation"; | ||
| 38 | import { useSidebarStageBackdropVariant } from "./SidebarStageBackdrop"; | 39 | import { useSidebarStageBackdropVariant } from "./SidebarStageBackdrop"; |
| 39 | import { useProjects } from "../state/entities"; | 40 | import { useProjects } from "../state/entities"; |
| 40 | import { | 41 | import { |
| ... | @@ -321,6 +322,7 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) { | ... | @@ -321,6 +322,7 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) { |
| 321 | {children} | 322 | {children} |
| 322 | <SidebarControl /> | 323 | <SidebarControl /> |
| 323 | <NavigationHistoryShortcuts /> | 324 | <NavigationHistoryShortcuts /> |
| 325 | <MainAppLocationTracker /> | ||
| 324 | </SidebarProvider> | 326 | </SidebarProvider> |
| 325 | </PanelAnimationSuppressionProvider> | 327 | </PanelAnimationSuppressionProvider> |
| 326 | ); | 328 | ); |
apps/web/src/components/sidebar/SidebarChrome.tsx+8-20| ... | @@ -1,7 +1,7 @@ | ... | @@ -1,7 +1,7 @@ |
| 1 | import { ArrowLeftIcon, ChartNoAxesColumnIcon, SettingsIcon } from "lucide-react"; | 1 | import { ArrowLeftIcon, ChartNoAxesColumnIcon, SettingsIcon } from "lucide-react"; |
| 2 | import type { ReactNode } from "react"; | 2 | import type { ReactNode } from "react"; |
| 3 | import { memo, useCallback } from "react"; | 3 | import { memo, useCallback } from "react"; |
| 4 | import { Link, useCanGoBack, useLocation, useNavigate } from "@tanstack/react-router"; | 4 | import { Link, useLocation, useNavigate } from "@tanstack/react-router"; |
| 5 | 5 | ||
| 6 | import { useEnvironmentIdentificationMode } from "../../hooks/useSettings"; | 6 | import { useEnvironmentIdentificationMode } from "../../hooks/useSettings"; |
| 7 | import { cn } from "../../lib/utils"; | 7 | import { cn } from "../../lib/utils"; |
| ... | @@ -24,6 +24,7 @@ import { | ... | @@ -24,6 +24,7 @@ import { |
| 24 | } from "../ui/sidebar"; | 24 | } from "../ui/sidebar"; |
| 25 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; | 25 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; |
| 26 | import { readPullRequestListPreferences } from "../pullRequest/pullRequestListPreferences"; | 26 | import { readPullRequestListPreferences } from "../pullRequest/pullRequestListPreferences"; |
| 27 | import { isSidebarUtilityPage, useNavigateToMainApp } from "./mainAppLocation"; | ||
| 27 | import { SidebarThreadUndoNotice } from "./SidebarThreadUndoNotice"; | 28 | import { SidebarThreadUndoNotice } from "./SidebarThreadUndoNotice"; |
| 28 | import { SidebarProviderUpdatePill } from "./SidebarProviderUpdatePill"; | 29 | import { SidebarProviderUpdatePill } from "./SidebarProviderUpdatePill"; |
| 29 | import { SidebarUpdateArchitectureWarning, SidebarUpdatePill } from "./SidebarUpdatePill"; | 30 | import { SidebarUpdateArchitectureWarning, SidebarUpdatePill } from "./SidebarUpdatePill"; |
| ... | @@ -127,19 +128,10 @@ function SidebarUtilityItem({ | ... | @@ -127,19 +128,10 @@ function SidebarUtilityItem({ |
| 127 | 128 | ||
| 128 | export const SidebarUtilityMenu = memo(function SidebarUtilityMenu() { | 129 | export const SidebarUtilityMenu = memo(function SidebarUtilityMenu() { |
| 129 | const navigate = useNavigate(); | 130 | const navigate = useNavigate(); |
| 130 | const canGoBack = useCanGoBack(); | 131 | const navigateToMainApp = useNavigateToMainApp(); |
| 131 | const { isMobile, setOpenMobile } = useSidebar(); | 132 | const { isMobile, setOpenMobile } = useSidebar(); |
| 132 | const currentFooterPage = useLocation({ | 133 | const isOnUtilityPage = useLocation({ |
| 133 | select: (location) => | 134 | select: (location) => isSidebarUtilityPage(location.pathname), |
| 134 | /^\/settings(?:\/|$)/.test(location.pathname) | ||
| 135 | ? "settings" | ||
| 136 | : /^\/projects\/[^/]+\/?$/.test(location.pathname) | ||
| 137 | ? "project-settings" | ||
| 138 | : location.pathname === "/usage" | ||
| 139 | ? "usage" | ||
| 140 | : location.pathname === "/pull-requests" | ||
| 141 | ? "pull-requests" | ||
| 142 | : null, | ||
| 143 | }); | 135 | }); |
| 144 | const { environments } = useEnvironments(); | 136 | const { environments } = useEnvironments(); |
| 145 | // The page reads every connected server, so one of them offering pull requests is enough for | 137 | // The page reads every connected server, so one of them offering pull requests is enough for |
| ... | @@ -173,16 +165,12 @@ export const SidebarUtilityMenu = memo(function SidebarUtilityMenu() { | ... | @@ -173,16 +165,12 @@ export const SidebarUtilityMenu = memo(function SidebarUtilityMenu() { |
| 173 | 165 | ||
| 174 | const handleBackClick = useCallback(() => { | 166 | const handleBackClick = useCallback(() => { |
| 175 | closeMobileSidebar(); | 167 | closeMobileSidebar(); |
| 176 | if (canGoBack) { | 168 | void navigateToMainApp(); |
| 177 | window.history.back(); | 169 | }, [closeMobileSidebar, navigateToMainApp]); |
| 178 | return; | ||
| 179 | } | ||
| 180 | void navigate({ to: "/" }); | ||
| 181 | }, [canGoBack, closeMobileSidebar, navigate]); | ||
| 182 | 170 | ||
| 183 | return ( | 171 | return ( |
| 184 | <SidebarMenu className="flex-row items-center"> | 172 | <SidebarMenu className="flex-row items-center"> |
| 185 | {currentFooterPage ? ( | 173 | {isOnUtilityPage ? ( |
| 186 | <SidebarMenuItem className="min-w-0 flex-1"> | 174 | <SidebarMenuItem className="min-w-0 flex-1"> |
| 187 | <SidebarMenuButton onClick={handleBackClick}> | 175 | <SidebarMenuButton onClick={handleBackClick}> |
| 188 | <ArrowLeftIcon /> | 176 | <ArrowLeftIcon /> |
apps/web/src/components/sidebar/mainAppLocation.ts created+36| ... | @@ -0,0 +1,36 @@ | ||
| 1 | import { useLocation, useNavigate } from "@tanstack/react-router"; | ||
| 2 | import { useCallback, useEffect } from "react"; | ||
| 3 | |||
| 4 | // Settings, Usage, and Pull Requests replace the sidebar utility row with a | ||
| 5 | // Back button. Everything else is the main app. Legacy `/projects/<key>` links | ||
| 6 | // redirect into settings, so they count too and are never remembered. | ||
| 7 | export function isSidebarUtilityPage(pathname: string) { | ||
| 8 | return ( | ||
| 9 | pathname === "/settings" || | ||
| 10 | pathname.startsWith("/settings/") || | ||
| 11 | pathname.startsWith("/projects/") || | ||
| 12 | pathname === "/usage" || | ||
| 13 | pathname === "/pull-requests" | ||
| 14 | ); | ||
| 15 | } | ||
| 16 | |||
| 17 | let mainAppHref: string | null = null; | ||
| 18 | |||
| 19 | // Mount once in the app shell. Records the latest main app URL so Back can | ||
| 20 | // return there no matter how many utility pages were visited since. | ||
| 21 | export function MainAppLocationTracker() { | ||
| 22 | const href = useLocation({ | ||
| 23 | select: (location) => (isSidebarUtilityPage(location.pathname) ? null : location.href), | ||
| 24 | }); | ||
| 25 | useEffect(() => { | ||
| 26 | if (href !== null) mainAppHref = href; | ||
| 27 | }, [href]); | ||
| 28 | return null; | ||
| 29 | } | ||
| 30 | |||
| 31 | // Leaves a utility page for the last main app URL, or the thread list when | ||
| 32 | // the app was opened directly on a utility page. | ||
| 33 | export function useNavigateToMainApp() { | ||
| 34 | const navigate = useNavigate(); | ||
| 35 | return useCallback(() => navigate({ href: mainAppHref ?? "/" }), [navigate]); | ||
| 36 | } | ||
apps/web/src/routes/settings.tsx+6-20| ... | @@ -1,18 +1,12 @@ | ... | @@ -1,18 +1,12 @@ |
| 1 | import { | 1 | import { Outlet, createFileRoute, redirect, useLocation } from "@tanstack/react-router"; |
| 2 | Outlet, | 2 | import { useEffect, useState, type ReactNode } from "react"; |
| 3 | createFileRoute, | ||
| 4 | redirect, | ||
| 5 | useCanGoBack, | ||
| 6 | useLocation, | ||
| 7 | useNavigate, | ||
| 8 | } from "@tanstack/react-router"; | ||
| 9 | import { useCallback, useEffect, useState, type ReactNode } from "react"; | ||
| 10 | import { RotateCcwIcon } from "lucide-react"; | 3 | import { RotateCcwIcon } from "lucide-react"; |
| 11 | import { Button } from "../components/ui/button"; | 4 | import { Button } from "../components/ui/button"; |
| 12 | import { useSettingsRestore } from "../components/settings/SettingsPanels"; | 5 | import { useSettingsRestore } from "../components/settings/SettingsPanels"; |
| 13 | 6 | ||
| 14 | import { SettingsBreadcrumb } from "../components/settings/SettingsBreadcrumb"; | 7 | import { SettingsBreadcrumb } from "../components/settings/SettingsBreadcrumb"; |
| 15 | import { SidebarInset } from "../components/ui/sidebar"; | 8 | import { SidebarInset } from "../components/ui/sidebar"; |
| 9 | import { useNavigateToMainApp } from "../components/sidebar/mainAppLocation"; | ||
| 16 | import { WorkspacePageHeader } from "../components/WorkspacePageHeader"; | 10 | import { WorkspacePageHeader } from "../components/WorkspacePageHeader"; |
| 17 | import { isElectron } from "../env"; | 11 | import { isElectron } from "../env"; |
| 18 | import { | 12 | import { |
| ... | @@ -117,17 +111,9 @@ function SettingsScopeBoundary({ pathname, children }: { pathname: string; child | ... | @@ -117,17 +111,9 @@ function SettingsScopeBoundary({ pathname, children }: { pathname: string; child |
| 117 | 111 | ||
| 118 | function SettingsContentLayout() { | 112 | function SettingsContentLayout() { |
| 119 | const location = useLocation(); | 113 | const location = useLocation(); |
| 120 | const navigate = useNavigate(); | 114 | const navigateToMainApp = useNavigateToMainApp(); |
| 121 | const canGoBack = useCanGoBack(); | ||
| 122 | const { search } = useSettingsScope(); | 115 | const { search } = useSettingsScope(); |
| 123 | const [restoreSignal, setRestoreSignal] = useState(0); | 116 | const [restoreSignal, setRestoreSignal] = useState(0); |
| 124 | const navigateBackWithinApp = useCallback(() => { | ||
| 125 | if (canGoBack) { | ||
| 126 | window.history.back(); | ||
| 127 | return; | ||
| 128 | } | ||
| 129 | void navigate({ to: "/" }); | ||
| 130 | }, [canGoBack, navigate]); | ||
| 131 | 117 | ||
| 132 | useEffect(() => { | 118 | useEffect(() => { |
| 133 | const onKeyDown = (event: KeyboardEvent) => { | 119 | const onKeyDown = (event: KeyboardEvent) => { |
| ... | @@ -140,7 +126,7 @@ function SettingsContentLayout() { | ... | @@ -140,7 +126,7 @@ function SettingsContentLayout() { |
| 140 | activeElement.blur(); | 126 | activeElement.blur(); |
| 141 | } | 127 | } |
| 142 | 128 | ||
| 143 | navigateBackWithinApp(); | 129 | void navigateToMainApp(); |
| 144 | } | 130 | } |
| 145 | }; | 131 | }; |
| 146 | 132 | ||
| ... | @@ -148,7 +134,7 @@ function SettingsContentLayout() { | ... | @@ -148,7 +134,7 @@ function SettingsContentLayout() { |
| 148 | return () => { | 134 | return () => { |
| 149 | window.removeEventListener("keydown", onKeyDown); | 135 | window.removeEventListener("keydown", onKeyDown); |
| 150 | }; | 136 | }; |
| 151 | }, [navigateBackWithinApp]); | 137 | }, [navigateToMainApp]); |
| 152 | 138 | ||
| 153 | return ( | 139 | return ( |
| 154 | <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none isolate"> | 140 | <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none isolate"> |