fix(app): support ipad desktop layout safely
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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,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}>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 ?? "";
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
Reference in New Issue
Block a user