From 51411182fe44343f0844944c8151fb7674e560eb Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 1 Apr 2026 10:55:01 +0700 Subject: [PATCH] fix(app): support ipad desktop layout safely --- packages/app/src/app/_layout.tsx | 16 ++-- packages/app/src/attachments/store.ts | 4 +- .../app/src/components/explorer-sidebar.tsx | 6 +- .../src/components/headers/menu-header.tsx | 5 +- .../src/components/headers/screen-header.tsx | 5 +- .../components/keyboard-shortcuts-dialog.tsx | 4 +- packages/app/src/components/left-sidebar.tsx | 19 ++-- .../src/components/sidebar-workspace-list.tsx | 15 ++-- packages/app/src/components/terminal-pane.tsx | 5 +- packages/app/src/constants/layout.ts | 54 +++++++---- .../explorer-sidebar-animation-context.tsx | 6 +- .../contexts/sidebar-animation-context.tsx | 6 +- .../desktop-permissions-section.tsx | 4 +- .../app/src/desktop/daemon/desktop-daemon.ts | 4 +- packages/app/src/desktop/host.ts | 6 +- .../permissions/use-desktop-permissions.ts | 22 ++--- .../src/desktop/updates/desktop-updates.ts | 4 +- .../app/src/desktop/updates/update-banner.tsx | 8 +- .../updates/use-desktop-app-updater.ts | 14 +-- .../app/src/hooks/use-audio-recorder.web.ts | 4 +- packages/app/src/hooks/use-command-center.ts | 6 +- .../hooks/use-dictation-audio-source.web.ts | 4 +- packages/app/src/hooks/use-favicon-status.ts | 4 +- .../src/hooks/use-image-attachment-picker.ts | 4 +- packages/app/src/hooks/use-is-local-daemon.ts | 6 +- .../app/src/hooks/use-keyboard-shortcuts.ts | 4 +- packages/app/src/hooks/use-shortcut-keys.ts | 8 +- .../src/screens/agent/draft-agent-screen.tsx | 6 +- .../app/src/screens/open-project-screen.tsx | 9 +- packages/app/src/screens/settings-screen.tsx | 53 ++++++----- .../settings/keyboard-shortcuts-section.tsx | 11 ++- .../workspace/workspace-desktop-tabs-row.tsx | 90 ++++++++++--------- .../screens/workspace/workspace-screen.tsx | 63 ++++++++++++- packages/app/src/utils/desktop-window.ts | 14 +-- packages/app/src/utils/shortcut-platform.ts | 4 +- packages/app/src/voice/audio-engine.web.ts | 4 +- 36 files changed, 299 insertions(+), 202 deletions(-) diff --git a/packages/app/src/app/_layout.tsx b/packages/app/src/app/_layout.tsx index 24ada6f27..6bc73f08e 100644 --- a/packages/app/src/app/_layout.tsx +++ b/packages/app/src/app/_layout.tsx @@ -57,7 +57,7 @@ import { HorizontalScrollProvider, useHorizontalScrollOptional, } from "@/contexts/horizontal-scroll-context"; -import { getIsDesktop } from "@/constants/layout"; +import { getIsElectronRuntime, isCompactFormFactor } from "@/constants/layout"; import { CommandCenter } from "@/components/command-center"; import { ProjectPickerModal } from "@/components/project-picker-modal"; import { KeyboardShortcutsDialog } from "@/components/keyboard-shortcuts-dialog"; @@ -103,7 +103,7 @@ function PushNotificationRouter() { let removeDesktopNotificationListener: (() => void) | null = null; let cancelled = false; - if (getIsDesktop()) { + if (getIsElectronRuntime()) { void ensureOsNotificationPermission(); const unlistenResult = getDesktopHost()?.events?.on?.( @@ -342,12 +342,12 @@ function AppContainer({ const toggleFocusMode = usePanelStore((state) => state.toggleFocusMode); const isFocusModeEnabled = usePanelStore((state) => state.desktop.focusModeEnabled); - const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; + const isCompactLayout = isCompactFormFactor(); const chromeEnabled = chromeEnabledOverride ?? daemons.length > 0; useKeyboardShortcuts({ enabled: chromeEnabled, - isMobile, + isMobile: isCompactLayout, toggleAgentList, selectedAgentId, toggleFileExplorer, @@ -363,10 +363,12 @@ function AppContainer({ const content = ( - {!isMobile && chromeEnabled && !isFocusModeEnabled && } + {!isCompactLayout && chromeEnabled && !isFocusModeEnabled && ( + + )} {children} - {isMobile && chromeEnabled && } + {isCompactLayout && chromeEnabled && } @@ -375,7 +377,7 @@ function AppContainer({ ); - if (!isMobile) { + if (!isCompactLayout) { return content; } diff --git a/packages/app/src/attachments/store.ts b/packages/app/src/attachments/store.ts index 55a884113..ea6fd2dc1 100644 --- a/packages/app/src/attachments/store.ts +++ b/packages/app/src/attachments/store.ts @@ -1,12 +1,12 @@ import { Platform } from "react-native"; -import { isDesktop } from "@/desktop/host"; +import { isElectronRuntime } from "@/desktop/host"; import type { AttachmentStore } from "@/attachments/types"; let attachmentStorePromise: Promise | null = null; async function createAttachmentStore(): Promise { if (Platform.OS === "web") { - if (isDesktop()) { + if (isElectronRuntime()) { const { createDesktopAttachmentStore } = await import( "../desktop/attachments/desktop-attachment-store" ); diff --git a/packages/app/src/components/explorer-sidebar.tsx b/packages/app/src/components/explorer-sidebar.tsx index 50bbfda5d..9656aead9 100644 --- a/packages/app/src/components/explorer-sidebar.tsx +++ b/packages/app/src/components/explorer-sidebar.tsx @@ -4,7 +4,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useIsFocused } from "@react-navigation/native"; import Animated, { useAnimatedStyle, useSharedValue, runOnJS } from "react-native-reanimated"; import { Gesture, GestureDetector } from "react-native-gesture-handler"; -import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles"; +import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { X } from "lucide-react-native"; import { usePanelStore, @@ -13,7 +13,7 @@ import { type ExplorerTab, } from "@/stores/panel-store"; import { useExplorerSidebarAnimation } from "@/contexts/explorer-sidebar-animation-context"; -import { HEADER_INNER_HEIGHT } from "@/constants/layout"; +import { HEADER_INNER_HEIGHT, isCompactFormFactor } from "@/constants/layout"; import { GitDiffPane } from "./git-diff-pane"; import { FileExplorerPane } from "./file-explorer-pane"; import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style"; @@ -40,7 +40,7 @@ export function ExplorerSidebar({ const { theme } = useUnistyles(); const isScreenFocused = useIsFocused(); const insets = useSafeAreaInsets(); - const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; + const isMobile = isCompactFormFactor(); const mobileView = usePanelStore((state) => state.mobileView); const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen); const closeToAgent = usePanelStore((state) => state.closeToAgent); diff --git a/packages/app/src/components/headers/menu-header.tsx b/packages/app/src/components/headers/menu-header.tsx index 83b9dabba..f7e5a8904 100644 --- a/packages/app/src/components/headers/menu-header.tsx +++ b/packages/app/src/components/headers/menu-header.tsx @@ -1,10 +1,11 @@ import type { ReactNode } from "react"; import { Text, View, type StyleProp, type ViewStyle } from "react-native"; -import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles"; +import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { PanelLeft } from "lucide-react-native"; import { ScreenHeader } from "./screen-header"; import { HeaderToggleButton } from "./header-toggle-button"; import { usePanelStore } from "@/stores/panel-store"; +import { isCompactFormFactor } from "@/constants/layout"; import { getShortcutOs } from "@/utils/shortcut-platform"; interface MenuHeaderProps { @@ -43,7 +44,7 @@ export function SidebarMenuToggle({ nativeID = "menu-button", }: SidebarMenuToggleProps = {}) { const { theme } = useUnistyles(); - const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; + const isMobile = isCompactFormFactor(); const mobileView = usePanelStore((state) => state.mobileView); const desktopAgentListOpen = usePanelStore((state) => state.desktop.agentListOpen); const toggleAgentList = usePanelStore((state) => state.toggleAgentList); diff --git a/packages/app/src/components/headers/screen-header.tsx b/packages/app/src/components/headers/screen-header.tsx index 420c950b8..e3ec9d216 100644 --- a/packages/app/src/components/headers/screen-header.tsx +++ b/packages/app/src/components/headers/screen-header.tsx @@ -1,11 +1,12 @@ import type { ReactNode } from "react"; import { View, type StyleProp, type ViewStyle } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles"; +import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { HEADER_INNER_HEIGHT, HEADER_INNER_HEIGHT_MOBILE, HEADER_TOP_PADDING_MOBILE, + isCompactFormFactor, } from "@/constants/layout"; import { useDesktopDragHandlers, useWindowControlsPadding } from "@/utils/desktop-window"; @@ -24,7 +25,7 @@ interface ScreenHeaderProps { export function ScreenHeader({ left, right, leftStyle, rightStyle, borderless }: ScreenHeaderProps) { const { theme } = useUnistyles(); const insets = useSafeAreaInsets(); - const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; + const isMobile = isCompactFormFactor(); const padding = useWindowControlsPadding("header"); // Only add extra padding on mobile for better touch targets; on desktop, only use safe area insets const topPadding = isMobile ? HEADER_TOP_PADDING_MOBILE : 0; diff --git a/packages/app/src/components/keyboard-shortcuts-dialog.tsx b/packages/app/src/components/keyboard-shortcuts-dialog.tsx index 78e4a546a..e882f87f6 100644 --- a/packages/app/src/components/keyboard-shortcuts-dialog.tsx +++ b/packages/app/src/components/keyboard-shortcuts-dialog.tsx @@ -1,7 +1,7 @@ import { useMemo } from "react"; import { Text, View } from "react-native"; import { StyleSheet } from "react-native-unistyles"; -import { getIsDesktop } from "@/constants/layout"; +import { getIsElectronRuntime } from "@/constants/layout"; import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet"; import { Shortcut } from "@/components/ui/shortcut"; import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store"; @@ -13,7 +13,7 @@ export function KeyboardShortcutsDialog() { const setOpen = useKeyboardShortcutsStore((s) => s.setShortcutsDialogOpen); const isMac = getShortcutOs() === "mac"; - const isDesktopApp = getIsDesktop(); + const isDesktopApp = getIsElectronRuntime(); const sections = useMemo( () => buildKeyboardShortcutHelpSections({ isMac, isDesktop: isDesktopApp }), [isDesktopApp, isMac], diff --git a/packages/app/src/components/left-sidebar.tsx b/packages/app/src/components/left-sidebar.tsx index 718575dda..246a1bd1a 100644 --- a/packages/app/src/components/left-sidebar.tsx +++ b/packages/app/src/components/left-sidebar.tsx @@ -20,7 +20,7 @@ import Animated, { useSharedValue, } from "react-native-reanimated"; import { Gesture, GestureDetector } from "react-native-gesture-handler"; -import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles"; +import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { MessagesSquare, Plus, Settings } from "lucide-react-native"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Shortcut } from "@/components/ui/shortcut"; @@ -39,7 +39,11 @@ import { useDesktopDragHandlers, useWindowControlsPadding } from "@/utils/deskto import { Combobox } from "@/components/ui/combobox"; import { getHostRuntimeStore, useHosts } from "@/runtime/host-runtime"; import { formatConnectionStatus } from "@/utils/daemons"; -import { HEADER_INNER_HEIGHT, HEADER_INNER_HEIGHT_MOBILE } from "@/constants/layout"; +import { + HEADER_INNER_HEIGHT, + HEADER_INNER_HEIGHT_MOBILE, + isCompactFormFactor, +} from "@/constants/layout"; import { buildHostSessionsRoute, buildHostSettingsRoute, @@ -94,6 +98,7 @@ interface MobileSidebarProps extends SidebarSharedProps { } interface DesktopSidebarProps extends SidebarSharedProps { + insetsTop: number; isOpen: boolean; handleViewMore: () => void; } @@ -105,7 +110,7 @@ export const LeftSidebar = memo(function LeftSidebar({ const { theme } = useUnistyles(); const insets = useSafeAreaInsets(); - const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; + const isCompactLayout = isCompactFormFactor(); const mobileView = usePanelStore((state) => state.mobileView); const desktopAgentListOpen = usePanelStore((state) => state.desktop.agentListOpen); const closeToAgent = usePanelStore((state) => state.closeToAgent); @@ -175,7 +180,7 @@ export const LeftSidebar = memo(function LeftSidebar({ const hostTriggerRef = useRef(null); const [isHostPickerOpen, setIsHostPickerOpen] = useState(false); - const isOpen = isMobile ? mobileView === "agent-list" : desktopAgentListOpen; + const isOpen = isCompactLayout ? mobileView === "agent-list" : desktopAgentListOpen; const { projects, isInitialLoad, isRevalidating, refreshAll } = useSidebarWorkspacesList({ serverId: activeServerId, @@ -265,7 +270,7 @@ export const LeftSidebar = memo(function LeftSidebar({ handleHostSelect, }; - if (isMobile) { + if (isCompactLayout) { return ( + {padding.top > 0 ? : null} diff --git a/packages/app/src/components/sidebar-workspace-list.tsx b/packages/app/src/components/sidebar-workspace-list.tsx index aced08534..c47fe09b5 100644 --- a/packages/app/src/components/sidebar-workspace-list.tsx +++ b/packages/app/src/components/sidebar-workspace-list.tsx @@ -22,7 +22,7 @@ import { } from "react"; import { router, usePathname } from "expo-router"; import { navigateToWorkspace } from "@/hooks/use-workspace-navigation"; -import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles"; +import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { type GestureType } from "react-native-gesture-handler"; import * as Clipboard from "expo-clipboard"; import { @@ -42,7 +42,7 @@ import { NestableScrollContainer } from "react-native-draggable-flatlist"; import { DraggableList, type DraggableRenderItemInfo } from "./draggable-list"; import type { DraggableListDragHandleProps } from "./draggable-list.types"; import { getHostRuntimeStore, isHostRuntimeConnected } from "@/runtime/host-runtime"; -import { getIsDesktop } from "@/constants/layout"; +import { getIsElectronRuntime, isCompactFormFactor } from "@/constants/layout"; import { projectIconQueryKey } from "@/hooks/use-project-icon-query"; import { parseHostWorkspaceRouteFromPathname } from "@/utils/host-routes"; import { prepareWorkspaceTab } from "@/utils/workspace-navigation"; @@ -664,8 +664,7 @@ function ProjectHeaderRow({ dragHandleProps, }: ProjectHeaderRowProps) { const [isHovered, setIsHovered] = useState(false); - const isMobileBreakpoint = - UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; + const isMobileBreakpoint = isCompactFormFactor(); const mergeWorkspaces = useSessionStore((state) => state.mergeWorkspaces); const toast = useToast(); @@ -836,7 +835,7 @@ function WorkspaceRowInner({ }: WorkspaceRowInnerProps) { const { theme } = useUnistyles(); const [isHovered, setIsHovered] = useState(false); - const isMobile = Platform.OS !== "web"; + const isTouchPlatform = Platform.OS !== "web"; const prHint = useWorkspacePrHint({ serverId: workspace.serverId, cwd: workspace.workspaceId, @@ -901,7 +900,7 @@ function WorkspaceRowInner({ {isCreating ? Creating... : null} - {onArchive && (isHovered || isMobile) ? ( + {onArchive && (isHovered || isTouchPlatform) ? ( >>( new Map(), ); - const isDesktopApp = getIsDesktop(); + const isDesktopApp = getIsElectronRuntime(); const altDown = useKeyboardShortcutsStore((state) => state.altDown); const cmdOrCtrlDown = useKeyboardShortcutsStore((state) => state.cmdOrCtrlDown); const showShortcutBadges = altDown || (isDesktopApp && cmdOrCtrlDown); diff --git a/packages/app/src/components/terminal-pane.tsx b/packages/app/src/components/terminal-pane.tsx index 0d8b84a28..df1f27e70 100644 --- a/packages/app/src/components/terminal-pane.tsx +++ b/packages/app/src/components/terminal-pane.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useFocusEffect, useIsFocused } from "@react-navigation/native"; import { ActivityIndicator, Pressable, ScrollView, Text, View } from "react-native"; import Animated, { runOnJS, useAnimatedReaction } from "react-native-reanimated"; -import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles"; +import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { encodeTerminalKeyInput } from "@server/shared/terminal-key-input"; import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime"; import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style"; @@ -21,6 +21,7 @@ import { import { usePanelStore } from "@/stores/panel-store"; import { toXtermTheme } from "@/utils/to-xterm-theme"; import TerminalEmulator, { type TerminalEmulatorHandle } from "./terminal-emulator"; +import { isCompactFormFactor } from "@/constants/layout"; interface TerminalPaneProps { serverId: string; @@ -91,7 +92,7 @@ export function TerminalPane({ const isAppVisible = useAppVisible(); const { theme } = useUnistyles(); const xtermTheme = useMemo(() => toXtermTheme(theme.colors.terminal), [theme.colors.terminal]); - const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; + const isMobile = isCompactFormFactor(); const mobileView = usePanelStore((state) => state.mobileView); const openAgentList = usePanelStore((state) => state.openAgentList); const openFileExplorer = usePanelStore((state) => state.openFileExplorer); diff --git a/packages/app/src/constants/layout.ts b/packages/app/src/constants/layout.ts index fea0e52ea..2a220ffa9 100644 --- a/packages/app/src/constants/layout.ts +++ b/packages/app/src/constants/layout.ts @@ -1,5 +1,6 @@ import { Platform } from "react-native"; -import { isDesktop, isDesktopMac } from "@/desktop/host"; +import { UnistylesRuntime } from "react-native-unistyles"; +import { isElectronRuntime, isElectronRuntimeMac } from "@/desktop/host"; export const FOOTER_HEIGHT = 75; @@ -23,40 +24,59 @@ export const DESKTOP_TRAFFIC_LIGHT_HEIGHT = 45; export const DESKTOP_WINDOW_CONTROLS_WIDTH = 140; export const DESKTOP_WINDOW_CONTROLS_HEIGHT = 48; -// Check if running in desktop app (any OS) -function isDesktopEnvironment(): boolean { +// Check if running in the Electron desktop runtime (any OS) +function isElectronDesktopRuntime(): boolean { if (Platform.OS !== "web") return false; - return isDesktop(); + return isElectronRuntime(); } -// Check if running in desktop host on macOS -function isDesktopEnvironmentMac(): boolean { +// Check if running in the Electron desktop runtime on macOS +function isElectronDesktopRuntimeMac(): boolean { if (Platform.OS !== "web") return false; - return isDesktopMac(); + return isElectronRuntimeMac(); } // Cached result - only cache true, keep checking if false (in case desktop globals load later) -let _isDesktopMacCached: boolean | null = null; -let _isDesktopCached: boolean | null = null; +let _isElectronRuntimeMacCached: boolean | null = null; +let _isElectronRuntimeCached: boolean | null = null; -export function getIsDesktopMac(): boolean { - if (_isDesktopMacCached === true) { +export function getIsElectronRuntimeMac(): boolean { + if (_isElectronRuntimeMacCached === true) { return true; } - const result = isDesktopEnvironmentMac(); + const result = isElectronDesktopRuntimeMac(); if (result) { - _isDesktopMacCached = true; + _isElectronRuntimeMacCached = true; } return result; } -export function getIsDesktop(): boolean { - if (_isDesktopCached === true) { +export function getIsElectronRuntime(): boolean { + if (_isElectronRuntimeCached === true) { return true; } - const result = isDesktopEnvironment(); + const result = isElectronDesktopRuntime(); if (result) { - _isDesktopCached = true; + _isElectronRuntimeCached = true; } return result; } + +export function isCompactFormFactor(): boolean { + return UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; +} + +export function isDesktopFormFactor(): boolean { + return !isCompactFormFactor(); +} + +export function isTouchDesktopFormFactor(): boolean { + return Platform.OS !== "web" && isDesktopFormFactor(); +} + +// SplitContainer relies on dnd-kit and DOM-backed accessibility helpers. +// Keep that capability distinct from desktop-width layout so touch tablets +// can use the desktop shell without entering web-only code paths. +export function supportsDesktopPaneSplits(): boolean { + return Platform.OS === "web"; +} diff --git a/packages/app/src/contexts/explorer-sidebar-animation-context.tsx b/packages/app/src/contexts/explorer-sidebar-animation-context.tsx index 156776a2f..c6e1c3715 100644 --- a/packages/app/src/contexts/explorer-sidebar-animation-context.tsx +++ b/packages/app/src/contexts/explorer-sidebar-animation-context.tsx @@ -2,7 +2,7 @@ import { createContext, useContext, useEffect, useRef, type ReactNode } from "re import { useWindowDimensions } from "react-native"; import { useSharedValue, withTiming, Easing, type SharedValue } from "react-native-reanimated"; import { type GestureType } from "react-native-gesture-handler"; -import { UnistylesRuntime } from "react-native-unistyles"; +import { isCompactFormFactor } from "@/constants/layout"; import { usePanelStore } from "@/stores/panel-store"; import { getRightSidebarAnimationTargets, @@ -27,12 +27,12 @@ const ExplorerSidebarAnimationContext = createContext state.mobileView); const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen); // Derive isOpen from the unified panel state - const isOpen = isMobile ? mobileView === "file-explorer" : desktopFileExplorerOpen; + const isOpen = isCompactLayout ? mobileView === "file-explorer" : desktopFileExplorerOpen; // Right sidebar: closed = +windowWidth (off-screen right), open = 0 const initialTargets = getRightSidebarAnimationTargets({ isOpen, windowWidth }); diff --git a/packages/app/src/contexts/sidebar-animation-context.tsx b/packages/app/src/contexts/sidebar-animation-context.tsx index 473359b27..4e10c0361 100644 --- a/packages/app/src/contexts/sidebar-animation-context.tsx +++ b/packages/app/src/contexts/sidebar-animation-context.tsx @@ -10,7 +10,7 @@ import { import { useWindowDimensions } from "react-native"; import { useSharedValue, withTiming, Easing, type SharedValue } from "react-native-reanimated"; import { type GestureType } from "react-native-gesture-handler"; -import { UnistylesRuntime } from "react-native-unistyles"; +import { isCompactFormFactor } from "@/constants/layout"; import { usePanelStore } from "@/stores/panel-store"; import { getLeftSidebarAnimationTargets, @@ -34,12 +34,12 @@ const SidebarAnimationContext = createContext state.mobileView); const desktopAgentListOpen = usePanelStore((state) => state.desktop.agentListOpen); // Derive isOpen from the unified panel state - const isOpen = isMobile ? mobileView === "agent-list" : desktopAgentListOpen; + const isOpen = isCompactLayout ? mobileView === "agent-list" : desktopAgentListOpen; // Initialize based on current state const initialTargets = getLeftSidebarAnimationTargets({ isOpen, windowWidth }); diff --git a/packages/app/src/desktop/components/desktop-permissions-section.tsx b/packages/app/src/desktop/components/desktop-permissions-section.tsx index dae1a88df..419032e0e 100644 --- a/packages/app/src/desktop/components/desktop-permissions-section.tsx +++ b/packages/app/src/desktop/components/desktop-permissions-section.tsx @@ -9,7 +9,7 @@ import { settingsStyles } from "@/styles/settings"; export function DesktopPermissionsSection() { const { theme } = useUnistyles(); const { - isDesktop, + isDesktopApp, snapshot, isRefreshing, requestingPermission, @@ -20,7 +20,7 @@ export function DesktopPermissionsSection() { sendTestNotification, } = useDesktopPermissions(); - if (!isDesktop) { + if (!isDesktopApp) { return null; } diff --git a/packages/app/src/desktop/daemon/desktop-daemon.ts b/packages/app/src/desktop/daemon/desktop-daemon.ts index ec17b870a..5dbca01d8 100644 --- a/packages/app/src/desktop/daemon/desktop-daemon.ts +++ b/packages/app/src/desktop/daemon/desktop-daemon.ts @@ -1,4 +1,4 @@ -import { getDesktopHost, isDesktop } from "@/desktop/host"; +import { getDesktopHost, isElectronRuntime } from "@/desktop/host"; import { invokeDesktopCommand } from "@/desktop/electron/invoke"; export type DesktopDaemonState = "starting" | "running" | "stopped" | "errored"; @@ -123,7 +123,7 @@ function parseCliSymlinkInstructionsInternal(raw: unknown): CliSymlinkInstructio } export function shouldUseDesktopDaemon(): boolean { - return isDesktop(); + return isElectronRuntime(); } export async function getDesktopDaemonStatus(): Promise { diff --git a/packages/app/src/desktop/host.ts b/packages/app/src/desktop/host.ts index 136dbfb69..775957dd1 100644 --- a/packages/app/src/desktop/host.ts +++ b/packages/app/src/desktop/host.ts @@ -97,12 +97,12 @@ export function getDesktopHost(): DesktopHostBridge | null { return getElectronHost(); } -export function isDesktop(): boolean { +export function isElectronRuntime(): boolean { return getDesktopHost() !== null; } -export function isDesktopMac(): boolean { - if (!isDesktop()) { +export function isElectronRuntimeMac(): boolean { + if (!isElectronRuntime()) { return false; } if (typeof navigator === "undefined") { diff --git a/packages/app/src/desktop/permissions/use-desktop-permissions.ts b/packages/app/src/desktop/permissions/use-desktop-permissions.ts index b2778c6db..551f368cb 100644 --- a/packages/app/src/desktop/permissions/use-desktop-permissions.ts +++ b/packages/app/src/desktop/permissions/use-desktop-permissions.ts @@ -9,7 +9,7 @@ import { import { sendOsNotification } from "@/utils/os-notifications"; export interface UseDesktopPermissionsReturn { - isDesktop: boolean; + isDesktopApp: boolean; snapshot: DesktopPermissionSnapshot | null; isRefreshing: boolean; requestingPermission: DesktopPermissionKind | null; @@ -31,7 +31,7 @@ const EMPTY_MICROPHONE_STATUS = { }; export function useDesktopPermissions(): UseDesktopPermissionsReturn { - const isDesktop = shouldShowDesktopPermissionSection(); + const isDesktopApp = shouldShowDesktopPermissionSection(); const isMountedRef = useRef(true); const [snapshot, setSnapshot] = useState(null); const [isRefreshing, setIsRefreshing] = useState(false); @@ -47,7 +47,7 @@ export function useDesktopPermissions(): UseDesktopPermissionsReturn { }, []); const refreshPermissions = useCallback(async () => { - if (!isDesktop) { + if (!isDesktopApp) { return; } @@ -65,11 +65,11 @@ export function useDesktopPermissions(): UseDesktopPermissionsReturn { setIsRefreshing(false); } } - }, [isDesktop]); + }, [isDesktopApp]); const requestPermission = useCallback( async (kind: DesktopPermissionKind) => { - if (!isDesktop) { + if (!isDesktopApp) { return; } @@ -110,13 +110,13 @@ export function useDesktopPermissions(): UseDesktopPermissionsReturn { await refreshPermissions(); } }, - [isDesktop, refreshPermissions], + [isDesktopApp, refreshPermissions], ); const [testNotificationError, setTestNotificationError] = useState(null); const sendTestNotification = useCallback(async () => { - if (!isDesktop) { + if (!isDesktopApp) { return; } @@ -137,18 +137,18 @@ export function useDesktopPermissions(): UseDesktopPermissionsReturn { setIsSendingTestNotification(false); } } - }, [isDesktop]); + }, [isDesktopApp]); useEffect(() => { - if (!isDesktop) { + if (!isDesktopApp) { return; } void refreshPermissions(); - }, [isDesktop, refreshPermissions]); + }, [isDesktopApp, refreshPermissions]); return { - isDesktop, + isDesktopApp, snapshot, isRefreshing, requestingPermission, diff --git a/packages/app/src/desktop/updates/desktop-updates.ts b/packages/app/src/desktop/updates/desktop-updates.ts index 64da7f256..04d429dec 100644 --- a/packages/app/src/desktop/updates/desktop-updates.ts +++ b/packages/app/src/desktop/updates/desktop-updates.ts @@ -1,5 +1,5 @@ import { Platform } from "react-native"; -import { isDesktop } from "@/desktop/host"; +import { isElectronRuntime } from "@/desktop/host"; import { invokeDesktopCommand } from "@/desktop/electron/invoke"; export interface DesktopAppUpdateCheckResult { @@ -49,7 +49,7 @@ function toNumberOr(defaultValue: number, value: unknown): number { } export function shouldShowDesktopUpdateSection(): boolean { - return Platform.OS === "web" && isDesktop(); + return Platform.OS === "web" && isElectronRuntime(); } export function parseLocalDaemonVersionResult(raw: unknown): LocalDaemonVersionResult { diff --git a/packages/app/src/desktop/updates/update-banner.tsx b/packages/app/src/desktop/updates/update-banner.tsx index 4fe0db78d..82fb40c85 100644 --- a/packages/app/src/desktop/updates/update-banner.tsx +++ b/packages/app/src/desktop/updates/update-banner.tsx @@ -11,7 +11,7 @@ const CHANGELOG_URL = "https://paseo.sh/changelog"; export function UpdateBanner() { const { theme } = useUnistyles(); const { - isDesktop, + isDesktopApp, status, availableUpdate, errorMessage, @@ -23,7 +23,7 @@ export function UpdateBanner() { const intervalRef = useRef | null>(null); useEffect(() => { - if (!isDesktop) return; + if (!isDesktopApp) return; void checkForUpdates({ silent: true }); @@ -36,9 +36,9 @@ export function UpdateBanner() { clearInterval(intervalRef.current); } }; - }, [isDesktop, checkForUpdates]); + }, [isDesktopApp, checkForUpdates]); - if (!isDesktop) return null; + if (!isDesktopApp) return null; if (dismissed) return null; if (status !== "available" && status !== "installed" && status !== "installing" && status !== "error") return null; diff --git a/packages/app/src/desktop/updates/use-desktop-app-updater.ts b/packages/app/src/desktop/updates/use-desktop-app-updater.ts index c50d795b1..6501e01ee 100644 --- a/packages/app/src/desktop/updates/use-desktop-app-updater.ts +++ b/packages/app/src/desktop/updates/use-desktop-app-updater.ts @@ -18,7 +18,7 @@ export type DesktopAppUpdateStatus = | "error"; export interface UseDesktopAppUpdaterReturn { - isDesktop: boolean; + isDesktopApp: boolean; status: DesktopAppUpdateStatus; statusText: string; availableUpdate: DesktopAppUpdateCheckResult | null; @@ -75,7 +75,7 @@ function formatStatusText(input: { } export function useDesktopAppUpdater(): UseDesktopAppUpdaterReturn { - const isDesktop = shouldShowDesktopUpdateSection(); + const isDesktopApp = shouldShowDesktopUpdateSection(); const requestVersionRef = useRef(0); const [status, setStatus] = useState("idle"); const [availableUpdate, setAvailableUpdate] = useState(null); @@ -85,7 +85,7 @@ export function useDesktopAppUpdater(): UseDesktopAppUpdaterReturn { const checkForUpdates = useCallback( async (options: { silent?: boolean } = {}) => { - if (!isDesktop) { + if (!isDesktopApp) { return null; } @@ -130,11 +130,11 @@ export function useDesktopAppUpdater(): UseDesktopAppUpdaterReturn { return null; } }, - [isDesktop], + [isDesktopApp], ); const installUpdate = useCallback(async () => { - if (!isDesktop) { + if (!isDesktopApp) { return null; } @@ -162,10 +162,10 @@ export function useDesktopAppUpdater(): UseDesktopAppUpdaterReturn { setErrorMessage(message); return null; } - }, [isDesktop]); + }, [isDesktopApp]); return { - isDesktop, + isDesktopApp, status, statusText: formatStatusText({ status, diff --git a/packages/app/src/hooks/use-audio-recorder.web.ts b/packages/app/src/hooks/use-audio-recorder.web.ts index c2b7e5047..170b44a36 100644 --- a/packages/app/src/hooks/use-audio-recorder.web.ts +++ b/packages/app/src/hooks/use-audio-recorder.web.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { AttemptCancelledError, AttemptGuard } from "@/utils/attempt-guard"; -import { isDesktop } from "@/desktop/host"; +import { isElectronRuntime } from "@/desktop/host"; export interface AudioCaptureConfig { sampleRate?: number; @@ -157,7 +157,7 @@ export function useAudioRecorder(config?: AudioCaptureConfig) { : true; const currentOrigin = typeof window !== "undefined" && window.location ? window.location.origin : "unknown"; - const isDesktopApp = isDesktop(); + const isDesktopApp = isElectronRuntime(); if (missingNavigator) { throw new Error("Microphone capture is not supported in this environment"); diff --git a/packages/app/src/hooks/use-command-center.ts b/packages/app/src/hooks/use-command-center.ts index 6c4857e1b..ceecac46b 100644 --- a/packages/app/src/hooks/use-command-center.ts +++ b/packages/app/src/hooks/use-command-center.ts @@ -17,7 +17,7 @@ import { chordStringToShortcutKeys } from "@/keyboard/shortcut-string"; import { getBindingIdForAction, getDefaultKeysForAction } from "@/keyboard/keyboard-shortcuts"; import { useKeyboardShortcutOverrides } from "@/hooks/use-keyboard-shortcut-overrides"; import { getShortcutOs } from "@/utils/shortcut-platform"; -import { getIsDesktop } from "@/constants/layout"; +import { getIsElectronRuntime } from "@/constants/layout"; import { prepareWorkspaceTab } from "@/utils/workspace-navigation"; import { focusWithRetries } from "@/utils/web-focus"; @@ -110,8 +110,8 @@ function resolveActionShortcutKeys( ): ShortcutKey[][] | undefined { if (!actionId) return undefined; const isMac = getShortcutOs() === "mac"; - const isDesktop = getIsDesktop(); - const platform = { isMac, isDesktop }; + const isDesktopApp = getIsElectronRuntime(); + const platform = { isMac, isDesktop: isDesktopApp }; const bindingId = getBindingIdForAction(actionId, platform); if (!bindingId) return undefined; const override = overrides[bindingId]; diff --git a/packages/app/src/hooks/use-dictation-audio-source.web.ts b/packages/app/src/hooks/use-dictation-audio-source.web.ts index 51800086f..b982597de 100644 --- a/packages/app/src/hooks/use-dictation-audio-source.web.ts +++ b/packages/app/src/hooks/use-dictation-audio-source.web.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { parsePcm16Wav } from "@/utils/pcm16-wav"; -import { isDesktop } from "@/desktop/host"; +import { isElectronRuntime } from "@/desktop/host"; import type { DictationAudioSource, @@ -168,7 +168,7 @@ export function useDictationAudioSource(config: DictationAudioSourceConfig): Dic : true; const currentOrigin = typeof window !== "undefined" && window.location ? window.location.origin : "unknown"; - const isDesktopApp = isDesktop(); + const isDesktopApp = isElectronRuntime(); if (missingNavigator) { throw new Error("Microphone capture is not supported in this environment"); diff --git a/packages/app/src/hooks/use-favicon-status.ts b/packages/app/src/hooks/use-favicon-status.ts index ab2d4470b..ebe47890e 100644 --- a/packages/app/src/hooks/use-favicon-status.ts +++ b/packages/app/src/hooks/use-favicon-status.ts @@ -1,6 +1,6 @@ import { useEffect, useRef, useState } from "react"; import { Platform } from "react-native"; -import { getIsDesktopMac } from "@/constants/layout"; +import { getIsElectronRuntimeMac } from "@/constants/layout"; import { useAggregatedAgents } from "./use-aggregated-agents"; import { getDesktopHost } from "@/desktop/host"; @@ -96,7 +96,7 @@ function getSystemColorScheme(): ColorScheme { } async function updateMacDockBadge(count?: number) { - if (Platform.OS !== "web" || !getIsDesktopMac()) return; + if (Platform.OS !== "web" || !getIsElectronRuntimeMac()) return; const desktopWindow = getDesktopHost()?.window?.getCurrentWindow?.(); if (!desktopWindow || typeof desktopWindow.setBadgeCount !== "function") { diff --git a/packages/app/src/hooks/use-image-attachment-picker.ts b/packages/app/src/hooks/use-image-attachment-picker.ts index fed8894fc..13009a44e 100644 --- a/packages/app/src/hooks/use-image-attachment-picker.ts +++ b/packages/app/src/hooks/use-image-attachment-picker.ts @@ -2,7 +2,7 @@ import { useCallback, useRef } from "react"; import { Alert } from "react-native"; import { Platform } from "react-native"; import * as ImagePicker from "expo-image-picker"; -import { isDesktop } from "@/desktop/host"; +import { isElectronRuntime } from "@/desktop/host"; import { normalizePickedImageAssets, openImagePathsWithDesktopDialog, @@ -45,7 +45,7 @@ export function useImageAttachmentPicker(): UseImageAttachmentPickerResult { isPickingRef.current = true; try { - if (Platform.OS === "web" && isDesktop()) { + if (Platform.OS === "web" && isElectronRuntime()) { const selectedPaths = await openImagePathsWithDesktopDialog(); if (selectedPaths.length === 0) { return null; diff --git a/packages/app/src/hooks/use-is-local-daemon.ts b/packages/app/src/hooks/use-is-local-daemon.ts index 63c4afb69..2557779a3 100644 --- a/packages/app/src/hooks/use-is-local-daemon.ts +++ b/packages/app/src/hooks/use-is-local-daemon.ts @@ -17,12 +17,12 @@ async function loadDesktopDaemonServerId(): Promise export function useIsLocalDaemon(serverId: string): boolean { const normalizedServerId = serverId.trim(); - const isDesktop = shouldUseDesktopDaemon(); + const isDesktopApp = shouldUseDesktopDaemon(); const query = useQuery({ queryKey: DESKTOP_DAEMON_SERVER_ID_QUERY_KEY, queryFn: loadDesktopDaemonServerId, - enabled: isDesktop, + enabled: isDesktopApp, staleTime: Infinity, gcTime: Infinity, refetchInterval: (query) => (query.state.data?.serverId ? false : 1000), @@ -32,7 +32,7 @@ export function useIsLocalDaemon(serverId: string): boolean { retry: false, }); - if (!isDesktop || normalizedServerId.length === 0) { + if (!isDesktopApp || normalizedServerId.length === 0) { return false; } diff --git a/packages/app/src/hooks/use-keyboard-shortcuts.ts b/packages/app/src/hooks/use-keyboard-shortcuts.ts index 2ec127b7a..c014f828c 100644 --- a/packages/app/src/hooks/use-keyboard-shortcuts.ts +++ b/packages/app/src/hooks/use-keyboard-shortcuts.ts @@ -1,7 +1,7 @@ import { useEffect, useMemo, useRef } from "react"; import { Platform } from "react-native"; import { usePathname } from "expo-router"; -import { getIsDesktop } from "@/constants/layout"; +import { getIsElectronRuntime } from "@/constants/layout"; import { useHosts } from "@/runtime/host-runtime"; import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store"; import { setCommandCenterFocusRestoreElement } from "@/utils/command-center-focus-restore"; @@ -66,7 +66,7 @@ export function useKeyboardShortcuts({ if (Platform.OS !== "web") return; if (isMobile) return; - const isDesktopApp = getIsDesktop(); + const isDesktopApp = getIsElectronRuntime(); const isMac = getShortcutOs() === "mac"; const shouldHandle = () => { diff --git a/packages/app/src/hooks/use-shortcut-keys.ts b/packages/app/src/hooks/use-shortcut-keys.ts index dd43ae9fc..4ed2a5dba 100644 --- a/packages/app/src/hooks/use-shortcut-keys.ts +++ b/packages/app/src/hooks/use-shortcut-keys.ts @@ -4,15 +4,15 @@ import { chordStringToShortcutKeys } from "@/keyboard/shortcut-string"; import { getBindingIdForAction, getDefaultKeysForAction } from "@/keyboard/keyboard-shortcuts"; import { useKeyboardShortcutOverrides } from "@/hooks/use-keyboard-shortcut-overrides"; import { getShortcutOs } from "@/utils/shortcut-platform"; -import { getIsDesktop } from "@/constants/layout"; +import { getIsElectronRuntime } from "@/constants/layout"; export function useShortcutKeys(actionId: string): ShortcutKey[][] | null { const { overrides } = useKeyboardShortcutOverrides(); const isMac = getShortcutOs() === "mac"; - const isDesktop = getIsDesktop(); + const isDesktopApp = getIsElectronRuntime(); return useMemo(() => { - const platform = { isMac, isDesktop }; + const platform = { isMac, isDesktop: isDesktopApp }; const bindingId = getBindingIdForAction(actionId, platform); if (!bindingId) return null; @@ -23,5 +23,5 @@ export function useShortcutKeys(actionId: string): ShortcutKey[][] | null { const defaultKeys = getDefaultKeysForAction(actionId, platform); return defaultKeys ? [defaultKeys] : null; - }, [actionId, overrides, isMac, isDesktop]); + }, [actionId, overrides, isMac, isDesktopApp]); } diff --git a/packages/app/src/screens/agent/draft-agent-screen.tsx b/packages/app/src/screens/agent/draft-agent-screen.tsx index eb175764d..a38fe98a4 100644 --- a/packages/app/src/screens/agent/draft-agent-screen.tsx +++ b/packages/app/src/screens/agent/draft-agent-screen.tsx @@ -4,7 +4,7 @@ import type { ImageAttachment } from "@/components/message-input"; import { View, Text, Pressable, ScrollView, Keyboard, Platform } from "react-native"; import { useLocalSearchParams, useRouter } from "expo-router"; import { useIsFocused } from "@react-navigation/native"; -import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles"; +import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { GestureDetector } from "react-native-gesture-handler"; import Animated from "react-native-reanimated"; @@ -40,7 +40,7 @@ import { } from "@/runtime/host-runtime"; import { ExplorerSidebarAnimationProvider } from "@/contexts/explorer-sidebar-animation-context"; import { usePanelStore, type ExplorerCheckoutContext } from "@/stores/panel-store"; -import { MAX_CONTENT_WIDTH } from "@/constants/layout"; +import { MAX_CONTENT_WIDTH, isCompactFormFactor } from "@/constants/layout"; import { WelcomeScreen } from "@/components/welcome-screen"; import type { Agent } from "@/contexts/session-context"; import { encodeImages } from "@/utils/encode-images"; @@ -233,7 +233,7 @@ function DraftAgentScreenContent({ isCreateFlow: true, onlineServerIds, }); - const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; + const isMobile = isCompactFormFactor(); const mobileView = usePanelStore((state) => state.mobileView); const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen); const toggleFileExplorer = usePanelStore((state) => state.toggleFileExplorer); diff --git a/packages/app/src/screens/open-project-screen.tsx b/packages/app/src/screens/open-project-screen.tsx index 4662ae816..f401de1c8 100644 --- a/packages/app/src/screens/open-project-screen.tsx +++ b/packages/app/src/screens/open-project-screen.tsx @@ -1,6 +1,6 @@ import { useEffect } from "react"; import { View, Text } from "react-native"; -import { StyleSheet, UnistylesRuntime } from "react-native-unistyles"; +import { StyleSheet } from "react-native-unistyles"; import { FolderOpen } from "lucide-react-native"; import { PaseoLogo } from "@/components/icons/paseo-logo"; import { Button } from "@/components/ui/button"; @@ -8,6 +8,7 @@ import { MenuHeader } from "@/components/headers/menu-header"; import { useOpenProjectPicker } from "@/hooks/use-open-project-picker"; import { usePanelStore } from "@/stores/panel-store"; import { useSessionStore } from "@/stores/session-store"; +import { isCompactFormFactor } from "@/constants/layout"; import { useDesktopDragHandlers } from "@/utils/desktop-window"; export function OpenProjectScreen({ serverId }: { serverId: string }) { @@ -16,14 +17,14 @@ export function OpenProjectScreen({ serverId }: { serverId: string }) { const hasHydrated = useSessionStore((s) => s.sessions[serverId]?.hasHydratedWorkspaces ?? false); const hasProjects = useSessionStore((s) => (s.sessions[serverId]?.workspaces?.size ?? 0) > 0); - const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; + const isCompactLayout = isCompactFormFactor(); const dragHandlers = useDesktopDragHandlers(); useEffect(() => { - if (!isMobile) { + if (!isCompactLayout) { openAgentList(); } - }, [isMobile, openAgentList]); + }, [isCompactLayout, openAgentList]); return ( diff --git a/packages/app/src/screens/settings-screen.tsx b/packages/app/src/screens/settings-screen.tsx index 25f7cb71d..c2e8235f6 100644 --- a/packages/app/src/screens/settings-screen.tsx +++ b/packages/app/src/screens/settings-screen.tsx @@ -4,7 +4,7 @@ import { View, Text, ScrollView, Alert, Platform, Pressable } from "react-native import { router, useLocalSearchParams } from "expo-router"; import { useFocusEffect } from "@react-navigation/native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles"; +import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { Buffer } from "buffer"; import { Sun, @@ -51,7 +51,7 @@ import { import { AdaptiveModalSheet, AdaptiveTextInput } from "@/components/adaptive-modal-sheet"; import { DesktopPermissionsSection } from "@/desktop/components/desktop-permissions-section"; import { LocalDaemonSection } from "@/desktop/components/desktop-updates-section"; -import { isDesktop as isDesktopHost } from "@/desktop/host"; +import { isElectronRuntime } from "@/desktop/host"; import { useDesktopAppUpdater } from "@/desktop/updates/use-desktop-app-updater"; import { formatVersionWithPrefix } from "@/desktop/updates/desktop-updates"; import { resolveAppVersion } from "@/utils/app-version"; @@ -59,6 +59,7 @@ import { settingsStyles } from "@/styles/settings"; import { THINKING_TONE_NATIVE_PCM_BASE64 } from "@/utils/thinking-tone.native-pcm"; import { useVoiceAudioEngineOptional } from "@/contexts/voice-context"; import { useIsLocalDaemon } from "@/hooks/use-is-local-daemon"; +import { isCompactFormFactor } from "@/constants/layout"; // --------------------------------------------------------------------------- // Section definitions @@ -79,7 +80,7 @@ interface SettingsSectionDef { icon: ComponentType<{ size: number; color: string }>; } -function getSettingsSections(context: { isDesktop: boolean }): SettingsSectionDef[] { +function getSettingsSections(context: { isDesktopApp: boolean }): SettingsSectionDef[] { const sections: SettingsSectionDef[] = [ { id: "hosts", label: "Hosts", icon: Server }, { id: "appearance", label: "Appearance", icon: Palette }, @@ -88,7 +89,7 @@ function getSettingsSections(context: { isDesktop: boolean }): SettingsSectionDe { id: "about", label: "About", icon: Info }, ]; - if (context.isDesktop) { + if (context.isDesktopApp) { sections.push( { id: "permissions", label: "Permissions", icon: Shield }, { id: "daemon", label: "Daemon", icon: Settings }, @@ -174,7 +175,6 @@ interface HostsSectionProps { daemons: HostProfile[]; settings: AppSettings; routeServerId: string; - isDesktop: boolean; theme: ReturnType["theme"]; handleEditDaemon: (profile: HostProfile) => void; setAddConnectionTargetServerId: (id: string | null) => void; @@ -464,10 +464,10 @@ function DiagnosticsSection({ interface AboutSectionProps { appVersionText: string; - isDesktop: boolean; + isDesktopApp: boolean; } -function AboutSection({ appVersionText, isDesktop }: AboutSectionProps) { +function AboutSection({ appVersionText, isDesktopApp }: AboutSectionProps) { return ( About @@ -478,7 +478,7 @@ function AboutSection({ appVersionText, isDesktop }: AboutSectionProps) { {appVersionText} - {isDesktop ? : null} + {isDesktopApp ? : null} ); @@ -496,7 +496,7 @@ interface SettingsSectionContentProps { aboutProps: AboutSectionProps; appVersion: string | null; isLocalDaemon: boolean; - isDesktop: boolean; + isDesktopApp: boolean; } function SettingsSectionContent({ @@ -507,7 +507,7 @@ function SettingsSectionContent({ aboutProps, appVersion, isLocalDaemon, - isDesktop, + isDesktopApp, }: SettingsSectionContentProps) { switch (sectionId) { case "hosts": @@ -521,9 +521,9 @@ function SettingsSectionContent({ case "about": return ; case "permissions": - return isDesktop ? : null; + return isDesktopApp ? : null; case "daemon": - return isDesktop ? ( + return isDesktopApp ? ( ) : null; } @@ -619,7 +619,7 @@ function SettingsDesktopLayout({ sections, sectionContentProps }: SettingsLayout function DesktopAppUpdateRow() { const { - isDesktop, + isDesktopApp, statusText, availableUpdate, errorMessage, @@ -631,23 +631,23 @@ function DesktopAppUpdateRow() { useFocusEffect( useCallback(() => { - if (!isDesktop) { + if (!isDesktopApp) { return undefined; } void checkForUpdates({ silent: true }); return undefined; - }, [checkForUpdates, isDesktop]), + }, [checkForUpdates, isDesktopApp]), ); const handleCheckForUpdates = useCallback(() => { - if (!isDesktop) { + if (!isDesktopApp) { return; } void checkForUpdates(); - }, [checkForUpdates, isDesktop]); + }, [checkForUpdates, isDesktopApp]); const handleInstallUpdate = useCallback(() => { - if (!isDesktop) { + if (!isDesktopApp) { return; } @@ -667,9 +667,9 @@ function DesktopAppUpdateRow() { console.error("[Settings] Failed to open app update confirmation", error); Alert.alert("Error", "Unable to open the update confirmation dialog."); }); - }, [installUpdate, isDesktop]); + }, [installUpdate, isDesktopApp]); - if (!isDesktop) { + if (!isDesktopApp) { return null; } @@ -745,7 +745,7 @@ export default function SettingsScreen() { const isLoading = settingsLoading; const isMountedRef = useRef(true); const lastHandledEditHostRef = useRef(null); - const isDesktop = isDesktopHost(); + const isDesktopApp = isElectronRuntime(); const isLocalDaemon = useIsLocalDaemon(routeServerId); const appVersion = resolveAppVersion(); const appVersionText = formatVersionWithPrefix(appVersion); @@ -929,14 +929,13 @@ export default function SettingsScreen() { } }, [isPlaybackTestRunning, voiceAudioEngine]); - const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; - const sections = getSettingsSections({ isDesktop }); + const isCompactLayout = isCompactFormFactor(); + const sections = getSettingsSections({ isDesktopApp }); const hostsProps: HostsSectionProps = { daemons, settings, routeServerId, - isDesktop, theme, handleEditDaemon, setAddConnectionTargetServerId, @@ -985,7 +984,7 @@ export default function SettingsScreen() { const aboutProps: AboutSectionProps = { appVersionText, - isDesktop, + isDesktopApp, }; const sectionContentProps: Omit = { @@ -995,7 +994,7 @@ export default function SettingsScreen() { aboutProps, appVersion, isLocalDaemon, - isDesktop, + isDesktopApp, }; if (isLoading) { @@ -1009,7 +1008,7 @@ export default function SettingsScreen() { return ( - {isMobile ? ( + {isCompactLayout ? ( ) : ( diff --git a/packages/app/src/screens/settings/keyboard-shortcuts-section.tsx b/packages/app/src/screens/settings/keyboard-shortcuts-section.tsx index 206c28c2d..796b3f8cc 100644 --- a/packages/app/src/screens/settings/keyboard-shortcuts-section.tsx +++ b/packages/app/src/screens/settings/keyboard-shortcuts-section.tsx @@ -17,7 +17,7 @@ import { } from "@/keyboard/shortcut-string"; import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store"; import { getShortcutOs } from "@/utils/shortcut-platform"; -import { getIsDesktop } from "@/constants/layout"; +import { getIsElectronRuntime } from "@/constants/layout"; function ShortcutSequence({ chord }: { chord: string[] | null }) { if (!chord || chord.length === 0) { @@ -93,8 +93,8 @@ export function KeyboardShortcutsSection() { const setCapturingShortcut = useKeyboardShortcutsStore((s) => s.setCapturingShortcut); const isMac = getShortcutOs() === "mac"; - const isDesktop = getIsDesktop(); - const sections = buildKeyboardShortcutHelpSections({ isMac, isDesktop }); + const isDesktopApp = getIsElectronRuntime(); + const sections = buildKeyboardShortcutHelpSections({ isMac, isDesktop: isDesktopApp }); function cancelCapture() { setCapturedCombos([]); @@ -180,7 +180,10 @@ export function KeyboardShortcutsSection() { {section.title} {section.rows.map(function (row, index) { - const bindingId = getBindingIdForAction(row.id, { isMac, isDesktop }); + const bindingId = getBindingIdForAction(row.id, { + isMac, + isDesktop: isDesktopApp, + }); const overrideCombo = bindingId ? overrides[bindingId] : undefined; return ( diff --git a/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx b/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx index fe69bc008..595be3647 100644 --- a/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx +++ b/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx @@ -83,6 +83,7 @@ type WorkspaceDesktopTabsRowProps = { externalDndContext?: boolean; activeDragTabId?: string | null; tabDropPreviewIndex?: number | null; + showPaneSplitActions?: boolean; }; function getFallbackTabLabel(tab: WorkspaceTabDescriptor): string { @@ -346,6 +347,7 @@ export function WorkspaceDesktopTabsRow({ externalDndContext = false, activeDragTabId = null, tabDropPreviewIndex = null, + showPaneSplitActions = true, }: WorkspaceDesktopTabsRowProps) { const { theme } = useUnistyles(); const newAgentTabKeys = useShortcutKeys("workspace-tab-new"); @@ -522,48 +524,52 @@ export function WorkspaceDesktopTabsRow({ - - [ - styles.newTabActionButton, - (hovered || pressed) && styles.newTabActionButtonHovered, - ]} - > - - - - - Split pane right - {splitRightKeys ? ( - - ) : null} - - - - - [ - styles.newTabActionButton, - (hovered || pressed) && styles.newTabActionButtonHovered, - ]} - > - - - - - Split pane down - {splitDownKeys ? ( - - ) : null} - - - + {showPaneSplitActions ? ( + <> + + [ + styles.newTabActionButton, + (hovered || pressed) && styles.newTabActionButtonHovered, + ]} + > + + + + + Split pane right + {splitRightKeys ? ( + + ) : null} + + + + + [ + styles.newTabActionButton, + (hovered || pressed) && styles.newTabActionButtonHovered, + ]} + > + + + + + Split pane down + {splitDownKeys ? ( + + ) : null} + + + + + ) : null} ); diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx index a6311e0f5..9ccb82a7f 100644 --- a/packages/app/src/screens/workspace/workspace-screen.tsx +++ b/packages/app/src/screens/workspace/workspace-screen.tsx @@ -29,7 +29,7 @@ import { import { GestureDetector } from "react-native-gesture-handler"; import Animated from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles"; +import { StyleSheet, useUnistyles } from "react-native-unistyles"; import invariant from "tiny-invariant"; import { SidebarMenuToggle } from "@/components/headers/menu-header"; import { HeaderToggleButton } from "@/components/headers/header-toggle-button"; @@ -86,6 +86,10 @@ import { WorkspaceTabIcon, WorkspaceTabOptionRow, } from "@/screens/workspace/workspace-tab-presentation"; +import { + WorkspaceDesktopTabsRow, + type WorkspaceDesktopTabRowItem, +} from "@/screens/workspace/workspace-desktop-tabs-row"; import { buildWorkspaceTabMenuEntries } from "@/screens/workspace/workspace-tab-menu"; import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types"; import { @@ -110,6 +114,7 @@ import { closeBulkWorkspaceTabs, } from "@/screens/workspace/workspace-bulk-close"; import { findAdjacentPane } from "@/utils/split-navigation"; +import { isCompactFormFactor, supportsDesktopPaneSplits } from "@/constants/layout"; const TERMINALS_QUERY_STALE_TIME = 5_000; const NEW_TAB_AGENT_OPTION_ID = "__new_tab_agent__"; @@ -576,7 +581,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps) const isDarkMode = useColorScheme() === "dark"; const mainBackgroundColor = isDarkMode ? theme.colors.surface1 : theme.colors.surface0; const toast = useToast(); - const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; + const isMobile = isCompactFormFactor(); const isFocusModeEnabled = usePanelStore((state) => state.desktop.focusModeEnabled); const normalizedServerId = trimNonEmpty(decodeSegment(serverId)) ?? ""; @@ -1703,6 +1708,8 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps) }); const activeTabDescriptor = activeTab?.descriptor ?? null; + const canRenderDesktopPaneSplits = supportsDesktopPaneSplits(); + const shouldRenderDesktopPaneFallback = !isMobile && !canRenderDesktopPaneSplits; useEffect(() => { if (Platform.OS !== "web" || typeof document === "undefined" || activeTabDescriptor) { return; @@ -1881,6 +1888,17 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps) [buildPaneContentModel], ); + const desktopTabRowItems = useMemo( + () => + tabs.map((tab) => ({ + tab, + isActive: tab.tabId === activeTabDescriptor?.tabId, + isCloseHovered: hoveredCloseTabKey === tab.key, + isClosingTab: closingTabIds.has(tab.tabId), + })), + [activeTabDescriptor?.tabId, closingTabIds, hoveredCloseTabKey, tabs], + ); + const handleFocusPane = useStableEvent(function handleFocusPane(paneId: string) { if (!persistenceKey || paneFocusSuppressedRef.current) { return; @@ -1932,6 +1950,19 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps) [persistenceKey, reorderWorkspaceTabsInPane], ); + const handleReorderTabsInFocusedPane = useCallback( + (nextTabs: WorkspaceTabDescriptor[]) => { + if (!focusedPaneId) { + return; + } + handleReorderTabsInPane( + focusedPaneId, + nextTabs.map((tab) => tab.tabId), + ); + }, + [focusedPaneId, handleReorderTabsInPane], + ); + const renderSplitPaneEmptyState = useCallback(function renderSplitPaneEmptyState() { return ( @@ -2187,6 +2218,32 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps) /> ) : null} + {shouldRenderDesktopPaneFallback ? ( + {}} + onSplitDown={() => {}} + showPaneSplitActions={false} + /> + ) : null} + {isMobile ? ( @@ -2194,7 +2251,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps) ) : ( - {workspaceLayout && persistenceKey ? ( + {canRenderDesktopPaneSplits && workspaceLayout && persistenceKey ? ( { if (!isActive) return; @@ -136,7 +136,7 @@ function useRawWindowControlsPadding(): RawWindowControlsPadding { const [isFullscreen, setIsFullscreen] = useState(false); useEffect(() => { - if (Platform.OS !== "web" || !getIsDesktop()) return; + if (Platform.OS !== "web" || !getIsElectronRuntime()) return; let disposed = false; let cleanup: (() => void) | undefined; @@ -188,11 +188,11 @@ function useRawWindowControlsPadding(): RawWindowControlsPadding { }, []); return useMemo((): RawWindowControlsPadding => { - if (!getIsDesktop() || isFullscreen) { + if (!getIsElectronRuntime() || isFullscreen) { return { left: 0, right: 0, top: 0 }; } - if (getIsDesktopMac()) { + if (getIsElectronRuntimeMac()) { return { left: DESKTOP_TRAFFIC_LIGHT_WIDTH, right: 0, diff --git a/packages/app/src/utils/shortcut-platform.ts b/packages/app/src/utils/shortcut-platform.ts index 9f4abad66..11a187f37 100644 --- a/packages/app/src/utils/shortcut-platform.ts +++ b/packages/app/src/utils/shortcut-platform.ts @@ -1,12 +1,12 @@ import { Platform } from "react-native"; -import { getIsDesktopMac } from "@/constants/layout"; +import { getIsElectronRuntimeMac } from "@/constants/layout"; import type { ShortcutOs } from "@/utils/format-shortcut"; export function getShortcutOs(): ShortcutOs { if (Platform.OS !== "web") { return Platform.OS === "ios" ? "mac" : "non-mac"; } - if (getIsDesktopMac()) return "mac"; + if (getIsElectronRuntimeMac()) return "mac"; if (typeof navigator === "undefined") return "non-mac"; const ua = navigator.userAgent ?? ""; const platform = (navigator as any).platform ?? ""; diff --git a/packages/app/src/voice/audio-engine.web.ts b/packages/app/src/voice/audio-engine.web.ts index ea3da68a2..e377c4f36 100644 --- a/packages/app/src/voice/audio-engine.web.ts +++ b/packages/app/src/voice/audio-engine.web.ts @@ -1,4 +1,4 @@ -import { isDesktop } from "@/desktop/host"; +import { isElectronRuntime } from "@/desktop/host"; import type { AudioEngine, AudioEngineCallbacks, @@ -289,7 +289,7 @@ export function createAudioEngine( : true; const currentOrigin = typeof window !== "undefined" && window.location ? window.location.origin : "unknown"; - const isDesktopApp = isDesktop(); + const isDesktopApp = isElectronRuntime(); if (missingNavigator) { throw new Error("Microphone capture is not supported in this environment");