import { useMemo, type ReactElement, type ReactNode } from "react"; import { Pressable, Text, View } from "react-native"; import { Check } from "lucide-react-native"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import invariant from "tiny-invariant"; import { SyncedLoader } from "@/components/synced-loader"; import { ensurePanelsRegistered } from "@/panels/register-panels"; import { getPanelRegistration } from "@/panels/panel-registry"; import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types"; import type { SidebarStateBucket } from "@/utils/sidebar-agent-state"; import { getStatusDotColor, isEmphasizedStatusDotBucket } from "@/utils/status-dot-color"; import { shouldRenderSyncedStatusLoader } from "@/utils/status-loader"; export interface WorkspaceTabPresentation { key: string; kind: WorkspaceTabDescriptor["kind"]; label: string; subtitle: string; titleState: "ready" | "loading"; icon: React.ComponentType<{ size: number; color: string }>; statusBucket: SidebarStateBucket | null; } const DEFAULT_STATUS_DOT_SIZE = 7; const EMPHASIZED_STATUS_DOT_SIZE = 9; const DEFAULT_STATUS_DOT_OFFSET = -2; const EMPHASIZED_STATUS_DOT_OFFSET = -3; type WorkspaceTabPresentationResolverProps = { tab: WorkspaceTabDescriptor; serverId: string; workspaceId: string; children: (presentation: WorkspaceTabPresentation) => ReactNode; }; type WorkspaceTabPresentationResolverInnerProps = WorkspaceTabPresentationResolverProps & { registration: NonNullable>; }; export function WorkspaceTabPresentationResolver({ tab, serverId, workspaceId, children, }: WorkspaceTabPresentationResolverProps): ReactElement { ensurePanelsRegistered(); const registration = getPanelRegistration(tab.kind); invariant(registration, `No panel registration for kind: ${tab.kind}`); return ( {children} ); } function WorkspaceTabPresentationResolverInner({ registration, tab, serverId, workspaceId, children, }: WorkspaceTabPresentationResolverInnerProps): ReactElement { const descriptor = registration.useDescriptor(tab.target as never, { serverId, workspaceId, }); const presentation = useMemo( () => ({ key: tab.key, kind: tab.kind, label: descriptor.label, subtitle: descriptor.subtitle, titleState: descriptor.titleState, icon: descriptor.icon, statusBucket: descriptor.statusBucket, }), [ descriptor.icon, descriptor.label, descriptor.statusBucket, descriptor.subtitle, descriptor.titleState, tab.key, tab.kind, ], ); return <>{children(presentation)}; } type WorkspaceTabIconProps = { presentation: WorkspaceTabPresentation; active?: boolean; size?: number; statusDotBorderColor?: string; }; export function WorkspaceTabIcon({ presentation, active = false, size = 14, statusDotBorderColor, }: WorkspaceTabIconProps): ReactElement { const { theme } = useUnistyles(); const iconColor = active ? theme.colors.foreground : theme.colors.foregroundMuted; const statusDotColor = presentation.statusBucket === null ? null : getStatusDotColor({ theme, bucket: presentation.statusBucket, showDoneAsInactive: false, }); const statusDotSize = isEmphasizedStatusDotBucket(presentation.statusBucket) ? EMPHASIZED_STATUS_DOT_SIZE : DEFAULT_STATUS_DOT_SIZE; const statusDotOffset = statusDotSize === EMPHASIZED_STATUS_DOT_SIZE ? EMPHASIZED_STATUS_DOT_OFFSET : DEFAULT_STATUS_DOT_OFFSET; const shouldShowLoader = shouldRenderSyncedStatusLoader({ bucket: presentation.statusBucket, }); const Icon = presentation.icon; if (shouldShowLoader) { return ( ); } return ( {statusDotColor ? ( ) : null} ); } type WorkspaceTabOptionRowProps = { presentation: WorkspaceTabPresentation; selected: boolean; active: boolean; onPress: () => void; trailingAccessory?: ReactNode; }; export function WorkspaceTabOptionRow({ presentation, selected, active, onPress, trailingAccessory, }: WorkspaceTabOptionRowProps): ReactElement { const { theme } = useUnistyles(); return ( [ styles.optionMainPressable, (hovered || pressed || active) && styles.optionRowActive, ]} > {presentation.titleState === "loading" ? "Loading..." : presentation.label} {selected ? ( ) : null} {trailingAccessory ? ( {trailingAccessory} ) : null} ); } const styles = StyleSheet.create((theme) => ({ agentIconWrapper: { position: "relative", alignItems: "center", justifyContent: "center", }, statusDot: { position: "absolute", right: DEFAULT_STATUS_DOT_OFFSET, bottom: DEFAULT_STATUS_DOT_OFFSET, width: DEFAULT_STATUS_DOT_SIZE, height: DEFAULT_STATUS_DOT_SIZE, borderRadius: theme.borderRadius.full, borderWidth: 1, }, optionRow: { flexDirection: "row", alignItems: "center", minHeight: 36, gap: theme.spacing[2], paddingHorizontal: theme.spacing[1], paddingVertical: theme.spacing[1], borderRadius: 0, marginHorizontal: theme.spacing[1], marginBottom: theme.spacing[1], }, optionMainPressable: { flex: 1, minWidth: 0, flexDirection: "row", alignItems: "center", gap: theme.spacing[2], paddingHorizontal: theme.spacing[2], paddingVertical: theme.spacing[2], }, optionRowActive: { backgroundColor: theme.colors.surface1, }, optionLeadingSlot: { width: 16, alignItems: "center", justifyContent: "center", }, optionContent: { flex: 1, flexShrink: 1, }, optionLabel: { fontSize: theme.fontSize.sm, color: theme.colors.foreground, }, optionTrailingSlot: { width: 16, alignItems: "center", justifyContent: "center", }, optionTrailingAccessorySlot: { alignItems: "center", justifyContent: "center", }, }));