authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-09-24 16:38:53-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-24 16:38:53-07:00
log9a91177c3ecadaaa088d00e467491de4be9d7ce3
treed1bccce18a0b261f8d46a7e04bf38d9196195ac1
parent720490adc9b8f3676c614f3611d8c7fd9f4dd368
signature Signed by PGP key B5690EEEBB952194

fix(web): sidebar Back always returns to the main app (#13516)

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";
3535import ThreadSidebar from "./Sidebar";
3636import { SettingsSidebarNav } from "./settings/SettingsSidebarNav";
3737import { SidebarChromeHeader } from "./sidebar/SidebarChrome";
38import { MainAppLocationTracker } from "./sidebar/mainAppLocation";
3839import { useSidebarStageBackdropVariant } from "./SidebarStageBackdrop";
3940import { useProjects } from "../state/entities";
4041import {
......@@ -321,6 +322,7 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) {
321322 {children}
322323 <SidebarControl />
323324 <NavigationHistoryShortcuts />
325 <MainAppLocationTracker />
324326 </SidebarProvider>
325327 </PanelAnimationSuppressionProvider>
326328 );
apps/web/src/components/sidebar/SidebarChrome.tsx+8-20
......@@ -1,7 +1,7 @@
11import { ArrowLeftIcon, ChartNoAxesColumnIcon, SettingsIcon } from "lucide-react";
22import type { ReactNode } from "react";
33import { memo, useCallback } from "react";
4import { Link, useCanGoBack, useLocation, useNavigate } from "@tanstack/react-router";
4import { Link, useLocation, useNavigate } from "@tanstack/react-router";
55
66import { useEnvironmentIdentificationMode } from "../../hooks/useSettings";
77import { cn } from "../../lib/utils";
......@@ -24,6 +24,7 @@ import {
2424} from "../ui/sidebar";
2525import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
2626import { readPullRequestListPreferences } from "../pullRequest/pullRequestListPreferences";
27import { isSidebarUtilityPage, useNavigateToMainApp } from "./mainAppLocation";
2728import { SidebarThreadUndoNotice } from "./SidebarThreadUndoNotice";
2829import { SidebarProviderUpdatePill } from "./SidebarProviderUpdatePill";
2930import { SidebarUpdateArchitectureWarning, SidebarUpdatePill } from "./SidebarUpdatePill";
......@@ -127,19 +128,10 @@ function SidebarUtilityItem({
127128
128129export const SidebarUtilityMenu = memo(function SidebarUtilityMenu() {
129130 const navigate = useNavigate();
130 const canGoBack = useCanGoBack();
131 const navigateToMainApp = useNavigateToMainApp();
131132 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),
143135 });
144136 const { environments } = useEnvironments();
145137 // 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() {
173165
174166 const handleBackClick = useCallback(() => {
175167 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]);
182170
183171 return (
184172 <SidebarMenu className="flex-row items-center">
185 {currentFooterPage ? (
173 {isOnUtilityPage ? (
186174 <SidebarMenuItem className="min-w-0 flex-1">
187175 <SidebarMenuButton onClick={handleBackClick}>
188176 <ArrowLeftIcon />
apps/web/src/components/sidebar/mainAppLocation.ts created+36
......@@ -0,0 +1,36 @@
1import { useLocation, useNavigate } from "@tanstack/react-router";
2import { 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.
7export 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
17let 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.
21export 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.
33export 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 @@
1import {
2 Outlet,
3 createFileRoute,
4 redirect,
5 useCanGoBack,
6 useLocation,
7 useNavigate,
8} from "@tanstack/react-router";
9import { useCallback, useEffect, useState, type ReactNode } from "react";
1import { Outlet, createFileRoute, redirect, useLocation } from "@tanstack/react-router";
2import { useEffect, useState, type ReactNode } from "react";
103import { RotateCcwIcon } from "lucide-react";
114import { Button } from "../components/ui/button";
125import { useSettingsRestore } from "../components/settings/SettingsPanels";
136
147import { SettingsBreadcrumb } from "../components/settings/SettingsBreadcrumb";
158import { SidebarInset } from "../components/ui/sidebar";
9import { useNavigateToMainApp } from "../components/sidebar/mainAppLocation";
1610import { WorkspacePageHeader } from "../components/WorkspacePageHeader";
1711import { isElectron } from "../env";
1812import {
......@@ -117,17 +111,9 @@ function SettingsScopeBoundary({ pathname, children }: { pathname: string; child
117111
118112function SettingsContentLayout() {
119113 const location = useLocation();
120 const navigate = useNavigate();
121 const canGoBack = useCanGoBack();
114 const navigateToMainApp = useNavigateToMainApp();
122115 const { search } = useSettingsScope();
123116 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]);
131117
132118 useEffect(() => {
133119 const onKeyDown = (event: KeyboardEvent) => {
......@@ -140,7 +126,7 @@ function SettingsContentLayout() {
140126 activeElement.blur();
141127 }
142128
143 navigateBackWithinApp();
129 void navigateToMainApp();
144130 }
145131 };
146132
......@@ -148,7 +134,7 @@ function SettingsContentLayout() {
148134 return () => {
149135 window.removeEventListener("keydown", onKeyDown);
150136 };
151 }, [navigateBackWithinApp]);
137 }, [navigateToMainApp]);
152138
153139 return (
154140 <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none isolate">