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";...@@ -35,6 +35,7 @@ import LegacyThreadSidebar from "./LegacySidebar";
35import ThreadSidebar from "./Sidebar";35import ThreadSidebar from "./Sidebar";
36import { SettingsSidebarNav } from "./settings/SettingsSidebarNav";36import { SettingsSidebarNav } from "./settings/SettingsSidebarNav";
37import { SidebarChromeHeader } from "./sidebar/SidebarChrome";37import { SidebarChromeHeader } from "./sidebar/SidebarChrome";
38import { MainAppLocationTracker } from "./sidebar/mainAppLocation";
38import { useSidebarStageBackdropVariant } from "./SidebarStageBackdrop";39import { useSidebarStageBackdropVariant } from "./SidebarStageBackdrop";
39import { useProjects } from "../state/entities";40import { useProjects } from "../state/entities";
40import {41import {
...@@ -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 @@
1import { ArrowLeftIcon, ChartNoAxesColumnIcon, SettingsIcon } from "lucide-react";1import { ArrowLeftIcon, ChartNoAxesColumnIcon, SettingsIcon } from "lucide-react";
2import type { ReactNode } from "react";2import type { ReactNode } from "react";
3import { memo, useCallback } from "react";3import { memo, useCallback } from "react";
4import { Link, useCanGoBack, useLocation, useNavigate } from "@tanstack/react-router";4import { Link, useLocation, useNavigate } from "@tanstack/react-router";
55
6import { useEnvironmentIdentificationMode } from "../../hooks/useSettings";6import { useEnvironmentIdentificationMode } from "../../hooks/useSettings";
7import { cn } from "../../lib/utils";7import { cn } from "../../lib/utils";
...@@ -24,6 +24,7 @@ import {...@@ -24,6 +24,7 @@ import {
24} from "../ui/sidebar";24} from "../ui/sidebar";
25import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";25import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
26import { readPullRequestListPreferences } from "../pullRequest/pullRequestListPreferences";26import { readPullRequestListPreferences } from "../pullRequest/pullRequestListPreferences";
27import { isSidebarUtilityPage, useNavigateToMainApp } from "./mainAppLocation";
27import { SidebarThreadUndoNotice } from "./SidebarThreadUndoNotice";28import { SidebarThreadUndoNotice } from "./SidebarThreadUndoNotice";
28import { SidebarProviderUpdatePill } from "./SidebarProviderUpdatePill";29import { SidebarProviderUpdatePill } from "./SidebarProviderUpdatePill";
29import { SidebarUpdateArchitectureWarning, SidebarUpdatePill } from "./SidebarUpdatePill";30import { SidebarUpdateArchitectureWarning, SidebarUpdatePill } from "./SidebarUpdatePill";
...@@ -127,19 +128,10 @@ function SidebarUtilityItem({...@@ -127,19 +128,10 @@ function SidebarUtilityItem({
127128
128export const SidebarUtilityMenu = memo(function SidebarUtilityMenu() {129export 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 for137 // 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() {
173165
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]);
182170
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 @@
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 @@...@@ -1,18 +1,12 @@
1import {1import { Outlet, createFileRoute, redirect, useLocation } from "@tanstack/react-router";
2 Outlet,2import { useEffect, useState, type ReactNode } from "react";
3 createFileRoute,
4 redirect,
5 useCanGoBack,
6 useLocation,
7 useNavigate,
8} from "@tanstack/react-router";
9import { useCallback, useEffect, useState, type ReactNode } from "react";
10import { RotateCcwIcon } from "lucide-react";3import { RotateCcwIcon } from "lucide-react";
11import { Button } from "../components/ui/button";4import { Button } from "../components/ui/button";
12import { useSettingsRestore } from "../components/settings/SettingsPanels";5import { useSettingsRestore } from "../components/settings/SettingsPanels";
136
14import { SettingsBreadcrumb } from "../components/settings/SettingsBreadcrumb";7import { SettingsBreadcrumb } from "../components/settings/SettingsBreadcrumb";
15import { SidebarInset } from "../components/ui/sidebar";8import { SidebarInset } from "../components/ui/sidebar";
9import { useNavigateToMainApp } from "../components/sidebar/mainAppLocation";
16import { WorkspacePageHeader } from "../components/WorkspacePageHeader";10import { WorkspacePageHeader } from "../components/WorkspacePageHeader";
17import { isElectron } from "../env";11import { isElectron } from "../env";
18import {12import {
...@@ -117,17 +111,9 @@ function SettingsScopeBoundary({ pathname, children }: { pathname: string; child...@@ -117,17 +111,9 @@ function SettingsScopeBoundary({ pathname, children }: { pathname: string; child
117111
118function SettingsContentLayout() {112function 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]);
131117
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 }
142128
143 navigateBackWithinApp();129 void navigateToMainApp();
144 }130 }
145 };131 };
146132
...@@ -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]);
152138
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">