Fix mobile autocomplete sidebar layering
This commit is contained in:
+15
-6
@@ -40,10 +40,12 @@ Two escape hatches in the codebase:
|
|||||||
(it has its own input) and tooltip (no input). **Not** fine for autocomplete
|
(it has its own input) and tooltip (no input). **Not** fine for autocomplete
|
||||||
(the composer's input must stay focused so the user keeps typing).
|
(the composer's input must stay focused so the user keeps typing).
|
||||||
- **`<Portal>` from `@gorhom/portal`** (hover-card, autocomplete-popover) —
|
- **`<Portal>` from `@gorhom/portal`** (hover-card, autocomplete-popover) —
|
||||||
hoists the React subtree to a fixed mount point (`PortalHost name="root"` in
|
hoists the React subtree to a fixed mount point whose bounds cover the
|
||||||
`app/_layout.tsx`) whose bounds cover the screen. Same window, same IME,
|
screen. Same window, same IME, hit-test works because the new parent is
|
||||||
hit-test works because the new parent is full-screen. This is the right
|
full-screen. This is the right default when you must keep IME attachment.
|
||||||
default when you must keep IME attachment.
|
Choose the host by layer: app-global overlays use the root host; composer
|
||||||
|
autocomplete uses the content floating-panel host so sliding sidebars cover
|
||||||
|
it.
|
||||||
|
|
||||||
Choose Modal vs Portal by whether you need the underlying input to keep its
|
Choose Modal vs Portal by whether you need the underlying input to keep its
|
||||||
keyboard.
|
keyboard.
|
||||||
@@ -63,8 +65,15 @@ quietly relying on:
|
|||||||
- **Transforms.** The composer is wrapped in a Reanimated `Animated.View` with
|
- **Transforms.** The composer is wrapped in a Reanimated `Animated.View` with
|
||||||
`translateY: -keyboardShift` (see `use-keyboard-shift-style.ts`). The chat
|
`translateY: -keyboardShift` (see `use-keyboard-shift-style.ts`). The chat
|
||||||
content has the same transform applied (`agent-panel.tsx:939`). They move
|
content has the same transform applied (`agent-panel.tsx:939`). They move
|
||||||
together because they share the SharedValue. A portal'd popover is at the
|
together because they share the SharedValue. A portal'd popover is outside
|
||||||
app root — it does not get that transform unless you apply it yourself.
|
the composer tree — it does not get that transform unless you apply it
|
||||||
|
yourself.
|
||||||
|
- **Layering.** The default root host renders after app content, so it sits
|
||||||
|
above compact sidebars. Autocomplete should use
|
||||||
|
`useFloatingPanelPortalHostName()` and render through the current
|
||||||
|
`FloatingPanelPortalHost` instead. The app shell provides a default content
|
||||||
|
host; workspace screens provide a per-workspace host between the center pane
|
||||||
|
and explorer sidebar so both compact sidebars cover autocomplete.
|
||||||
|
|
||||||
The fix for transforms is Gotcha 3.
|
The fix for transforms is Gotcha 3.
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,16 @@ interface PopoverFrameRecorderWindow extends Window {
|
|||||||
__stopComposerAutocompleteFrameRecorder?: () => void;
|
__stopComposerAutocompleteFrameRecorder?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getTopTestIdAtPoint(page: Page, x: number, y: number) {
|
||||||
|
return page.evaluate(
|
||||||
|
([pointX, pointY]) => {
|
||||||
|
const element = document.elementFromPoint(pointX, pointY);
|
||||||
|
return element?.closest("[data-testid]")?.getAttribute("data-testid") ?? null;
|
||||||
|
},
|
||||||
|
[x, y],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function getServerId(): string {
|
function getServerId(): string {
|
||||||
const serverId = process.env.E2E_SERVER_ID;
|
const serverId = process.env.E2E_SERVER_ID;
|
||||||
if (!serverId) {
|
if (!serverId) {
|
||||||
@@ -165,7 +175,10 @@ async function cleanupWithin(timeoutMs: number, cleanup: () => Promise<void>): P
|
|||||||
await Promise.race([operation, new Promise<void>((resolve) => setTimeout(resolve, timeoutMs))]);
|
await Promise.race([operation, new Promise<void>((resolve) => setTimeout(resolve, timeoutMs))]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openReadyMockAgent(page: Page): Promise<{
|
async function openReadyMockAgent(
|
||||||
|
page: Page,
|
||||||
|
options?: { expectWorkspaceTab?: boolean },
|
||||||
|
): Promise<{
|
||||||
cleanup: () => Promise<void>;
|
cleanup: () => Promise<void>;
|
||||||
}> {
|
}> {
|
||||||
const repo = await createTempGitRepo("autocomplete-popover-");
|
const repo = await createTempGitRepo("autocomplete-popover-");
|
||||||
@@ -189,7 +202,9 @@ async function openReadyMockAgent(page: Page): Promise<{
|
|||||||
(url) => url.pathname.includes("/workspace/") && !url.searchParams.has("open"),
|
(url) => url.pathname.includes("/workspace/") && !url.searchParams.has("open"),
|
||||||
{ timeout: 60_000 },
|
{ timeout: 60_000 },
|
||||||
);
|
);
|
||||||
await expectWorkspaceTabVisible(page, agent.id);
|
if (options?.expectWorkspaceTab !== false) {
|
||||||
|
await expectWorkspaceTabVisible(page, agent.id);
|
||||||
|
}
|
||||||
await expectComposerVisible(page);
|
await expectComposerVisible(page);
|
||||||
return {
|
return {
|
||||||
cleanup: async () => {
|
cleanup: async () => {
|
||||||
@@ -455,4 +470,40 @@ test.describe("Composer autocomplete", () => {
|
|||||||
await agent.cleanup();
|
await agent.cleanup();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe("compact sidebar layering", () => {
|
||||||
|
test.use({ viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true });
|
||||||
|
|
||||||
|
test("keeps the mobile agent sidebar above autocomplete", async ({ page }) => {
|
||||||
|
await installListCommandsStub(page);
|
||||||
|
const agent = await openReadyMockAgent(page, { expectWorkspaceTab: false });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const input = composerLocator(page);
|
||||||
|
await expect(input).toBeEditable({ timeout: 30_000 });
|
||||||
|
|
||||||
|
await input.fill("/");
|
||||||
|
const popover = page.getByTestId("composer-autocomplete-popover");
|
||||||
|
await expect(popover.getByText("/help", { exact: true }).first()).toBeVisible({
|
||||||
|
timeout: 30_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "Open menu" }).click();
|
||||||
|
await expect(page.getByTestId("sidebar-sessions")).toBeInViewport({ timeout: 5_000 });
|
||||||
|
|
||||||
|
const popoverBox = await popover.boundingBox();
|
||||||
|
expect(popoverBox).not.toBeNull();
|
||||||
|
|
||||||
|
const topTestId = await getTopTestIdAtPoint(
|
||||||
|
page,
|
||||||
|
popoverBox!.x + popoverBox!.width / 2,
|
||||||
|
popoverBox!.y + popoverBox!.height / 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(topTestId).not.toBe("composer-autocomplete-popover");
|
||||||
|
} finally {
|
||||||
|
await agent.cleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import { LeftSidebar } from "@/components/left-sidebar";
|
|||||||
import { ProjectPickerModal } from "@/components/project-picker-modal";
|
import { ProjectPickerModal } from "@/components/project-picker-modal";
|
||||||
import { WorkspaceSetupDialog } from "@/components/workspace-setup-dialog";
|
import { WorkspaceSetupDialog } from "@/components/workspace-setup-dialog";
|
||||||
import { WorkspaceShortcutTargetsSubscriber } from "@/components/workspace-shortcut-targets-subscriber";
|
import { WorkspaceShortcutTargetsSubscriber } from "@/components/workspace-shortcut-targets-subscriber";
|
||||||
|
import { FloatingPanelPortalHost } from "@/components/ui/floating-panel-portal";
|
||||||
import { getIsElectronRuntime, useIsCompactFormFactor } from "@/constants/layout";
|
import { getIsElectronRuntime, useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import { isNative, isWeb } from "@/constants/platform";
|
import { isNative, isWeb } from "@/constants/platform";
|
||||||
import {
|
import {
|
||||||
@@ -457,6 +458,7 @@ function AppContainer({
|
|||||||
)}
|
)}
|
||||||
<View style={flexStyle}>{children}</View>
|
<View style={flexStyle}>{children}</View>
|
||||||
</View>
|
</View>
|
||||||
|
<FloatingPanelPortalHost />
|
||||||
{isCompactLayout && chromeEnabled && <LeftSidebar selectedAgentId={selectedAgentId} />}
|
{isCompactLayout && chromeEnabled && <LeftSidebar selectedAgentId={selectedAgentId} />}
|
||||||
<DownloadToast />
|
<DownloadToast />
|
||||||
<RosettaCalloutSource />
|
<RosettaCalloutSource />
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller
|
|||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { StyleSheet } from "react-native-unistyles";
|
import { StyleSheet } from "react-native-unistyles";
|
||||||
import { Autocomplete, type AutocompleteOption } from "@/components/ui/autocomplete";
|
import { Autocomplete, type AutocompleteOption } from "@/components/ui/autocomplete";
|
||||||
|
import { useFloatingPanelPortalHostName } from "@/components/ui/floating-panel-portal";
|
||||||
import { SPACING } from "@/styles/theme";
|
import { SPACING } from "@/styles/theme";
|
||||||
import { inlineUnistylesStyle } from "@/styles/unistyles-inline-style";
|
import { inlineUnistylesStyle } from "@/styles/unistyles-inline-style";
|
||||||
|
|
||||||
@@ -56,6 +57,7 @@ export function AutocompletePopover({
|
|||||||
const [anchorRect, setAnchorRect] = useState<Rect | null>(null);
|
const [anchorRect, setAnchorRect] = useState<Rect | null>(null);
|
||||||
const { height: windowHeight } = useWindowDimensions();
|
const { height: windowHeight } = useWindowDimensions();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
const portalHostName = useFloatingPanelPortalHostName();
|
||||||
|
|
||||||
const { height: rawKeyboardHeight } = useReanimatedKeyboardAnimation();
|
const { height: rawKeyboardHeight } = useReanimatedKeyboardAnimation();
|
||||||
const bottomInsetSV = useSharedValue(insets.bottom);
|
const bottomInsetSV = useSharedValue(insets.bottom);
|
||||||
@@ -127,7 +129,7 @@ export function AutocompletePopover({
|
|||||||
if (!visible || !anchorRect || !baseStyle) return null;
|
if (!visible || !anchorRect || !baseStyle) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Portal>
|
<Portal hostName={portalHostName}>
|
||||||
<View style={styles.overlay} pointerEvents="box-none">
|
<View style={styles.overlay} pointerEvents="box-none">
|
||||||
<Animated.View testID="composer-autocomplete-popover" style={composedStyle}>
|
<Animated.View testID="composer-autocomplete-popover" style={composedStyle}>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { createContext, type ReactElement, type ReactNode, useContext } from "react";
|
||||||
|
import { PortalHost } from "@gorhom/portal";
|
||||||
|
|
||||||
|
export const DEFAULT_FLOATING_PANEL_PORTAL_HOST = "content-floating-panels";
|
||||||
|
|
||||||
|
const FloatingPanelPortalHostNameContext = createContext(DEFAULT_FLOATING_PANEL_PORTAL_HOST);
|
||||||
|
|
||||||
|
export function FloatingPanelPortalHostNameProvider({
|
||||||
|
hostName,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
hostName: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}): ReactElement {
|
||||||
|
return (
|
||||||
|
<FloatingPanelPortalHostNameContext.Provider value={hostName}>
|
||||||
|
{children}
|
||||||
|
</FloatingPanelPortalHostNameContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFloatingPanelPortalHostName(): string {
|
||||||
|
return useContext(FloatingPanelPortalHostNameContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FloatingPanelPortalHost({
|
||||||
|
name = DEFAULT_FLOATING_PANEL_PORTAL_HOST,
|
||||||
|
}: {
|
||||||
|
name?: string;
|
||||||
|
}): ReactElement {
|
||||||
|
return <PortalHost name={name} />;
|
||||||
|
}
|
||||||
@@ -53,6 +53,10 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
|
import {
|
||||||
|
FloatingPanelPortalHost,
|
||||||
|
FloatingPanelPortalHostNameProvider,
|
||||||
|
} from "@/components/ui/floating-panel-portal";
|
||||||
import { ExplorerSidebar } from "@/components/explorer-sidebar";
|
import { ExplorerSidebar } from "@/components/explorer-sidebar";
|
||||||
import { SplitContainer } from "@/components/split-container";
|
import { SplitContainer } from "@/components/split-container";
|
||||||
import { SourceControlPanelIcon } from "@/components/icons/source-control-panel-icon";
|
import { SourceControlPanelIcon } from "@/components/icons/source-control-panel-icon";
|
||||||
@@ -171,6 +175,7 @@ import {
|
|||||||
} from "@/workspace/file-open";
|
} from "@/workspace/file-open";
|
||||||
|
|
||||||
const WORKSPACE_SETUP_AUTO_OPEN_WINDOW_MS = 30_000;
|
const WORKSPACE_SETUP_AUTO_OPEN_WINDOW_MS = 30_000;
|
||||||
|
const WORKSPACE_FLOATING_PANEL_PORTAL_HOST_PREFIX = "workspace-floating-panels";
|
||||||
const EMPTY_UI_TABS: WorkspaceTab[] = [];
|
const EMPTY_UI_TABS: WorkspaceTab[] = [];
|
||||||
const EMPTY_WORKSPACE_SCRIPTS: WorkspaceDescriptor["scripts"] = [];
|
const EMPTY_WORKSPACE_SCRIPTS: WorkspaceDescriptor["scripts"] = [];
|
||||||
const EMPTY_PINNED_AGENT_IDS = new Set<string>();
|
const EMPTY_PINNED_AGENT_IDS = new Set<string>();
|
||||||
@@ -3179,6 +3184,11 @@ function WorkspaceScreenContent({
|
|||||||
() => isFocusModeEnabled && !isMobile,
|
() => isFocusModeEnabled && !isMobile,
|
||||||
[isFocusModeEnabled, isMobile],
|
[isFocusModeEnabled, isMobile],
|
||||||
);
|
);
|
||||||
|
const workspaceFloatingPanelPortalHostName = useMemo(
|
||||||
|
() =>
|
||||||
|
`${WORKSPACE_FLOATING_PANEL_PORTAL_HOST_PREFIX}:${normalizedServerId}:${normalizedWorkspaceId}`,
|
||||||
|
[normalizedServerId, normalizedWorkspaceId],
|
||||||
|
);
|
||||||
const desktopContent = useMemo(() => {
|
const desktopContent = useMemo(() => {
|
||||||
if (!canRenderDesktopPaneSplits || !workspaceLayout || !persistenceKey) {
|
if (!canRenderDesktopPaneSplits || !workspaceLayout || !persistenceKey) {
|
||||||
return content;
|
return content;
|
||||||
@@ -3254,6 +3264,120 @@ function WorkspaceScreenContent({
|
|||||||
renderSplitPaneEmptyState,
|
renderSplitPaneEmptyState,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const workspaceCenterColumn = (
|
||||||
|
<View style={styles.centerColumn}>
|
||||||
|
{showScreenHeader && (
|
||||||
|
<ScreenHeader
|
||||||
|
onRowLayout={onHeaderLayout}
|
||||||
|
left={
|
||||||
|
<>
|
||||||
|
<SidebarMenuToggle />
|
||||||
|
<WorkspaceHeaderTitleBar
|
||||||
|
isLoading={isWorkspaceHeaderLoading}
|
||||||
|
title={workspaceHeaderTitle}
|
||||||
|
subtitle={workspaceHeaderSubtitle}
|
||||||
|
showSubtitle={shouldShowWorkspaceHeaderSubtitle}
|
||||||
|
currentBranchName={currentBranchName}
|
||||||
|
isGitCheckout={isGitCheckout}
|
||||||
|
normalizedServerId={normalizedServerId}
|
||||||
|
normalizedWorkspaceId={normalizedWorkspaceId}
|
||||||
|
workspaceScripts={workspaceScripts}
|
||||||
|
liveTerminalIds={liveTerminalIds}
|
||||||
|
showWorkspaceSetup={showWorkspaceSetup}
|
||||||
|
showCreateBrowserTab={showCreateBrowserTab}
|
||||||
|
isMobile={isMobile}
|
||||||
|
createTerminalDisabled={createTerminalDisabled}
|
||||||
|
importAgentDisabled={!canOpenImportSheet}
|
||||||
|
menuNewAgentIcon={menuNewAgentIcon}
|
||||||
|
menuNewTerminalIcon={menuNewTerminalIcon}
|
||||||
|
menuNewBrowserIcon={MENU_NEW_BROWSER_ICON}
|
||||||
|
menuImportIcon={MENU_IMPORT_ICON}
|
||||||
|
menuCopyIcon={menuCopyIcon}
|
||||||
|
menuSettingsIcon={menuSettingsIcon}
|
||||||
|
onCreateDraftTab={handleCreateDraftTab}
|
||||||
|
onCreateTerminal={handleCreateTerminal}
|
||||||
|
onCreateBrowser={handleCreateBrowserTab}
|
||||||
|
onOpenImportSheet={openImportSheet}
|
||||||
|
onCopyWorkspacePath={handleCopyWorkspacePath}
|
||||||
|
onCopyBranchName={handleCopyBranchName}
|
||||||
|
onOpenSetupTab={handleOpenSetupTab}
|
||||||
|
onScriptTerminalStarted={handleScriptTerminalStarted}
|
||||||
|
onViewScriptTerminal={handleViewScriptTerminal}
|
||||||
|
onOpenUrlInBrowserTab={handleOpenUrlInBrowserTab}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
right={headerRight}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isMobile ? (
|
||||||
|
<MobileWorkspaceTabSwitcher
|
||||||
|
tabs={tabs}
|
||||||
|
activeTabKey={activeTabKey}
|
||||||
|
activeTab={activeTabDescriptor}
|
||||||
|
tabSwitcherOptions={tabSwitcherOptions}
|
||||||
|
tabByKey={tabByKey}
|
||||||
|
normalizedServerId={normalizedServerId}
|
||||||
|
normalizedWorkspaceId={normalizedWorkspaceId}
|
||||||
|
onSelectSwitcherTab={handleSelectSwitcherTab}
|
||||||
|
onCopyResumeCommand={handleCopyResumeCommand}
|
||||||
|
onCopyAgentId={handleCopyAgentId}
|
||||||
|
onReloadAgent={handleReloadAgent}
|
||||||
|
onRenameTab={handleRenameTab}
|
||||||
|
onCloseTab={handleCloseTabById}
|
||||||
|
onCloseTabsAbove={handleCloseTabsToLeft}
|
||||||
|
onCloseTabsBelow={handleCloseTabsToRight}
|
||||||
|
onCloseOtherTabs={handleCloseOtherTabs}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{shouldRenderDesktopPaneFallback ? (
|
||||||
|
<WorkspaceDesktopTabsRow
|
||||||
|
paneId={focusedPaneIdOrUndefined}
|
||||||
|
isFocused={isRouteFocused}
|
||||||
|
tabs={desktopTabRowItems}
|
||||||
|
normalizedServerId={normalizedServerId}
|
||||||
|
normalizedWorkspaceId={normalizedWorkspaceId}
|
||||||
|
setHoveredTabKey={setHoveredTabKey}
|
||||||
|
setHoveredCloseTabKey={setHoveredCloseTabKey}
|
||||||
|
onNavigateTab={navigateToTabId}
|
||||||
|
onCloseTab={handleCloseTabById}
|
||||||
|
onCopyResumeCommand={handleCopyResumeCommand}
|
||||||
|
onCopyAgentId={handleCopyAgentId}
|
||||||
|
onReloadAgent={handleReloadAgent}
|
||||||
|
onRenameTab={handleRenameTab}
|
||||||
|
onCloseTabsToLeft={handleCloseTabsToLeft}
|
||||||
|
onCloseTabsToRight={handleCloseTabsToRight}
|
||||||
|
onCloseOtherTabs={handleCloseOtherTabs}
|
||||||
|
onCreateDraftTab={handleCreateDraftTab}
|
||||||
|
onCreateTerminalTab={handleCreateTerminal}
|
||||||
|
onCreateBrowserTab={handleCreateBrowserTab}
|
||||||
|
showCreateBrowserTab={showCreateBrowserTab}
|
||||||
|
disableCreateTerminal={createTerminalMutation.isPending}
|
||||||
|
isWaitingOnTerminalReadiness={pendingTerminalCreateInput !== null}
|
||||||
|
onReorderTabs={handleReorderTabsInFocusedPane}
|
||||||
|
onSplitRight={noop}
|
||||||
|
onSplitDown={noop}
|
||||||
|
showPaneSplitActions={false}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<View style={styles.centerContent}>
|
||||||
|
{isMobile ? (
|
||||||
|
<GestureDetector
|
||||||
|
gesture={explorerOpenGesture}
|
||||||
|
touchAction={COMPACT_WEB_GESTURE_TOUCH_ACTION}
|
||||||
|
>
|
||||||
|
<View style={styles.content}>{content}</View>
|
||||||
|
</GestureDetector>
|
||||||
|
) : (
|
||||||
|
<View style={styles.content}>{desktopContent}</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
gatedWorkspaceScreen ?? (
|
gatedWorkspaceScreen ?? (
|
||||||
<WorkspaceFocusProvider workspaceKey={persistenceKey}>
|
<WorkspaceFocusProvider workspaceKey={persistenceKey}>
|
||||||
@@ -3265,117 +3389,11 @@ function WorkspaceScreenContent({
|
|||||||
isRouteFocused={isRouteFocused}
|
isRouteFocused={isRouteFocused}
|
||||||
/>
|
/>
|
||||||
<View style={styles.threePaneRow}>
|
<View style={styles.threePaneRow}>
|
||||||
<View style={styles.centerColumn}>
|
<FloatingPanelPortalHostNameProvider hostName={workspaceFloatingPanelPortalHostName}>
|
||||||
{showScreenHeader && (
|
{workspaceCenterColumn}
|
||||||
<ScreenHeader
|
</FloatingPanelPortalHostNameProvider>
|
||||||
onRowLayout={onHeaderLayout}
|
|
||||||
left={
|
|
||||||
<>
|
|
||||||
<SidebarMenuToggle />
|
|
||||||
<WorkspaceHeaderTitleBar
|
|
||||||
isLoading={isWorkspaceHeaderLoading}
|
|
||||||
title={workspaceHeaderTitle}
|
|
||||||
subtitle={workspaceHeaderSubtitle}
|
|
||||||
showSubtitle={shouldShowWorkspaceHeaderSubtitle}
|
|
||||||
currentBranchName={currentBranchName}
|
|
||||||
isGitCheckout={isGitCheckout}
|
|
||||||
normalizedServerId={normalizedServerId}
|
|
||||||
normalizedWorkspaceId={normalizedWorkspaceId}
|
|
||||||
workspaceScripts={workspaceScripts}
|
|
||||||
liveTerminalIds={liveTerminalIds}
|
|
||||||
showWorkspaceSetup={showWorkspaceSetup}
|
|
||||||
showCreateBrowserTab={showCreateBrowserTab}
|
|
||||||
isMobile={isMobile}
|
|
||||||
createTerminalDisabled={createTerminalDisabled}
|
|
||||||
importAgentDisabled={!canOpenImportSheet}
|
|
||||||
menuNewAgentIcon={menuNewAgentIcon}
|
|
||||||
menuNewTerminalIcon={menuNewTerminalIcon}
|
|
||||||
menuNewBrowserIcon={MENU_NEW_BROWSER_ICON}
|
|
||||||
menuImportIcon={MENU_IMPORT_ICON}
|
|
||||||
menuCopyIcon={menuCopyIcon}
|
|
||||||
menuSettingsIcon={menuSettingsIcon}
|
|
||||||
onCreateDraftTab={handleCreateDraftTab}
|
|
||||||
onCreateTerminal={handleCreateTerminal}
|
|
||||||
onCreateBrowser={handleCreateBrowserTab}
|
|
||||||
onOpenImportSheet={openImportSheet}
|
|
||||||
onCopyWorkspacePath={handleCopyWorkspacePath}
|
|
||||||
onCopyBranchName={handleCopyBranchName}
|
|
||||||
onOpenSetupTab={handleOpenSetupTab}
|
|
||||||
onScriptTerminalStarted={handleScriptTerminalStarted}
|
|
||||||
onViewScriptTerminal={handleViewScriptTerminal}
|
|
||||||
onOpenUrlInBrowserTab={handleOpenUrlInBrowserTab}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
right={headerRight}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isMobile ? (
|
<FloatingPanelPortalHost name={workspaceFloatingPanelPortalHostName} />
|
||||||
<MobileWorkspaceTabSwitcher
|
|
||||||
tabs={tabs}
|
|
||||||
activeTabKey={activeTabKey}
|
|
||||||
activeTab={activeTabDescriptor}
|
|
||||||
tabSwitcherOptions={tabSwitcherOptions}
|
|
||||||
tabByKey={tabByKey}
|
|
||||||
normalizedServerId={normalizedServerId}
|
|
||||||
normalizedWorkspaceId={normalizedWorkspaceId}
|
|
||||||
onSelectSwitcherTab={handleSelectSwitcherTab}
|
|
||||||
onCopyResumeCommand={handleCopyResumeCommand}
|
|
||||||
onCopyAgentId={handleCopyAgentId}
|
|
||||||
onReloadAgent={handleReloadAgent}
|
|
||||||
onRenameTab={handleRenameTab}
|
|
||||||
onCloseTab={handleCloseTabById}
|
|
||||||
onCloseTabsAbove={handleCloseTabsToLeft}
|
|
||||||
onCloseTabsBelow={handleCloseTabsToRight}
|
|
||||||
onCloseOtherTabs={handleCloseOtherTabs}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{shouldRenderDesktopPaneFallback ? (
|
|
||||||
<WorkspaceDesktopTabsRow
|
|
||||||
paneId={focusedPaneIdOrUndefined}
|
|
||||||
isFocused={isRouteFocused}
|
|
||||||
tabs={desktopTabRowItems}
|
|
||||||
normalizedServerId={normalizedServerId}
|
|
||||||
normalizedWorkspaceId={normalizedWorkspaceId}
|
|
||||||
setHoveredTabKey={setHoveredTabKey}
|
|
||||||
setHoveredCloseTabKey={setHoveredCloseTabKey}
|
|
||||||
onNavigateTab={navigateToTabId}
|
|
||||||
onCloseTab={handleCloseTabById}
|
|
||||||
onCopyResumeCommand={handleCopyResumeCommand}
|
|
||||||
onCopyAgentId={handleCopyAgentId}
|
|
||||||
onReloadAgent={handleReloadAgent}
|
|
||||||
onRenameTab={handleRenameTab}
|
|
||||||
onCloseTabsToLeft={handleCloseTabsToLeft}
|
|
||||||
onCloseTabsToRight={handleCloseTabsToRight}
|
|
||||||
onCloseOtherTabs={handleCloseOtherTabs}
|
|
||||||
onCreateDraftTab={handleCreateDraftTab}
|
|
||||||
onCreateTerminalTab={handleCreateTerminal}
|
|
||||||
onCreateBrowserTab={handleCreateBrowserTab}
|
|
||||||
showCreateBrowserTab={showCreateBrowserTab}
|
|
||||||
disableCreateTerminal={createTerminalMutation.isPending}
|
|
||||||
isWaitingOnTerminalReadiness={pendingTerminalCreateInput !== null}
|
|
||||||
onReorderTabs={handleReorderTabsInFocusedPane}
|
|
||||||
onSplitRight={noop}
|
|
||||||
onSplitDown={noop}
|
|
||||||
showPaneSplitActions={false}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<View style={styles.centerContent}>
|
|
||||||
{isMobile ? (
|
|
||||||
<GestureDetector
|
|
||||||
gesture={explorerOpenGesture}
|
|
||||||
touchAction={COMPACT_WEB_GESTURE_TOUCH_ACTION}
|
|
||||||
>
|
|
||||||
<View style={styles.content}>{content}</View>
|
|
||||||
</GestureDetector>
|
|
||||||
) : (
|
|
||||||
<View style={styles.content}>{desktopContent}</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{showExplorerSidebar && workspaceDirectory ? (
|
{showExplorerSidebar && workspaceDirectory ? (
|
||||||
<ExplorerSidebar
|
<ExplorerSidebar
|
||||||
|
|||||||
Reference in New Issue
Block a user