Fix desktop layout and window controls at half-screen width (#1983)

* fix(app): preserve desktop layout in half-screen windows

Treat native window controls as physical corner obstructions instead of deriving clearance from compact layout state. Keep 751px windows in desktop mode while preserving usable content when persisted sidebars are wide.

* fix(app): retain sidebar width across window resizes

* fix(app): yield app sidebar to narrow settings

* test(app): wait for mobile sidebar motion to settle

* fix(app): yield navigation to narrow workspace explorer

* fix(app): mark sidebar sizing helpers as worklets

* fix(app): hide app navigation when space is constrained
This commit is contained in:
Mohamed Boudra
2026-07-14 23:01:14 +02:00
committed by GitHub
parent 1b8af2e58e
commit 50ed0d0ab1
27 changed files with 1263 additions and 718 deletions
+38 -42
View File
@@ -57,7 +57,7 @@ import { useHostRuntimeIsConnected, useHosts } from "@/runtime/host-runtime";
import { useSessionStore } from "@/stores/session-store";
import { orderHostsLocalFirst, type HostProfile } from "@/types/host-connection";
import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region";
import { useWindowControlsPadding } from "@/utils/desktop-window";
import { WindowChromeRegion, WindowChromeSafeArea } from "@/utils/desktop-window";
import { confirmDialog } from "@/utils/confirm-dialog";
import { BackHeader } from "@/components/headers/back-header";
import { ScreenHeader } from "@/components/headers/screen-header";
@@ -97,7 +97,7 @@ import {
} from "@/screens/settings/host-page";
import ProjectsScreen from "@/screens/projects-screen";
import ProjectSettingsScreen from "@/screens/project-settings-screen";
import { useIsCompactFormFactor } from "@/constants/layout";
import { SETTINGS_DESKTOP_SIDEBAR_WIDTH, useIsCompactFormFactor } from "@/constants/layout";
import { useLocalDaemonServerId } from "@/hooks/use-is-local-daemon";
import {
type EnableBuiltInDaemonOption,
@@ -992,7 +992,6 @@ function SettingsSidebar({
const isDesktopApp = isElectronRuntime();
const items = SIDEBAR_SECTION_ITEMS.filter((item) => !item.desktopOnly || isDesktopApp);
const insets = useSafeAreaInsets();
const padding = useWindowControlsPadding("sidebar");
const isDesktop = layout === "desktop";
const outerContainerStyle = useMemo(
() => [isDesktop ? sidebarStyles.desktopContainer : sidebarStyles.mobileContainer],
@@ -1005,7 +1004,6 @@ function SettingsSidebar({
const selectedSectionId = view.kind === "section" ? view.section : null;
const selectedHostSection = view.kind === "host" ? view.section : null;
const isProjectsSelected = view.kind === "projects" || view.kind === "project";
const paddingTopStyle = useMemo(() => ({ height: padding.top }), [padding.top]);
const sidebarBody = (
<>
@@ -1087,7 +1085,7 @@ function SettingsSidebar({
<View style={innerContainerStyle}>
<View style={sidebarStyles.sidebarDragArea}>
<TitlebarDragRegion />
{padding.top > 0 ? <View style={paddingTopStyle} /> : null}
<WindowChromeSafeArea placement="below" />
<SidebarHeaderRow
icon={ArrowLeft}
label={t("settings.backToWorkspace")}
@@ -1434,6 +1432,16 @@ export default function SettingsScreen({ view, openAddHostIntent = null }: Setti
);
}
const desktopDetailHeaderLeft = detailHeader ? (
<>
<HeaderIconBadge>
<detailHeader.Icon size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
</HeaderIconBadge>
<ScreenTitle testID="settings-detail-header-title">{detailHeader.title}</ScreenTitle>
{detailHeader.titleAccessory}
</>
) : null;
const addHostModals = (
<>
<AddHostMethodModal
@@ -1508,43 +1516,31 @@ export default function SettingsScreen({ view, openAddHostIntent = null }: Setti
return (
<View style={styles.container}>
<View style={desktopStyles.row}>
<SettingsSidebar
view={view}
onSelectSection={handleSelectSection}
onSelectHostSection={handleSelectHostSection}
onSelectHost={handleSelectHost}
onSelectProjects={handleSelectProjects}
onAddHost={handleAddHost}
onBackToWorkspace={handleBackToWorkspace}
activeHostServerId={activeHostServerId}
layout="desktop"
/>
<View style={desktopStyles.contentPane}>
<ScreenHeader
borderless={!detailHeader}
windowControlsPaddingRole="detailHeader"
left={
detailHeader ? (
<>
<HeaderIconBadge>
<detailHeader.Icon
size={theme.iconSize.md}
color={theme.colors.foregroundMuted}
/>
</HeaderIconBadge>
<ScreenTitle testID="settings-detail-header-title">
{detailHeader.title}
</ScreenTitle>
{detailHeader.titleAccessory}
</>
) : null
}
leftStyle={desktopStyles.detailLeft}
<WindowChromeRegion corners="top-left">
<SettingsSidebar
view={view}
onSelectSection={handleSelectSection}
onSelectHostSection={handleSelectHostSection}
onSelectHost={handleSelectHost}
onSelectProjects={handleSelectProjects}
onAddHost={handleAddHost}
onBackToWorkspace={handleBackToWorkspace}
activeHostServerId={activeHostServerId}
layout="desktop"
/>
<ScrollView style={styles.scrollView} contentContainerStyle={insetBottomStyle}>
<View style={styles.content}>{content}</View>
</ScrollView>
</View>
</WindowChromeRegion>
<WindowChromeRegion corners="top-right">
<View style={desktopStyles.contentPane} testID="settings-detail-pane">
<ScreenHeader
borderless={!detailHeader}
left={desktopDetailHeaderLeft}
leftStyle={desktopStyles.detailLeft}
/>
<ScrollView style={styles.scrollView} contentContainerStyle={insetBottomStyle}>
<View style={styles.content}>{content}</View>
</ScrollView>
</View>
</WindowChromeRegion>
</View>
{addHostModals}
</View>
@@ -1654,7 +1650,7 @@ const desktopStyles = StyleSheet.create((theme) => ({
const sidebarStyles = StyleSheet.create((theme) => ({
desktopContainer: {
width: 320,
width: SETTINGS_DESKTOP_SIDEBAR_WIDTH,
borderRightWidth: 1,
borderRightColor: theme.colors.border,
backgroundColor: theme.colors.surfaceSidebar,
@@ -6,6 +6,7 @@ import {
useRef,
useState,
type ReactElement,
type ComponentProps,
type ReactNode,
} from "react";
import { useStoreWithEqualityFn } from "zustand/traditional";
@@ -61,6 +62,7 @@ import {
import { ExplorerSidebar } from "@/components/explorer-sidebar";
import { SplitContainer } from "@/components/split-container";
import { RetainedPanel } from "@/components/retained-panel";
import { WindowChromeRegion } from "@/utils/desktop-window";
import { SourceControlPanelIcon } from "@/components/icons/source-control-panel-icon";
import { WorkspaceActions } from "@/git/workspace-actions";
import { WorkspaceOpenInEditorButton } from "@/screens/workspace/workspace-open-in-editor-button";
@@ -1566,6 +1568,46 @@ function shouldShowWorkspaceExplorerSidebar(input: {
return !input.isMobile && input.isRouteFocused && shouldShowWorkspaceScreenHeader(input);
}
interface WorkspaceChromeRowProps extends Omit<
ComponentProps<typeof ExplorerSidebar>,
"workspaceRoot"
> {
children: ReactNode;
explorerOpen: boolean;
portalHostName: string;
showExplorerSidebar: boolean;
workspaceRoot: string | null;
}
function WorkspaceChromeRow({
children,
explorerOpen,
portalHostName,
showExplorerSidebar,
workspaceRoot,
...explorerProps
}: WorkspaceChromeRowProps) {
const explorerRendered = showExplorerSidebar && explorerOpen && workspaceRoot !== null;
return (
<View style={styles.threePaneRow}>
<WindowChromeRegion corners={explorerRendered ? "top-left" : "both"}>
<FloatingPanelPortalHostNameProvider hostName={portalHostName}>
{children}
</FloatingPanelPortalHostNameProvider>
</WindowChromeRegion>
<FloatingPanelPortalHost name={portalHostName} />
{showExplorerSidebar && workspaceRoot ? (
<WindowChromeRegion corners="top-right">
<ExplorerSidebar {...explorerProps} workspaceRoot={workspaceRoot} />
</WindowChromeRegion>
) : null}
</View>
);
}
function buildWorkspaceTerminalScopeKey(serverId: string, workspaceId: string): string | null {
if (!serverId || !workspaceId) {
return null;
@@ -3624,23 +3666,18 @@ function WorkspaceScreenContent({
workspaceId={normalizedWorkspaceId}
isRouteFocused={isRouteFocused}
/>
<View style={styles.threePaneRow}>
<FloatingPanelPortalHostNameProvider hostName={workspaceFloatingPanelPortalHostName}>
{workspaceCenterColumn}
</FloatingPanelPortalHostNameProvider>
<FloatingPanelPortalHost name={workspaceFloatingPanelPortalHostName} />
{showExplorerSidebar && workspaceDirectory ? (
<ExplorerSidebar
serverId={normalizedServerId}
workspaceId={normalizedWorkspaceId}
workspaceRoot={workspaceDirectory}
isGit={isGitCheckout}
onOpenFile={handleOpenFileFromExplorer}
/>
) : null}
</View>
<WorkspaceChromeRow
portalHostName={workspaceFloatingPanelPortalHostName}
showExplorerSidebar={showExplorerSidebar}
explorerOpen={isExplorerOpen}
serverId={normalizedServerId}
workspaceId={normalizedWorkspaceId}
workspaceRoot={workspaceDirectory}
isGit={isGitCheckout}
onOpenFile={handleOpenFileFromExplorer}
>
{workspaceCenterColumn}
</WorkspaceChromeRow>
<ImportSessionSheet
visible={isImportSheetVisible}
client={client}