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
+37 -92
View File
@@ -1,107 +1,52 @@
import { describe, expect, it } from "vitest";
import {
resolveRawWindowControlsPadding,
resolveWindowControlsPadding,
intersectWindowChromeCorners,
resolveWindowChromeObstruction,
resolveWindowChromeSafeArea,
} from "@/utils/desktop-window";
const rawPadding = {
left: 80,
right: 48,
top: 28,
};
describe("resolveWindowControlsPadding", () => {
it("keeps mac traffic-light padding available when the app window is not fullscreen", () => {
describe("window chrome", () => {
it("has no corner obstruction outside Electron or in fullscreen", () => {
expect(
resolveRawWindowControlsPadding({ isElectron: true, isMac: true, isFullscreen: false }),
).toEqual({
left: 78,
right: 0,
top: 45,
});
resolveWindowChromeObstruction({ isElectron: false, isMac: true, isFullscreen: false }),
).toEqual({ topLeft: null, topRight: null });
expect(
resolveWindowChromeObstruction({ isElectron: true, isMac: true, isFullscreen: true }),
).toEqual({ topLeft: null, topRight: null });
});
it("keeps Windows and Linux window-control padding available when the app window is not fullscreen", () => {
it("places native controls in their physical top corner", () => {
expect(
resolveRawWindowControlsPadding({ isElectron: true, isMac: false, isFullscreen: false }),
).toEqual({
left: 0,
right: 140,
top: 48,
});
resolveWindowChromeObstruction({ isElectron: true, isMac: true, isFullscreen: false }),
).toEqual({ topLeft: { width: 78, height: 45 }, topRight: null });
expect(
resolveWindowChromeObstruction({ isElectron: true, isMac: false, isFullscreen: false }),
).toEqual({ topLeft: null, topRight: { width: 140, height: 48 } });
});
it("does not reserve window-control padding when the app window is fullscreen", () => {
it("insets and reserves only claimed corners", () => {
const obstruction = { topLeft: { width: 80, height: 28 }, topRight: { width: 48, height: 32 } };
expect(
resolveRawWindowControlsPadding({ isElectron: true, isMac: true, isFullscreen: true }),
).toEqual({
left: 0,
right: 0,
top: 0,
});
resolveWindowChromeSafeArea({ obstruction, corners: "top-left", placement: "inline" }),
).toEqual({ paddingLeft: 80, paddingRight: 0 });
expect(
resolveWindowChromeSafeArea({ obstruction, corners: "top-right", placement: "below" }),
).toEqual({ height: 32 });
expect(
resolveWindowChromeSafeArea({ obstruction, corners: "both", placement: "below" }),
).toEqual({ height: 32 });
expect(
resolveWindowChromeSafeArea({ obstruction, corners: "top-right", placement: "inline" }),
).toEqual({ paddingLeft: 0, paddingRight: 48 });
});
it("pads the main header for window controls when the app sidebar is closed", () => {
expect(
resolveWindowControlsPadding({
role: "header",
rawPadding,
sidebarClosed: true,
explorerOpen: false,
focusModeEnabled: false,
}),
).toEqual({
left: 80,
right: 48,
top: 0,
});
});
it("does not add left padding to detail headers with their own sidebar", () => {
expect(
resolveWindowControlsPadding({
role: "detailHeader",
rawPadding,
sidebarClosed: true,
explorerOpen: false,
focusModeEnabled: false,
}),
).toEqual({
left: 0,
right: 48,
top: 0,
});
});
it("pads a focus-mode tab row away from mac traffic lights even when the sidebar is logically open", () => {
expect(
resolveWindowControlsPadding({
role: "tabRow",
rawPadding,
sidebarClosed: false,
explorerOpen: false,
focusModeEnabled: true,
}),
).toEqual({
left: 80,
right: 48,
top: 0,
});
});
it("pads a focus-mode tab row away from right-side window controls even when the explorer is logically open", () => {
expect(
resolveWindowControlsPadding({
role: "tabRow",
rawPadding: { left: 0, right: 140, top: 48 },
sidebarClosed: true,
explorerOpen: true,
focusModeEnabled: true,
}),
).toEqual({
left: 0,
right: 140,
top: 0,
});
it("intersects identical and empty corner claims", () => {
expect(intersectWindowChromeCorners("both", "both")).toBe("both");
expect(intersectWindowChromeCorners("top-left", "top-left")).toBe("top-left");
expect(intersectWindowChromeCorners("none", "both")).toBe("none");
expect(intersectWindowChromeCorners("both", "none")).toBe("none");
expect(intersectWindowChromeCorners("both", "top-left")).toBe("top-left");
expect(intersectWindowChromeCorners("top-right", "both")).toBe("top-right");
expect(intersectWindowChromeCorners("top-left", "top-right")).toBe("none");
});
});
-187
View File
@@ -1,187 +0,0 @@
import { useEffect, useMemo, useState } from "react";
import {
getIsElectronRuntimeMac,
getIsElectronRuntime,
DESKTOP_TRAFFIC_LIGHT_WIDTH,
DESKTOP_TRAFFIC_LIGHT_HEIGHT,
DESKTOP_WINDOW_CONTROLS_WIDTH,
DESKTOP_WINDOW_CONTROLS_HEIGHT,
} from "@/constants/layout";
import { getDesktopWindow } from "@/desktop/electron/window";
import { usePanelStore } from "@/stores/panel-store";
import { isNative } from "@/constants/platform";
interface RawWindowControlsPadding {
left: number;
right: number;
top: number;
}
type WindowControlsPaddingRole =
| "sidebar"
| "header"
| "detailHeader"
| "tabRow"
| "explorerSidebar";
// Module-level cache so hook remounts (e.g., on navigation) don't briefly
// fall back to the default `false` while the async fullscreen check resolves.
// Without this, in fullscreen the sidebar flashes with traffic-light padding
// on first frame and then snaps to 0 once the async read completes.
let cachedIsFullscreen = false;
const fullscreenSubscribers = new Set<(value: boolean) => void>();
let fullscreenSubscriptionStarted = false;
function setCachedFullscreen(value: boolean) {
if (cachedIsFullscreen === value) return;
cachedIsFullscreen = value;
for (const sub of fullscreenSubscribers) {
sub(value);
}
}
function startFullscreenSubscription() {
if (fullscreenSubscriptionStarted) return;
if (isNative || !getIsElectronRuntime()) return;
fullscreenSubscriptionStarted = true;
void (async () => {
const win = getDesktopWindow();
if (!win) return;
if (typeof win.isFullscreen === "function") {
try {
setCachedFullscreen(await win.isFullscreen());
} catch (error) {
console.warn("[DesktopWindow] Failed to read fullscreen state", error);
}
}
if (typeof win.onResized !== "function") return;
try {
await win.onResized(async () => {
if (typeof win.isFullscreen !== "function") return;
try {
setCachedFullscreen(await win.isFullscreen());
} catch (error) {
console.warn("[DesktopWindow] Failed to read fullscreen state", error);
}
});
} catch (error) {
console.warn("[DesktopWindow] Failed to subscribe to resize", error);
}
})();
}
function useRawWindowControlsPadding(): RawWindowControlsPadding {
const [isFullscreen, setIsFullscreen] = useState(cachedIsFullscreen);
useEffect(() => {
startFullscreenSubscription();
// Sync to any value that resolved between render and effect.
setIsFullscreen(cachedIsFullscreen);
fullscreenSubscribers.add(setIsFullscreen);
return () => {
fullscreenSubscribers.delete(setIsFullscreen);
};
}, []);
return resolveRawWindowControlsPadding({
isElectron: getIsElectronRuntime(),
isMac: getIsElectronRuntimeMac(),
isFullscreen,
});
}
export function resolveRawWindowControlsPadding(input: {
isElectron: boolean;
isMac: boolean;
isFullscreen: boolean;
}): RawWindowControlsPadding {
if (!input.isElectron || input.isFullscreen) {
return { left: 0, right: 0, top: 0 };
}
if (input.isMac) {
return {
left: DESKTOP_TRAFFIC_LIGHT_WIDTH,
right: 0,
top: DESKTOP_TRAFFIC_LIGHT_HEIGHT,
};
}
return {
left: 0,
right: DESKTOP_WINDOW_CONTROLS_WIDTH,
top: DESKTOP_WINDOW_CONTROLS_HEIGHT,
};
}
export function useWindowControlsPadding(role: WindowControlsPaddingRole): {
left: number;
right: number;
top: number;
} {
const sidebarOpen = usePanelStore((state) => state.desktop.agentListOpen);
const explorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen);
const focusModeEnabled = usePanelStore((state) => state.desktop.focusModeEnabled);
const rawPadding = useRawWindowControlsPadding();
const sidebarClosed = !sidebarOpen;
const { left, right, top } = resolveWindowControlsPadding({
role,
rawPadding,
sidebarClosed,
explorerOpen,
focusModeEnabled,
});
return useMemo(() => ({ left, right, top }), [left, right, top]);
}
export function resolveWindowControlsPadding(input: {
role: WindowControlsPaddingRole;
rawPadding: RawWindowControlsPadding;
sidebarClosed: boolean;
explorerOpen: boolean;
focusModeEnabled: boolean;
}): RawWindowControlsPadding {
if (input.role === "sidebar") {
return {
left: input.rawPadding.left,
right: 0,
top: input.rawPadding.top,
};
}
if (input.role === "header") {
return {
left: input.sidebarClosed ? input.rawPadding.left : 0,
right: input.explorerOpen ? 0 : input.rawPadding.right,
top: 0,
};
}
if (input.role === "detailHeader") {
return {
left: 0,
right: input.rawPadding.right,
top: 0,
};
}
if (input.role === "tabRow") {
return {
left: input.focusModeEnabled ? input.rawPadding.left : 0,
right: input.focusModeEnabled ? input.rawPadding.right : 0,
top: 0,
};
}
return {
left: 0,
right: input.rawPadding.right,
top: 0,
};
}
+238
View File
@@ -0,0 +1,238 @@
import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from "react";
import { View, type ViewProps } from "react-native";
import {
DESKTOP_TRAFFIC_LIGHT_HEIGHT,
DESKTOP_TRAFFIC_LIGHT_WIDTH,
DESKTOP_WINDOW_CONTROLS_HEIGHT,
DESKTOP_WINDOW_CONTROLS_WIDTH,
getIsElectronRuntime,
getIsElectronRuntimeMac,
} from "@/constants/layout";
import { getDesktopWindow } from "@/desktop/electron/window";
import { isNative } from "@/constants/platform";
export type WindowChromeCorners = "none" | "top-left" | "top-right" | "both";
type WindowChromeSafeAreaPlacement = "inline" | "below";
interface WindowChromeCornerObstruction {
width: number;
height: number;
}
interface WindowChromeObstruction {
topLeft: WindowChromeCornerObstruction | null;
topRight: WindowChromeCornerObstruction | null;
}
type WindowChromeSafeAreaStyle = { height: number } | { paddingLeft: number; paddingRight: number };
const EMPTY_OBSTRUCTION: WindowChromeObstruction = { topLeft: null, topRight: null };
const WindowChromeContext = createContext<WindowChromeObstruction>(EMPTY_OBSTRUCTION);
const WindowChromeCornersContext = createContext<WindowChromeCorners>("none");
function windowChromeCornersFromFlags(topLeft: boolean, topRight: boolean): WindowChromeCorners {
if (topLeft && topRight) return "both";
if (topLeft) return "top-left";
if (topRight) return "top-right";
return "none";
}
export function intersectWindowChromeCorners(
inherited: WindowChromeCorners,
declared: WindowChromeCorners,
): WindowChromeCorners {
const inheritedTopLeft = inherited === "top-left" || inherited === "both";
const inheritedTopRight = inherited === "top-right" || inherited === "both";
const declaredTopLeft = declared === "top-left" || declared === "both";
const declaredTopRight = declared === "top-right" || declared === "both";
return windowChromeCornersFromFlags(
inheritedTopLeft && declaredTopLeft,
inheritedTopRight && declaredTopRight,
);
}
export function resolveWindowChromeObstruction(input: {
isElectron: boolean;
isMac: boolean;
isFullscreen: boolean;
}): WindowChromeObstruction {
if (!input.isElectron || input.isFullscreen) return EMPTY_OBSTRUCTION;
if (input.isMac) {
return {
topLeft: { width: DESKTOP_TRAFFIC_LIGHT_WIDTH, height: DESKTOP_TRAFFIC_LIGHT_HEIGHT },
topRight: null,
};
}
return {
topLeft: null,
topRight: { width: DESKTOP_WINDOW_CONTROLS_WIDTH, height: DESKTOP_WINDOW_CONTROLS_HEIGHT },
};
}
export function resolveWindowChromeSafeArea(input: {
obstruction: WindowChromeObstruction;
corners: WindowChromeCorners;
placement: WindowChromeSafeAreaPlacement;
}): WindowChromeSafeAreaStyle {
const ownsTopLeft = input.corners === "top-left" || input.corners === "both";
const ownsTopRight = input.corners === "top-right" || input.corners === "both";
const topLeft = ownsTopLeft ? input.obstruction.topLeft : null;
const topRight = ownsTopRight ? input.obstruction.topRight : null;
if (input.placement === "below") {
return { height: Math.max(topLeft?.height ?? 0, topRight?.height ?? 0) };
}
return { paddingLeft: topLeft?.width ?? 0, paddingRight: topRight?.width ?? 0 };
}
export function WindowChromeProvider({ children }: { children: ReactNode }) {
const [isElectronReady, setIsElectronReady] = useState(getIsElectronRuntime);
const [isFullscreen, setIsFullscreen] = useState(false);
useEffect(() => {
let active = true;
let dispose: (() => void) | undefined;
let connecting = false;
let retryCount = 0;
let retryTimer: ReturnType<typeof setTimeout> | undefined;
function scheduleRetry(warnOnExhaustion = false) {
if (!active || dispose || retryTimer) return;
if (retryCount >= 40) {
if (warnOnExhaustion) {
console.warn("[DesktopWindow] Chrome bridge unavailable; window controls may overlap UI");
}
return;
}
retryCount += 1;
retryTimer = setTimeout(() => {
retryTimer = undefined;
connect();
}, 250);
}
function connect() {
if (!active || dispose || connecting) return;
if (!getIsElectronRuntime()) return scheduleRetry();
const desktopWindow = getDesktopWindow();
if (
!desktopWindow ||
typeof desktopWindow.isFullscreen !== "function" ||
typeof desktopWindow.onResized !== "function"
)
return scheduleRetry(true);
const readFullscreen = desktopWindow.isFullscreen;
const subscribeToResized = desktopWindow.onResized;
connecting = true;
void (async () => {
async function syncFullscreen() {
try {
const fullscreen = await readFullscreen();
if (active) setIsFullscreen(fullscreen);
} catch (error) {
if (active) console.warn("[DesktopWindow] Failed to read fullscreen state", error);
}
}
try {
const nextDispose = await subscribeToResized(syncFullscreen);
if (!active) return nextDispose();
dispose = nextDispose;
setIsElectronReady(true);
await syncFullscreen();
} catch (error) {
if (active) console.warn("[DesktopWindow] Failed to subscribe to resize", error);
} finally {
connecting = false;
if (!dispose) scheduleRetry();
}
})();
}
if (!isNative) connect();
return () => {
active = false;
if (retryTimer) clearTimeout(retryTimer);
dispose?.();
};
}, []);
const obstruction = useMemo(
() =>
resolveWindowChromeObstruction({
isElectron: isElectronReady,
isMac: getIsElectronRuntimeMac(),
isFullscreen,
}),
[isElectronReady, isFullscreen],
);
return (
<WindowChromeContext.Provider value={obstruction}>
<WindowChromeCornersContext.Provider value="both">
{children}
</WindowChromeCornersContext.Provider>
</WindowChromeContext.Provider>
);
}
/** Narrows inherited corner ownership to the corners occupied by this child surface. */
export function WindowChromeRegion({
corners,
children,
}: {
corners: WindowChromeCorners;
children: ReactNode;
}) {
const inheritedCorners = useContext(WindowChromeCornersContext);
const ownedCorners = intersectWindowChromeCorners(inheritedCorners, corners);
return (
<WindowChromeCornersContext.Provider value={ownedCorners}>
{children}
</WindowChromeCornersContext.Provider>
);
}
/** Restarts ownership for a new physical viewport such as a Modal or full-window overlay. */
export function WindowChromeRootRegion({
corners,
children,
}: {
corners: WindowChromeCorners;
children: ReactNode;
}) {
return (
<WindowChromeCornersContext.Provider value={corners}>
{children}
</WindowChromeCornersContext.Provider>
);
}
export function useWindowChromeCorners(): WindowChromeCorners {
return useContext(WindowChromeCornersContext);
}
type WindowChromeSafeAreaProps = ViewProps & {
placement: WindowChromeSafeAreaPlacement;
horizontalPadding?: number;
};
export function WindowChromeSafeArea({
placement,
horizontalPadding = 0,
style,
...props
}: WindowChromeSafeAreaProps) {
const obstruction = useContext(WindowChromeContext);
const corners = useContext(WindowChromeCornersContext);
const safeAreaStyle = useMemo(() => {
const resolved = resolveWindowChromeSafeArea({ obstruction, corners, placement });
if (placement === "below") return resolved;
const paddingLeft = "paddingLeft" in resolved ? resolved.paddingLeft : 0;
const paddingRight = "paddingRight" in resolved ? resolved.paddingRight : 0;
return {
paddingLeft: paddingLeft + horizontalPadding,
paddingRight: paddingRight + horizontalPadding,
};
}, [corners, horizontalPadding, obstruction, placement]);
const combinedStyle = useMemo(() => [style, safeAreaStyle], [safeAreaStyle, style]);
return <View {...props} style={combinedStyle} />;
}