fix(app): support ipad desktop layout safely

This commit is contained in:
Mohamed Boudra
2026-04-01 13:37:51 +07:00
parent f53c770a71
commit 51411182fe
36 changed files with 299 additions and 202 deletions
+9 -7
View File
@@ -57,7 +57,7 @@ import {
HorizontalScrollProvider, HorizontalScrollProvider,
useHorizontalScrollOptional, useHorizontalScrollOptional,
} from "@/contexts/horizontal-scroll-context"; } from "@/contexts/horizontal-scroll-context";
import { getIsDesktop } from "@/constants/layout"; import { getIsElectronRuntime, isCompactFormFactor } from "@/constants/layout";
import { CommandCenter } from "@/components/command-center"; import { CommandCenter } from "@/components/command-center";
import { ProjectPickerModal } from "@/components/project-picker-modal"; import { ProjectPickerModal } from "@/components/project-picker-modal";
import { KeyboardShortcutsDialog } from "@/components/keyboard-shortcuts-dialog"; import { KeyboardShortcutsDialog } from "@/components/keyboard-shortcuts-dialog";
@@ -103,7 +103,7 @@ function PushNotificationRouter() {
let removeDesktopNotificationListener: (() => void) | null = null; let removeDesktopNotificationListener: (() => void) | null = null;
let cancelled = false; let cancelled = false;
if (getIsDesktop()) { if (getIsElectronRuntime()) {
void ensureOsNotificationPermission(); void ensureOsNotificationPermission();
const unlistenResult = getDesktopHost()?.events?.on?.( const unlistenResult = getDesktopHost()?.events?.on?.(
@@ -342,12 +342,12 @@ function AppContainer({
const toggleFocusMode = usePanelStore((state) => state.toggleFocusMode); const toggleFocusMode = usePanelStore((state) => state.toggleFocusMode);
const isFocusModeEnabled = usePanelStore((state) => state.desktop.focusModeEnabled); const isFocusModeEnabled = usePanelStore((state) => state.desktop.focusModeEnabled);
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; const isCompactLayout = isCompactFormFactor();
const chromeEnabled = chromeEnabledOverride ?? daemons.length > 0; const chromeEnabled = chromeEnabledOverride ?? daemons.length > 0;
useKeyboardShortcuts({ useKeyboardShortcuts({
enabled: chromeEnabled, enabled: chromeEnabled,
isMobile, isMobile: isCompactLayout,
toggleAgentList, toggleAgentList,
selectedAgentId, selectedAgentId,
toggleFileExplorer, toggleFileExplorer,
@@ -363,10 +363,12 @@ function AppContainer({
const content = ( const content = (
<View style={containerStyle}> <View style={containerStyle}>
<View style={rowStyle}> <View style={rowStyle}>
{!isMobile && chromeEnabled && !isFocusModeEnabled && <LeftSidebar selectedAgentId={selectedAgentId} />} {!isCompactLayout && chromeEnabled && !isFocusModeEnabled && (
<LeftSidebar selectedAgentId={selectedAgentId} />
)}
<View style={flexStyle}>{children}</View> <View style={flexStyle}>{children}</View>
</View> </View>
{isMobile && chromeEnabled && <LeftSidebar selectedAgentId={selectedAgentId} />} {isCompactLayout && chromeEnabled && <LeftSidebar selectedAgentId={selectedAgentId} />}
<DownloadToast /> <DownloadToast />
<UpdateBanner /> <UpdateBanner />
<CommandCenter /> <CommandCenter />
@@ -375,7 +377,7 @@ function AppContainer({
</View> </View>
); );
if (!isMobile) { if (!isCompactLayout) {
return content; return content;
} }
+2 -2
View File
@@ -1,12 +1,12 @@
import { Platform } from "react-native"; import { Platform } from "react-native";
import { isDesktop } from "@/desktop/host"; import { isElectronRuntime } from "@/desktop/host";
import type { AttachmentStore } from "@/attachments/types"; import type { AttachmentStore } from "@/attachments/types";
let attachmentStorePromise: Promise<AttachmentStore> | null = null; let attachmentStorePromise: Promise<AttachmentStore> | null = null;
async function createAttachmentStore(): Promise<AttachmentStore> { async function createAttachmentStore(): Promise<AttachmentStore> {
if (Platform.OS === "web") { if (Platform.OS === "web") {
if (isDesktop()) { if (isElectronRuntime()) {
const { createDesktopAttachmentStore } = await import( const { createDesktopAttachmentStore } = await import(
"../desktop/attachments/desktop-attachment-store" "../desktop/attachments/desktop-attachment-store"
); );
@@ -4,7 +4,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useIsFocused } from "@react-navigation/native"; import { useIsFocused } from "@react-navigation/native";
import Animated, { useAnimatedStyle, useSharedValue, runOnJS } from "react-native-reanimated"; import Animated, { useAnimatedStyle, useSharedValue, runOnJS } from "react-native-reanimated";
import { Gesture, GestureDetector } from "react-native-gesture-handler"; 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 { X } from "lucide-react-native";
import { import {
usePanelStore, usePanelStore,
@@ -13,7 +13,7 @@ import {
type ExplorerTab, type ExplorerTab,
} from "@/stores/panel-store"; } from "@/stores/panel-store";
import { useExplorerSidebarAnimation } from "@/contexts/explorer-sidebar-animation-context"; 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 { GitDiffPane } from "./git-diff-pane";
import { FileExplorerPane } from "./file-explorer-pane"; import { FileExplorerPane } from "./file-explorer-pane";
import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style"; import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
@@ -40,7 +40,7 @@ export function ExplorerSidebar({
const { theme } = useUnistyles(); const { theme } = useUnistyles();
const isScreenFocused = useIsFocused(); const isScreenFocused = useIsFocused();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; const isMobile = isCompactFormFactor();
const mobileView = usePanelStore((state) => state.mobileView); const mobileView = usePanelStore((state) => state.mobileView);
const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen); const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen);
const closeToAgent = usePanelStore((state) => state.closeToAgent); const closeToAgent = usePanelStore((state) => state.closeToAgent);
@@ -1,10 +1,11 @@
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { Text, View, type StyleProp, type ViewStyle } from "react-native"; 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 { PanelLeft } from "lucide-react-native";
import { ScreenHeader } from "./screen-header"; import { ScreenHeader } from "./screen-header";
import { HeaderToggleButton } from "./header-toggle-button"; import { HeaderToggleButton } from "./header-toggle-button";
import { usePanelStore } from "@/stores/panel-store"; import { usePanelStore } from "@/stores/panel-store";
import { isCompactFormFactor } from "@/constants/layout";
import { getShortcutOs } from "@/utils/shortcut-platform"; import { getShortcutOs } from "@/utils/shortcut-platform";
interface MenuHeaderProps { interface MenuHeaderProps {
@@ -43,7 +44,7 @@ export function SidebarMenuToggle({
nativeID = "menu-button", nativeID = "menu-button",
}: SidebarMenuToggleProps = {}) { }: SidebarMenuToggleProps = {}) {
const { theme } = useUnistyles(); const { theme } = useUnistyles();
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; const isMobile = isCompactFormFactor();
const mobileView = usePanelStore((state) => state.mobileView); const mobileView = usePanelStore((state) => state.mobileView);
const desktopAgentListOpen = usePanelStore((state) => state.desktop.agentListOpen); const desktopAgentListOpen = usePanelStore((state) => state.desktop.agentListOpen);
const toggleAgentList = usePanelStore((state) => state.toggleAgentList); const toggleAgentList = usePanelStore((state) => state.toggleAgentList);
@@ -1,11 +1,12 @@
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { View, type StyleProp, type ViewStyle } from "react-native"; import { View, type StyleProp, type ViewStyle } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles"; import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { import {
HEADER_INNER_HEIGHT, HEADER_INNER_HEIGHT,
HEADER_INNER_HEIGHT_MOBILE, HEADER_INNER_HEIGHT_MOBILE,
HEADER_TOP_PADDING_MOBILE, HEADER_TOP_PADDING_MOBILE,
isCompactFormFactor,
} from "@/constants/layout"; } from "@/constants/layout";
import { useDesktopDragHandlers, useWindowControlsPadding } from "@/utils/desktop-window"; import { useDesktopDragHandlers, useWindowControlsPadding } from "@/utils/desktop-window";
@@ -24,7 +25,7 @@ interface ScreenHeaderProps {
export function ScreenHeader({ left, right, leftStyle, rightStyle, borderless }: ScreenHeaderProps) { export function ScreenHeader({ left, right, leftStyle, rightStyle, borderless }: ScreenHeaderProps) {
const { theme } = useUnistyles(); const { theme } = useUnistyles();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; const isMobile = isCompactFormFactor();
const padding = useWindowControlsPadding("header"); const padding = useWindowControlsPadding("header");
// Only add extra padding on mobile for better touch targets; on desktop, only use safe area insets // 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; const topPadding = isMobile ? HEADER_TOP_PADDING_MOBILE : 0;
@@ -1,7 +1,7 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { Text, View } from "react-native"; import { Text, View } from "react-native";
import { StyleSheet } from "react-native-unistyles"; import { StyleSheet } from "react-native-unistyles";
import { getIsDesktop } from "@/constants/layout"; import { getIsElectronRuntime } from "@/constants/layout";
import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet"; import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet";
import { Shortcut } from "@/components/ui/shortcut"; import { Shortcut } from "@/components/ui/shortcut";
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store"; import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
@@ -13,7 +13,7 @@ export function KeyboardShortcutsDialog() {
const setOpen = useKeyboardShortcutsStore((s) => s.setShortcutsDialogOpen); const setOpen = useKeyboardShortcutsStore((s) => s.setShortcutsDialogOpen);
const isMac = getShortcutOs() === "mac"; const isMac = getShortcutOs() === "mac";
const isDesktopApp = getIsDesktop(); const isDesktopApp = getIsElectronRuntime();
const sections = useMemo( const sections = useMemo(
() => buildKeyboardShortcutHelpSections({ isMac, isDesktop: isDesktopApp }), () => buildKeyboardShortcutHelpSections({ isMac, isDesktop: isDesktopApp }),
[isDesktopApp, isMac], [isDesktopApp, isMac],
+13 -6
View File
@@ -20,7 +20,7 @@ import Animated, {
useSharedValue, useSharedValue,
} from "react-native-reanimated"; } from "react-native-reanimated";
import { Gesture, GestureDetector } from "react-native-gesture-handler"; 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 { MessagesSquare, Plus, Settings } from "lucide-react-native";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { Shortcut } from "@/components/ui/shortcut"; import { Shortcut } from "@/components/ui/shortcut";
@@ -39,7 +39,11 @@ import { useDesktopDragHandlers, useWindowControlsPadding } from "@/utils/deskto
import { Combobox } from "@/components/ui/combobox"; import { Combobox } from "@/components/ui/combobox";
import { getHostRuntimeStore, useHosts } from "@/runtime/host-runtime"; import { getHostRuntimeStore, useHosts } from "@/runtime/host-runtime";
import { formatConnectionStatus } from "@/utils/daemons"; 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 { import {
buildHostSessionsRoute, buildHostSessionsRoute,
buildHostSettingsRoute, buildHostSettingsRoute,
@@ -94,6 +98,7 @@ interface MobileSidebarProps extends SidebarSharedProps {
} }
interface DesktopSidebarProps extends SidebarSharedProps { interface DesktopSidebarProps extends SidebarSharedProps {
insetsTop: number;
isOpen: boolean; isOpen: boolean;
handleViewMore: () => void; handleViewMore: () => void;
} }
@@ -105,7 +110,7 @@ export const LeftSidebar = memo(function LeftSidebar({
const { theme } = useUnistyles(); const { theme } = useUnistyles();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; const isCompactLayout = isCompactFormFactor();
const mobileView = usePanelStore((state) => state.mobileView); const mobileView = usePanelStore((state) => state.mobileView);
const desktopAgentListOpen = usePanelStore((state) => state.desktop.agentListOpen); const desktopAgentListOpen = usePanelStore((state) => state.desktop.agentListOpen);
const closeToAgent = usePanelStore((state) => state.closeToAgent); const closeToAgent = usePanelStore((state) => state.closeToAgent);
@@ -175,7 +180,7 @@ export const LeftSidebar = memo(function LeftSidebar({
const hostTriggerRef = useRef<View | null>(null); const hostTriggerRef = useRef<View | null>(null);
const [isHostPickerOpen, setIsHostPickerOpen] = useState(false); 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({ const { projects, isInitialLoad, isRevalidating, refreshAll } = useSidebarWorkspacesList({
serverId: activeServerId, serverId: activeServerId,
@@ -265,7 +270,7 @@ export const LeftSidebar = memo(function LeftSidebar({
handleHostSelect, handleHostSelect,
}; };
if (isMobile) { if (isCompactLayout) {
return ( return (
<MobileSidebar <MobileSidebar
{...sharedProps} {...sharedProps}
@@ -283,6 +288,7 @@ export const LeftSidebar = memo(function LeftSidebar({
return ( return (
<DesktopSidebar <DesktopSidebar
{...sharedProps} {...sharedProps}
insetsTop={insets.top}
isOpen={isOpen} isOpen={isOpen}
handleOpenProject={handleOpenProjectDesktop} handleOpenProject={handleOpenProjectDesktop}
handleSettings={handleSettingsDesktop} handleSettings={handleSettingsDesktop}
@@ -627,6 +633,7 @@ function DesktopSidebar({
handleHostSelect, handleHostSelect,
handleOpenProject, handleOpenProject,
handleSettings, handleSettings,
insetsTop,
isOpen, isOpen,
handleViewMore, handleViewMore,
}: DesktopSidebarProps) { }: DesktopSidebarProps) {
@@ -681,7 +688,7 @@ function DesktopSidebar({
} }
return ( return (
<Animated.View style={[styles.desktopSidebar, resizeAnimatedStyle]}> <Animated.View style={[styles.desktopSidebar, resizeAnimatedStyle, { paddingTop: insetsTop }]}>
{padding.top > 0 ? <View style={{ height: padding.top }} {...dragHandlers} /> : null} {padding.top > 0 ? <View style={{ height: padding.top }} {...dragHandlers} /> : null}
<View style={styles.sidebarHeader} {...dragHandlers}> <View style={styles.sidebarHeader} {...dragHandlers}>
<View style={styles.sidebarHeaderRow}> <View style={styles.sidebarHeaderRow}>
@@ -22,7 +22,7 @@ import {
} from "react"; } from "react";
import { router, usePathname } from "expo-router"; import { router, usePathname } from "expo-router";
import { navigateToWorkspace } from "@/hooks/use-workspace-navigation"; 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 { type GestureType } from "react-native-gesture-handler";
import * as Clipboard from "expo-clipboard"; import * as Clipboard from "expo-clipboard";
import { import {
@@ -42,7 +42,7 @@ import { NestableScrollContainer } from "react-native-draggable-flatlist";
import { DraggableList, type DraggableRenderItemInfo } from "./draggable-list"; import { DraggableList, type DraggableRenderItemInfo } from "./draggable-list";
import type { DraggableListDragHandleProps } from "./draggable-list.types"; import type { DraggableListDragHandleProps } from "./draggable-list.types";
import { getHostRuntimeStore, isHostRuntimeConnected } from "@/runtime/host-runtime"; 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 { projectIconQueryKey } from "@/hooks/use-project-icon-query";
import { parseHostWorkspaceRouteFromPathname } from "@/utils/host-routes"; import { parseHostWorkspaceRouteFromPathname } from "@/utils/host-routes";
import { prepareWorkspaceTab } from "@/utils/workspace-navigation"; import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
@@ -664,8 +664,7 @@ function ProjectHeaderRow({
dragHandleProps, dragHandleProps,
}: ProjectHeaderRowProps) { }: ProjectHeaderRowProps) {
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const isMobileBreakpoint = const isMobileBreakpoint = isCompactFormFactor();
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
const mergeWorkspaces = useSessionStore((state) => state.mergeWorkspaces); const mergeWorkspaces = useSessionStore((state) => state.mergeWorkspaces);
const toast = useToast(); const toast = useToast();
@@ -836,7 +835,7 @@ function WorkspaceRowInner({
}: WorkspaceRowInnerProps) { }: WorkspaceRowInnerProps) {
const { theme } = useUnistyles(); const { theme } = useUnistyles();
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const isMobile = Platform.OS !== "web"; const isTouchPlatform = Platform.OS !== "web";
const prHint = useWorkspacePrHint({ const prHint = useWorkspacePrHint({
serverId: workspace.serverId, serverId: workspace.serverId,
cwd: workspace.workspaceId, cwd: workspace.workspaceId,
@@ -901,7 +900,7 @@ function WorkspaceRowInner({
</View> </View>
<View style={styles.workspaceRowRight}> <View style={styles.workspaceRowRight}>
{isCreating ? <Text style={styles.workspaceCreatingText}>Creating...</Text> : null} {isCreating ? <Text style={styles.workspaceCreatingText}>Creating...</Text> : null}
{onArchive && (isHovered || isMobile) ? ( {onArchive && (isHovered || isTouchPlatform) ? (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger <DropdownMenuTrigger
hitSlop={8} hitSlop={8}
@@ -1638,7 +1637,7 @@ export function SidebarWorkspaceList({
listFooterComponent, listFooterComponent,
parentGestureRef, parentGestureRef,
}: SidebarWorkspaceListProps) { }: SidebarWorkspaceListProps) {
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; const isMobile = isCompactFormFactor();
const isNative = Platform.OS !== "web"; const isNative = Platform.OS !== "web";
const pathname = usePathname(); const pathname = usePathname();
const activeWorkspaceSelection = useNavigationActiveWorkspaceSelection(); const activeWorkspaceSelection = useNavigationActiveWorkspaceSelection();
@@ -1646,7 +1645,7 @@ export function SidebarWorkspaceList({
const creatingWorkspaceTimeoutsRef = useRef<Map<string, ReturnType<typeof setTimeout>>>( const creatingWorkspaceTimeoutsRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(
new Map(), new Map(),
); );
const isDesktopApp = getIsDesktop(); const isDesktopApp = getIsElectronRuntime();
const altDown = useKeyboardShortcutsStore((state) => state.altDown); const altDown = useKeyboardShortcutsStore((state) => state.altDown);
const cmdOrCtrlDown = useKeyboardShortcutsStore((state) => state.cmdOrCtrlDown); const cmdOrCtrlDown = useKeyboardShortcutsStore((state) => state.cmdOrCtrlDown);
const showShortcutBadges = altDown || (isDesktopApp && cmdOrCtrlDown); const showShortcutBadges = altDown || (isDesktopApp && cmdOrCtrlDown);
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useFocusEffect, useIsFocused } from "@react-navigation/native"; import { useFocusEffect, useIsFocused } from "@react-navigation/native";
import { ActivityIndicator, Pressable, ScrollView, Text, View } from "react-native"; import { ActivityIndicator, Pressable, ScrollView, Text, View } from "react-native";
import Animated, { runOnJS, useAnimatedReaction } from "react-native-reanimated"; 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 { encodeTerminalKeyInput } from "@server/shared/terminal-key-input";
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime"; import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style"; import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
@@ -21,6 +21,7 @@ import {
import { usePanelStore } from "@/stores/panel-store"; import { usePanelStore } from "@/stores/panel-store";
import { toXtermTheme } from "@/utils/to-xterm-theme"; import { toXtermTheme } from "@/utils/to-xterm-theme";
import TerminalEmulator, { type TerminalEmulatorHandle } from "./terminal-emulator"; import TerminalEmulator, { type TerminalEmulatorHandle } from "./terminal-emulator";
import { isCompactFormFactor } from "@/constants/layout";
interface TerminalPaneProps { interface TerminalPaneProps {
serverId: string; serverId: string;
@@ -91,7 +92,7 @@ export function TerminalPane({
const isAppVisible = useAppVisible(); const isAppVisible = useAppVisible();
const { theme } = useUnistyles(); const { theme } = useUnistyles();
const xtermTheme = useMemo(() => toXtermTheme(theme.colors.terminal), [theme.colors.terminal]); 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 mobileView = usePanelStore((state) => state.mobileView);
const openAgentList = usePanelStore((state) => state.openAgentList); const openAgentList = usePanelStore((state) => state.openAgentList);
const openFileExplorer = usePanelStore((state) => state.openFileExplorer); const openFileExplorer = usePanelStore((state) => state.openFileExplorer);
+37 -17
View File
@@ -1,5 +1,6 @@
import { Platform } from "react-native"; 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; 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_WIDTH = 140;
export const DESKTOP_WINDOW_CONTROLS_HEIGHT = 48; export const DESKTOP_WINDOW_CONTROLS_HEIGHT = 48;
// Check if running in desktop app (any OS) // Check if running in the Electron desktop runtime (any OS)
function isDesktopEnvironment(): boolean { function isElectronDesktopRuntime(): boolean {
if (Platform.OS !== "web") return false; if (Platform.OS !== "web") return false;
return isDesktop(); return isElectronRuntime();
} }
// Check if running in desktop host on macOS // Check if running in the Electron desktop runtime on macOS
function isDesktopEnvironmentMac(): boolean { function isElectronDesktopRuntimeMac(): boolean {
if (Platform.OS !== "web") return false; 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) // Cached result - only cache true, keep checking if false (in case desktop globals load later)
let _isDesktopMacCached: boolean | null = null; let _isElectronRuntimeMacCached: boolean | null = null;
let _isDesktopCached: boolean | null = null; let _isElectronRuntimeCached: boolean | null = null;
export function getIsDesktopMac(): boolean { export function getIsElectronRuntimeMac(): boolean {
if (_isDesktopMacCached === true) { if (_isElectronRuntimeMacCached === true) {
return true; return true;
} }
const result = isDesktopEnvironmentMac(); const result = isElectronDesktopRuntimeMac();
if (result) { if (result) {
_isDesktopMacCached = true; _isElectronRuntimeMacCached = true;
} }
return result; return result;
} }
export function getIsDesktop(): boolean { export function getIsElectronRuntime(): boolean {
if (_isDesktopCached === true) { if (_isElectronRuntimeCached === true) {
return true; return true;
} }
const result = isDesktopEnvironment(); const result = isElectronDesktopRuntime();
if (result) { if (result) {
_isDesktopCached = true; _isElectronRuntimeCached = true;
} }
return result; 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";
}
@@ -2,7 +2,7 @@ import { createContext, useContext, useEffect, useRef, type ReactNode } from "re
import { useWindowDimensions } from "react-native"; import { useWindowDimensions } from "react-native";
import { useSharedValue, withTiming, Easing, type SharedValue } from "react-native-reanimated"; import { useSharedValue, withTiming, Easing, type SharedValue } from "react-native-reanimated";
import { type GestureType } from "react-native-gesture-handler"; 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 { usePanelStore } from "@/stores/panel-store";
import { import {
getRightSidebarAnimationTargets, getRightSidebarAnimationTargets,
@@ -27,12 +27,12 @@ const ExplorerSidebarAnimationContext = createContext<ExplorerSidebarAnimationCo
export function ExplorerSidebarAnimationProvider({ children }: { children: ReactNode }) { export function ExplorerSidebarAnimationProvider({ children }: { children: ReactNode }) {
const { width: windowWidth } = useWindowDimensions(); const { width: windowWidth } = useWindowDimensions();
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; const isCompactLayout = isCompactFormFactor();
const mobileView = usePanelStore((state) => state.mobileView); const mobileView = usePanelStore((state) => state.mobileView);
const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen); const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen);
// Derive isOpen from the unified panel state // 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 // Right sidebar: closed = +windowWidth (off-screen right), open = 0
const initialTargets = getRightSidebarAnimationTargets({ isOpen, windowWidth }); const initialTargets = getRightSidebarAnimationTargets({ isOpen, windowWidth });
@@ -10,7 +10,7 @@ import {
import { useWindowDimensions } from "react-native"; import { useWindowDimensions } from "react-native";
import { useSharedValue, withTiming, Easing, type SharedValue } from "react-native-reanimated"; import { useSharedValue, withTiming, Easing, type SharedValue } from "react-native-reanimated";
import { type GestureType } from "react-native-gesture-handler"; 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 { usePanelStore } from "@/stores/panel-store";
import { import {
getLeftSidebarAnimationTargets, getLeftSidebarAnimationTargets,
@@ -34,12 +34,12 @@ const SidebarAnimationContext = createContext<SidebarAnimationContextValue | nul
export function SidebarAnimationProvider({ children }: { children: ReactNode }) { export function SidebarAnimationProvider({ children }: { children: ReactNode }) {
const { width: windowWidth } = useWindowDimensions(); const { width: windowWidth } = useWindowDimensions();
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; const isCompactLayout = isCompactFormFactor();
const mobileView = usePanelStore((state) => state.mobileView); const mobileView = usePanelStore((state) => state.mobileView);
const desktopAgentListOpen = usePanelStore((state) => state.desktop.agentListOpen); const desktopAgentListOpen = usePanelStore((state) => state.desktop.agentListOpen);
// Derive isOpen from the unified panel state // 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 // Initialize based on current state
const initialTargets = getLeftSidebarAnimationTargets({ isOpen, windowWidth }); const initialTargets = getLeftSidebarAnimationTargets({ isOpen, windowWidth });
@@ -9,7 +9,7 @@ import { settingsStyles } from "@/styles/settings";
export function DesktopPermissionsSection() { export function DesktopPermissionsSection() {
const { theme } = useUnistyles(); const { theme } = useUnistyles();
const { const {
isDesktop, isDesktopApp,
snapshot, snapshot,
isRefreshing, isRefreshing,
requestingPermission, requestingPermission,
@@ -20,7 +20,7 @@ export function DesktopPermissionsSection() {
sendTestNotification, sendTestNotification,
} = useDesktopPermissions(); } = useDesktopPermissions();
if (!isDesktop) { if (!isDesktopApp) {
return null; return null;
} }
@@ -1,4 +1,4 @@
import { getDesktopHost, isDesktop } from "@/desktop/host"; import { getDesktopHost, isElectronRuntime } from "@/desktop/host";
import { invokeDesktopCommand } from "@/desktop/electron/invoke"; import { invokeDesktopCommand } from "@/desktop/electron/invoke";
export type DesktopDaemonState = "starting" | "running" | "stopped" | "errored"; export type DesktopDaemonState = "starting" | "running" | "stopped" | "errored";
@@ -123,7 +123,7 @@ function parseCliSymlinkInstructionsInternal(raw: unknown): CliSymlinkInstructio
} }
export function shouldUseDesktopDaemon(): boolean { export function shouldUseDesktopDaemon(): boolean {
return isDesktop(); return isElectronRuntime();
} }
export async function getDesktopDaemonStatus(): Promise<DesktopDaemonStatus> { export async function getDesktopDaemonStatus(): Promise<DesktopDaemonStatus> {
+3 -3
View File
@@ -97,12 +97,12 @@ export function getDesktopHost(): DesktopHostBridge | null {
return getElectronHost(); return getElectronHost();
} }
export function isDesktop(): boolean { export function isElectronRuntime(): boolean {
return getDesktopHost() !== null; return getDesktopHost() !== null;
} }
export function isDesktopMac(): boolean { export function isElectronRuntimeMac(): boolean {
if (!isDesktop()) { if (!isElectronRuntime()) {
return false; return false;
} }
if (typeof navigator === "undefined") { if (typeof navigator === "undefined") {
@@ -9,7 +9,7 @@ import {
import { sendOsNotification } from "@/utils/os-notifications"; import { sendOsNotification } from "@/utils/os-notifications";
export interface UseDesktopPermissionsReturn { export interface UseDesktopPermissionsReturn {
isDesktop: boolean; isDesktopApp: boolean;
snapshot: DesktopPermissionSnapshot | null; snapshot: DesktopPermissionSnapshot | null;
isRefreshing: boolean; isRefreshing: boolean;
requestingPermission: DesktopPermissionKind | null; requestingPermission: DesktopPermissionKind | null;
@@ -31,7 +31,7 @@ const EMPTY_MICROPHONE_STATUS = {
}; };
export function useDesktopPermissions(): UseDesktopPermissionsReturn { export function useDesktopPermissions(): UseDesktopPermissionsReturn {
const isDesktop = shouldShowDesktopPermissionSection(); const isDesktopApp = shouldShowDesktopPermissionSection();
const isMountedRef = useRef(true); const isMountedRef = useRef(true);
const [snapshot, setSnapshot] = useState<DesktopPermissionSnapshot | null>(null); const [snapshot, setSnapshot] = useState<DesktopPermissionSnapshot | null>(null);
const [isRefreshing, setIsRefreshing] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false);
@@ -47,7 +47,7 @@ export function useDesktopPermissions(): UseDesktopPermissionsReturn {
}, []); }, []);
const refreshPermissions = useCallback(async () => { const refreshPermissions = useCallback(async () => {
if (!isDesktop) { if (!isDesktopApp) {
return; return;
} }
@@ -65,11 +65,11 @@ export function useDesktopPermissions(): UseDesktopPermissionsReturn {
setIsRefreshing(false); setIsRefreshing(false);
} }
} }
}, [isDesktop]); }, [isDesktopApp]);
const requestPermission = useCallback( const requestPermission = useCallback(
async (kind: DesktopPermissionKind) => { async (kind: DesktopPermissionKind) => {
if (!isDesktop) { if (!isDesktopApp) {
return; return;
} }
@@ -110,13 +110,13 @@ export function useDesktopPermissions(): UseDesktopPermissionsReturn {
await refreshPermissions(); await refreshPermissions();
} }
}, },
[isDesktop, refreshPermissions], [isDesktopApp, refreshPermissions],
); );
const [testNotificationError, setTestNotificationError] = useState<string | null>(null); const [testNotificationError, setTestNotificationError] = useState<string | null>(null);
const sendTestNotification = useCallback(async () => { const sendTestNotification = useCallback(async () => {
if (!isDesktop) { if (!isDesktopApp) {
return; return;
} }
@@ -137,18 +137,18 @@ export function useDesktopPermissions(): UseDesktopPermissionsReturn {
setIsSendingTestNotification(false); setIsSendingTestNotification(false);
} }
} }
}, [isDesktop]); }, [isDesktopApp]);
useEffect(() => { useEffect(() => {
if (!isDesktop) { if (!isDesktopApp) {
return; return;
} }
void refreshPermissions(); void refreshPermissions();
}, [isDesktop, refreshPermissions]); }, [isDesktopApp, refreshPermissions]);
return { return {
isDesktop, isDesktopApp,
snapshot, snapshot,
isRefreshing, isRefreshing,
requestingPermission, requestingPermission,
@@ -1,5 +1,5 @@
import { Platform } from "react-native"; import { Platform } from "react-native";
import { isDesktop } from "@/desktop/host"; import { isElectronRuntime } from "@/desktop/host";
import { invokeDesktopCommand } from "@/desktop/electron/invoke"; import { invokeDesktopCommand } from "@/desktop/electron/invoke";
export interface DesktopAppUpdateCheckResult { export interface DesktopAppUpdateCheckResult {
@@ -49,7 +49,7 @@ function toNumberOr(defaultValue: number, value: unknown): number {
} }
export function shouldShowDesktopUpdateSection(): boolean { export function shouldShowDesktopUpdateSection(): boolean {
return Platform.OS === "web" && isDesktop(); return Platform.OS === "web" && isElectronRuntime();
} }
export function parseLocalDaemonVersionResult(raw: unknown): LocalDaemonVersionResult { export function parseLocalDaemonVersionResult(raw: unknown): LocalDaemonVersionResult {
@@ -11,7 +11,7 @@ const CHANGELOG_URL = "https://paseo.sh/changelog";
export function UpdateBanner() { export function UpdateBanner() {
const { theme } = useUnistyles(); const { theme } = useUnistyles();
const { const {
isDesktop, isDesktopApp,
status, status,
availableUpdate, availableUpdate,
errorMessage, errorMessage,
@@ -23,7 +23,7 @@ export function UpdateBanner() {
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null); const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
useEffect(() => { useEffect(() => {
if (!isDesktop) return; if (!isDesktopApp) return;
void checkForUpdates({ silent: true }); void checkForUpdates({ silent: true });
@@ -36,9 +36,9 @@ export function UpdateBanner() {
clearInterval(intervalRef.current); clearInterval(intervalRef.current);
} }
}; };
}, [isDesktop, checkForUpdates]); }, [isDesktopApp, checkForUpdates]);
if (!isDesktop) return null; if (!isDesktopApp) return null;
if (dismissed) return null; if (dismissed) return null;
if (status !== "available" && status !== "installed" && status !== "installing" && status !== "error") if (status !== "available" && status !== "installed" && status !== "installing" && status !== "error")
return null; return null;
@@ -18,7 +18,7 @@ export type DesktopAppUpdateStatus =
| "error"; | "error";
export interface UseDesktopAppUpdaterReturn { export interface UseDesktopAppUpdaterReturn {
isDesktop: boolean; isDesktopApp: boolean;
status: DesktopAppUpdateStatus; status: DesktopAppUpdateStatus;
statusText: string; statusText: string;
availableUpdate: DesktopAppUpdateCheckResult | null; availableUpdate: DesktopAppUpdateCheckResult | null;
@@ -75,7 +75,7 @@ function formatStatusText(input: {
} }
export function useDesktopAppUpdater(): UseDesktopAppUpdaterReturn { export function useDesktopAppUpdater(): UseDesktopAppUpdaterReturn {
const isDesktop = shouldShowDesktopUpdateSection(); const isDesktopApp = shouldShowDesktopUpdateSection();
const requestVersionRef = useRef(0); const requestVersionRef = useRef(0);
const [status, setStatus] = useState<DesktopAppUpdateStatus>("idle"); const [status, setStatus] = useState<DesktopAppUpdateStatus>("idle");
const [availableUpdate, setAvailableUpdate] = useState<DesktopAppUpdateCheckResult | null>(null); const [availableUpdate, setAvailableUpdate] = useState<DesktopAppUpdateCheckResult | null>(null);
@@ -85,7 +85,7 @@ export function useDesktopAppUpdater(): UseDesktopAppUpdaterReturn {
const checkForUpdates = useCallback( const checkForUpdates = useCallback(
async (options: { silent?: boolean } = {}) => { async (options: { silent?: boolean } = {}) => {
if (!isDesktop) { if (!isDesktopApp) {
return null; return null;
} }
@@ -130,11 +130,11 @@ export function useDesktopAppUpdater(): UseDesktopAppUpdaterReturn {
return null; return null;
} }
}, },
[isDesktop], [isDesktopApp],
); );
const installUpdate = useCallback(async () => { const installUpdate = useCallback(async () => {
if (!isDesktop) { if (!isDesktopApp) {
return null; return null;
} }
@@ -162,10 +162,10 @@ export function useDesktopAppUpdater(): UseDesktopAppUpdaterReturn {
setErrorMessage(message); setErrorMessage(message);
return null; return null;
} }
}, [isDesktop]); }, [isDesktopApp]);
return { return {
isDesktop, isDesktopApp,
status, status,
statusText: formatStatusText({ statusText: formatStatusText({
status, status,
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { AttemptCancelledError, AttemptGuard } from "@/utils/attempt-guard"; import { AttemptCancelledError, AttemptGuard } from "@/utils/attempt-guard";
import { isDesktop } from "@/desktop/host"; import { isElectronRuntime } from "@/desktop/host";
export interface AudioCaptureConfig { export interface AudioCaptureConfig {
sampleRate?: number; sampleRate?: number;
@@ -157,7 +157,7 @@ export function useAudioRecorder(config?: AudioCaptureConfig) {
: true; : true;
const currentOrigin = const currentOrigin =
typeof window !== "undefined" && window.location ? window.location.origin : "unknown"; typeof window !== "undefined" && window.location ? window.location.origin : "unknown";
const isDesktopApp = isDesktop(); const isDesktopApp = isElectronRuntime();
if (missingNavigator) { if (missingNavigator) {
throw new Error("Microphone capture is not supported in this environment"); throw new Error("Microphone capture is not supported in this environment");
+3 -3
View File
@@ -17,7 +17,7 @@ import { chordStringToShortcutKeys } from "@/keyboard/shortcut-string";
import { getBindingIdForAction, getDefaultKeysForAction } from "@/keyboard/keyboard-shortcuts"; import { getBindingIdForAction, getDefaultKeysForAction } from "@/keyboard/keyboard-shortcuts";
import { useKeyboardShortcutOverrides } from "@/hooks/use-keyboard-shortcut-overrides"; import { useKeyboardShortcutOverrides } from "@/hooks/use-keyboard-shortcut-overrides";
import { getShortcutOs } from "@/utils/shortcut-platform"; import { getShortcutOs } from "@/utils/shortcut-platform";
import { getIsDesktop } from "@/constants/layout"; import { getIsElectronRuntime } from "@/constants/layout";
import { prepareWorkspaceTab } from "@/utils/workspace-navigation"; import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
import { focusWithRetries } from "@/utils/web-focus"; import { focusWithRetries } from "@/utils/web-focus";
@@ -110,8 +110,8 @@ function resolveActionShortcutKeys(
): ShortcutKey[][] | undefined { ): ShortcutKey[][] | undefined {
if (!actionId) return undefined; if (!actionId) return undefined;
const isMac = getShortcutOs() === "mac"; const isMac = getShortcutOs() === "mac";
const isDesktop = getIsDesktop(); const isDesktopApp = getIsElectronRuntime();
const platform = { isMac, isDesktop }; const platform = { isMac, isDesktop: isDesktopApp };
const bindingId = getBindingIdForAction(actionId, platform); const bindingId = getBindingIdForAction(actionId, platform);
if (!bindingId) return undefined; if (!bindingId) return undefined;
const override = overrides[bindingId]; const override = overrides[bindingId];
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { parsePcm16Wav } from "@/utils/pcm16-wav"; import { parsePcm16Wav } from "@/utils/pcm16-wav";
import { isDesktop } from "@/desktop/host"; import { isElectronRuntime } from "@/desktop/host";
import type { import type {
DictationAudioSource, DictationAudioSource,
@@ -168,7 +168,7 @@ export function useDictationAudioSource(config: DictationAudioSourceConfig): Dic
: true; : true;
const currentOrigin = const currentOrigin =
typeof window !== "undefined" && window.location ? window.location.origin : "unknown"; typeof window !== "undefined" && window.location ? window.location.origin : "unknown";
const isDesktopApp = isDesktop(); const isDesktopApp = isElectronRuntime();
if (missingNavigator) { if (missingNavigator) {
throw new Error("Microphone capture is not supported in this environment"); throw new Error("Microphone capture is not supported in this environment");
+2 -2
View File
@@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { Platform } from "react-native"; import { Platform } from "react-native";
import { getIsDesktopMac } from "@/constants/layout"; import { getIsElectronRuntimeMac } from "@/constants/layout";
import { useAggregatedAgents } from "./use-aggregated-agents"; import { useAggregatedAgents } from "./use-aggregated-agents";
import { getDesktopHost } from "@/desktop/host"; import { getDesktopHost } from "@/desktop/host";
@@ -96,7 +96,7 @@ function getSystemColorScheme(): ColorScheme {
} }
async function updateMacDockBadge(count?: number) { async function updateMacDockBadge(count?: number) {
if (Platform.OS !== "web" || !getIsDesktopMac()) return; if (Platform.OS !== "web" || !getIsElectronRuntimeMac()) return;
const desktopWindow = getDesktopHost()?.window?.getCurrentWindow?.(); const desktopWindow = getDesktopHost()?.window?.getCurrentWindow?.();
if (!desktopWindow || typeof desktopWindow.setBadgeCount !== "function") { if (!desktopWindow || typeof desktopWindow.setBadgeCount !== "function") {
@@ -2,7 +2,7 @@ import { useCallback, useRef } from "react";
import { Alert } from "react-native"; import { Alert } from "react-native";
import { Platform } from "react-native"; import { Platform } from "react-native";
import * as ImagePicker from "expo-image-picker"; import * as ImagePicker from "expo-image-picker";
import { isDesktop } from "@/desktop/host"; import { isElectronRuntime } from "@/desktop/host";
import { import {
normalizePickedImageAssets, normalizePickedImageAssets,
openImagePathsWithDesktopDialog, openImagePathsWithDesktopDialog,
@@ -45,7 +45,7 @@ export function useImageAttachmentPicker(): UseImageAttachmentPickerResult {
isPickingRef.current = true; isPickingRef.current = true;
try { try {
if (Platform.OS === "web" && isDesktop()) { if (Platform.OS === "web" && isElectronRuntime()) {
const selectedPaths = await openImagePathsWithDesktopDialog(); const selectedPaths = await openImagePathsWithDesktopDialog();
if (selectedPaths.length === 0) { if (selectedPaths.length === 0) {
return null; return null;
@@ -17,12 +17,12 @@ async function loadDesktopDaemonServerId(): Promise<DesktopDaemonServerIdResult>
export function useIsLocalDaemon(serverId: string): boolean { export function useIsLocalDaemon(serverId: string): boolean {
const normalizedServerId = serverId.trim(); const normalizedServerId = serverId.trim();
const isDesktop = shouldUseDesktopDaemon(); const isDesktopApp = shouldUseDesktopDaemon();
const query = useQuery({ const query = useQuery({
queryKey: DESKTOP_DAEMON_SERVER_ID_QUERY_KEY, queryKey: DESKTOP_DAEMON_SERVER_ID_QUERY_KEY,
queryFn: loadDesktopDaemonServerId, queryFn: loadDesktopDaemonServerId,
enabled: isDesktop, enabled: isDesktopApp,
staleTime: Infinity, staleTime: Infinity,
gcTime: Infinity, gcTime: Infinity,
refetchInterval: (query) => (query.state.data?.serverId ? false : 1000), refetchInterval: (query) => (query.state.data?.serverId ? false : 1000),
@@ -32,7 +32,7 @@ export function useIsLocalDaemon(serverId: string): boolean {
retry: false, retry: false,
}); });
if (!isDesktop || normalizedServerId.length === 0) { if (!isDesktopApp || normalizedServerId.length === 0) {
return false; return false;
} }
@@ -1,7 +1,7 @@
import { useEffect, useMemo, useRef } from "react"; import { useEffect, useMemo, useRef } from "react";
import { Platform } from "react-native"; import { Platform } from "react-native";
import { usePathname } from "expo-router"; import { usePathname } from "expo-router";
import { getIsDesktop } from "@/constants/layout"; import { getIsElectronRuntime } from "@/constants/layout";
import { useHosts } from "@/runtime/host-runtime"; import { useHosts } from "@/runtime/host-runtime";
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store"; import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
import { setCommandCenterFocusRestoreElement } from "@/utils/command-center-focus-restore"; import { setCommandCenterFocusRestoreElement } from "@/utils/command-center-focus-restore";
@@ -66,7 +66,7 @@ export function useKeyboardShortcuts({
if (Platform.OS !== "web") return; if (Platform.OS !== "web") return;
if (isMobile) return; if (isMobile) return;
const isDesktopApp = getIsDesktop(); const isDesktopApp = getIsElectronRuntime();
const isMac = getShortcutOs() === "mac"; const isMac = getShortcutOs() === "mac";
const shouldHandle = () => { const shouldHandle = () => {
+4 -4
View File
@@ -4,15 +4,15 @@ import { chordStringToShortcutKeys } from "@/keyboard/shortcut-string";
import { getBindingIdForAction, getDefaultKeysForAction } from "@/keyboard/keyboard-shortcuts"; import { getBindingIdForAction, getDefaultKeysForAction } from "@/keyboard/keyboard-shortcuts";
import { useKeyboardShortcutOverrides } from "@/hooks/use-keyboard-shortcut-overrides"; import { useKeyboardShortcutOverrides } from "@/hooks/use-keyboard-shortcut-overrides";
import { getShortcutOs } from "@/utils/shortcut-platform"; import { getShortcutOs } from "@/utils/shortcut-platform";
import { getIsDesktop } from "@/constants/layout"; import { getIsElectronRuntime } from "@/constants/layout";
export function useShortcutKeys(actionId: string): ShortcutKey[][] | null { export function useShortcutKeys(actionId: string): ShortcutKey[][] | null {
const { overrides } = useKeyboardShortcutOverrides(); const { overrides } = useKeyboardShortcutOverrides();
const isMac = getShortcutOs() === "mac"; const isMac = getShortcutOs() === "mac";
const isDesktop = getIsDesktop(); const isDesktopApp = getIsElectronRuntime();
return useMemo(() => { return useMemo(() => {
const platform = { isMac, isDesktop }; const platform = { isMac, isDesktop: isDesktopApp };
const bindingId = getBindingIdForAction(actionId, platform); const bindingId = getBindingIdForAction(actionId, platform);
if (!bindingId) return null; if (!bindingId) return null;
@@ -23,5 +23,5 @@ export function useShortcutKeys(actionId: string): ShortcutKey[][] | null {
const defaultKeys = getDefaultKeysForAction(actionId, platform); const defaultKeys = getDefaultKeysForAction(actionId, platform);
return defaultKeys ? [defaultKeys] : null; return defaultKeys ? [defaultKeys] : null;
}, [actionId, overrides, isMac, isDesktop]); }, [actionId, overrides, isMac, isDesktopApp]);
} }
@@ -4,7 +4,7 @@ import type { ImageAttachment } from "@/components/message-input";
import { View, Text, Pressable, ScrollView, Keyboard, Platform } from "react-native"; import { View, Text, Pressable, ScrollView, Keyboard, Platform } from "react-native";
import { useLocalSearchParams, useRouter } from "expo-router"; import { useLocalSearchParams, useRouter } from "expo-router";
import { useIsFocused } from "@react-navigation/native"; 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 { useSafeAreaInsets } from "react-native-safe-area-context";
import { GestureDetector } from "react-native-gesture-handler"; import { GestureDetector } from "react-native-gesture-handler";
import Animated from "react-native-reanimated"; import Animated from "react-native-reanimated";
@@ -40,7 +40,7 @@ import {
} from "@/runtime/host-runtime"; } from "@/runtime/host-runtime";
import { ExplorerSidebarAnimationProvider } from "@/contexts/explorer-sidebar-animation-context"; import { ExplorerSidebarAnimationProvider } from "@/contexts/explorer-sidebar-animation-context";
import { usePanelStore, type ExplorerCheckoutContext } from "@/stores/panel-store"; 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 { WelcomeScreen } from "@/components/welcome-screen";
import type { Agent } from "@/contexts/session-context"; import type { Agent } from "@/contexts/session-context";
import { encodeImages } from "@/utils/encode-images"; import { encodeImages } from "@/utils/encode-images";
@@ -233,7 +233,7 @@ function DraftAgentScreenContent({
isCreateFlow: true, isCreateFlow: true,
onlineServerIds, onlineServerIds,
}); });
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; const isMobile = isCompactFormFactor();
const mobileView = usePanelStore((state) => state.mobileView); const mobileView = usePanelStore((state) => state.mobileView);
const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen); const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen);
const toggleFileExplorer = usePanelStore((state) => state.toggleFileExplorer); const toggleFileExplorer = usePanelStore((state) => state.toggleFileExplorer);
@@ -1,6 +1,6 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { View, Text } from "react-native"; 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 { FolderOpen } from "lucide-react-native";
import { PaseoLogo } from "@/components/icons/paseo-logo"; import { PaseoLogo } from "@/components/icons/paseo-logo";
import { Button } from "@/components/ui/button"; 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 { useOpenProjectPicker } from "@/hooks/use-open-project-picker";
import { usePanelStore } from "@/stores/panel-store"; import { usePanelStore } from "@/stores/panel-store";
import { useSessionStore } from "@/stores/session-store"; import { useSessionStore } from "@/stores/session-store";
import { isCompactFormFactor } from "@/constants/layout";
import { useDesktopDragHandlers } from "@/utils/desktop-window"; import { useDesktopDragHandlers } from "@/utils/desktop-window";
export function OpenProjectScreen({ serverId }: { serverId: string }) { 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 hasHydrated = useSessionStore((s) => s.sessions[serverId]?.hasHydratedWorkspaces ?? false);
const hasProjects = useSessionStore((s) => (s.sessions[serverId]?.workspaces?.size ?? 0) > 0); 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(); const dragHandlers = useDesktopDragHandlers();
useEffect(() => { useEffect(() => {
if (!isMobile) { if (!isCompactLayout) {
openAgentList(); openAgentList();
} }
}, [isMobile, openAgentList]); }, [isCompactLayout, openAgentList]);
return ( return (
<View style={styles.container}> <View style={styles.container}>
+26 -27
View File
@@ -4,7 +4,7 @@ import { View, Text, ScrollView, Alert, Platform, Pressable } from "react-native
import { router, useLocalSearchParams } from "expo-router"; import { router, useLocalSearchParams } from "expo-router";
import { useFocusEffect } from "@react-navigation/native"; import { useFocusEffect } from "@react-navigation/native";
import { useSafeAreaInsets } from "react-native-safe-area-context"; 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 { Buffer } from "buffer";
import { import {
Sun, Sun,
@@ -51,7 +51,7 @@ import {
import { AdaptiveModalSheet, AdaptiveTextInput } from "@/components/adaptive-modal-sheet"; import { AdaptiveModalSheet, AdaptiveTextInput } from "@/components/adaptive-modal-sheet";
import { DesktopPermissionsSection } from "@/desktop/components/desktop-permissions-section"; import { DesktopPermissionsSection } from "@/desktop/components/desktop-permissions-section";
import { LocalDaemonSection } from "@/desktop/components/desktop-updates-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 { useDesktopAppUpdater } from "@/desktop/updates/use-desktop-app-updater";
import { formatVersionWithPrefix } from "@/desktop/updates/desktop-updates"; import { formatVersionWithPrefix } from "@/desktop/updates/desktop-updates";
import { resolveAppVersion } from "@/utils/app-version"; 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 { THINKING_TONE_NATIVE_PCM_BASE64 } from "@/utils/thinking-tone.native-pcm";
import { useVoiceAudioEngineOptional } from "@/contexts/voice-context"; import { useVoiceAudioEngineOptional } from "@/contexts/voice-context";
import { useIsLocalDaemon } from "@/hooks/use-is-local-daemon"; import { useIsLocalDaemon } from "@/hooks/use-is-local-daemon";
import { isCompactFormFactor } from "@/constants/layout";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Section definitions // Section definitions
@@ -79,7 +80,7 @@ interface SettingsSectionDef {
icon: ComponentType<{ size: number; color: string }>; icon: ComponentType<{ size: number; color: string }>;
} }
function getSettingsSections(context: { isDesktop: boolean }): SettingsSectionDef[] { function getSettingsSections(context: { isDesktopApp: boolean }): SettingsSectionDef[] {
const sections: SettingsSectionDef[] = [ const sections: SettingsSectionDef[] = [
{ id: "hosts", label: "Hosts", icon: Server }, { id: "hosts", label: "Hosts", icon: Server },
{ id: "appearance", label: "Appearance", icon: Palette }, { id: "appearance", label: "Appearance", icon: Palette },
@@ -88,7 +89,7 @@ function getSettingsSections(context: { isDesktop: boolean }): SettingsSectionDe
{ id: "about", label: "About", icon: Info }, { id: "about", label: "About", icon: Info },
]; ];
if (context.isDesktop) { if (context.isDesktopApp) {
sections.push( sections.push(
{ id: "permissions", label: "Permissions", icon: Shield }, { id: "permissions", label: "Permissions", icon: Shield },
{ id: "daemon", label: "Daemon", icon: Settings }, { id: "daemon", label: "Daemon", icon: Settings },
@@ -174,7 +175,6 @@ interface HostsSectionProps {
daemons: HostProfile[]; daemons: HostProfile[];
settings: AppSettings; settings: AppSettings;
routeServerId: string; routeServerId: string;
isDesktop: boolean;
theme: ReturnType<typeof useUnistyles>["theme"]; theme: ReturnType<typeof useUnistyles>["theme"];
handleEditDaemon: (profile: HostProfile) => void; handleEditDaemon: (profile: HostProfile) => void;
setAddConnectionTargetServerId: (id: string | null) => void; setAddConnectionTargetServerId: (id: string | null) => void;
@@ -464,10 +464,10 @@ function DiagnosticsSection({
interface AboutSectionProps { interface AboutSectionProps {
appVersionText: string; appVersionText: string;
isDesktop: boolean; isDesktopApp: boolean;
} }
function AboutSection({ appVersionText, isDesktop }: AboutSectionProps) { function AboutSection({ appVersionText, isDesktopApp }: AboutSectionProps) {
return ( return (
<View style={settingsStyles.section}> <View style={settingsStyles.section}>
<Text style={settingsStyles.sectionTitle}>About</Text> <Text style={settingsStyles.sectionTitle}>About</Text>
@@ -478,7 +478,7 @@ function AboutSection({ appVersionText, isDesktop }: AboutSectionProps) {
</View> </View>
<Text style={styles.aboutValue}>{appVersionText}</Text> <Text style={styles.aboutValue}>{appVersionText}</Text>
</View> </View>
{isDesktop ? <DesktopAppUpdateRow /> : null} {isDesktopApp ? <DesktopAppUpdateRow /> : null}
</View> </View>
</View> </View>
); );
@@ -496,7 +496,7 @@ interface SettingsSectionContentProps {
aboutProps: AboutSectionProps; aboutProps: AboutSectionProps;
appVersion: string | null; appVersion: string | null;
isLocalDaemon: boolean; isLocalDaemon: boolean;
isDesktop: boolean; isDesktopApp: boolean;
} }
function SettingsSectionContent({ function SettingsSectionContent({
@@ -507,7 +507,7 @@ function SettingsSectionContent({
aboutProps, aboutProps,
appVersion, appVersion,
isLocalDaemon, isLocalDaemon,
isDesktop, isDesktopApp,
}: SettingsSectionContentProps) { }: SettingsSectionContentProps) {
switch (sectionId) { switch (sectionId) {
case "hosts": case "hosts":
@@ -521,9 +521,9 @@ function SettingsSectionContent({
case "about": case "about":
return <AboutSection {...aboutProps} />; return <AboutSection {...aboutProps} />;
case "permissions": case "permissions":
return isDesktop ? <DesktopPermissionsSection /> : null; return isDesktopApp ? <DesktopPermissionsSection /> : null;
case "daemon": case "daemon":
return isDesktop ? ( return isDesktopApp ? (
<LocalDaemonSection appVersion={appVersion} showLifecycleControls={isLocalDaemon} /> <LocalDaemonSection appVersion={appVersion} showLifecycleControls={isLocalDaemon} />
) : null; ) : null;
} }
@@ -619,7 +619,7 @@ function SettingsDesktopLayout({ sections, sectionContentProps }: SettingsLayout
function DesktopAppUpdateRow() { function DesktopAppUpdateRow() {
const { const {
isDesktop, isDesktopApp,
statusText, statusText,
availableUpdate, availableUpdate,
errorMessage, errorMessage,
@@ -631,23 +631,23 @@ function DesktopAppUpdateRow() {
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
if (!isDesktop) { if (!isDesktopApp) {
return undefined; return undefined;
} }
void checkForUpdates({ silent: true }); void checkForUpdates({ silent: true });
return undefined; return undefined;
}, [checkForUpdates, isDesktop]), }, [checkForUpdates, isDesktopApp]),
); );
const handleCheckForUpdates = useCallback(() => { const handleCheckForUpdates = useCallback(() => {
if (!isDesktop) { if (!isDesktopApp) {
return; return;
} }
void checkForUpdates(); void checkForUpdates();
}, [checkForUpdates, isDesktop]); }, [checkForUpdates, isDesktopApp]);
const handleInstallUpdate = useCallback(() => { const handleInstallUpdate = useCallback(() => {
if (!isDesktop) { if (!isDesktopApp) {
return; return;
} }
@@ -667,9 +667,9 @@ function DesktopAppUpdateRow() {
console.error("[Settings] Failed to open app update confirmation", error); console.error("[Settings] Failed to open app update confirmation", error);
Alert.alert("Error", "Unable to open the update confirmation dialog."); Alert.alert("Error", "Unable to open the update confirmation dialog.");
}); });
}, [installUpdate, isDesktop]); }, [installUpdate, isDesktopApp]);
if (!isDesktop) { if (!isDesktopApp) {
return null; return null;
} }
@@ -745,7 +745,7 @@ export default function SettingsScreen() {
const isLoading = settingsLoading; const isLoading = settingsLoading;
const isMountedRef = useRef(true); const isMountedRef = useRef(true);
const lastHandledEditHostRef = useRef<string | null>(null); const lastHandledEditHostRef = useRef<string | null>(null);
const isDesktop = isDesktopHost(); const isDesktopApp = isElectronRuntime();
const isLocalDaemon = useIsLocalDaemon(routeServerId); const isLocalDaemon = useIsLocalDaemon(routeServerId);
const appVersion = resolveAppVersion(); const appVersion = resolveAppVersion();
const appVersionText = formatVersionWithPrefix(appVersion); const appVersionText = formatVersionWithPrefix(appVersion);
@@ -929,14 +929,13 @@ export default function SettingsScreen() {
} }
}, [isPlaybackTestRunning, voiceAudioEngine]); }, [isPlaybackTestRunning, voiceAudioEngine]);
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; const isCompactLayout = isCompactFormFactor();
const sections = getSettingsSections({ isDesktop }); const sections = getSettingsSections({ isDesktopApp });
const hostsProps: HostsSectionProps = { const hostsProps: HostsSectionProps = {
daemons, daemons,
settings, settings,
routeServerId, routeServerId,
isDesktop,
theme, theme,
handleEditDaemon, handleEditDaemon,
setAddConnectionTargetServerId, setAddConnectionTargetServerId,
@@ -985,7 +984,7 @@ export default function SettingsScreen() {
const aboutProps: AboutSectionProps = { const aboutProps: AboutSectionProps = {
appVersionText, appVersionText,
isDesktop, isDesktopApp,
}; };
const sectionContentProps: Omit<SettingsSectionContentProps, "sectionId"> = { const sectionContentProps: Omit<SettingsSectionContentProps, "sectionId"> = {
@@ -995,7 +994,7 @@ export default function SettingsScreen() {
aboutProps, aboutProps,
appVersion, appVersion,
isLocalDaemon, isLocalDaemon,
isDesktop, isDesktopApp,
}; };
if (isLoading) { if (isLoading) {
@@ -1009,7 +1008,7 @@ export default function SettingsScreen() {
return ( return (
<View style={styles.container}> <View style={styles.container}>
<MenuHeader title="Settings" /> <MenuHeader title="Settings" />
{isMobile ? ( {isCompactLayout ? (
<SettingsMobileLayout sections={sections} sectionContentProps={sectionContentProps} /> <SettingsMobileLayout sections={sections} sectionContentProps={sectionContentProps} />
) : ( ) : (
<SettingsDesktopLayout sections={sections} sectionContentProps={sectionContentProps} /> <SettingsDesktopLayout sections={sections} sectionContentProps={sectionContentProps} />
@@ -17,7 +17,7 @@ import {
} from "@/keyboard/shortcut-string"; } from "@/keyboard/shortcut-string";
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store"; import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
import { getShortcutOs } from "@/utils/shortcut-platform"; import { getShortcutOs } from "@/utils/shortcut-platform";
import { getIsDesktop } from "@/constants/layout"; import { getIsElectronRuntime } from "@/constants/layout";
function ShortcutSequence({ chord }: { chord: string[] | null }) { function ShortcutSequence({ chord }: { chord: string[] | null }) {
if (!chord || chord.length === 0) { if (!chord || chord.length === 0) {
@@ -93,8 +93,8 @@ export function KeyboardShortcutsSection() {
const setCapturingShortcut = useKeyboardShortcutsStore((s) => s.setCapturingShortcut); const setCapturingShortcut = useKeyboardShortcutsStore((s) => s.setCapturingShortcut);
const isMac = getShortcutOs() === "mac"; const isMac = getShortcutOs() === "mac";
const isDesktop = getIsDesktop(); const isDesktopApp = getIsElectronRuntime();
const sections = buildKeyboardShortcutHelpSections({ isMac, isDesktop }); const sections = buildKeyboardShortcutHelpSections({ isMac, isDesktop: isDesktopApp });
function cancelCapture() { function cancelCapture() {
setCapturedCombos([]); setCapturedCombos([]);
@@ -180,7 +180,10 @@ export function KeyboardShortcutsSection() {
<Text style={styles.subsectionTitle}>{section.title}</Text> <Text style={styles.subsectionTitle}>{section.title}</Text>
<View style={settingsStyles.card}> <View style={settingsStyles.card}>
{section.rows.map(function (row, index) { {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; const overrideCombo = bindingId ? overrides[bindingId] : undefined;
return ( return (
@@ -83,6 +83,7 @@ type WorkspaceDesktopTabsRowProps = {
externalDndContext?: boolean; externalDndContext?: boolean;
activeDragTabId?: string | null; activeDragTabId?: string | null;
tabDropPreviewIndex?: number | null; tabDropPreviewIndex?: number | null;
showPaneSplitActions?: boolean;
}; };
function getFallbackTabLabel(tab: WorkspaceTabDescriptor): string { function getFallbackTabLabel(tab: WorkspaceTabDescriptor): string {
@@ -346,6 +347,7 @@ export function WorkspaceDesktopTabsRow({
externalDndContext = false, externalDndContext = false,
activeDragTabId = null, activeDragTabId = null,
tabDropPreviewIndex = null, tabDropPreviewIndex = null,
showPaneSplitActions = true,
}: WorkspaceDesktopTabsRowProps) { }: WorkspaceDesktopTabsRowProps) {
const { theme } = useUnistyles(); const { theme } = useUnistyles();
const newAgentTabKeys = useShortcutKeys("workspace-tab-new"); const newAgentTabKeys = useShortcutKeys("workspace-tab-new");
@@ -522,48 +524,52 @@ export function WorkspaceDesktopTabsRow({
</View> </View>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}> {showPaneSplitActions ? (
<TooltipTrigger <>
onPress={onSplitRight} <Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
accessibilityRole="button" <TooltipTrigger
accessibilityLabel="Split pane right" onPress={onSplitRight}
style={({ hovered, pressed }) => [ accessibilityRole="button"
styles.newTabActionButton, accessibilityLabel="Split pane right"
(hovered || pressed) && styles.newTabActionButtonHovered, style={({ hovered, pressed }) => [
]} styles.newTabActionButton,
> (hovered || pressed) && styles.newTabActionButtonHovered,
<Columns2 size={theme.iconSize.sm} color={theme.colors.foregroundMuted} /> ]}
</TooltipTrigger> >
<TooltipContent side="bottom" align="center" offset={8}> <Columns2 size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
<View style={styles.newTabTooltipRow}> </TooltipTrigger>
<Text style={styles.newTabTooltipText}>Split pane right</Text> <TooltipContent side="bottom" align="center" offset={8}>
{splitRightKeys ? ( <View style={styles.newTabTooltipRow}>
<Shortcut chord={splitRightKeys} style={styles.newTabTooltipShortcut} /> <Text style={styles.newTabTooltipText}>Split pane right</Text>
) : null} {splitRightKeys ? (
</View> <Shortcut chord={splitRightKeys} style={styles.newTabTooltipShortcut} />
</TooltipContent> ) : null}
</Tooltip> </View>
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}> </TooltipContent>
<TooltipTrigger </Tooltip>
onPress={onSplitDown} <Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
accessibilityRole="button" <TooltipTrigger
accessibilityLabel="Split pane down" onPress={onSplitDown}
style={({ hovered, pressed }) => [ accessibilityRole="button"
styles.newTabActionButton, accessibilityLabel="Split pane down"
(hovered || pressed) && styles.newTabActionButtonHovered, style={({ hovered, pressed }) => [
]} styles.newTabActionButton,
> (hovered || pressed) && styles.newTabActionButtonHovered,
<Rows2 size={theme.iconSize.sm} color={theme.colors.foregroundMuted} /> ]}
</TooltipTrigger> >
<TooltipContent side="bottom" align="center" offset={8}> <Rows2 size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
<View style={styles.newTabTooltipRow}> </TooltipTrigger>
<Text style={styles.newTabTooltipText}>Split pane down</Text> <TooltipContent side="bottom" align="center" offset={8}>
{splitDownKeys ? ( <View style={styles.newTabTooltipRow}>
<Shortcut chord={splitDownKeys} style={styles.newTabTooltipShortcut} /> <Text style={styles.newTabTooltipText}>Split pane down</Text>
) : null} {splitDownKeys ? (
</View> <Shortcut chord={splitDownKeys} style={styles.newTabTooltipShortcut} />
</TooltipContent> ) : null}
</Tooltip> </View>
</TooltipContent>
</Tooltip>
</>
) : null}
</View> </View>
</View> </View>
); );
@@ -29,7 +29,7 @@ import {
import { GestureDetector } from "react-native-gesture-handler"; import { GestureDetector } from "react-native-gesture-handler";
import Animated from "react-native-reanimated"; import Animated from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context"; 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 invariant from "tiny-invariant";
import { SidebarMenuToggle } from "@/components/headers/menu-header"; import { SidebarMenuToggle } from "@/components/headers/menu-header";
import { HeaderToggleButton } from "@/components/headers/header-toggle-button"; import { HeaderToggleButton } from "@/components/headers/header-toggle-button";
@@ -86,6 +86,10 @@ import {
WorkspaceTabIcon, WorkspaceTabIcon,
WorkspaceTabOptionRow, WorkspaceTabOptionRow,
} from "@/screens/workspace/workspace-tab-presentation"; } 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 { buildWorkspaceTabMenuEntries } from "@/screens/workspace/workspace-tab-menu";
import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types"; import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types";
import { import {
@@ -110,6 +114,7 @@ import {
closeBulkWorkspaceTabs, closeBulkWorkspaceTabs,
} from "@/screens/workspace/workspace-bulk-close"; } from "@/screens/workspace/workspace-bulk-close";
import { findAdjacentPane } from "@/utils/split-navigation"; import { findAdjacentPane } from "@/utils/split-navigation";
import { isCompactFormFactor, supportsDesktopPaneSplits } from "@/constants/layout";
const TERMINALS_QUERY_STALE_TIME = 5_000; const TERMINALS_QUERY_STALE_TIME = 5_000;
const NEW_TAB_AGENT_OPTION_ID = "__new_tab_agent__"; const NEW_TAB_AGENT_OPTION_ID = "__new_tab_agent__";
@@ -576,7 +581,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
const isDarkMode = useColorScheme() === "dark"; const isDarkMode = useColorScheme() === "dark";
const mainBackgroundColor = isDarkMode ? theme.colors.surface1 : theme.colors.surface0; const mainBackgroundColor = isDarkMode ? theme.colors.surface1 : theme.colors.surface0;
const toast = useToast(); const toast = useToast();
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; const isMobile = isCompactFormFactor();
const isFocusModeEnabled = usePanelStore((state) => state.desktop.focusModeEnabled); const isFocusModeEnabled = usePanelStore((state) => state.desktop.focusModeEnabled);
const normalizedServerId = trimNonEmpty(decodeSegment(serverId)) ?? ""; const normalizedServerId = trimNonEmpty(decodeSegment(serverId)) ?? "";
@@ -1703,6 +1708,8 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
}); });
const activeTabDescriptor = activeTab?.descriptor ?? null; const activeTabDescriptor = activeTab?.descriptor ?? null;
const canRenderDesktopPaneSplits = supportsDesktopPaneSplits();
const shouldRenderDesktopPaneFallback = !isMobile && !canRenderDesktopPaneSplits;
useEffect(() => { useEffect(() => {
if (Platform.OS !== "web" || typeof document === "undefined" || activeTabDescriptor) { if (Platform.OS !== "web" || typeof document === "undefined" || activeTabDescriptor) {
return; return;
@@ -1881,6 +1888,17 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
[buildPaneContentModel], [buildPaneContentModel],
); );
const desktopTabRowItems = useMemo<WorkspaceDesktopTabRowItem[]>(
() =>
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) { const handleFocusPane = useStableEvent(function handleFocusPane(paneId: string) {
if (!persistenceKey || paneFocusSuppressedRef.current) { if (!persistenceKey || paneFocusSuppressedRef.current) {
return; return;
@@ -1932,6 +1950,19 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
[persistenceKey, reorderWorkspaceTabsInPane], [persistenceKey, reorderWorkspaceTabsInPane],
); );
const handleReorderTabsInFocusedPane = useCallback(
(nextTabs: WorkspaceTabDescriptor[]) => {
if (!focusedPaneId) {
return;
}
handleReorderTabsInPane(
focusedPaneId,
nextTabs.map((tab) => tab.tabId),
);
},
[focusedPaneId, handleReorderTabsInPane],
);
const renderSplitPaneEmptyState = useCallback(function renderSplitPaneEmptyState() { const renderSplitPaneEmptyState = useCallback(function renderSplitPaneEmptyState() {
return ( return (
<View style={styles.emptyState}> <View style={styles.emptyState}>
@@ -2187,6 +2218,32 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
/> />
) : null} ) : null}
{shouldRenderDesktopPaneFallback ? (
<WorkspaceDesktopTabsRow
paneId={focusedPaneId ?? undefined}
isFocused
tabs={desktopTabRowItems}
normalizedServerId={normalizedServerId}
normalizedWorkspaceId={normalizedWorkspaceId}
setHoveredTabKey={setHoveredTabKey}
setHoveredCloseTabKey={setHoveredCloseTabKey}
onNavigateTab={navigateToTabId}
onCloseTab={handleCloseTabById}
onCopyResumeCommand={handleCopyResumeCommand}
onCopyAgentId={handleCopyAgentId}
onCloseTabsToLeft={handleCloseTabsToLeft}
onCloseTabsToRight={handleCloseTabsToRight}
onCloseOtherTabs={handleCloseOtherTabs}
onSelectNewTabOption={handleSelectNewTabOption}
newTabAgentOptionId={NEW_TAB_AGENT_OPTION_ID}
onReorderTabs={handleReorderTabsInFocusedPane}
onNewTerminalTab={handleCreateTerminal}
onSplitRight={() => {}}
onSplitDown={() => {}}
showPaneSplitActions={false}
/>
) : null}
<View style={styles.centerContent}> <View style={styles.centerContent}>
{isMobile ? ( {isMobile ? (
<GestureDetector gesture={explorerOpenGesture} touchAction="pan-y"> <GestureDetector gesture={explorerOpenGesture} touchAction="pan-y">
@@ -2194,7 +2251,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
</GestureDetector> </GestureDetector>
) : ( ) : (
<View style={styles.content}> <View style={styles.content}>
{workspaceLayout && persistenceKey ? ( {canRenderDesktopPaneSplits && workspaceLayout && persistenceKey ? (
<SplitContainer <SplitContainer
layout={workspaceLayout} layout={workspaceLayout}
focusModeEnabled={isFocusModeEnabled && !isMobile} focusModeEnabled={isFocusModeEnabled && !isMobile}
+7 -7
View File
@@ -1,15 +1,15 @@
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { Platform, type PointerEvent as RNPointerEvent, type ViewProps } from "react-native"; import { Platform, type PointerEvent as RNPointerEvent, type ViewProps } from "react-native";
import { import {
getIsDesktopMac, getIsElectronRuntimeMac,
getIsDesktop, getIsElectronRuntime,
DESKTOP_TRAFFIC_LIGHT_WIDTH, DESKTOP_TRAFFIC_LIGHT_WIDTH,
DESKTOP_TRAFFIC_LIGHT_HEIGHT, DESKTOP_TRAFFIC_LIGHT_HEIGHT,
DESKTOP_WINDOW_CONTROLS_WIDTH, DESKTOP_WINDOW_CONTROLS_WIDTH,
DESKTOP_WINDOW_CONTROLS_HEIGHT, DESKTOP_WINDOW_CONTROLS_HEIGHT,
} from "@/constants/layout"; } from "@/constants/layout";
import { getDesktopWindow } from "@/desktop/electron/window"; import { getDesktopWindow } from "@/desktop/electron/window";
import { isDesktop } from "@/desktop/host"; import { isElectronRuntime } from "@/desktop/host";
import { usePanelStore } from "@/stores/panel-store"; import { usePanelStore } from "@/stores/panel-store";
import { readFiniteScreenPoint } from "./desktop-window-drag-coordinates"; import { readFiniteScreenPoint } from "./desktop-window-drag-coordinates";
@@ -56,7 +56,7 @@ export function isInteractiveDesktopDragTarget(target: unknown): boolean {
export function useDesktopDragHandlers(): DesktopDragViewProps { export function useDesktopDragHandlers(): DesktopDragViewProps {
const isDragging = useRef(false); const isDragging = useRef(false);
const lastPointerDownAt = useRef(0); const lastPointerDownAt = useRef(0);
const isActive = Platform.OS === "web" && isDesktop(); const isActive = Platform.OS === "web" && isElectronRuntime();
useEffect(() => { useEffect(() => {
if (!isActive) return; if (!isActive) return;
@@ -136,7 +136,7 @@ function useRawWindowControlsPadding(): RawWindowControlsPadding {
const [isFullscreen, setIsFullscreen] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false);
useEffect(() => { useEffect(() => {
if (Platform.OS !== "web" || !getIsDesktop()) return; if (Platform.OS !== "web" || !getIsElectronRuntime()) return;
let disposed = false; let disposed = false;
let cleanup: (() => void) | undefined; let cleanup: (() => void) | undefined;
@@ -188,11 +188,11 @@ function useRawWindowControlsPadding(): RawWindowControlsPadding {
}, []); }, []);
return useMemo((): RawWindowControlsPadding => { return useMemo((): RawWindowControlsPadding => {
if (!getIsDesktop() || isFullscreen) { if (!getIsElectronRuntime() || isFullscreen) {
return { left: 0, right: 0, top: 0 }; return { left: 0, right: 0, top: 0 };
} }
if (getIsDesktopMac()) { if (getIsElectronRuntimeMac()) {
return { return {
left: DESKTOP_TRAFFIC_LIGHT_WIDTH, left: DESKTOP_TRAFFIC_LIGHT_WIDTH,
right: 0, right: 0,
+2 -2
View File
@@ -1,12 +1,12 @@
import { Platform } from "react-native"; import { Platform } from "react-native";
import { getIsDesktopMac } from "@/constants/layout"; import { getIsElectronRuntimeMac } from "@/constants/layout";
import type { ShortcutOs } from "@/utils/format-shortcut"; import type { ShortcutOs } from "@/utils/format-shortcut";
export function getShortcutOs(): ShortcutOs { export function getShortcutOs(): ShortcutOs {
if (Platform.OS !== "web") { if (Platform.OS !== "web") {
return Platform.OS === "ios" ? "mac" : "non-mac"; return Platform.OS === "ios" ? "mac" : "non-mac";
} }
if (getIsDesktopMac()) return "mac"; if (getIsElectronRuntimeMac()) return "mac";
if (typeof navigator === "undefined") return "non-mac"; if (typeof navigator === "undefined") return "non-mac";
const ua = navigator.userAgent ?? ""; const ua = navigator.userAgent ?? "";
const platform = (navigator as any).platform ?? ""; const platform = (navigator as any).platform ?? "";
+2 -2
View File
@@ -1,4 +1,4 @@
import { isDesktop } from "@/desktop/host"; import { isElectronRuntime } from "@/desktop/host";
import type { import type {
AudioEngine, AudioEngine,
AudioEngineCallbacks, AudioEngineCallbacks,
@@ -289,7 +289,7 @@ export function createAudioEngine(
: true; : true;
const currentOrigin = const currentOrigin =
typeof window !== "undefined" && window.location ? window.location.origin : "unknown"; typeof window !== "undefined" && window.location ? window.location.origin : "unknown";
const isDesktopApp = isDesktop(); const isDesktopApp = isElectronRuntime();
if (missingNavigator) { if (missingNavigator) {
throw new Error("Microphone capture is not supported in this environment"); throw new Error("Microphone capture is not supported in this environment");