authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-08-19 15:50:25-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-19 15:50:25-04:00
log07f8027d9a542f72b12db927caceebb2c9c72aa7
tree22131031c7f6700663e4e90ab289250527501dc3
parentcf251c3bd00ff0e0cc90a78b20c043cf58f330ad
signature Signed by PGP key B5690EEEBB952194

feat(web): unify workspace navigation (#7153)


12 files changed, 167 insertions(+), 194 deletions(-)

apps/web/src/components/ChatView.tsx+6-15
...@@ -170,7 +170,6 @@ import {...@@ -170,7 +170,6 @@ import {
170 WifiOffIcon,170 WifiOffIcon,
171} from "lucide-react";171} from "lucide-react";
172import { cn, randomHex } from "~/lib/utils";172import { cn, randomHex } from "~/lib/utils";
173import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "~/workspaceTitlebar";
174import { stackedThreadToast, toastManager } from "./ui/toast";173import { stackedThreadToast, toastManager } from "./ui/toast";
175import { decodeProjectScriptKeybindingRule } from "~/lib/projectScriptKeybindings";174import { decodeProjectScriptKeybindingRule } from "~/lib/projectScriptKeybindings";
176import { type NewProjectScriptInput } from "./ProjectScriptsControl";175import { type NewProjectScriptInput } from "./ProjectScriptsControl";
...@@ -259,6 +258,7 @@ import { ChatHeader } from "./chat/ChatHeader";...@@ -259,6 +258,7 @@ import { ChatHeader } from "./chat/ChatHeader";
259import { PanelLayoutControls, RightPanelMaximizeControl } from "./chat/PanelLayoutControls";258import { PanelLayoutControls, RightPanelMaximizeControl } from "./chat/PanelLayoutControls";
260import { type ExpandedImagePreview } from "./chat/ExpandedImagePreview";259import { type ExpandedImagePreview } from "./chat/ExpandedImagePreview";
261import { NoActiveThreadState } from "./NoActiveThreadState";260import { NoActiveThreadState } from "./NoActiveThreadState";
261import { WorkspacePageHeader } from "./WorkspacePageHeader";
262import {262import {
263 resolveEffectiveEnvMode,263 resolveEffectiveEnvMode,
264 resolveLocalCheckoutBranchMismatch,264 resolveLocalCheckoutBranchMismatch,
...@@ -6240,20 +6240,11 @@ function ChatViewContent(props: ChatViewProps) {...@@ -6240,20 +6240,11 @@ function ChatViewContent(props: ChatViewProps) {
6240 data-chat-column-maximized-away={rightPanelMaximized ? "true" : "false"}6240 data-chat-column-maximized-away={rightPanelMaximized ? "true" : "false"}
6241 >6241 >
6242 {/* Top bar */}6242 {/* Top bar */}
6243 <header6243 <WorkspacePageHeader
6244 data-chat-header6244 data-chat-header
6245 className={cn(6245 electron={isElectron}
6246 "bg-background transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none",6246 reserveNativeControls={reserveTitleBarControlInset && !inlineRightPanelOwnsTitleBar}
6247 isElectron6247 className="relative bg-background"
6248 ? cn(
6249 "drag-region relative flex h-[var(--workspace-topbar-height)] min-h-[var(--workspace-topbar-height)] shrink-0 items-center px-3 sm:px-5",
6250 reserveTitleBarControlInset &&
6251 !inlineRightPanelOwnsTitleBar &&
6252 "wco:pr-[var(--workspace-native-controls-inset)]",
6253 )
6254 : "flex h-[var(--workspace-topbar-height)] min-h-[var(--workspace-topbar-height)] shrink-0 items-center pl-[calc(env(safe-area-inset-left)+0.75rem)] pr-[calc(env(safe-area-inset-right)+0.75rem)] sm:pl-[calc(env(safe-area-inset-left)+1.25rem)] sm:pr-[calc(env(safe-area-inset-right)+1.25rem)]",
6255 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,
6256 )}
6257 >6248 >
6258 {!rightPanelOpen ? panelLayoutControls : null}6249 {!rightPanelOpen ? panelLayoutControls : null}
6259 <ChatHeader6250 <ChatHeader
...@@ -6284,7 +6275,7 @@ function ChatViewContent(props: ChatViewProps) {...@@ -6284,7 +6275,7 @@ function ChatViewContent(props: ChatViewProps) {
6284 onUpdateProjectScript={updateProjectScript}6275 onUpdateProjectScript={updateProjectScript}
6285 onDeleteProjectScript={deleteProjectScript}6276 onDeleteProjectScript={deleteProjectScript}
6286 />6277 />
6287 </header>6278 </WorkspacePageHeader>
62886279
6289 <ThreadErrorBanner6280 <ThreadErrorBanner
6290 error={visibleThreadError}6281 error={visibleThreadError}
apps/web/src/components/NoActiveThreadState.tsx+4-15
...@@ -1,26 +1,15 @@...@@ -1,26 +1,15 @@
1import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from "./ui/empty";1import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from "./ui/empty";
2import { SidebarInset } from "./ui/sidebar";2import { SidebarInset } from "./ui/sidebar";
3import { isElectron } from "../env";3import { isElectron } from "../env";
4import { cn } from "~/lib/utils";4import { WorkspacePageHeader } from "./WorkspacePageHeader";
5import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "~/workspaceTitlebar";
65
7export function NoActiveThreadState() {6export function NoActiveThreadState() {
8 return (7 return (
9 <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground">8 <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground">
10 <div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden bg-background">9 <div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden bg-background">
11 <header10 <WorkspacePageHeader electron={isElectron} className="border-b border-border">
12 className={cn(
13 "border-b border-border px-3 transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none sm:px-5",
14 isElectron
15 ? "drag-region flex h-[var(--workspace-topbar-height)] min-h-[var(--workspace-topbar-height)] shrink-0 items-center"
16 : "flex h-[var(--workspace-topbar-height)] min-h-[var(--workspace-topbar-height)] shrink-0 items-center",
17 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,
18 )}
19 >
20 {isElectron ? (11 {isElectron ? (
21 <span className="text-xs text-muted-foreground/50 wco:pr-[var(--workspace-native-controls-inset)]">12 <span className="text-xs text-muted-foreground/50">No active thread</span>
22 No active thread
23 </span>
24 ) : (13 ) : (
25 <div className="flex items-center gap-2">14 <div className="flex items-center gap-2">
26 <span className="text-sm font-medium text-foreground md:text-muted-foreground/60">15 <span className="text-sm font-medium text-foreground md:text-muted-foreground/60">
...@@ -28,7 +17,7 @@ export function NoActiveThreadState() {...@@ -28,7 +17,7 @@ export function NoActiveThreadState() {
28 </span>17 </span>
29 </div>18 </div>
30 )}19 )}
31 </header>20 </WorkspacePageHeader>
3221
33 <Empty className="flex-1">22 <Empty className="flex-1">
34 <div className="w-full max-w-lg px-8 py-12">23 <div className="w-full max-w-lg px-8 py-12">
apps/web/src/components/WorkspacePageHeader.tsx created+28
...@@ -0,0 +1,28 @@
1import type { ComponentPropsWithoutRef } from "react";
2
3import { cn } from "../lib/utils";
4import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "../workspaceTitlebar";
5
6/** Shared workspace top-bar geometry. */
7export function WorkspacePageHeader({
8 electron = false,
9 reserveNativeControls = electron,
10 className,
11 ...props
12}: ComponentPropsWithoutRef<"header"> & {
13 readonly electron?: boolean;
14 readonly reserveNativeControls?: boolean;
15}) {
16 return (
17 <header
18 className={cn(
19 "flex h-[var(--workspace-topbar-height)] min-h-[var(--workspace-topbar-height)] shrink-0 items-center gap-3 pl-[calc(env(safe-area-inset-left)+0.75rem)] pr-[calc(env(safe-area-inset-right)+0.75rem)] transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none sm:pl-[calc(env(safe-area-inset-left)+1.25rem)] sm:pr-[calc(env(safe-area-inset-right)+1.25rem)]",
20 electron && "drag-region",
21 reserveNativeControls && "wco:pr-[var(--workspace-native-controls-inset)]",
22 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,
23 className,
24 )}
25 {...props}
26 />
27 );
28}
apps/web/src/components/settings/DiagnosticsSettings.tsx+1-1
...@@ -992,7 +992,7 @@ export function DiagnosticsSettingsPanel() {...@@ -992,7 +992,7 @@ export function DiagnosticsSettingsPanel() {
992 : false;992 : false;
993993
994 return (994 return (
995 <SettingsPageContainer className="max-w-6xl gap-10">995 <SettingsPageContainer width="expanded" className="gap-10">
996 <ResourceTelemetryDiagnostics />996 <ResourceTelemetryDiagnostics />
997997
998 <SettingsSection998 <SettingsSection
apps/web/src/components/settings/KeybindingsSettings.tsx+1-1
...@@ -1195,7 +1195,7 @@ export function KeybindingsSettingsPanel() {...@@ -1195,7 +1195,7 @@ export function KeybindingsSettingsPanel() {
1195 );1195 );
11961196
1197 return (1197 return (
1198 <SettingsPageContainer className="max-w-5xl">1198 <SettingsPageContainer width="wide">
1199 <SettingsSection1199 <SettingsSection
1200 {...searchableSetting("keybindings")}1200 {...searchableSetting("keybindings")}
1201 headerAction={1201 headerAction={
apps/web/src/components/settings/ProjectSettingsPanel.tsx+4-22
...@@ -81,8 +81,6 @@ import {...@@ -81,8 +81,6 @@ import {
81 type NewProjectScriptInput,81 type NewProjectScriptInput,
82 type ProjectScriptEditorRequest,82 type ProjectScriptEditorRequest,
83} from "../projectScriptEditor";83} from "../projectScriptEditor";
84import { cn } from "../../lib/utils";
85import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "../../workspaceTitlebar";
86import { Button } from "../ui/button";84import { Button } from "../ui/button";
87import { Input } from "../ui/input";85import { Input } from "../ui/input";
88import {86import {
...@@ -103,6 +101,7 @@ import {...@@ -103,6 +101,7 @@ import {
103 WorkspaceBreadcrumbItem,101 WorkspaceBreadcrumbItem,
104 WorkspaceBreadcrumbSeparator,102 WorkspaceBreadcrumbSeparator,
105} from "../WorkspaceBreadcrumb";103} from "../WorkspaceBreadcrumb";
104import { WorkspacePageHeader } from "../WorkspacePageHeader";
106import {105import {
107 SettingResetButton,106 SettingResetButton,
108 SettingsPageContainer,107 SettingsPageContainer,
...@@ -175,26 +174,9 @@ export function ProjectSettingsPage({ projectKey }: { projectKey: string }) {...@@ -175,26 +174,9 @@ export function ProjectSettingsPage({ projectKey }: { projectKey: string }) {
175 return (174 return (
176 <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground isolate">175 <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground isolate">
177 <div className="flex min-h-0 min-w-0 flex-1 flex-col bg-background text-foreground">176 <div className="flex min-h-0 min-w-0 flex-1 flex-col bg-background text-foreground">
178 {!isElectron && (177 <WorkspacePageHeader electron={isElectron}>
179 <header178 <ProjectSettingsBreadcrumb projectKey={projectKey} />
180 className={cn(179 </WorkspacePageHeader>
181 "flex h-[var(--workspace-topbar-height)] min-h-[var(--workspace-topbar-height)] shrink-0 items-center px-3 transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none sm:px-5",
182 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,
183 )}
184 >
185 <ProjectSettingsBreadcrumb projectKey={projectKey} />
186 </header>
187 )}
188 {isElectron && (
189 <div
190 className={cn(
191 "drag-region flex h-[52px] shrink-0 items-center px-5 transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none wco:h-[env(titlebar-area-height)] wco:pr-[calc(100vw-env(titlebar-area-width)-env(titlebar-area-x)+1em)]",
192 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,
193 )}
194 >
195 <ProjectSettingsBreadcrumb projectKey={projectKey} />
196 </div>
197 )}
198 <ProjectSettingsPanel projectKey={projectKey} />180 <ProjectSettingsPanel projectKey={projectKey} />
199 </div>181 </div>
200 </SidebarInset>182 </SidebarInset>
apps/web/src/components/settings/SettingsSidebarNav.tsx+5-22
...@@ -9,7 +9,6 @@ import {...@@ -9,7 +9,6 @@ import {
9} from "react";9} from "react";
10import {10import {
11 ArchiveIcon,11 ArchiveIcon,
12 ArrowLeftIcon,
13 BlocksIcon,12 BlocksIcon,
14 BotIcon,13 BotIcon,
15 GitBranchIcon,14 GitBranchIcon,
...@@ -20,7 +19,7 @@ import {...@@ -20,7 +19,7 @@ import {
20 Settings2Icon,19 Settings2Icon,
21 XIcon,20 XIcon,
22} from "lucide-react";21} from "lucide-react";
23import { useCanGoBack, useLocation, useNavigate } from "@tanstack/react-router";22import { useLocation, useNavigate } from "@tanstack/react-router";
2423
25import { Button } from "../ui/button";24import { Button } from "../ui/button";
26import { Input } from "../ui/input";25import { Input } from "../ui/input";
...@@ -35,6 +34,7 @@ import {...@@ -35,6 +34,7 @@ import {
35 useSidebar,34 useSidebar,
36} from "../ui/sidebar";35} from "../ui/sidebar";
37import { T3ConnectSidebarAvatar, T3ConnectSidebarSignIn } from "../clerk/T3ConnectSidebarSignIn";36import { T3ConnectSidebarAvatar, T3ConnectSidebarSignIn } from "../clerk/T3ConnectSidebarSignIn";
37import { SidebarUtilityMenu } from "../sidebar/SidebarChrome";
38import { scrollToSettingsTarget } from "./settingsLayout";38import { scrollToSettingsTarget } from "./settingsLayout";
39import {39import {
40 searchSettings,40 searchSettings,
...@@ -74,7 +74,6 @@ function SettingsSectionIcon({ to }: { to: SettingsPath }) {...@@ -74,7 +74,6 @@ function SettingsSectionIcon({ to }: { to: SettingsPath }) {
74export function SettingsSidebarNav({ pathname }: { pathname: string }) {74export function SettingsSidebarNav({ pathname }: { pathname: string }) {
75 const navigate = useNavigate();75 const navigate = useNavigate();
76 const currentHash = useLocation({ select: (location) => location.hash });76 const currentHash = useLocation({ select: (location) => location.hash });
77 const canGoBack = useCanGoBack();
78 const { isMobile, setOpenMobile, open, setOpen } = useSidebar();77 const { isMobile, setOpenMobile, open, setOpen } = useSidebar();
79 const searchInputRef = useRef<HTMLInputElement>(null);78 const searchInputRef = useRef<HTMLInputElement>(null);
80 const [query, setQuery] = useState("");79 const [query, setQuery] = useState("");
...@@ -178,17 +177,6 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {...@@ -178,17 +177,6 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
178 },177 },
179 [activeResultIndex, clearSearch, handleSearchResultClick, isSearching, results],178 [activeResultIndex, clearSearch, handleSearchResultClick, isSearching, results],
180 );179 );
181 const handleBackClick = useCallback(() => {
182 if (isMobile) {
183 setOpenMobile(false);
184 }
185 if (canGoBack) {
186 window.history.back();
187 return;
188 }
189 void navigate({ to: "/" });
190 }, [canGoBack, isMobile, navigate, setOpenMobile]);
191
192 return (180 return (
193 <>181 <>
194 <SidebarContent className="overflow-x-hidden">182 <SidebarContent className="overflow-x-hidden">
...@@ -298,14 +286,9 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {...@@ -298,14 +286,9 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
298 <SidebarFooter className="p-[var(--sidebar-content-inset)]">286 <SidebarFooter className="p-[var(--sidebar-content-inset)]">
299 <T3ConnectSidebarSignIn />287 <T3ConnectSidebarSignIn />
300 <div className="flex items-center gap-1">288 <div className="flex items-center gap-1">
301 <SidebarMenu className="min-w-0 flex-1">289 <div className="min-w-0 flex-1">
302 <SidebarMenuItem>290 <SidebarUtilityMenu />
303 <SidebarMenuButton onClick={handleBackClick}>291 </div>
304 <ArrowLeftIcon />
305 <span>Back</span>
306 </SidebarMenuButton>
307 </SidebarMenuItem>
308 </SidebarMenu>
309 <T3ConnectSidebarAvatar />292 <T3ConnectSidebarAvatar />
310 </div>293 </div>
311 </SidebarFooter>294 </SidebarFooter>
apps/web/src/components/settings/settingsLayout.tsx+18-2
...@@ -15,6 +15,14 @@ import { cn } from "../../lib/utils";...@@ -15,6 +15,14 @@ import { cn } from "../../lib/utils";
15import { Button } from "../ui/button";15import { Button } from "../ui/button";
16import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";16import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
1717
18type SettingsPageWidth = "readable" | "wide" | "expanded";
19
20const SETTINGS_PAGE_WIDTH_CLASS: Record<SettingsPageWidth, string> = {
21 readable: "max-w-4xl",
22 wide: "max-w-5xl",
23 expanded: "max-w-6xl",
24};
25
18interface SettingsSearchTargetContextValue {26interface SettingsSearchTargetContextValue {
19 readonly targetId: string | null;27 readonly targetId: string | null;
20 readonly onTargetHandled: () => void;28 readonly onTargetHandled: () => void;
...@@ -233,9 +241,11 @@ export function SettingResetButton({...@@ -233,9 +241,11 @@ export function SettingResetButton({
233export function SettingsPageContainer({241export function SettingsPageContainer({
234 children,242 children,
235 className,243 className,
244 width = "readable",
236}: {245}: {
237 children: ReactNode;246 children: ReactNode;
238 className?: string;247 className?: string;
248 width?: SettingsPageWidth;
239}) {249}) {
240 const navigate = useNavigate();250 const navigate = useNavigate();
241 const hash = useLocation({ select: (location) => location.hash });251 const hash = useLocation({ select: (location) => location.hash });
...@@ -247,10 +257,16 @@ export function SettingsPageContainer({...@@ -247,10 +257,16 @@ export function SettingsPageContainer({
247 return (257 return (
248 <SettingsSearchTargetProvider targetId={targetId} onTargetHandled={clearTargetHash}>258 <SettingsSearchTargetProvider targetId={targetId} onTargetHandled={clearTargetHash}>
249 <div259 <div
250 className="topbar-scroll-fade scrollbar-gutter-both flex-1 overflow-y-auto px-4 pt-10 pb-7 sm:px-8 sm:pt-12 sm:pb-10"260 className="topbar-scroll-fade scrollbar-gutter-both flex-1 overflow-y-auto [--topbar-scroll-fade-height:1.5rem] sm:[--topbar-scroll-fade-height:1.5rem]"
251 data-settings-page-scroll261 data-settings-page-scroll
252 >262 >
253 <div className={cn("mx-auto flex w-full max-w-4xl flex-col gap-12", className)}>263 <div
264 className={cn(
265 "mx-auto flex w-full flex-col gap-12 px-5 pt-6 pb-12 sm:px-6",
266 SETTINGS_PAGE_WIDTH_CLASS[width],
267 className,
268 )}
269 >
254 {children}270 {children}
255 </div>271 </div>
256 </div>272 </div>
apps/web/src/components/sidebar/SidebarChrome.tsx+78-68
...@@ -4,8 +4,9 @@ import {...@@ -4,8 +4,9 @@ import {
4 GitPullRequestIcon,4 GitPullRequestIcon,
5 SettingsIcon,5 SettingsIcon,
6} from "lucide-react";6} from "lucide-react";
7import type { ReactNode } from "react";
7import { memo, useCallback } from "react";8import { memo, useCallback } from "react";
8import { Link, useLocation, useNavigate } from "@tanstack/react-router";9import { Link, useCanGoBack, useLocation, useNavigate } from "@tanstack/react-router";
910
10import { useEnvironmentIdentificationMode } from "../../hooks/useSettings";11import { useEnvironmentIdentificationMode } from "../../hooks/useSettings";
11import { cn } from "../../lib/utils";12import { cn } from "../../lib/utils";
...@@ -117,16 +118,44 @@ function T3Wordmark() {...@@ -117,16 +118,44 @@ function T3Wordmark() {
117 );118 );
118}119}
119120
120export const SidebarChromeFooter = memo(function SidebarChromeFooter() {121function SidebarUtilityItem({
122 icon,
123 label,
124 onClick,
125}: {
126 icon: ReactNode;
127 label: string;
128 onClick: () => void;
129}) {
130 return (
131 <SidebarMenuItem className="shrink-0">
132 <Tooltip>
133 <TooltipTrigger
134 render={
135 <SidebarMenuButton aria-label={label} onClick={onClick} size="icon">
136 {icon}
137 </SidebarMenuButton>
138 }
139 />
140 <TooltipPopup side="top">{label}</TooltipPopup>
141 </Tooltip>
142 </SidebarMenuItem>
143 );
144}
145
146export const SidebarUtilityMenu = memo(function SidebarUtilityMenu() {
121 const navigate = useNavigate();147 const navigate = useNavigate();
148 const canGoBack = useCanGoBack();
122 const { isMobile, setOpenMobile } = useSidebar();149 const { isMobile, setOpenMobile } = useSidebar();
123 const currentFooterPage = useLocation({150 const currentFooterPage = useLocation({
124 select: (location) =>151 select: (location) =>
125 location.pathname === "/usage"152 /^\/settings(?:\/|$)/.test(location.pathname)
126 ? "usage"153 ? "settings"
127 : location.pathname === "/pull-requests"154 : location.pathname === "/usage"
128 ? "pull-requests"155 ? "usage"
129 : null,156 : location.pathname === "/pull-requests"
157 ? "pull-requests"
158 : null,
130 });159 });
131 const { environments } = useEnvironments();160 const { environments } = useEnvironments();
132 // The page reads every connected server, so one of them offering pull requests is enough for161 // The page reads every connected server, so one of them offering pull requests is enough for
...@@ -157,73 +186,54 @@ export const SidebarChromeFooter = memo(function SidebarChromeFooter() {...@@ -157,73 +186,54 @@ export const SidebarChromeFooter = memo(function SidebarChromeFooter() {
157186
158 const handleBackClick = useCallback(() => {187 const handleBackClick = useCallback(() => {
159 closeMobileSidebar();188 closeMobileSidebar();
189 if (canGoBack) {
190 window.history.back();
191 return;
192 }
160 void navigate({ to: "/" });193 void navigate({ to: "/" });
161 }, [closeMobileSidebar, navigate]);194 }, [canGoBack, closeMobileSidebar, navigate]);
162195
196 return (
197 <SidebarMenu className="flex-row items-center">
198 {currentFooterPage ? (
199 <SidebarMenuItem className="min-w-0 flex-1">
200 <SidebarMenuButton onClick={handleBackClick}>
201 <ArrowLeftIcon />
202 <span>Back</span>
203 </SidebarMenuButton>
204 </SidebarMenuItem>
205 ) : (
206 <>
207 <SidebarUtilityItem
208 icon={<SettingsIcon />}
209 label="Settings"
210 onClick={handleSettingsClick}
211 />
212 {pullRequestsSupported ? (
213 <SidebarUtilityItem
214 icon={<GitPullRequestIcon />}
215 label="Pull Requests"
216 onClick={handlePullRequestsClick}
217 />
218 ) : null}
219 <SidebarUtilityItem
220 icon={<ChartNoAxesColumnIcon />}
221 label="Usage"
222 onClick={handleUsageClick}
223 />
224 </>
225 )}
226 <SidebarUpdatePill />
227 </SidebarMenu>
228 );
229});
230
231export const SidebarChromeFooter = memo(function SidebarChromeFooter() {
163 return (232 return (
164 <SidebarFooter className="p-[var(--sidebar-content-inset)]">233 <SidebarFooter className="p-[var(--sidebar-content-inset)]">
165 <SidebarProviderUpdatePill />234 <SidebarProviderUpdatePill />
166 <SidebarUpdateArchitectureWarning />235 <SidebarUpdateArchitectureWarning />
167 <SidebarMenu className="flex-row items-center">236 <SidebarUtilityMenu />
168 {currentFooterPage ? (
169 <SidebarMenuItem className="min-w-0 flex-1">
170 <SidebarMenuButton onClick={handleBackClick}>
171 <ArrowLeftIcon />
172 <span>Back</span>
173 </SidebarMenuButton>
174 </SidebarMenuItem>
175 ) : (
176 <>
177 <SidebarMenuItem className="shrink-0">
178 <Tooltip>
179 <TooltipTrigger
180 render={
181 <SidebarMenuButton
182 aria-label="Settings"
183 onClick={handleSettingsClick}
184 size="icon"
185 >
186 <SettingsIcon />
187 </SidebarMenuButton>
188 }
189 />
190 <TooltipPopup side="top">Settings</TooltipPopup>
191 </Tooltip>
192 </SidebarMenuItem>
193 {pullRequestsSupported ? (
194 <SidebarMenuItem className="shrink-0">
195 <Tooltip>
196 <TooltipTrigger
197 render={
198 <SidebarMenuButton
199 aria-label="Pull Requests"
200 onClick={handlePullRequestsClick}
201 size="icon"
202 >
203 <GitPullRequestIcon />
204 </SidebarMenuButton>
205 }
206 />
207 <TooltipPopup side="top">Pull Requests</TooltipPopup>
208 </Tooltip>
209 </SidebarMenuItem>
210 ) : null}
211 <SidebarMenuItem className="shrink-0">
212 <Tooltip>
213 <TooltipTrigger
214 render={
215 <SidebarMenuButton aria-label="Usage" onClick={handleUsageClick} size="icon">
216 <ChartNoAxesColumnIcon />
217 </SidebarMenuButton>
218 }
219 />
220 <TooltipPopup side="top">Usage</TooltipPopup>
221 </Tooltip>
222 </SidebarMenuItem>
223 </>
224 )}
225 <SidebarUpdatePill />
226 </SidebarMenu>
227 </SidebarFooter>237 </SidebarFooter>
228 );238 );
229});239});
apps/web/src/routes/-chatIndexTitlebar.test.ts+9-3
...@@ -15,9 +15,15 @@ describe("hosted static onboarding header", () => {...@@ -15,9 +15,15 @@ describe("hosted static onboarding header", () => {
1515
16 const onboardingHeader = routeSource.slice(onboardingStart, onboardingEnd);16 const onboardingHeader = routeSource.slice(onboardingStart, onboardingEnd);
1717
18 expect(onboardingHeader).toContain("h-[var(--workspace-topbar-height)]");18 expect(onboardingHeader).toContain("<WorkspacePageHeader");
19 expect(onboardingHeader).toContain("min-h-[var(--workspace-topbar-height)]");
20 expect(onboardingHeader).toContain("COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS");
21 expect(onboardingHeader).not.toMatch(/(?:^|\s)(?:[\w-]+:)*py-/);19 expect(onboardingHeader).not.toMatch(/(?:^|\s)(?:[\w-]+:)*py-/);
20
21 const headerSource = NodeFS.readFileSync(
22 new URL("../components/WorkspacePageHeader.tsx", import.meta.url),
23 "utf8",
24 );
25 expect(headerSource).toContain("h-[var(--workspace-topbar-height)]");
26 expect(headerSource).toContain("min-h-[var(--workspace-topbar-height)]");
27 expect(headerSource).toContain("COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS");
22 });28 });
23});29});
apps/web/src/routes/_chat.index.tsx+3-9
...@@ -8,6 +8,7 @@ import { sortScopedProjectsForSidebar } from "../components/Sidebar.logic";...@@ -8,6 +8,7 @@ import { sortScopedProjectsForSidebar } from "../components/Sidebar.logic";
8import { Button } from "../components/ui/button";8import { Button } from "../components/ui/button";
9import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from "../components/ui/empty";9import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from "../components/ui/empty";
10import { SidebarInset } from "../components/ui/sidebar";10import { SidebarInset } from "../components/ui/sidebar";
11import { WorkspacePageHeader } from "../components/WorkspacePageHeader";
11import { useNewThreadHandler } from "../hooks/useHandleNewThread";12import { useNewThreadHandler } from "../hooks/useHandleNewThread";
12import {13import {
13 useAllEnvironmentShellsBootstrapped,14 useAllEnvironmentShellsBootstrapped,
...@@ -17,8 +18,6 @@ import {...@@ -17,8 +18,6 @@ import {
17import { useEnvironments } from "../state/environments";18import { useEnvironments } from "../state/environments";
18import { APP_DISPLAY_NAME } from "~/branding";19import { APP_DISPLAY_NAME } from "~/branding";
19import { hasCloudPublicConfig } from "~/cloud/publicConfig";20import { hasCloudPublicConfig } from "~/cloud/publicConfig";
20import { cn } from "~/lib/utils";
21import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "~/workspaceTitlebar";
2221
23function ChatIndexRouteView() {22function ChatIndexRouteView() {
24 const { authGateState } = Route.useRouteContext();23 const { authGateState } = Route.useRouteContext();
...@@ -143,18 +142,13 @@ function HostedStaticOnboardingState() {...@@ -143,18 +142,13 @@ function HostedStaticOnboardingState() {
143 return (142 return (
144 <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground">143 <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground">
145 <div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden bg-background">144 <div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden bg-background">
146 <header145 <WorkspacePageHeader className="border-b border-border">
147 className={cn(
148 "flex h-[var(--workspace-topbar-height)] min-h-[var(--workspace-topbar-height)] shrink-0 items-center border-b border-border px-3 transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none sm:px-5",
149 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,
150 )}
151 >
152 <div className="flex items-center gap-2">146 <div className="flex items-center gap-2">
153 <span className="text-sm font-medium text-foreground md:text-muted-foreground/60">147 <span className="text-sm font-medium text-foreground md:text-muted-foreground/60">
154 {APP_DISPLAY_NAME}148 {APP_DISPLAY_NAME}
155 </span>149 </span>
156 </div>150 </div>
157 </header>151 </WorkspacePageHeader>
158152
159 <Empty className="flex-1">153 <Empty className="flex-1">
160 <div className="w-full max-w-xl rounded-3xl border border-border/55 bg-card/20 px-8 py-12 shadow-sm/5">154 <div className="w-full max-w-xl rounded-3xl border border-border/55 bg-card/20 px-8 py-12 shadow-sm/5">
apps/web/src/routes/settings.tsx+10-36
...@@ -13,9 +13,8 @@ import { useSettingsRestore } from "../components/settings/SettingsPanels";...@@ -13,9 +13,8 @@ import { useSettingsRestore } from "../components/settings/SettingsPanels";
13import { SettingsBreadcrumb } from "../components/settings/SettingsBreadcrumb";13import { SettingsBreadcrumb } from "../components/settings/SettingsBreadcrumb";
14import { Button } from "../components/ui/button";14import { Button } from "../components/ui/button";
15import { SidebarInset } from "../components/ui/sidebar";15import { SidebarInset } from "../components/ui/sidebar";
16import { WorkspacePageHeader } from "../components/WorkspacePageHeader";
16import { isElectron } from "../env";17import { isElectron } from "../env";
17import { cn } from "~/lib/utils";
18import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "~/workspaceTitlebar";
1918
20function RestoreDefaultsButton({ onRestored }: { onRestored: () => void }) {19function RestoreDefaultsButton({ onRestored }: { onRestored: () => void }) {
21 const { changedSettingLabels, restoreDefaults } = useSettingsRestore(onRestored);20 const { changedSettingLabels, restoreDefaults } = useSettingsRestore(onRestored);
...@@ -72,41 +71,16 @@ function SettingsContentLayout() {...@@ -72,41 +71,16 @@ function SettingsContentLayout() {
72 return (71 return (
73 <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground isolate">72 <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground isolate">
74 <div className="flex min-h-0 min-w-0 flex-1 flex-col bg-background text-foreground">73 <div className="flex min-h-0 min-w-0 flex-1 flex-col bg-background text-foreground">
75 {!isElectron && (74 <WorkspacePageHeader electron={isElectron}>
76 <header75 <div className="flex w-full items-center gap-3">
77 className={cn(76 <SettingsBreadcrumb pathname={location.pathname} />
78 "flex h-[var(--workspace-topbar-height)] min-h-[var(--workspace-topbar-height)] shrink-0 items-center px-3 transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none sm:px-5",77 {showRestoreDefaults ? (
79 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,78 <div className="ms-auto flex items-center gap-2">
80 )}79 <RestoreDefaultsButton onRestored={handleRestored} />
81 >80 </div>
82 <div className="flex w-full items-center gap-2">81 ) : null}
83 <SettingsBreadcrumb pathname={location.pathname} />
84 {showRestoreDefaults ? (
85 <div className="ms-auto flex items-center gap-2">
86 <RestoreDefaultsButton onRestored={handleRestored} />
87 </div>
88 ) : null}
89 </div>
90 </header>
91 )}
92
93 {isElectron && (
94 <div
95 className={cn(
96 "drag-region flex h-[52px] shrink-0 items-center px-5 transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none wco:h-[env(titlebar-area-height)] wco:pr-[calc(100vw-env(titlebar-area-width)-env(titlebar-area-x)+1em)]",
97 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,
98 )}
99 >
100 <div className="flex w-full items-center gap-2">
101 <SettingsBreadcrumb pathname={location.pathname} />
102 {showRestoreDefaults ? (
103 <div className="ms-auto flex items-center gap-2">
104 <RestoreDefaultsButton onRestored={handleRestored} />
105 </div>
106 ) : null}
107 </div>
108 </div>82 </div>
109 )}83 </WorkspacePageHeader>
11084
111 <div key={restoreSignal} className="min-h-0 flex flex-1 flex-col">85 <div key={restoreSignal} className="min-h-0 flex flex-1 flex-col">
112 <Outlet />86 <Outlet />