| 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 | 35 | import ThreadSidebar from "./Sidebar"; |
| 36 | 36 | import { SettingsSidebarNav } from "./settings/SettingsSidebarNav"; |
| 37 | 37 | import { SidebarChromeHeader } from "./sidebar/SidebarChrome"; |
| 38 | import { MainAppLocationTracker } from "./sidebar/mainAppLocation"; | |
| 38 | 39 | import { useSidebarStageBackdropVariant } from "./SidebarStageBackdrop"; |
| 39 | 40 | import { useProjects } from "../state/entities"; |
| 40 | 41 | import { |
| ... | ... | @@ -321,6 +322,7 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) { |
| 321 | 322 | {children} |
| 322 | 323 | <SidebarControl /> |
| 323 | 324 | <NavigationHistoryShortcuts /> |
| 325 | <MainAppLocationTracker /> | |
| 324 | 326 | </SidebarProvider> |
| 325 | 327 | </PanelAnimationSuppressionProvider> |
| 326 | 328 | ); |
apps/web/src/components/sidebar/SidebarChrome.tsx+8-20| ... | ... | @@ -1,7 +1,7 @@ |
| 1 | 1 | import { ArrowLeftIcon, ChartNoAxesColumnIcon, SettingsIcon } from "lucide-react"; |
| 2 | 2 | import type { ReactNode } from "react"; |
| 3 | 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 | 6 | import { useEnvironmentIdentificationMode } from "../../hooks/useSettings"; |
| 7 | 7 | import { cn } from "../../lib/utils"; |
| ... | ... | @@ -24,6 +24,7 @@ import { |
| 24 | 24 | } from "../ui/sidebar"; |
| 25 | 25 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; |
| 26 | 26 | import { readPullRequestListPreferences } from "../pullRequest/pullRequestListPreferences"; |
| 27 | import { isSidebarUtilityPage, useNavigateToMainApp } from "./mainAppLocation"; | |
| 27 | 28 | import { SidebarThreadUndoNotice } from "./SidebarThreadUndoNotice"; |
| 28 | 29 | import { SidebarProviderUpdatePill } from "./SidebarProviderUpdatePill"; |
| 29 | 30 | import { SidebarUpdateArchitectureWarning, SidebarUpdatePill } from "./SidebarUpdatePill"; |
| ... | ... | @@ -127,19 +128,10 @@ function SidebarUtilityItem({ |
| 127 | 128 | |
| 128 | 129 | export const SidebarUtilityMenu = memo(function SidebarUtilityMenu() { |
| 129 | 130 | const navigate = useNavigate(); |
| 130 | const canGoBack = useCanGoBack(); | |
| 131 | const navigateToMainApp = useNavigateToMainApp(); | |
| 131 | 132 | const { isMobile, setOpenMobile } = useSidebar(); |
| 132 | const currentFooterPage = useLocation({ | |
| 133 | select: (location) => | |
| 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, | |
| 133 | const isOnUtilityPage = useLocation({ | |
| 134 | select: (location) => isSidebarUtilityPage(location.pathname), | |
| 143 | 135 | }); |
| 144 | 136 | const { environments } = useEnvironments(); |
| 145 | 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 | 165 | |
| 174 | 166 | const handleBackClick = useCallback(() => { |
| 175 | 167 | closeMobileSidebar(); |
| 176 | if (canGoBack) { | |
| 177 | window.history.back(); | |
| 178 | return; | |
| 179 | } | |
| 180 | void navigate({ to: "/" }); | |
| 181 | }, [canGoBack, closeMobileSidebar, navigate]); | |
| 168 | void navigateToMainApp(); | |
| 169 | }, [closeMobileSidebar, navigateToMainApp]); | |
| 182 | 170 | |
| 183 | 171 | return ( |
| 184 | 172 | <SidebarMenu className="flex-row items-center"> |
| 185 | {currentFooterPage ? ( | |
| 173 | {isOnUtilityPage ? ( | |
| 186 | 174 | <SidebarMenuItem className="min-w-0 flex-1"> |
| 187 | 175 | <SidebarMenuButton onClick={handleBackClick}> |
| 188 | 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 | import { | |
| 2 | Outlet, | |
| 3 | createFileRoute, | |
| 4 | redirect, | |
| 5 | useCanGoBack, | |
| 6 | useLocation, | |
| 7 | useNavigate, | |
| 8 | } from "@tanstack/react-router"; | |
| 9 | import { useCallback, useEffect, useState, type ReactNode } from "react"; | |
| 1 | import { Outlet, createFileRoute, redirect, useLocation } from "@tanstack/react-router"; | |
| 2 | import { useEffect, useState, type ReactNode } from "react"; | |
| 10 | 3 | import { RotateCcwIcon } from "lucide-react"; |
| 11 | 4 | import { Button } from "../components/ui/button"; |
| 12 | 5 | import { useSettingsRestore } from "../components/settings/SettingsPanels"; |
| 13 | 6 | |
| 14 | 7 | import { SettingsBreadcrumb } from "../components/settings/SettingsBreadcrumb"; |
| 15 | 8 | import { SidebarInset } from "../components/ui/sidebar"; |
| 9 | import { useNavigateToMainApp } from "../components/sidebar/mainAppLocation"; | |
| 16 | 10 | import { WorkspacePageHeader } from "../components/WorkspacePageHeader"; |
| 17 | 11 | import { isElectron } from "../env"; |
| 18 | 12 | import { |
| ... | ... | @@ -117,17 +111,9 @@ function SettingsScopeBoundary({ pathname, children }: { pathname: string; child |
| 117 | 111 | |
| 118 | 112 | function SettingsContentLayout() { |
| 119 | 113 | const location = useLocation(); |
| 120 | const navigate = useNavigate(); | |
| 121 | const canGoBack = useCanGoBack(); | |
| 114 | const navigateToMainApp = useNavigateToMainApp(); | |
| 122 | 115 | const { search } = useSettingsScope(); |
| 123 | 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 | 118 | useEffect(() => { |
| 133 | 119 | const onKeyDown = (event: KeyboardEvent) => { |
| ... | ... | @@ -140,7 +126,7 @@ function SettingsContentLayout() { |
| 140 | 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 | 134 | return () => { |
| 149 | 135 | window.removeEventListener("keydown", onKeyDown); |
| 150 | 136 | }; |
| 151 | }, [navigateBackWithinApp]); | |
| 137 | }, [navigateToMainApp]); | |
| 152 | 138 | |
| 153 | 139 | return ( |
| 154 | 140 | <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none isolate"> |