fix(app): support ipad desktop layout safely
This commit is contained in:
@@ -4,7 +4,7 @@ import type { ImageAttachment } from "@/components/message-input";
|
||||
import { View, Text, Pressable, ScrollView, Keyboard, Platform } from "react-native";
|
||||
import { useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useIsFocused } from "@react-navigation/native";
|
||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { GestureDetector } from "react-native-gesture-handler";
|
||||
import Animated from "react-native-reanimated";
|
||||
@@ -40,7 +40,7 @@ import {
|
||||
} from "@/runtime/host-runtime";
|
||||
import { ExplorerSidebarAnimationProvider } from "@/contexts/explorer-sidebar-animation-context";
|
||||
import { usePanelStore, type ExplorerCheckoutContext } from "@/stores/panel-store";
|
||||
import { MAX_CONTENT_WIDTH } from "@/constants/layout";
|
||||
import { MAX_CONTENT_WIDTH, isCompactFormFactor } from "@/constants/layout";
|
||||
import { WelcomeScreen } from "@/components/welcome-screen";
|
||||
import type { Agent } from "@/contexts/session-context";
|
||||
import { encodeImages } from "@/utils/encode-images";
|
||||
@@ -233,7 +233,7 @@ function DraftAgentScreenContent({
|
||||
isCreateFlow: true,
|
||||
onlineServerIds,
|
||||
});
|
||||
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
||||
const isMobile = isCompactFormFactor();
|
||||
const mobileView = usePanelStore((state) => state.mobileView);
|
||||
const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen);
|
||||
const toggleFileExplorer = usePanelStore((state) => state.toggleFileExplorer);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect } from "react";
|
||||
import { View, Text } from "react-native";
|
||||
import { StyleSheet, UnistylesRuntime } from "react-native-unistyles";
|
||||
import { StyleSheet } from "react-native-unistyles";
|
||||
import { FolderOpen } from "lucide-react-native";
|
||||
import { PaseoLogo } from "@/components/icons/paseo-logo";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -8,6 +8,7 @@ import { MenuHeader } from "@/components/headers/menu-header";
|
||||
import { useOpenProjectPicker } from "@/hooks/use-open-project-picker";
|
||||
import { usePanelStore } from "@/stores/panel-store";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import { isCompactFormFactor } from "@/constants/layout";
|
||||
import { useDesktopDragHandlers } from "@/utils/desktop-window";
|
||||
|
||||
export function OpenProjectScreen({ serverId }: { serverId: string }) {
|
||||
@@ -16,14 +17,14 @@ export function OpenProjectScreen({ serverId }: { serverId: string }) {
|
||||
const hasHydrated = useSessionStore((s) => s.sessions[serverId]?.hasHydratedWorkspaces ?? false);
|
||||
const hasProjects = useSessionStore((s) => (s.sessions[serverId]?.workspaces?.size ?? 0) > 0);
|
||||
|
||||
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
||||
const isCompactLayout = isCompactFormFactor();
|
||||
const dragHandlers = useDesktopDragHandlers();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMobile) {
|
||||
if (!isCompactLayout) {
|
||||
openAgentList();
|
||||
}
|
||||
}, [isMobile, openAgentList]);
|
||||
}, [isCompactLayout, openAgentList]);
|
||||
|
||||
return (
|
||||
<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 { useFocusEffect } from "@react-navigation/native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { Buffer } from "buffer";
|
||||
import {
|
||||
Sun,
|
||||
@@ -51,7 +51,7 @@ import {
|
||||
import { AdaptiveModalSheet, AdaptiveTextInput } from "@/components/adaptive-modal-sheet";
|
||||
import { DesktopPermissionsSection } from "@/desktop/components/desktop-permissions-section";
|
||||
import { LocalDaemonSection } from "@/desktop/components/desktop-updates-section";
|
||||
import { isDesktop as isDesktopHost } from "@/desktop/host";
|
||||
import { isElectronRuntime } from "@/desktop/host";
|
||||
import { useDesktopAppUpdater } from "@/desktop/updates/use-desktop-app-updater";
|
||||
import { formatVersionWithPrefix } from "@/desktop/updates/desktop-updates";
|
||||
import { resolveAppVersion } from "@/utils/app-version";
|
||||
@@ -59,6 +59,7 @@ import { settingsStyles } from "@/styles/settings";
|
||||
import { THINKING_TONE_NATIVE_PCM_BASE64 } from "@/utils/thinking-tone.native-pcm";
|
||||
import { useVoiceAudioEngineOptional } from "@/contexts/voice-context";
|
||||
import { useIsLocalDaemon } from "@/hooks/use-is-local-daemon";
|
||||
import { isCompactFormFactor } from "@/constants/layout";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Section definitions
|
||||
@@ -79,7 +80,7 @@ interface SettingsSectionDef {
|
||||
icon: ComponentType<{ size: number; color: string }>;
|
||||
}
|
||||
|
||||
function getSettingsSections(context: { isDesktop: boolean }): SettingsSectionDef[] {
|
||||
function getSettingsSections(context: { isDesktopApp: boolean }): SettingsSectionDef[] {
|
||||
const sections: SettingsSectionDef[] = [
|
||||
{ id: "hosts", label: "Hosts", icon: Server },
|
||||
{ id: "appearance", label: "Appearance", icon: Palette },
|
||||
@@ -88,7 +89,7 @@ function getSettingsSections(context: { isDesktop: boolean }): SettingsSectionDe
|
||||
{ id: "about", label: "About", icon: Info },
|
||||
];
|
||||
|
||||
if (context.isDesktop) {
|
||||
if (context.isDesktopApp) {
|
||||
sections.push(
|
||||
{ id: "permissions", label: "Permissions", icon: Shield },
|
||||
{ id: "daemon", label: "Daemon", icon: Settings },
|
||||
@@ -174,7 +175,6 @@ interface HostsSectionProps {
|
||||
daemons: HostProfile[];
|
||||
settings: AppSettings;
|
||||
routeServerId: string;
|
||||
isDesktop: boolean;
|
||||
theme: ReturnType<typeof useUnistyles>["theme"];
|
||||
handleEditDaemon: (profile: HostProfile) => void;
|
||||
setAddConnectionTargetServerId: (id: string | null) => void;
|
||||
@@ -464,10 +464,10 @@ function DiagnosticsSection({
|
||||
|
||||
interface AboutSectionProps {
|
||||
appVersionText: string;
|
||||
isDesktop: boolean;
|
||||
isDesktopApp: boolean;
|
||||
}
|
||||
|
||||
function AboutSection({ appVersionText, isDesktop }: AboutSectionProps) {
|
||||
function AboutSection({ appVersionText, isDesktopApp }: AboutSectionProps) {
|
||||
return (
|
||||
<View style={settingsStyles.section}>
|
||||
<Text style={settingsStyles.sectionTitle}>About</Text>
|
||||
@@ -478,7 +478,7 @@ function AboutSection({ appVersionText, isDesktop }: AboutSectionProps) {
|
||||
</View>
|
||||
<Text style={styles.aboutValue}>{appVersionText}</Text>
|
||||
</View>
|
||||
{isDesktop ? <DesktopAppUpdateRow /> : null}
|
||||
{isDesktopApp ? <DesktopAppUpdateRow /> : null}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
@@ -496,7 +496,7 @@ interface SettingsSectionContentProps {
|
||||
aboutProps: AboutSectionProps;
|
||||
appVersion: string | null;
|
||||
isLocalDaemon: boolean;
|
||||
isDesktop: boolean;
|
||||
isDesktopApp: boolean;
|
||||
}
|
||||
|
||||
function SettingsSectionContent({
|
||||
@@ -507,7 +507,7 @@ function SettingsSectionContent({
|
||||
aboutProps,
|
||||
appVersion,
|
||||
isLocalDaemon,
|
||||
isDesktop,
|
||||
isDesktopApp,
|
||||
}: SettingsSectionContentProps) {
|
||||
switch (sectionId) {
|
||||
case "hosts":
|
||||
@@ -521,9 +521,9 @@ function SettingsSectionContent({
|
||||
case "about":
|
||||
return <AboutSection {...aboutProps} />;
|
||||
case "permissions":
|
||||
return isDesktop ? <DesktopPermissionsSection /> : null;
|
||||
return isDesktopApp ? <DesktopPermissionsSection /> : null;
|
||||
case "daemon":
|
||||
return isDesktop ? (
|
||||
return isDesktopApp ? (
|
||||
<LocalDaemonSection appVersion={appVersion} showLifecycleControls={isLocalDaemon} />
|
||||
) : null;
|
||||
}
|
||||
@@ -619,7 +619,7 @@ function SettingsDesktopLayout({ sections, sectionContentProps }: SettingsLayout
|
||||
|
||||
function DesktopAppUpdateRow() {
|
||||
const {
|
||||
isDesktop,
|
||||
isDesktopApp,
|
||||
statusText,
|
||||
availableUpdate,
|
||||
errorMessage,
|
||||
@@ -631,23 +631,23 @@ function DesktopAppUpdateRow() {
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
if (!isDesktop) {
|
||||
if (!isDesktopApp) {
|
||||
return undefined;
|
||||
}
|
||||
void checkForUpdates({ silent: true });
|
||||
return undefined;
|
||||
}, [checkForUpdates, isDesktop]),
|
||||
}, [checkForUpdates, isDesktopApp]),
|
||||
);
|
||||
|
||||
const handleCheckForUpdates = useCallback(() => {
|
||||
if (!isDesktop) {
|
||||
if (!isDesktopApp) {
|
||||
return;
|
||||
}
|
||||
void checkForUpdates();
|
||||
}, [checkForUpdates, isDesktop]);
|
||||
}, [checkForUpdates, isDesktopApp]);
|
||||
|
||||
const handleInstallUpdate = useCallback(() => {
|
||||
if (!isDesktop) {
|
||||
if (!isDesktopApp) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -667,9 +667,9 @@ function DesktopAppUpdateRow() {
|
||||
console.error("[Settings] Failed to open app update confirmation", error);
|
||||
Alert.alert("Error", "Unable to open the update confirmation dialog.");
|
||||
});
|
||||
}, [installUpdate, isDesktop]);
|
||||
}, [installUpdate, isDesktopApp]);
|
||||
|
||||
if (!isDesktop) {
|
||||
if (!isDesktopApp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -745,7 +745,7 @@ export default function SettingsScreen() {
|
||||
const isLoading = settingsLoading;
|
||||
const isMountedRef = useRef(true);
|
||||
const lastHandledEditHostRef = useRef<string | null>(null);
|
||||
const isDesktop = isDesktopHost();
|
||||
const isDesktopApp = isElectronRuntime();
|
||||
const isLocalDaemon = useIsLocalDaemon(routeServerId);
|
||||
const appVersion = resolveAppVersion();
|
||||
const appVersionText = formatVersionWithPrefix(appVersion);
|
||||
@@ -929,14 +929,13 @@ export default function SettingsScreen() {
|
||||
}
|
||||
}, [isPlaybackTestRunning, voiceAudioEngine]);
|
||||
|
||||
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
||||
const sections = getSettingsSections({ isDesktop });
|
||||
const isCompactLayout = isCompactFormFactor();
|
||||
const sections = getSettingsSections({ isDesktopApp });
|
||||
|
||||
const hostsProps: HostsSectionProps = {
|
||||
daemons,
|
||||
settings,
|
||||
routeServerId,
|
||||
isDesktop,
|
||||
theme,
|
||||
handleEditDaemon,
|
||||
setAddConnectionTargetServerId,
|
||||
@@ -985,7 +984,7 @@ export default function SettingsScreen() {
|
||||
|
||||
const aboutProps: AboutSectionProps = {
|
||||
appVersionText,
|
||||
isDesktop,
|
||||
isDesktopApp,
|
||||
};
|
||||
|
||||
const sectionContentProps: Omit<SettingsSectionContentProps, "sectionId"> = {
|
||||
@@ -995,7 +994,7 @@ export default function SettingsScreen() {
|
||||
aboutProps,
|
||||
appVersion,
|
||||
isLocalDaemon,
|
||||
isDesktop,
|
||||
isDesktopApp,
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
@@ -1009,7 +1008,7 @@ export default function SettingsScreen() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<MenuHeader title="Settings" />
|
||||
{isMobile ? (
|
||||
{isCompactLayout ? (
|
||||
<SettingsMobileLayout sections={sections} sectionContentProps={sectionContentProps} />
|
||||
) : (
|
||||
<SettingsDesktopLayout sections={sections} sectionContentProps={sectionContentProps} />
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from "@/keyboard/shortcut-string";
|
||||
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
|
||||
import { getShortcutOs } from "@/utils/shortcut-platform";
|
||||
import { getIsDesktop } from "@/constants/layout";
|
||||
import { getIsElectronRuntime } from "@/constants/layout";
|
||||
|
||||
function ShortcutSequence({ chord }: { chord: string[] | null }) {
|
||||
if (!chord || chord.length === 0) {
|
||||
@@ -93,8 +93,8 @@ export function KeyboardShortcutsSection() {
|
||||
const setCapturingShortcut = useKeyboardShortcutsStore((s) => s.setCapturingShortcut);
|
||||
|
||||
const isMac = getShortcutOs() === "mac";
|
||||
const isDesktop = getIsDesktop();
|
||||
const sections = buildKeyboardShortcutHelpSections({ isMac, isDesktop });
|
||||
const isDesktopApp = getIsElectronRuntime();
|
||||
const sections = buildKeyboardShortcutHelpSections({ isMac, isDesktop: isDesktopApp });
|
||||
|
||||
function cancelCapture() {
|
||||
setCapturedCombos([]);
|
||||
@@ -180,7 +180,10 @@ export function KeyboardShortcutsSection() {
|
||||
<Text style={styles.subsectionTitle}>{section.title}</Text>
|
||||
<View style={settingsStyles.card}>
|
||||
{section.rows.map(function (row, index) {
|
||||
const bindingId = getBindingIdForAction(row.id, { isMac, isDesktop });
|
||||
const bindingId = getBindingIdForAction(row.id, {
|
||||
isMac,
|
||||
isDesktop: isDesktopApp,
|
||||
});
|
||||
const overrideCombo = bindingId ? overrides[bindingId] : undefined;
|
||||
|
||||
return (
|
||||
|
||||
@@ -83,6 +83,7 @@ type WorkspaceDesktopTabsRowProps = {
|
||||
externalDndContext?: boolean;
|
||||
activeDragTabId?: string | null;
|
||||
tabDropPreviewIndex?: number | null;
|
||||
showPaneSplitActions?: boolean;
|
||||
};
|
||||
|
||||
function getFallbackTabLabel(tab: WorkspaceTabDescriptor): string {
|
||||
@@ -346,6 +347,7 @@ export function WorkspaceDesktopTabsRow({
|
||||
externalDndContext = false,
|
||||
activeDragTabId = null,
|
||||
tabDropPreviewIndex = null,
|
||||
showPaneSplitActions = true,
|
||||
}: WorkspaceDesktopTabsRowProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const newAgentTabKeys = useShortcutKeys("workspace-tab-new");
|
||||
@@ -522,48 +524,52 @@ export function WorkspaceDesktopTabsRow({
|
||||
</View>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
||||
<TooltipTrigger
|
||||
onPress={onSplitRight}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Split pane right"
|
||||
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}>
|
||||
<View style={styles.newTabTooltipRow}>
|
||||
<Text style={styles.newTabTooltipText}>Split pane right</Text>
|
||||
{splitRightKeys ? (
|
||||
<Shortcut chord={splitRightKeys} style={styles.newTabTooltipShortcut} />
|
||||
) : null}
|
||||
</View>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
||||
<TooltipTrigger
|
||||
onPress={onSplitDown}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Split pane down"
|
||||
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}>
|
||||
<View style={styles.newTabTooltipRow}>
|
||||
<Text style={styles.newTabTooltipText}>Split pane down</Text>
|
||||
{splitDownKeys ? (
|
||||
<Shortcut chord={splitDownKeys} style={styles.newTabTooltipShortcut} />
|
||||
) : null}
|
||||
</View>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{showPaneSplitActions ? (
|
||||
<>
|
||||
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
||||
<TooltipTrigger
|
||||
onPress={onSplitRight}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Split pane right"
|
||||
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}>
|
||||
<View style={styles.newTabTooltipRow}>
|
||||
<Text style={styles.newTabTooltipText}>Split pane right</Text>
|
||||
{splitRightKeys ? (
|
||||
<Shortcut chord={splitRightKeys} style={styles.newTabTooltipShortcut} />
|
||||
) : null}
|
||||
</View>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
||||
<TooltipTrigger
|
||||
onPress={onSplitDown}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Split pane down"
|
||||
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}>
|
||||
<View style={styles.newTabTooltipRow}>
|
||||
<Text style={styles.newTabTooltipText}>Split pane down</Text>
|
||||
{splitDownKeys ? (
|
||||
<Shortcut chord={splitDownKeys} style={styles.newTabTooltipShortcut} />
|
||||
) : null}
|
||||
</View>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
import { GestureDetector } from "react-native-gesture-handler";
|
||||
import Animated from "react-native-reanimated";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import invariant from "tiny-invariant";
|
||||
import { SidebarMenuToggle } from "@/components/headers/menu-header";
|
||||
import { HeaderToggleButton } from "@/components/headers/header-toggle-button";
|
||||
@@ -86,6 +86,10 @@ import {
|
||||
WorkspaceTabIcon,
|
||||
WorkspaceTabOptionRow,
|
||||
} from "@/screens/workspace/workspace-tab-presentation";
|
||||
import {
|
||||
WorkspaceDesktopTabsRow,
|
||||
type WorkspaceDesktopTabRowItem,
|
||||
} from "@/screens/workspace/workspace-desktop-tabs-row";
|
||||
import { buildWorkspaceTabMenuEntries } from "@/screens/workspace/workspace-tab-menu";
|
||||
import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types";
|
||||
import {
|
||||
@@ -110,6 +114,7 @@ import {
|
||||
closeBulkWorkspaceTabs,
|
||||
} from "@/screens/workspace/workspace-bulk-close";
|
||||
import { findAdjacentPane } from "@/utils/split-navigation";
|
||||
import { isCompactFormFactor, supportsDesktopPaneSplits } from "@/constants/layout";
|
||||
|
||||
const TERMINALS_QUERY_STALE_TIME = 5_000;
|
||||
const NEW_TAB_AGENT_OPTION_ID = "__new_tab_agent__";
|
||||
@@ -576,7 +581,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
const isDarkMode = useColorScheme() === "dark";
|
||||
const mainBackgroundColor = isDarkMode ? theme.colors.surface1 : theme.colors.surface0;
|
||||
const toast = useToast();
|
||||
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
||||
const isMobile = isCompactFormFactor();
|
||||
const isFocusModeEnabled = usePanelStore((state) => state.desktop.focusModeEnabled);
|
||||
|
||||
const normalizedServerId = trimNonEmpty(decodeSegment(serverId)) ?? "";
|
||||
@@ -1703,6 +1708,8 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
});
|
||||
|
||||
const activeTabDescriptor = activeTab?.descriptor ?? null;
|
||||
const canRenderDesktopPaneSplits = supportsDesktopPaneSplits();
|
||||
const shouldRenderDesktopPaneFallback = !isMobile && !canRenderDesktopPaneSplits;
|
||||
useEffect(() => {
|
||||
if (Platform.OS !== "web" || typeof document === "undefined" || activeTabDescriptor) {
|
||||
return;
|
||||
@@ -1881,6 +1888,17 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
[buildPaneContentModel],
|
||||
);
|
||||
|
||||
const desktopTabRowItems = useMemo<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) {
|
||||
if (!persistenceKey || paneFocusSuppressedRef.current) {
|
||||
return;
|
||||
@@ -1932,6 +1950,19 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
[persistenceKey, reorderWorkspaceTabsInPane],
|
||||
);
|
||||
|
||||
const handleReorderTabsInFocusedPane = useCallback(
|
||||
(nextTabs: WorkspaceTabDescriptor[]) => {
|
||||
if (!focusedPaneId) {
|
||||
return;
|
||||
}
|
||||
handleReorderTabsInPane(
|
||||
focusedPaneId,
|
||||
nextTabs.map((tab) => tab.tabId),
|
||||
);
|
||||
},
|
||||
[focusedPaneId, handleReorderTabsInPane],
|
||||
);
|
||||
|
||||
const renderSplitPaneEmptyState = useCallback(function renderSplitPaneEmptyState() {
|
||||
return (
|
||||
<View style={styles.emptyState}>
|
||||
@@ -2187,6 +2218,32 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
/>
|
||||
) : 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}>
|
||||
{isMobile ? (
|
||||
<GestureDetector gesture={explorerOpenGesture} touchAction="pan-y">
|
||||
@@ -2194,7 +2251,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
</GestureDetector>
|
||||
) : (
|
||||
<View style={styles.content}>
|
||||
{workspaceLayout && persistenceKey ? (
|
||||
{canRenderDesktopPaneSplits && workspaceLayout && persistenceKey ? (
|
||||
<SplitContainer
|
||||
layout={workspaceLayout}
|
||||
focusModeEnabled={isFocusModeEnabled && !isMobile}
|
||||
|
||||
Reference in New Issue
Block a user