* fix(chat): load older timeline history consistently Pagination depended on scroll events, so short or compacted initial pages could never request older history. Reevaluate edge visibility as layout and history change, and standardize loading indicators on the canonical app spinner. * fix(chat): keep older history loading reliably Use authoritative timeline cursor progress so merged rows cannot stall pagination. Wait for bottom anchoring before evaluating the history edge, and re-arm retries on a fresh upward gesture without automatic failure loops.
1421 lines
44 KiB
TypeScript
1421 lines
44 KiB
TypeScript
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
|
import React, {
|
|
useCallback,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
type Dispatch,
|
|
type SetStateAction,
|
|
} from "react";
|
|
import {
|
|
Pressable,
|
|
ScrollView,
|
|
Text,
|
|
View,
|
|
type LayoutChangeEvent,
|
|
type PressableStateCallbackType,
|
|
} from "react-native";
|
|
import {
|
|
CopyX,
|
|
ArrowLeftToLine,
|
|
ArrowRightToLine,
|
|
ChevronDown,
|
|
Columns2,
|
|
Copy,
|
|
Pencil,
|
|
RotateCw,
|
|
Rows2,
|
|
Globe,
|
|
Plus,
|
|
SquarePen,
|
|
SquareTerminal,
|
|
X,
|
|
} from "lucide-react-native";
|
|
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useRouter, type Href } from "expo-router";
|
|
import { SortableInlineList } from "@/components/sortable-inline-list";
|
|
import type {
|
|
DraggableListDragHandleProps,
|
|
DraggableRenderItemInfo,
|
|
} from "@/components/draggable-list.types";
|
|
import { isNative, isWeb } from "@/constants/platform";
|
|
import {
|
|
ContextMenu,
|
|
ContextMenuContent,
|
|
ContextMenuItem,
|
|
ContextMenuSeparator,
|
|
ContextMenuTrigger,
|
|
} from "@/components/ui/context-menu";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuLabel,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import { Shortcut } from "@/components/ui/shortcut";
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
|
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
|
|
import { WORKSPACE_SECONDARY_HEADER_HEIGHT } from "@/constants/layout";
|
|
import type { ShortcutKey } from "@/utils/format-shortcut";
|
|
import { useWorkspaceTabLayout } from "@/screens/workspace/use-workspace-tab-layout";
|
|
import {
|
|
WorkspaceTabPresentationResolver,
|
|
WorkspaceTabIcon,
|
|
type WorkspaceTabPresentation,
|
|
} from "@/screens/workspace/workspace-tab-presentation";
|
|
import { buildDeterministicWorkspaceTabId } from "@/workspace-tabs/identity";
|
|
import {
|
|
buildWorkspaceDesktopTabActions,
|
|
type WorkspaceDesktopTabActions,
|
|
type WorkspaceTabMenuEntry,
|
|
type WorkspaceTabMenuLabels,
|
|
} from "@/screens/workspace/workspace-tab-menu";
|
|
import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types";
|
|
import type { Theme } from "@/styles/theme";
|
|
import { RenderProfile } from "@/utils/render-profiler";
|
|
import { useDaemonConfig } from "@/hooks/use-daemon-config";
|
|
import {
|
|
getTerminalProfileIcon,
|
|
resolveTerminalProfiles,
|
|
} from "@getpaseo/protocol/terminal-profiles";
|
|
import { buildSettingsHostSectionRoute } from "@/utils/host-routes";
|
|
import type { TerminalProfileInput } from "@/screens/workspace/terminals/use-workspace-terminals";
|
|
import { ProfileIcon, usePinnedLaunchers } from "@/workspace-pins/launch";
|
|
import { runPinnedTabTarget, type TabTargetHandlers } from "@/workspace-pins/run";
|
|
import type { PinnedTabTarget } from "@/workspace-pins/target";
|
|
import { PinnedTargetsRow } from "@/workspace-pins/pinned-targets-row";
|
|
import { PinnableMenuItem } from "@/workspace-pins/pinnable-menu-item";
|
|
|
|
const DROPDOWN_WIDTH = 220;
|
|
const LOADING_TAB_LABEL_SKELETON_WIDTH = 80;
|
|
const DEFAULT_INLINE_ADD_BUTTON_RESERVED_WIDTH = 36;
|
|
|
|
const ThemedLoadingSpinner = withUnistyles(LoadingSpinner);
|
|
const ThemedX = withUnistyles(X);
|
|
const ThemedCopy = withUnistyles(Copy);
|
|
const ThemedRotateCw = withUnistyles(RotateCw);
|
|
const ThemedArrowLeftToLine = withUnistyles(ArrowLeftToLine);
|
|
const ThemedArrowRightToLine = withUnistyles(ArrowRightToLine);
|
|
const ThemedCopyX = withUnistyles(CopyX);
|
|
const ThemedPencil = withUnistyles(Pencil);
|
|
const ThemedSquarePen = withUnistyles(SquarePen);
|
|
const ThemedSquareTerminal = withUnistyles(SquareTerminal);
|
|
const ThemedChevronDown = withUnistyles(ChevronDown);
|
|
const ThemedGlobe = withUnistyles(Globe);
|
|
const ThemedColumns2 = withUnistyles(Columns2);
|
|
const ThemedRows2 = withUnistyles(Rows2);
|
|
const ThemedPlus = withUnistyles(Plus);
|
|
const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground });
|
|
const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
|
|
|
const AGENT_ICON = <ThemedSquarePen size={14} uniProps={mutedColorMapping} />;
|
|
const TERMINAL_ICON = <ThemedSquareTerminal size={14} uniProps={mutedColorMapping} />;
|
|
const BROWSER_ICON = <ThemedGlobe size={14} uniProps={mutedColorMapping} />;
|
|
|
|
const DRAFT_TARGET: PinnedTabTarget = { kind: "draft" };
|
|
const TERMINAL_TARGET: PinnedTabTarget = { kind: "terminal" };
|
|
const BROWSER_TARGET: PinnedTabTarget = { kind: "browser" };
|
|
|
|
function newTabActionButtonStyle({ hovered, pressed }: PressableStateCallbackType) {
|
|
return [styles.newTabActionButton, (hovered || pressed) && styles.newTabActionButtonHovered];
|
|
}
|
|
|
|
function inlineAddActionButtonStyle({ hovered, pressed }: PressableStateCallbackType) {
|
|
return [styles.inlineAddActionButton, (hovered || pressed) && styles.newTabActionButtonHovered];
|
|
}
|
|
|
|
function updateMeasuredWidth(setWidth: Dispatch<SetStateAction<number>>, event: LayoutChangeEvent) {
|
|
const nextWidth = Math.round(event.nativeEvent.layout.width);
|
|
setWidth((current) => (Math.abs(current - nextWidth) > 1 ? nextWidth : current));
|
|
}
|
|
|
|
function ProfileLeadingIcon({ iconKey }: { iconKey: string | undefined }) {
|
|
return (
|
|
<View style={styles.terminalProfileIconWrapper}>
|
|
<ProfileIcon iconKey={iconKey} />
|
|
</View>
|
|
);
|
|
}
|
|
|
|
interface PinnableProfileMenuItemProps {
|
|
profile: { id: string; name: string; command: string; args?: string[]; icon?: string };
|
|
disabled?: boolean;
|
|
onLaunch: (target: PinnedTabTarget) => void;
|
|
}
|
|
|
|
function PinnableProfileMenuItem({ profile, disabled, onLaunch }: PinnableProfileMenuItemProps) {
|
|
const target = useMemo<PinnedTabTarget>(
|
|
() => ({ kind: "profile", profileId: profile.id }),
|
|
[profile.id],
|
|
);
|
|
const leading = useMemo(
|
|
() => <ProfileLeadingIcon iconKey={getTerminalProfileIcon(profile)} />,
|
|
[profile],
|
|
);
|
|
const handleSelect = useCallback(() => onLaunch(target), [onLaunch, target]);
|
|
|
|
return (
|
|
<PinnableMenuItem
|
|
target={target}
|
|
label={profile.name}
|
|
leading={leading}
|
|
disabled={disabled}
|
|
onSelect={handleSelect}
|
|
/>
|
|
);
|
|
}
|
|
|
|
interface WorkspaceInlineAddTabButtonProps {
|
|
shortcutKeys: ShortcutKey[][] | null;
|
|
onCreateAgentTab: () => void;
|
|
onLayout: (event: LayoutChangeEvent) => void;
|
|
}
|
|
|
|
function WorkspaceInlineAddTabButton({
|
|
shortcutKeys,
|
|
onCreateAgentTab,
|
|
onLayout,
|
|
}: WorkspaceInlineAddTabButtonProps) {
|
|
const { t } = useTranslation();
|
|
const tooltipText = t("workspace.tabs.actions.newAgent");
|
|
|
|
return (
|
|
<View style={styles.inlineAddButton} onLayout={onLayout}>
|
|
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
|
<TooltipTrigger
|
|
testID="workspace-new-agent-tab-inline"
|
|
onPress={onCreateAgentTab}
|
|
accessibilityRole="button"
|
|
accessibilityLabel={tooltipText}
|
|
style={inlineAddActionButtonStyle}
|
|
>
|
|
<ThemedPlus size={14} uniProps={mutedColorMapping} />
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom" align="center" offset={8}>
|
|
<View style={styles.newTabTooltipRow}>
|
|
<Text style={styles.newTabTooltipText}>{tooltipText}</Text>
|
|
{shortcutKeys ? (
|
|
<Shortcut chord={shortcutKeys} style={styles.newTabTooltipShortcut} />
|
|
) : null}
|
|
</View>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
interface WorkspaceTabRowExtrasProps {
|
|
onCreateAgentTab: () => void;
|
|
onCreateTerminal: () => void;
|
|
onCreateBrowser: () => void;
|
|
onCreateTerminalWithProfile: (profile: TerminalProfileInput) => void;
|
|
onEditProfiles: () => void;
|
|
normalizedServerId: string;
|
|
showCreateBrowserTab: boolean;
|
|
terminalDisabled: boolean;
|
|
}
|
|
|
|
function WorkspaceTabRowExtras({
|
|
onCreateAgentTab,
|
|
onCreateTerminal,
|
|
onCreateBrowser,
|
|
onCreateTerminalWithProfile,
|
|
onEditProfiles,
|
|
normalizedServerId,
|
|
showCreateBrowserTab,
|
|
terminalDisabled,
|
|
}: WorkspaceTabRowExtrasProps) {
|
|
const { t } = useTranslation();
|
|
const { config } = useDaemonConfig(normalizedServerId);
|
|
const profiles = useMemo(
|
|
() => resolveTerminalProfiles(config?.terminalProfiles),
|
|
[config?.terminalProfiles],
|
|
);
|
|
|
|
const handlers = useMemo<TabTargetHandlers>(
|
|
() => ({
|
|
createDraft: onCreateAgentTab,
|
|
createTerminal: onCreateTerminal,
|
|
createBrowser: onCreateBrowser,
|
|
createTerminalWithProfile: onCreateTerminalWithProfile,
|
|
}),
|
|
[onCreateAgentTab, onCreateBrowser, onCreateTerminal, onCreateTerminalWithProfile],
|
|
);
|
|
|
|
const onLaunch = useCallback(
|
|
(target: PinnedTabTarget) => {
|
|
runPinnedTabTarget(target, profiles, handlers);
|
|
},
|
|
[handlers, profiles],
|
|
);
|
|
|
|
const launchers = usePinnedLaunchers({ serverId: normalizedServerId, onLaunch });
|
|
|
|
return (
|
|
<>
|
|
<DropdownMenu>
|
|
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
|
<TooltipTrigger asChild triggerRefProp="triggerRef">
|
|
<DropdownMenuTrigger
|
|
testID="workspace-new-tab-menu-trigger"
|
|
accessibilityRole="button"
|
|
accessibilityLabel={t("workspace.tabs.actions.moreActions")}
|
|
style={newTabActionButtonStyle}
|
|
>
|
|
<ThemedChevronDown size={14} uniProps={mutedColorMapping} />
|
|
</DropdownMenuTrigger>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom" align="center" offset={8}>
|
|
<Text style={styles.newTabTooltipText}>{t("workspace.tabs.actions.moreActions")}</Text>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
<DropdownMenuContent side="bottom" align="end" offset={4} minWidth={200}>
|
|
<PinnableMenuItem
|
|
testID="workspace-new-tab-menu-agent"
|
|
target={DRAFT_TARGET}
|
|
label={t("workspace.tabs.actions.newAgent")}
|
|
leading={AGENT_ICON}
|
|
onSelect={onCreateAgentTab}
|
|
/>
|
|
<PinnableMenuItem
|
|
testID="workspace-new-tab-menu-terminal"
|
|
target={TERMINAL_TARGET}
|
|
label={t("workspace.tabs.actions.newTerminal")}
|
|
leading={TERMINAL_ICON}
|
|
disabled={terminalDisabled}
|
|
onSelect={terminalDisabled ? undefined : onCreateTerminal}
|
|
/>
|
|
{showCreateBrowserTab ? (
|
|
<PinnableMenuItem
|
|
testID="workspace-new-tab-menu-browser"
|
|
target={BROWSER_TARGET}
|
|
label={t("workspace.tabs.actions.newBrowser")}
|
|
leading={BROWSER_ICON}
|
|
onSelect={onCreateBrowser}
|
|
/>
|
|
) : null}
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuLabel>{t("workspace.tabs.actions.terminalProfilesMenu")}</DropdownMenuLabel>
|
|
{profiles.map((profile) => (
|
|
<PinnableProfileMenuItem
|
|
key={profile.id}
|
|
profile={profile}
|
|
disabled={terminalDisabled}
|
|
onLaunch={onLaunch}
|
|
/>
|
|
))}
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem testID="workspace-new-tab-menu-edit-profiles" onSelect={onEditProfiles}>
|
|
{t("workspace.tabs.actions.editTerminalProfiles")}
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
<PinnedTargetsRow launchers={launchers} testIdPrefix="workspace-pinned-target" />
|
|
</>
|
|
);
|
|
}
|
|
|
|
function TabContextMenuItem({
|
|
entry,
|
|
}: {
|
|
entry: Extract<WorkspaceTabMenuEntry, { kind: "item" }>;
|
|
}) {
|
|
const leading = useMemo(() => {
|
|
switch (entry.icon) {
|
|
case "copy":
|
|
return <ThemedCopy size={16} uniProps={mutedColorMapping} />;
|
|
case "rotate-cw":
|
|
return <ThemedRotateCw size={16} uniProps={mutedColorMapping} />;
|
|
case "arrow-left-to-line":
|
|
return <ThemedArrowLeftToLine size={16} uniProps={mutedColorMapping} />;
|
|
case "arrow-right-to-line":
|
|
return <ThemedArrowRightToLine size={16} uniProps={mutedColorMapping} />;
|
|
case "copy-x":
|
|
return <ThemedCopyX size={16} uniProps={mutedColorMapping} />;
|
|
case "pencil":
|
|
return <ThemedPencil size={16} uniProps={mutedColorMapping} />;
|
|
case "x":
|
|
return <ThemedX size={16} uniProps={mutedColorMapping} />;
|
|
default:
|
|
return undefined;
|
|
}
|
|
}, [entry.icon]);
|
|
const trailing = useMemo(
|
|
() => (entry.hint ? <Text style={styles.menuItemHint}>{entry.hint}</Text> : undefined),
|
|
[entry.hint],
|
|
);
|
|
return (
|
|
<ContextMenuItem
|
|
testID={entry.testID}
|
|
disabled={entry.disabled}
|
|
destructive={entry.destructive}
|
|
onSelect={entry.onSelect}
|
|
tooltip={entry.tooltip}
|
|
leading={leading}
|
|
trailing={trailing}
|
|
>
|
|
{entry.label}
|
|
</ContextMenuItem>
|
|
);
|
|
}
|
|
|
|
function tabKeyExtractor(tab: WorkspaceDesktopTabRowItem) {
|
|
return `${tab.tab.key}:${tab.tab.kind}`;
|
|
}
|
|
|
|
export interface WorkspaceDesktopTabRowItem {
|
|
tab: WorkspaceTabDescriptor;
|
|
isActive: boolean;
|
|
isCloseHovered: boolean;
|
|
isClosingTab: boolean;
|
|
}
|
|
|
|
interface SplitActionButtonProps {
|
|
onPress: () => void;
|
|
label: string;
|
|
shortcutKeys: ShortcutKey[][] | null;
|
|
icon: "split-right" | "split-down";
|
|
}
|
|
|
|
function SplitActionButton({ onPress, label, shortcutKeys, icon }: SplitActionButtonProps) {
|
|
return (
|
|
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
|
<TooltipTrigger
|
|
onPress={onPress}
|
|
accessibilityRole="button"
|
|
accessibilityLabel={label}
|
|
style={newTabActionButtonStyle}
|
|
>
|
|
{icon === "split-right" ? (
|
|
<ThemedColumns2 size={14} uniProps={mutedColorMapping} />
|
|
) : (
|
|
<ThemedRows2 size={14} uniProps={mutedColorMapping} />
|
|
)}
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom" align="center" offset={8}>
|
|
<View style={styles.newTabTooltipRow}>
|
|
<Text style={styles.newTabTooltipText}>{label}</Text>
|
|
{shortcutKeys ? (
|
|
<Shortcut chord={shortcutKeys} style={styles.newTabTooltipShortcut} />
|
|
) : null}
|
|
</View>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
);
|
|
}
|
|
|
|
interface WorkspaceDesktopTabsRowProps {
|
|
paneId?: string;
|
|
isFocused?: boolean;
|
|
tabs: WorkspaceDesktopTabRowItem[];
|
|
normalizedServerId: string;
|
|
normalizedWorkspaceId: string;
|
|
setHoveredCloseTabKey: Dispatch<SetStateAction<string | null>>;
|
|
onNavigateTab: (tabId: string) => void;
|
|
onCloseTab: (tabId: string) => Promise<void> | void;
|
|
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
|
|
onCopyAgentId: (agentId: string) => Promise<void> | void;
|
|
onCopyTerminalId: (terminalId: string) => Promise<void> | void;
|
|
onCopyFilePath: (path: string) => Promise<void> | void;
|
|
onReloadAgent: (agentId: string) => Promise<void> | void;
|
|
onRenameTab: (tab: WorkspaceTabDescriptor) => void;
|
|
onCloseTabsToLeft: (tabId: string) => Promise<void> | void;
|
|
onCloseTabsToRight: (tabId: string) => Promise<void> | void;
|
|
onCloseOtherTabs: (tabId: string) => Promise<void> | void;
|
|
onCreateDraftTab: (input: { paneId?: string }) => void;
|
|
onCreateTerminalTab: (input: { paneId?: string; profile?: TerminalProfileInput }) => void;
|
|
onCreateBrowserTab: (input: { paneId?: string }) => void;
|
|
showCreateBrowserTab?: boolean;
|
|
disableCreateTerminal?: boolean;
|
|
isWaitingOnTerminalReadiness?: boolean;
|
|
onReorderTabs: (nextTabs: WorkspaceTabDescriptor[]) => void;
|
|
onSplitRight: () => void;
|
|
onSplitDown: () => void;
|
|
externalDndContext?: boolean;
|
|
activeDragTabId?: string | null;
|
|
tabDropPreviewIndex?: number | null;
|
|
showPaneSplitActions?: boolean;
|
|
focusModeEnabled: boolean;
|
|
onExitFocusMode: () => void;
|
|
}
|
|
|
|
function getFallbackTabLabel(
|
|
tab: WorkspaceTabDescriptor,
|
|
labels: { newAgent: string; setup: string; terminal: string; agent: string; changes: string },
|
|
): string {
|
|
if (tab.target.kind === "draft") {
|
|
return labels.newAgent;
|
|
}
|
|
if (tab.target.kind === "setup") {
|
|
return labels.setup;
|
|
}
|
|
if (tab.target.kind === "terminal") {
|
|
return labels.terminal;
|
|
}
|
|
if (tab.target.kind === "file") {
|
|
return tab.target.path.split("/").findLast(Boolean) ?? tab.target.path;
|
|
}
|
|
if (tab.target.kind === "working_diff") {
|
|
return labels.changes;
|
|
}
|
|
return labels.agent;
|
|
}
|
|
|
|
function useMiddleClickClose(onClose: () => void) {
|
|
const ref = useRef<View>(null);
|
|
|
|
useEffect(() => {
|
|
if (isNative) return;
|
|
const node = ref.current as unknown as HTMLElement | null;
|
|
if (!node) return;
|
|
|
|
function handleAuxClick(event: MouseEvent) {
|
|
if (event.button === 1) {
|
|
event.preventDefault();
|
|
onClose();
|
|
}
|
|
}
|
|
|
|
node.addEventListener("auxclick", handleAuxClick);
|
|
return () => node.removeEventListener("auxclick", handleAuxClick);
|
|
}, [onClose]);
|
|
|
|
return ref;
|
|
}
|
|
|
|
function TabHandleContent({
|
|
presentation,
|
|
isHighlighted,
|
|
showLabel,
|
|
tabLabelSkeletonStyle,
|
|
tabLabelStyle,
|
|
}: {
|
|
presentation: WorkspaceTabPresentation;
|
|
isHighlighted: boolean;
|
|
showLabel: boolean;
|
|
tabLabelSkeletonStyle: React.ComponentProps<typeof View>["style"];
|
|
tabLabelStyle: React.ComponentProps<typeof Text>["style"];
|
|
}) {
|
|
const tabHandleDataSet = useMemo(
|
|
() => ({ statusBucket: presentation.statusBucket ?? "none" }),
|
|
[presentation.statusBucket],
|
|
);
|
|
|
|
return (
|
|
<View style={styles.tabHandle} dataSet={tabHandleDataSet}>
|
|
<View style={styles.tabIcon}>
|
|
<WorkspaceTabIcon presentation={presentation} active={isHighlighted} />
|
|
</View>
|
|
{showLabel && presentation.titleState === "loading" ? (
|
|
<View style={tabLabelSkeletonStyle} />
|
|
) : null}
|
|
{showLabel && presentation.titleState !== "loading" ? (
|
|
<Text style={tabLabelStyle} selectable={false} numberOfLines={1} ellipsizeMode="tail">
|
|
{presentation.label}
|
|
</Text>
|
|
) : null}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function TabChip({
|
|
tab,
|
|
isActive,
|
|
isDragging,
|
|
isFocused,
|
|
resolvedTabWidth,
|
|
showLabel,
|
|
showCloseButton,
|
|
isCloseHovered,
|
|
isClosingTab,
|
|
presentation,
|
|
tooltipLabel,
|
|
resolvedTab,
|
|
setHoveredCloseTabKey,
|
|
onNavigateTab,
|
|
onCloseTab,
|
|
dragHandleProps,
|
|
}: {
|
|
tab: WorkspaceTabDescriptor;
|
|
isActive: boolean;
|
|
isDragging: boolean;
|
|
isFocused: boolean;
|
|
resolvedTabWidth: number;
|
|
showLabel: boolean;
|
|
showCloseButton: boolean;
|
|
isCloseHovered: boolean;
|
|
isClosingTab: boolean;
|
|
presentation: WorkspaceTabPresentation;
|
|
tooltipLabel: string;
|
|
resolvedTab: WorkspaceDesktopTabActions;
|
|
setHoveredCloseTabKey: Dispatch<SetStateAction<string | null>>;
|
|
onNavigateTab: (tabId: string) => void;
|
|
onCloseTab: (tabId: string) => Promise<void> | void;
|
|
dragHandleProps: DraggableListDragHandleProps | undefined;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
const { closeButtonTestId, contextMenuTestId, menuEntries } = resolvedTab;
|
|
const middleClickRef = useMiddleClickClose(
|
|
useCallback(() => void onCloseTab(tab.tabId), [onCloseTab, tab.tabId]),
|
|
);
|
|
const [hovered, setHovered] = useState(false);
|
|
const isHighlighted = isActive || hovered || isCloseHovered;
|
|
const showTrailingAffordance = showCloseButton || presentation.modified;
|
|
const closeButtonDragBlockers = isWeb
|
|
? ({
|
|
onPointerDown: (event: { stopPropagation?: () => void }) => {
|
|
event.stopPropagation?.();
|
|
},
|
|
onMouseDown: (event: { stopPropagation?: () => void }) => {
|
|
event.stopPropagation?.();
|
|
},
|
|
} as const)
|
|
: undefined;
|
|
|
|
const tabChipStyle = useCallback(
|
|
() => [
|
|
styles.tab,
|
|
isWeb && isDragging && ({ cursor: "grabbing" } as object),
|
|
{
|
|
minWidth: resolvedTabWidth,
|
|
width: resolvedTabWidth,
|
|
maxWidth: resolvedTabWidth,
|
|
},
|
|
],
|
|
[isDragging, resolvedTabWidth],
|
|
);
|
|
|
|
const handleTabHoverIn = useCallback(() => {
|
|
setHovered(true);
|
|
}, []);
|
|
|
|
const handleTabHoverOut = useCallback(() => {
|
|
setHovered(false);
|
|
}, []);
|
|
|
|
const handleNavigateTab = useCallback(() => {
|
|
onNavigateTab(tab.tabId);
|
|
}, [onNavigateTab, tab.tabId]);
|
|
|
|
const handleCloseButtonPressIn = useCallback((event: { stopPropagation?: () => void }) => {
|
|
event.stopPropagation?.();
|
|
}, []);
|
|
|
|
const handleCloseButtonHoverIn = useCallback(() => {
|
|
setHoveredCloseTabKey(tab.key);
|
|
}, [setHoveredCloseTabKey, tab.key]);
|
|
|
|
const handleCloseButtonHoverOut = useCallback(() => {
|
|
setHoveredCloseTabKey((current) => (current === tab.key ? null : current));
|
|
}, [setHoveredCloseTabKey, tab.key]);
|
|
|
|
const handleCloseButtonPress = useCallback(
|
|
(event: { stopPropagation?: () => void }) => {
|
|
event.stopPropagation?.();
|
|
void onCloseTab(tab.tabId);
|
|
},
|
|
[onCloseTab, tab.tabId],
|
|
);
|
|
|
|
const closeButtonStyle = useCallback(
|
|
({ hovered: isButtonHovered, pressed }: PressableStateCallbackType & { hovered?: boolean }) => [
|
|
styles.tabCloseButton,
|
|
styles.tabCloseButtonShown,
|
|
(Boolean(isButtonHovered) || pressed) && styles.tabCloseButtonActive,
|
|
],
|
|
[],
|
|
);
|
|
|
|
const tabAccessibilityState = useMemo(() => ({ selected: isActive }), [isActive]);
|
|
const tabFocusIndicatorStyle = useMemo(
|
|
() => [styles.tabFocusIndicator, !isFocused && styles.tabFocusIndicatorUnfocused],
|
|
[isFocused],
|
|
);
|
|
const tabLabelSkeletonStyle = useMemo(
|
|
() => [
|
|
styles.tabLabelSkeleton,
|
|
showTrailingAffordance && styles.tabLabelSkeletonWithCloseButton,
|
|
],
|
|
[showTrailingAffordance],
|
|
);
|
|
const tabLabelStyle = useMemo(
|
|
() => [
|
|
styles.tabLabel,
|
|
isHighlighted && styles.tabLabelActive,
|
|
showTrailingAffordance && styles.tabLabelWithCloseButton,
|
|
],
|
|
[isHighlighted, showTrailingAffordance],
|
|
);
|
|
|
|
return (
|
|
<View ref={middleClickRef}>
|
|
<ContextMenu key={tab.key}>
|
|
<Tooltip delayDuration={400} enabledOnDesktop enabledOnMobile={false}>
|
|
<TooltipTrigger asChild triggerRefProp="triggerRef">
|
|
<ContextMenuTrigger
|
|
{...(dragHandleProps?.attributes as object | undefined)}
|
|
{...(dragHandleProps?.listeners as object | undefined)}
|
|
testID={`workspace-tab-${buildDeterministicWorkspaceTabId(tab.target)}`}
|
|
triggerRef={dragHandleProps?.setActivatorNodeRef as unknown as undefined}
|
|
enabledOnMobile={false}
|
|
style={tabChipStyle}
|
|
onHoverIn={handleTabHoverIn}
|
|
onHoverOut={handleTabHoverOut}
|
|
onPressIn={handleNavigateTab}
|
|
onPress={handleNavigateTab}
|
|
accessibilityRole="button"
|
|
accessibilityLabel={tooltipLabel}
|
|
accessibilityState={tabAccessibilityState}
|
|
aria-selected={isActive}
|
|
>
|
|
{isActive && <View style={tabFocusIndicatorStyle} />}
|
|
<TabHandleContent
|
|
presentation={presentation}
|
|
isHighlighted={isHighlighted}
|
|
showLabel={showLabel}
|
|
tabLabelSkeletonStyle={tabLabelSkeletonStyle}
|
|
tabLabelStyle={tabLabelStyle}
|
|
/>
|
|
|
|
{showTrailingAffordance ? (
|
|
<Pressable
|
|
{...(closeButtonDragBlockers as object | undefined)}
|
|
testID={closeButtonTestId}
|
|
disabled={isClosingTab}
|
|
onPressIn={handleCloseButtonPressIn}
|
|
onHoverIn={handleCloseButtonHoverIn}
|
|
onHoverOut={handleCloseButtonHoverOut}
|
|
onPress={handleCloseButtonPress}
|
|
style={closeButtonStyle}
|
|
>
|
|
{({ hovered: closeHovered, pressed }) => {
|
|
const highlighted = closeHovered || pressed;
|
|
if (isClosingTab) {
|
|
return (
|
|
<ThemedLoadingSpinner
|
|
size={12}
|
|
uniProps={highlighted ? foregroundColorMapping : mutedColorMapping}
|
|
/>
|
|
);
|
|
}
|
|
if (highlighted || !presentation.modified) {
|
|
return (
|
|
<ThemedX
|
|
size={12}
|
|
uniProps={highlighted ? foregroundColorMapping : mutedColorMapping}
|
|
/>
|
|
);
|
|
}
|
|
return (
|
|
<View
|
|
style={styles.tabModifiedDot}
|
|
accessibilityLabel={t("workspace.tabs.modified")}
|
|
testID={`workspace-tab-modified-${buildDeterministicWorkspaceTabId(tab.target)}`}
|
|
/>
|
|
);
|
|
}}
|
|
</Pressable>
|
|
) : null}
|
|
</ContextMenuTrigger>
|
|
</TooltipTrigger>
|
|
<TooltipContent
|
|
side="bottom"
|
|
align="center"
|
|
offset={8}
|
|
maxWidth={720}
|
|
testID={`workspace-tab-tooltip-${buildDeterministicWorkspaceTabId(tab.target)}`}
|
|
>
|
|
{tab.target.kind === "agent" ? (
|
|
<View style={styles.tooltipAgentRow}>
|
|
<Text style={styles.newTabTooltipText}>{tooltipLabel}</Text>
|
|
<Text style={styles.tooltipAgentId}>{tab.target.agentId.slice(0, 7)}</Text>
|
|
</View>
|
|
) : (
|
|
<Text style={styles.newTabTooltipText}>{tooltipLabel}</Text>
|
|
)}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
|
|
<ContextMenuContent align="start" width={DROPDOWN_WIDTH} testID={contextMenuTestId}>
|
|
{menuEntries.map((entry) =>
|
|
entry.kind === "separator" ? (
|
|
<ContextMenuSeparator key={entry.key} />
|
|
) : (
|
|
<TabContextMenuItem key={entry.key} entry={entry} />
|
|
),
|
|
)}
|
|
</ContextMenuContent>
|
|
</ContextMenu>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
export function WorkspaceDesktopTabsRow({
|
|
paneId,
|
|
isFocused = false,
|
|
tabs,
|
|
normalizedServerId,
|
|
normalizedWorkspaceId,
|
|
setHoveredCloseTabKey,
|
|
onNavigateTab,
|
|
onCloseTab,
|
|
onCopyResumeCommand,
|
|
onCopyAgentId,
|
|
onCopyTerminalId,
|
|
onCopyFilePath,
|
|
onReloadAgent,
|
|
onRenameTab,
|
|
onCloseTabsToLeft,
|
|
onCloseTabsToRight,
|
|
onCloseOtherTabs,
|
|
onCreateDraftTab,
|
|
onCreateTerminalTab,
|
|
onCreateBrowserTab,
|
|
showCreateBrowserTab = false,
|
|
disableCreateTerminal = false,
|
|
isWaitingOnTerminalReadiness = false,
|
|
onReorderTabs,
|
|
onSplitRight,
|
|
onSplitDown,
|
|
externalDndContext = false,
|
|
activeDragTabId = null,
|
|
tabDropPreviewIndex = null,
|
|
showPaneSplitActions = true,
|
|
focusModeEnabled,
|
|
onExitFocusMode,
|
|
}: WorkspaceDesktopTabsRowProps) {
|
|
const { t } = useTranslation();
|
|
const router = useRouter();
|
|
const newTabKeys = useShortcutKeys("workspace-tab-new");
|
|
const focusModeKeys = useShortcutKeys("toggle-focus");
|
|
const splitRightKeys = useShortcutKeys("workspace-pane-split-right");
|
|
const splitDownKeys = useShortcutKeys("workspace-pane-split-down");
|
|
const [tabsContainerWidth, setTabsContainerWidth] = useState<number>(0);
|
|
const [tabsActionsWidth, setTabsActionsWidth] = useState<number>(0);
|
|
const [inlineAddButtonWidth, setInlineAddButtonWidth] = useState<number>(0);
|
|
const [exitFocusModeWidth, setExitFocusModeWidth] = useState<number>(0);
|
|
|
|
const handleTabsContainerLayout = useCallback((event: LayoutChangeEvent) => {
|
|
updateMeasuredWidth(setTabsContainerWidth, event);
|
|
}, []);
|
|
|
|
const handleTabsActionsLayout = useCallback((event: LayoutChangeEvent) => {
|
|
updateMeasuredWidth(setTabsActionsWidth, event);
|
|
}, []);
|
|
|
|
const handleInlineAddButtonLayout = useCallback((event: LayoutChangeEvent) => {
|
|
updateMeasuredWidth(setInlineAddButtonWidth, event);
|
|
}, []);
|
|
|
|
const handleExitFocusModeLayout = useCallback((event: LayoutChangeEvent) => {
|
|
updateMeasuredWidth(setExitFocusModeWidth, event);
|
|
}, []);
|
|
|
|
const layoutMetrics = useMemo(
|
|
() => ({
|
|
rowHorizontalInset: 0,
|
|
actionsReservedWidth: Math.max(
|
|
0,
|
|
tabsActionsWidth +
|
|
(inlineAddButtonWidth || DEFAULT_INLINE_ADD_BUTTON_RESERVED_WIDTH) +
|
|
(focusModeEnabled ? exitFocusModeWidth : 0),
|
|
),
|
|
rowPaddingHorizontal: 0,
|
|
tabGap: 0,
|
|
maxTabWidth: 200,
|
|
tabIconWidth: 14,
|
|
tabHorizontalPadding: 12,
|
|
estimatedCharWidth: 7,
|
|
closeButtonWidth: 22,
|
|
}),
|
|
[exitFocusModeWidth, focusModeEnabled, inlineAddButtonWidth, tabsActionsWidth],
|
|
);
|
|
|
|
const fallbackTabLabels = useMemo(
|
|
() => ({
|
|
newAgent: t("workspace.tabs.fallback.newAgent"),
|
|
setup: t("workspace.tabs.fallback.setup"),
|
|
terminal: t("workspace.tabs.fallback.terminal"),
|
|
agent: t("workspace.tabs.fallback.agent"),
|
|
changes: t("panels.diff.changesLabel"),
|
|
}),
|
|
[t],
|
|
);
|
|
const tabMenuLabels = useMemo<WorkspaceTabMenuLabels>(
|
|
() => ({
|
|
copyResumeCommand: t("workspace.tabs.menu.copyResumeCommand"),
|
|
copyAgentId: t("workspace.tabs.menu.copyAgentId"),
|
|
copyTerminalId: t("workspace.tabs.menu.copyTerminalId"),
|
|
copyFilePath: t("workspace.tabs.menu.copyFilePath"),
|
|
rename: t("workspace.tabs.menu.rename"),
|
|
closeAbove: t("workspace.tabs.menu.closeAbove"),
|
|
closeBelow: t("workspace.tabs.menu.closeBelow"),
|
|
closeLeft: t("workspace.tabs.menu.closeLeft"),
|
|
closeRight: t("workspace.tabs.menu.closeRight"),
|
|
closeOthers: t("workspace.tabs.menu.closeOthers"),
|
|
reloadAgent: t("workspace.tabs.menu.reloadAgent"),
|
|
reloadAgentTooltip: t("workspace.tabs.menu.reloadAgentTooltip"),
|
|
close: t("workspace.tabs.menu.close"),
|
|
}),
|
|
[t],
|
|
);
|
|
const tabLabelLengths = useMemo(
|
|
() =>
|
|
tabs.map((tab) => {
|
|
const label = getFallbackTabLabel(tab.tab, fallbackTabLabels);
|
|
return label.length;
|
|
}),
|
|
[fallbackTabLabels, tabs],
|
|
);
|
|
|
|
const { layout } = useWorkspaceTabLayout({
|
|
tabLabelLengths,
|
|
viewportWidthOverride: tabsContainerWidth > 0 ? tabsContainerWidth : null,
|
|
metrics: layoutMetrics,
|
|
});
|
|
|
|
const handleDragEnd = useCallback(
|
|
(nextTabs: WorkspaceDesktopTabRowItem[]) => {
|
|
onReorderTabs(nextTabs.map((tab) => tab.tab));
|
|
},
|
|
[onReorderTabs],
|
|
);
|
|
|
|
const getTabDragData = useMemo(() => {
|
|
if (!paneId) return undefined;
|
|
return (tab: WorkspaceDesktopTabRowItem) => ({
|
|
kind: "workspace-tab" as const,
|
|
paneId,
|
|
tabId: tab.tab.tabId,
|
|
});
|
|
}, [paneId]);
|
|
|
|
const handleCreateAgentTab = useCallback(() => {
|
|
onCreateDraftTab({ paneId });
|
|
}, [onCreateDraftTab, paneId]);
|
|
|
|
const handleCreateTerminal = useCallback(() => {
|
|
onCreateTerminalTab({ paneId });
|
|
}, [onCreateTerminalTab, paneId]);
|
|
|
|
const handleCreateTerminalWithProfile = useCallback(
|
|
(profile: TerminalProfileInput) => {
|
|
onCreateTerminalTab({ paneId, profile });
|
|
},
|
|
[onCreateTerminalTab, paneId],
|
|
);
|
|
|
|
const handleEditProfiles = useCallback(() => {
|
|
router.push(buildSettingsHostSectionRoute(normalizedServerId, "terminals") as Href);
|
|
}, [normalizedServerId, router]);
|
|
|
|
const handleCreateBrowser = useCallback(() => {
|
|
onCreateBrowserTab({ paneId });
|
|
}, [onCreateBrowserTab, paneId]);
|
|
|
|
const terminalDisabled = disableCreateTerminal || isWaitingOnTerminalReadiness;
|
|
|
|
const renderTab = useCallback(
|
|
({
|
|
item,
|
|
index,
|
|
dragHandleProps,
|
|
isActive,
|
|
}: DraggableRenderItemInfo<WorkspaceDesktopTabRowItem>) => {
|
|
const shouldShowCloseButton = layout.closeButtonPolicy === "all";
|
|
const layoutItem = layout.items[index] ?? null;
|
|
const resolvedTabWidth = layoutItem?.width ?? 150;
|
|
const showLabel = layoutItem?.showLabel ?? true;
|
|
const showDropIndicatorBefore = activeDragTabId !== null && tabDropPreviewIndex === index;
|
|
const showDropIndicatorAfter =
|
|
activeDragTabId !== null &&
|
|
tabDropPreviewIndex === tabs.length &&
|
|
index === tabs.length - 1;
|
|
|
|
return (
|
|
<ResolvedDesktopTabChip
|
|
key={`${item.tab.key}:${item.tab.kind}`}
|
|
item={item}
|
|
isFocused={isFocused}
|
|
isDragging={isActive}
|
|
index={index}
|
|
tabCount={tabs.length}
|
|
normalizedServerId={normalizedServerId}
|
|
normalizedWorkspaceId={normalizedWorkspaceId}
|
|
onCopyResumeCommand={onCopyResumeCommand}
|
|
onCopyAgentId={onCopyAgentId}
|
|
onCopyTerminalId={onCopyTerminalId}
|
|
onCopyFilePath={onCopyFilePath}
|
|
onReloadAgent={onReloadAgent}
|
|
onRenameTab={onRenameTab}
|
|
onCloseTabsToLeft={onCloseTabsToLeft}
|
|
onCloseTabsToRight={onCloseTabsToRight}
|
|
onCloseOtherTabs={onCloseOtherTabs}
|
|
resolvedTabWidth={resolvedTabWidth}
|
|
showLabel={showLabel}
|
|
showCloseButton={shouldShowCloseButton}
|
|
setHoveredCloseTabKey={setHoveredCloseTabKey}
|
|
onNavigateTab={onNavigateTab}
|
|
onCloseTab={onCloseTab}
|
|
labels={tabMenuLabels}
|
|
dragHandleProps={dragHandleProps}
|
|
showDropIndicatorBefore={showDropIndicatorBefore}
|
|
showDropIndicatorAfter={showDropIndicatorAfter}
|
|
/>
|
|
);
|
|
},
|
|
[
|
|
activeDragTabId,
|
|
isFocused,
|
|
layout.closeButtonPolicy,
|
|
layout.items,
|
|
normalizedServerId,
|
|
normalizedWorkspaceId,
|
|
onCloseOtherTabs,
|
|
onCloseTab,
|
|
onCloseTabsToLeft,
|
|
onCloseTabsToRight,
|
|
onCopyAgentId,
|
|
onCopyTerminalId,
|
|
onCopyFilePath,
|
|
onCopyResumeCommand,
|
|
onNavigateTab,
|
|
onReloadAgent,
|
|
onRenameTab,
|
|
setHoveredCloseTabKey,
|
|
tabMenuLabels,
|
|
tabDropPreviewIndex,
|
|
tabs.length,
|
|
],
|
|
);
|
|
|
|
const tabsScrollStyle = useMemo(
|
|
() => [
|
|
styles.tabsScroll,
|
|
layout.requiresHorizontalScrollFallback
|
|
? styles.tabsScrollOverflow
|
|
: styles.tabsScrollFitContent,
|
|
],
|
|
[layout.requiresHorizontalScrollFallback],
|
|
);
|
|
|
|
const row = (
|
|
<View
|
|
style={styles.tabsContainer}
|
|
testID="workspace-tabs-row"
|
|
onLayout={handleTabsContainerLayout}
|
|
>
|
|
{focusModeEnabled ? (
|
|
<View style={styles.exitFocusModeSlot} onLayout={handleExitFocusModeLayout}>
|
|
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
|
<TooltipTrigger
|
|
testID="workspace-exit-focus-mode"
|
|
onPress={onExitFocusMode}
|
|
accessibilityRole="button"
|
|
accessibilityLabel={t("workspace.tabs.actions.exitFocusMode")}
|
|
style={inlineAddActionButtonStyle}
|
|
>
|
|
<ThemedX size={14} uniProps={mutedColorMapping} />
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom" align="center" offset={8}>
|
|
<View style={styles.newTabTooltipRow}>
|
|
<Text style={styles.newTabTooltipText}>
|
|
{t("workspace.tabs.actions.exitFocusMode")}
|
|
</Text>
|
|
{focusModeKeys ? (
|
|
<Shortcut chord={focusModeKeys} style={styles.newTabTooltipShortcut} />
|
|
) : null}
|
|
</View>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</View>
|
|
) : null}
|
|
<ScrollView
|
|
horizontal
|
|
scrollEnabled={layout.requiresHorizontalScrollFallback}
|
|
testID="workspace-tabs-scroll"
|
|
style={tabsScrollStyle}
|
|
contentContainerStyle={styles.tabsContent}
|
|
showsHorizontalScrollIndicator={false}
|
|
>
|
|
<SortableInlineList
|
|
data={tabs}
|
|
keyExtractor={tabKeyExtractor}
|
|
useDragHandle
|
|
disabled={!externalDndContext && tabs.length < 2}
|
|
onDragEnd={handleDragEnd}
|
|
externalDndContext={externalDndContext}
|
|
activeId={activeDragTabId}
|
|
getItemData={getTabDragData}
|
|
renderItem={renderTab}
|
|
/>
|
|
<WorkspaceInlineAddTabButton
|
|
shortcutKeys={newTabKeys}
|
|
onCreateAgentTab={handleCreateAgentTab}
|
|
onLayout={handleInlineAddButtonLayout}
|
|
/>
|
|
</ScrollView>
|
|
<View style={styles.tabsActions} onLayout={handleTabsActionsLayout}>
|
|
<WorkspaceTabRowExtras
|
|
onCreateAgentTab={handleCreateAgentTab}
|
|
onCreateTerminal={handleCreateTerminal}
|
|
onCreateBrowser={handleCreateBrowser}
|
|
onCreateTerminalWithProfile={handleCreateTerminalWithProfile}
|
|
onEditProfiles={handleEditProfiles}
|
|
normalizedServerId={normalizedServerId}
|
|
showCreateBrowserTab={showCreateBrowserTab}
|
|
terminalDisabled={terminalDisabled}
|
|
/>
|
|
{showPaneSplitActions ? (
|
|
<>
|
|
<SplitActionButton
|
|
icon="split-right"
|
|
onPress={onSplitRight}
|
|
label={t("workspace.tabs.actions.splitRight")}
|
|
shortcutKeys={splitRightKeys}
|
|
/>
|
|
<SplitActionButton
|
|
icon="split-down"
|
|
onPress={onSplitDown}
|
|
label={t("workspace.tabs.actions.splitDown")}
|
|
shortcutKeys={splitDownKeys}
|
|
/>
|
|
</>
|
|
) : null}
|
|
</View>
|
|
</View>
|
|
);
|
|
|
|
return <RenderProfile id="WorkspaceDesktopTabsRow">{row}</RenderProfile>;
|
|
}
|
|
function ResolvedDesktopTabChip({
|
|
item,
|
|
isFocused,
|
|
isDragging,
|
|
index,
|
|
tabCount,
|
|
normalizedServerId,
|
|
normalizedWorkspaceId,
|
|
onCopyResumeCommand,
|
|
onCopyAgentId,
|
|
onCopyTerminalId,
|
|
onCopyFilePath,
|
|
onReloadAgent,
|
|
onRenameTab,
|
|
onCloseTabsToLeft,
|
|
onCloseTabsToRight,
|
|
onCloseOtherTabs,
|
|
resolvedTabWidth,
|
|
showLabel,
|
|
showCloseButton,
|
|
setHoveredCloseTabKey,
|
|
onNavigateTab,
|
|
onCloseTab,
|
|
labels,
|
|
dragHandleProps,
|
|
showDropIndicatorBefore,
|
|
showDropIndicatorAfter,
|
|
}: {
|
|
item: WorkspaceDesktopTabRowItem;
|
|
isFocused: boolean;
|
|
isDragging: boolean;
|
|
index: number;
|
|
tabCount: number;
|
|
normalizedServerId: string;
|
|
normalizedWorkspaceId: string;
|
|
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
|
|
onCopyAgentId: (agentId: string) => Promise<void> | void;
|
|
onCopyTerminalId: (terminalId: string) => Promise<void> | void;
|
|
onCopyFilePath: (path: string) => Promise<void> | void;
|
|
onReloadAgent: (agentId: string) => Promise<void> | void;
|
|
onRenameTab: (tab: WorkspaceTabDescriptor) => void;
|
|
onCloseTabsToLeft: (tabId: string) => Promise<void> | void;
|
|
onCloseTabsToRight: (tabId: string) => Promise<void> | void;
|
|
onCloseOtherTabs: (tabId: string) => Promise<void> | void;
|
|
resolvedTabWidth: number;
|
|
showLabel: boolean;
|
|
showCloseButton: boolean;
|
|
setHoveredCloseTabKey: Dispatch<SetStateAction<string | null>>;
|
|
onNavigateTab: (tabId: string) => void;
|
|
onCloseTab: (tabId: string) => Promise<void> | void;
|
|
labels: WorkspaceTabMenuLabels;
|
|
dragHandleProps: DraggableListDragHandleProps | undefined;
|
|
showDropIndicatorBefore: boolean;
|
|
showDropIndicatorAfter: boolean;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
const resolvedTab = useMemo(
|
|
() =>
|
|
buildWorkspaceDesktopTabActions({
|
|
tab: item.tab,
|
|
index,
|
|
tabCount,
|
|
onCopyResumeCommand,
|
|
onCopyAgentId,
|
|
onCopyTerminalId,
|
|
onCopyFilePath,
|
|
onReloadAgent,
|
|
onRenameTab,
|
|
onCloseTab,
|
|
onCloseTabsToLeft,
|
|
onCloseTabsToRight,
|
|
onCloseOtherTabs,
|
|
labels,
|
|
}),
|
|
[
|
|
index,
|
|
item.tab,
|
|
onCloseOtherTabs,
|
|
onCloseTab,
|
|
onCloseTabsToLeft,
|
|
onCloseTabsToRight,
|
|
onCopyAgentId,
|
|
onCopyTerminalId,
|
|
onCopyFilePath,
|
|
onCopyResumeCommand,
|
|
labels,
|
|
onReloadAgent,
|
|
onRenameTab,
|
|
tabCount,
|
|
],
|
|
);
|
|
|
|
return (
|
|
<WorkspaceTabPresentationResolver
|
|
tab={item.tab}
|
|
serverId={normalizedServerId}
|
|
workspaceId={normalizedWorkspaceId}
|
|
>
|
|
{(presentation) => {
|
|
const tooltipLabel =
|
|
presentation.titleState === "loading"
|
|
? t("workspace.tabs.loadingAgentTitle")
|
|
: presentation.tooltip;
|
|
|
|
return (
|
|
<View style={styles.tabSlot}>
|
|
{showDropIndicatorBefore ? (
|
|
<View style={[styles.tabDropIndicator, styles.tabDropIndicatorBefore]} />
|
|
) : null}
|
|
<TabChip
|
|
tab={item.tab}
|
|
isActive={item.isActive}
|
|
isDragging={isDragging}
|
|
isFocused={isFocused}
|
|
resolvedTabWidth={resolvedTabWidth}
|
|
showLabel={showLabel}
|
|
showCloseButton={showCloseButton}
|
|
isCloseHovered={item.isCloseHovered}
|
|
isClosingTab={item.isClosingTab}
|
|
presentation={presentation}
|
|
tooltipLabel={tooltipLabel}
|
|
resolvedTab={resolvedTab}
|
|
setHoveredCloseTabKey={setHoveredCloseTabKey}
|
|
onNavigateTab={onNavigateTab}
|
|
onCloseTab={onCloseTab}
|
|
dragHandleProps={dragHandleProps}
|
|
/>
|
|
{showDropIndicatorAfter ? (
|
|
<View style={[styles.tabDropIndicator, styles.tabDropIndicatorAfter]} />
|
|
) : null}
|
|
</View>
|
|
);
|
|
}}
|
|
</WorkspaceTabPresentationResolver>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create((theme) => ({
|
|
tabsContainer: {
|
|
minWidth: 0,
|
|
height: WORKSPACE_SECONDARY_HEADER_HEIGHT,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: theme.colors.border,
|
|
backgroundColor: theme.colors.surface0,
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
overflow: "visible",
|
|
},
|
|
tabsScroll: {
|
|
minWidth: 0,
|
|
},
|
|
tabsScrollFitContent: {
|
|
flex: 1,
|
|
},
|
|
tabsScrollOverflow: {
|
|
flex: 1,
|
|
},
|
|
tabsContent: {
|
|
flexDirection: "row",
|
|
alignItems: "stretch",
|
|
},
|
|
tabsActions: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
paddingHorizontal: theme.spacing[2],
|
|
},
|
|
exitFocusModeSlot: {
|
|
alignSelf: "stretch",
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
paddingHorizontal: theme.spacing[1],
|
|
borderRightWidth: 1,
|
|
borderRightColor: theme.colors.border,
|
|
},
|
|
inlineAddButton: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
paddingHorizontal: theme.spacing[1],
|
|
},
|
|
tab: {
|
|
paddingHorizontal: theme.spacing[3],
|
|
paddingVertical: theme.spacing[2],
|
|
borderRightWidth: 1,
|
|
borderRightColor: theme.colors.border,
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: theme.spacing[1],
|
|
userSelect: "none",
|
|
},
|
|
tabSlot: {
|
|
position: "relative",
|
|
overflow: "visible",
|
|
},
|
|
tabHandle: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: theme.spacing[1],
|
|
flex: 1,
|
|
minWidth: 0,
|
|
userSelect: "none",
|
|
},
|
|
tabIcon: {
|
|
flexShrink: 0,
|
|
},
|
|
tabFocusIndicator: {
|
|
position: "absolute",
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
height: 2,
|
|
backgroundColor: theme.colors.accent,
|
|
},
|
|
tabFocusIndicatorUnfocused: {
|
|
backgroundColor: theme.colors.borderAccent,
|
|
},
|
|
tabDropIndicator: {
|
|
position: "absolute",
|
|
top: theme.spacing[2],
|
|
bottom: theme.spacing[2],
|
|
width: 5,
|
|
borderRadius: theme.borderRadius.full,
|
|
backgroundColor: theme.colors.accent,
|
|
zIndex: 10,
|
|
pointerEvents: "none",
|
|
},
|
|
tabDropIndicatorBefore: {
|
|
left: -3,
|
|
},
|
|
tabDropIndicatorAfter: {
|
|
right: -3,
|
|
},
|
|
tabLabel: {
|
|
flexShrink: 1,
|
|
minWidth: 0,
|
|
color: theme.colors.foregroundMuted,
|
|
fontSize: theme.fontSize.sm,
|
|
fontWeight: theme.fontWeight.normal,
|
|
userSelect: "none",
|
|
},
|
|
tabLabelSkeleton: {
|
|
width: 96,
|
|
maxWidth: "100%",
|
|
flexShrink: 1,
|
|
minWidth: 0,
|
|
height: 10,
|
|
borderRadius: theme.borderRadius.full,
|
|
backgroundColor: theme.colors.surface3,
|
|
opacity: 0.9,
|
|
},
|
|
tabLabelSkeletonWithCloseButton: {
|
|
width: LOADING_TAB_LABEL_SKELETON_WIDTH,
|
|
},
|
|
tabLabelWithCloseButton: {
|
|
paddingRight: 0,
|
|
},
|
|
tabLabelActive: {
|
|
color: theme.colors.foreground,
|
|
},
|
|
tabCloseButton: {
|
|
width: 18,
|
|
height: 18,
|
|
marginLeft: 0,
|
|
borderRadius: theme.borderRadius.sm,
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
},
|
|
tabCloseButtonShown: {
|
|
opacity: 1,
|
|
},
|
|
tabCloseButtonActive: {
|
|
backgroundColor: theme.colors.surface3,
|
|
},
|
|
tabModifiedDot: {
|
|
width: 8,
|
|
height: 8,
|
|
borderRadius: theme.borderRadius.full,
|
|
backgroundColor: theme.colors.foregroundMuted,
|
|
},
|
|
newTabActionButton: {
|
|
width: 22,
|
|
height: 22,
|
|
borderRadius: theme.borderRadius.md,
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
},
|
|
inlineAddActionButton: {
|
|
width: 28,
|
|
height: 28,
|
|
borderRadius: theme.borderRadius.md,
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
},
|
|
newTabActionButtonDisabled: {
|
|
opacity: 0.5,
|
|
},
|
|
newTabActionButtonHovered: {
|
|
backgroundColor: theme.colors.surface2,
|
|
},
|
|
newTabTooltipText: {
|
|
color: theme.colors.foreground,
|
|
fontSize: theme.fontSize.sm,
|
|
},
|
|
newTabTooltipRow: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: theme.spacing[2],
|
|
},
|
|
newTabTooltipShortcut: {},
|
|
tooltipAgentRow: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: theme.spacing[2],
|
|
},
|
|
tooltipAgentId: {
|
|
color: theme.colors.foregroundMuted,
|
|
fontSize: theme.fontSize.xs,
|
|
},
|
|
menuItemHint: {
|
|
color: theme.colors.foregroundMuted,
|
|
fontSize: theme.fontSize.xs,
|
|
},
|
|
terminalProfileIconWrapper: {
|
|
width: 14,
|
|
height: 14,
|
|
},
|
|
}));
|