Treat the appearance setting as the rendered base size so the type ramp scales predictably. Native starts one pixel larger while web and desktop preserve the authored 14px base, with legacy values migrated on load.
684 lines
21 KiB
TypeScript
684 lines
21 KiB
TypeScript
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
import { Pressable, type PressableStateCallbackType, Text, View } from "react-native";
|
|
import { useMutation, useQueries, useQueryClient } from "@tanstack/react-query";
|
|
import { useTranslation } from "react-i18next";
|
|
import type {
|
|
DaemonClient,
|
|
FetchRecentProviderSessionEntry,
|
|
} from "@getpaseo/client/internal/daemon-client";
|
|
import type { AgentProvider } from "@getpaseo/protocol/agent-types";
|
|
import { ChevronDown, Inbox, Layers, RotateCw } from "lucide-react-native";
|
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
|
import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet";
|
|
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
|
import { Combobox, ComboboxItem, type ComboboxOption } from "@/components/ui/combobox";
|
|
import { getProviderIcon } from "@/components/provider-icons";
|
|
import { formatTimeAgo } from "@/utils/time";
|
|
import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot";
|
|
import { useHostFeature } from "@/runtime/host-features";
|
|
import { i18n } from "@/i18n/i18next";
|
|
import {
|
|
aggregateSessionEntries,
|
|
ALL_FILTER_VALUE,
|
|
buildProviderLabelMap,
|
|
collectErroredProviderLabels,
|
|
computeEmptyState,
|
|
getPromptPreview,
|
|
getSessionTitle,
|
|
PER_PROVIDER_LIMIT,
|
|
resolveProvidersToFetch,
|
|
requiresImportSessionsHostUpgrade,
|
|
sumFilteredAlreadyImportedCount,
|
|
} from "@/components/import-session-sheet-view-model";
|
|
|
|
const IMPORT_SHEET_SNAP_POINTS = ["70%", "92%"];
|
|
const DISABLED_ACCESSIBILITY_STATE = { disabled: true };
|
|
|
|
type RecentProviderSessionsClient = Pick<
|
|
DaemonClient,
|
|
"fetchRecentProviderSessions" | "importAgent"
|
|
>;
|
|
|
|
type ImportedAgent = Awaited<ReturnType<RecentProviderSessionsClient["importAgent"]>>;
|
|
|
|
interface ImportSessionSheetProps {
|
|
visible: boolean;
|
|
client: RecentProviderSessionsClient | null;
|
|
serverId: string | null;
|
|
cwd?: string | null;
|
|
workspaceId?: string | null;
|
|
onClose: () => void;
|
|
onImportedAgent?: (agentId: string) => void;
|
|
onImported?: (agent: ImportedAgent) => void;
|
|
}
|
|
|
|
type RecentSessionsResponse = Awaited<
|
|
ReturnType<RecentProviderSessionsClient["fetchRecentProviderSessions"]>
|
|
>;
|
|
|
|
interface SessionsQueryConfig {
|
|
queryKey: ReadonlyArray<string | null>;
|
|
enabled: boolean;
|
|
queryFn: () => Promise<RecentSessionsResponse>;
|
|
}
|
|
|
|
function buildSessionsQueriesConfig(args: {
|
|
providersToFetch: AgentProvider[] | null;
|
|
sessionsQueryRoot: ReadonlyArray<string | null>;
|
|
visible: boolean;
|
|
client: RecentProviderSessionsClient | null;
|
|
cwd: string | null | undefined;
|
|
hostDisconnectedMessage?: string;
|
|
}): SessionsQueryConfig[] {
|
|
const { providersToFetch, sessionsQueryRoot, visible, client, cwd, hostDisconnectedMessage } =
|
|
args;
|
|
if (providersToFetch === null) return [];
|
|
const enabled = visible && Boolean(client);
|
|
return providersToFetch.map((provider) => ({
|
|
queryKey: [...sessionsQueryRoot, provider],
|
|
enabled,
|
|
queryFn: async () => {
|
|
if (!client) {
|
|
throw new Error(hostDisconnectedMessage ?? i18n.t("workspace.terminal.hostDisconnected"));
|
|
}
|
|
return await client.fetchRecentProviderSessions({
|
|
...(cwd ? { cwd } : {}),
|
|
providers: [provider],
|
|
limit: PER_PROVIDER_LIMIT,
|
|
});
|
|
},
|
|
}));
|
|
}
|
|
|
|
interface SheetStatusMessagesProps {
|
|
isClientReady: boolean;
|
|
isSnapshotUnsupported: boolean;
|
|
hasNoImportableProviders: boolean;
|
|
isLoadingSessions: boolean;
|
|
hasRows: boolean;
|
|
allQueriesErrored: boolean;
|
|
erroredProviderLabels: ReadonlyArray<string>;
|
|
importErrored: boolean;
|
|
}
|
|
|
|
function SheetStatusMessages({
|
|
isClientReady,
|
|
isSnapshotUnsupported,
|
|
hasNoImportableProviders,
|
|
isLoadingSessions,
|
|
hasRows,
|
|
allQueriesErrored,
|
|
erroredProviderLabels,
|
|
importErrored,
|
|
}: SheetStatusMessagesProps) {
|
|
const { theme } = useUnistyles();
|
|
const { t } = useTranslation();
|
|
if (!isClientReady) {
|
|
return <Text style={styles.statusText}>{t("importSession.status.connectHost")}</Text>;
|
|
}
|
|
if (isSnapshotUnsupported) {
|
|
return <Text style={styles.statusText}>{t("importSession.status.updateHost")}</Text>;
|
|
}
|
|
return (
|
|
<>
|
|
{hasNoImportableProviders ? (
|
|
<Text style={styles.statusText}>{t("importSession.status.noProviders")}</Text>
|
|
) : null}
|
|
{isLoadingSessions && !hasRows ? (
|
|
<View style={styles.statusRow}>
|
|
<LoadingSpinner color={theme.colors.foregroundMuted} />
|
|
<Text style={styles.statusText}>{t("importSession.status.loading")}</Text>
|
|
</View>
|
|
) : null}
|
|
{allQueriesErrored ? (
|
|
<Text style={styles.statusText}>{t("importSession.status.failedAll")}</Text>
|
|
) : null}
|
|
{!allQueriesErrored && erroredProviderLabels.length > 0 ? (
|
|
<Text style={styles.statusText}>
|
|
{t("importSession.status.failedProviders", {
|
|
providers: erroredProviderLabels.join(", "),
|
|
})}
|
|
</Text>
|
|
) : null}
|
|
{importErrored ? (
|
|
<Text style={styles.statusText}>{t("importSession.status.failedImport")}</Text>
|
|
) : null}
|
|
</>
|
|
);
|
|
}
|
|
|
|
function RefreshAction({ isRefreshing, onPress }: { isRefreshing: boolean; onPress: () => void }) {
|
|
const { theme } = useUnistyles();
|
|
const { t } = useTranslation();
|
|
const pressableStyle = useCallback(
|
|
({ pressed }: PressableStateCallbackType) => [
|
|
styles.refreshButton,
|
|
pressed && styles.refreshButtonPressed,
|
|
],
|
|
[],
|
|
);
|
|
return (
|
|
<Pressable
|
|
onPress={onPress}
|
|
disabled={isRefreshing}
|
|
accessibilityLabel={t("importSession.actions.refresh")}
|
|
accessibilityRole="button"
|
|
testID="import-session-refresh"
|
|
style={pressableStyle}
|
|
>
|
|
<View style={styles.refreshIconSlot}>
|
|
{isRefreshing ? (
|
|
<LoadingSpinner color={theme.colors.foregroundMuted} />
|
|
) : (
|
|
<RotateCw size={16} color={theme.colors.foregroundMuted} />
|
|
)}
|
|
</View>
|
|
</Pressable>
|
|
);
|
|
}
|
|
|
|
function SheetEmptyState({ title }: { title: string }) {
|
|
const { theme } = useUnistyles();
|
|
return (
|
|
<View style={styles.emptyState} testID="import-session-empty-state">
|
|
<View style={styles.emptyStateIcon}>
|
|
<Inbox size={theme.iconSize.lg} color={theme.colors.foregroundMuted} strokeWidth={1.5} />
|
|
</View>
|
|
<Text style={styles.emptyStateTitle}>{title}</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function ImportSessionSheetRow({
|
|
entry,
|
|
disabled,
|
|
importing,
|
|
showCwd,
|
|
onImportSession,
|
|
}: {
|
|
entry: FetchRecentProviderSessionEntry;
|
|
disabled: boolean;
|
|
importing: boolean;
|
|
showCwd: boolean;
|
|
onImportSession: (entry: FetchRecentProviderSessionEntry) => void;
|
|
}) {
|
|
const { theme } = useUnistyles();
|
|
const { t } = useTranslation();
|
|
const title = getSessionTitle(entry);
|
|
const promptPreview = getPromptPreview(entry);
|
|
const lastActivity = formatTimeAgo(new Date(entry.lastActivityAt));
|
|
const ProviderIcon = getProviderIcon(entry.providerId);
|
|
const accessibilityState = useMemo(
|
|
() => (disabled ? DISABLED_ACCESSIBILITY_STATE : undefined),
|
|
[disabled],
|
|
);
|
|
const handlePress = useCallback(() => {
|
|
onImportSession(entry);
|
|
}, [entry, onImportSession]);
|
|
const pressableStyle = useCallback(
|
|
({ pressed, hovered = false }: PressableStateCallbackType & { hovered?: boolean }) => [
|
|
styles.row,
|
|
Boolean(hovered) && styles.rowHovered,
|
|
pressed && styles.rowPressed,
|
|
],
|
|
[],
|
|
);
|
|
|
|
return (
|
|
<Pressable
|
|
disabled={disabled}
|
|
onPress={handlePress}
|
|
accessibilityRole="button"
|
|
accessibilityState={accessibilityState}
|
|
style={pressableStyle}
|
|
testID={`import-session-session-${entry.providerId}-${entry.providerHandleId}`}
|
|
>
|
|
<View style={styles.rowIconWrap}>
|
|
<ProviderIcon size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
|
</View>
|
|
<View style={styles.rowContent}>
|
|
<View style={styles.rowHeader}>
|
|
<Text style={styles.rowTitle} numberOfLines={1}>
|
|
{title}
|
|
</Text>
|
|
<Text style={styles.rowMeta}>
|
|
{importing ? t("importSession.row.importing") : lastActivity}
|
|
</Text>
|
|
</View>
|
|
<Text style={styles.rowPreview} numberOfLines={2}>
|
|
{promptPreview}
|
|
</Text>
|
|
{showCwd && entry.cwd ? (
|
|
<Text style={styles.rowCwd} numberOfLines={1}>
|
|
{entry.cwd}
|
|
</Text>
|
|
) : null}
|
|
</View>
|
|
</Pressable>
|
|
);
|
|
}
|
|
|
|
export function ImportSessionSheet({
|
|
visible,
|
|
client,
|
|
serverId,
|
|
cwd,
|
|
workspaceId,
|
|
onClose,
|
|
onImportedAgent,
|
|
onImported,
|
|
}: ImportSessionSheetProps) {
|
|
const { t } = useTranslation();
|
|
const queryClient = useQueryClient();
|
|
const { theme } = useUnistyles();
|
|
|
|
const { entries: snapshotEntries, supportsSnapshot } = useProvidersSnapshot(serverId, {
|
|
cwd,
|
|
enabled: visible,
|
|
});
|
|
const supportsWorkspaceTarget = useHostFeature(serverId, "importSessionWorkspaceTarget");
|
|
const requiresHostUpgrade = requiresImportSessionsHostUpgrade({
|
|
supportsSnapshot,
|
|
workspaceId,
|
|
supportsWorkspaceTarget,
|
|
});
|
|
|
|
const providersToFetch = useMemo(
|
|
() => (requiresHostUpgrade ? null : resolveProvidersToFetch(supportsSnapshot, snapshotEntries)),
|
|
[requiresHostUpgrade, supportsSnapshot, snapshotEntries],
|
|
);
|
|
|
|
const providerLabelById = useMemo(
|
|
() => buildProviderLabelMap(snapshotEntries),
|
|
[snapshotEntries],
|
|
);
|
|
|
|
const sessionsQueryRoot = useMemo(
|
|
() => ["recent-provider-sessions", cwd ?? null] as const,
|
|
[cwd],
|
|
);
|
|
|
|
const queriesConfig = useMemo(
|
|
() =>
|
|
buildSessionsQueriesConfig({
|
|
providersToFetch,
|
|
sessionsQueryRoot,
|
|
visible,
|
|
client,
|
|
cwd,
|
|
hostDisconnectedMessage: t("workspace.terminal.hostDisconnected"),
|
|
}),
|
|
[providersToFetch, sessionsQueryRoot, visible, client, cwd, t],
|
|
);
|
|
|
|
const queries = useQueries({ queries: queriesConfig });
|
|
|
|
const aggregatedEntries = useMemo(() => aggregateSessionEntries(queries), [queries]);
|
|
const totalAlreadyImportedCount = useMemo(
|
|
() => sumFilteredAlreadyImportedCount(queries),
|
|
[queries],
|
|
);
|
|
|
|
const filterProviders = useMemo(() => [...(providersToFetch ?? [])].sort(), [providersToFetch]);
|
|
|
|
const [selectedProvider, setSelectedProvider] = useState<string>(ALL_FILTER_VALUE);
|
|
const [isFilterOpen, setIsFilterOpen] = useState(false);
|
|
const filterAnchorRef = useRef<View>(null);
|
|
|
|
useEffect(() => {
|
|
if (
|
|
!visible ||
|
|
(selectedProvider !== ALL_FILTER_VALUE && !filterProviders.includes(selectedProvider))
|
|
) {
|
|
setSelectedProvider(ALL_FILTER_VALUE);
|
|
}
|
|
}, [visible, filterProviders, selectedProvider]);
|
|
|
|
const visibleEntries = useMemo(() => {
|
|
if (selectedProvider === ALL_FILTER_VALUE) return aggregatedEntries;
|
|
return aggregatedEntries.filter((entry) => entry.providerId === selectedProvider);
|
|
}, [aggregatedEntries, selectedProvider]);
|
|
|
|
const filterComboboxOptions = useMemo<ComboboxOption[]>(
|
|
() => [
|
|
{ id: ALL_FILTER_VALUE, label: t("importSession.filters.all") },
|
|
...filterProviders.map((provider) => ({
|
|
id: provider,
|
|
label: providerLabelById.get(provider) ?? provider,
|
|
})),
|
|
],
|
|
[filterProviders, providerLabelById, t],
|
|
);
|
|
|
|
const selectedProviderLabel = useMemo(
|
|
() =>
|
|
filterComboboxOptions.find((opt) => opt.id === selectedProvider)?.label ??
|
|
t("importSession.filters.all"),
|
|
[filterComboboxOptions, selectedProvider, t],
|
|
);
|
|
|
|
const handleFilterOpen = useCallback(() => setIsFilterOpen(true), []);
|
|
|
|
const filterTriggerStyle = useCallback(
|
|
({ pressed, hovered = false }: PressableStateCallbackType & { hovered?: boolean }) => [
|
|
styles.filterTrigger,
|
|
Boolean(hovered) && styles.filterTriggerHovered,
|
|
pressed && styles.filterTriggerPressed,
|
|
],
|
|
[],
|
|
);
|
|
|
|
const handleFilterSelect = useCallback((id: string) => {
|
|
setSelectedProvider(id);
|
|
setIsFilterOpen(false);
|
|
}, []);
|
|
|
|
const filterOptionIcons = useMemo(() => {
|
|
const map = new Map<string, React.ReactNode>();
|
|
map.set(ALL_FILTER_VALUE, <Layers size={14} color={theme.colors.foregroundMuted} />);
|
|
for (const provider of filterProviders) {
|
|
const ProviderIcon = getProviderIcon(provider);
|
|
map.set(provider, <ProviderIcon size={14} color={theme.colors.foregroundMuted} />);
|
|
}
|
|
return map;
|
|
}, [filterProviders, theme.colors.foregroundMuted]);
|
|
|
|
const renderFilterOption = useCallback(
|
|
({
|
|
option,
|
|
selected,
|
|
active,
|
|
onPress,
|
|
}: {
|
|
option: ComboboxOption;
|
|
selected: boolean;
|
|
active: boolean;
|
|
onPress: () => void;
|
|
}) => (
|
|
<ComboboxItem
|
|
label={option.label}
|
|
selected={selected}
|
|
active={active}
|
|
onPress={onPress}
|
|
leadingSlot={filterOptionIcons.get(option.id)}
|
|
/>
|
|
),
|
|
[filterOptionIcons],
|
|
);
|
|
|
|
const importMutation = useMutation({
|
|
mutationFn: async (entry: FetchRecentProviderSessionEntry) => {
|
|
if (!client) {
|
|
throw new Error(t("workspace.terminal.hostDisconnected"));
|
|
}
|
|
if (!entry.cwd) {
|
|
throw new Error("Session is missing a working directory");
|
|
}
|
|
const agent = await client.importAgent({
|
|
providerId: entry.providerId,
|
|
providerHandleId: entry.providerHandleId,
|
|
cwd: entry.cwd,
|
|
...(workspaceId ? { workspaceId } : {}),
|
|
});
|
|
return agent;
|
|
},
|
|
onSuccess: async (agent) => {
|
|
await queryClient.invalidateQueries({ queryKey: sessionsQueryRoot });
|
|
onClose();
|
|
onImportedAgent?.(agent.id);
|
|
onImported?.(agent);
|
|
},
|
|
});
|
|
|
|
const importingSessionKey =
|
|
importMutation.isPending && importMutation.variables
|
|
? `${importMutation.variables.providerId}:${importMutation.variables.providerHandleId}`
|
|
: null;
|
|
|
|
const handleImportSession = useCallback(
|
|
(entry: FetchRecentProviderSessionEntry) => {
|
|
importMutation.mutate(entry);
|
|
},
|
|
[importMutation],
|
|
);
|
|
|
|
const erroredProviderLabels = useMemo(
|
|
() => collectErroredProviderLabels(providersToFetch, queries, providerLabelById),
|
|
[queries, providersToFetch, providerLabelById],
|
|
);
|
|
|
|
const isRefreshing = queries.some((query) => query.isFetching);
|
|
|
|
const handleRefresh = useCallback(() => {
|
|
void queryClient.invalidateQueries({ queryKey: sessionsQueryRoot });
|
|
}, [queryClient, sessionsQueryRoot]);
|
|
|
|
const header = useMemo<SheetHeader>(
|
|
() => ({
|
|
title: t("importSession.title"),
|
|
actions: <RefreshAction isRefreshing={isRefreshing} onPress={handleRefresh} />,
|
|
}),
|
|
[isRefreshing, handleRefresh, t],
|
|
);
|
|
|
|
const isSnapshotUnsupported = requiresHostUpgrade;
|
|
const isWaitingForSnapshot = supportsSnapshot && snapshotEntries === undefined;
|
|
const hasNoImportableProviders = providersToFetch !== null && providersToFetch.length === 0;
|
|
const isQueryingProviders = queries.length > 0;
|
|
const isLoadingSessions =
|
|
isWaitingForSnapshot ||
|
|
(isQueryingProviders && queries.some((query) => query.isLoading || query.isPending));
|
|
const allQueriesErrored = isQueryingProviders && queries.every((query) => query.isError);
|
|
const allQueriesSettled =
|
|
isQueryingProviders && queries.every((query) => !query.isLoading && !query.isPending);
|
|
const { showEmptyState, emptyStateTitle } = computeEmptyState({
|
|
isLoadingSessions,
|
|
allQueriesErrored,
|
|
isQueryingProviders,
|
|
allQueriesSettled,
|
|
selectedProvider,
|
|
aggregatedCount: aggregatedEntries.length,
|
|
visibleCount: visibleEntries.length,
|
|
totalAlreadyImportedCount,
|
|
providerLabelById,
|
|
});
|
|
const showFilter = filterProviders.length > 1;
|
|
|
|
return (
|
|
<AdaptiveModalSheet
|
|
visible={visible}
|
|
onClose={onClose}
|
|
header={header}
|
|
testID="import-session-sheet"
|
|
desktopMaxWidth={560}
|
|
snapPoints={IMPORT_SHEET_SNAP_POINTS}
|
|
>
|
|
{showFilter ? (
|
|
<View ref={filterAnchorRef} collapsable={false} style={styles.filterTriggerWrap}>
|
|
<Pressable
|
|
onPress={handleFilterOpen}
|
|
style={filterTriggerStyle}
|
|
testID="import-session-filter-trigger"
|
|
accessibilityRole="button"
|
|
accessibilityLabel={`Filter: ${selectedProviderLabel}`}
|
|
>
|
|
{selectedProvider === ALL_FILTER_VALUE ? (
|
|
<Layers size={14} color={theme.colors.foregroundMuted} />
|
|
) : (
|
|
(() => {
|
|
const ProviderIcon = getProviderIcon(selectedProvider);
|
|
return <ProviderIcon size={14} color={theme.colors.foregroundMuted} />;
|
|
})()
|
|
)}
|
|
<Text style={styles.filterTriggerText} numberOfLines={1}>
|
|
{selectedProviderLabel}
|
|
</Text>
|
|
<ChevronDown size={14} color={theme.colors.foregroundMuted} />
|
|
</Pressable>
|
|
<Combobox
|
|
options={filterComboboxOptions}
|
|
value={selectedProvider}
|
|
onSelect={handleFilterSelect}
|
|
renderOption={renderFilterOption}
|
|
searchable={false}
|
|
title="Filter by provider"
|
|
open={isFilterOpen}
|
|
onOpenChange={setIsFilterOpen}
|
|
anchorRef={filterAnchorRef}
|
|
desktopPlacement="bottom-start"
|
|
desktopPreventInitialFlash
|
|
/>
|
|
</View>
|
|
) : null}
|
|
<SheetStatusMessages
|
|
isClientReady={Boolean(client)}
|
|
isSnapshotUnsupported={isSnapshotUnsupported}
|
|
hasNoImportableProviders={hasNoImportableProviders}
|
|
isLoadingSessions={isLoadingSessions}
|
|
hasRows={visibleEntries.length > 0}
|
|
allQueriesErrored={allQueriesErrored}
|
|
erroredProviderLabels={erroredProviderLabels}
|
|
importErrored={importMutation.isError}
|
|
/>
|
|
{visibleEntries.length > 0 ? (
|
|
<View style={styles.list}>
|
|
{visibleEntries.map((entry) => (
|
|
<ImportSessionSheetRow
|
|
key={`${entry.providerId}:${entry.providerHandleId}`}
|
|
entry={entry}
|
|
disabled={importMutation.isPending}
|
|
importing={importingSessionKey === `${entry.providerId}:${entry.providerHandleId}`}
|
|
showCwd={!cwd}
|
|
onImportSession={handleImportSession}
|
|
/>
|
|
))}
|
|
</View>
|
|
) : null}
|
|
{showEmptyState ? <SheetEmptyState title={emptyStateTitle} /> : null}
|
|
</AdaptiveModalSheet>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create((theme) => ({
|
|
filterTriggerWrap: {
|
|
paddingBottom: theme.spacing[2],
|
|
},
|
|
filterTrigger: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: theme.spacing[1.5],
|
|
alignSelf: "flex-start",
|
|
paddingVertical: theme.spacing[1.5],
|
|
paddingHorizontal: theme.spacing[3],
|
|
borderRadius: theme.borderRadius.md,
|
|
backgroundColor: theme.colors.surface1,
|
|
borderWidth: theme.borderWidth[1],
|
|
borderColor: theme.colors.border,
|
|
},
|
|
filterTriggerHovered: {
|
|
backgroundColor: theme.colors.surface2,
|
|
},
|
|
filterTriggerPressed: {
|
|
backgroundColor: theme.colors.surface3,
|
|
},
|
|
filterTriggerText: {
|
|
color: theme.colors.foreground,
|
|
fontSize: theme.fontSize.base,
|
|
fontWeight: theme.fontWeight.medium,
|
|
},
|
|
list: {
|
|
gap: theme.spacing[1],
|
|
},
|
|
row: {
|
|
flexDirection: "row",
|
|
alignItems: "flex-start",
|
|
gap: theme.spacing[2],
|
|
paddingVertical: theme.spacing[2],
|
|
paddingHorizontal: theme.spacing[2],
|
|
marginHorizontal: -theme.spacing[2],
|
|
borderRadius: theme.borderRadius.lg,
|
|
},
|
|
rowHovered: {
|
|
backgroundColor: theme.colors.surface1,
|
|
},
|
|
rowPressed: {
|
|
backgroundColor: theme.colors.surface2,
|
|
},
|
|
rowIconWrap: {
|
|
width: theme.iconSize.md,
|
|
paddingTop: 2,
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
},
|
|
rowContent: {
|
|
flex: 1,
|
|
minWidth: 0,
|
|
gap: theme.spacing[1],
|
|
},
|
|
rowHeader: {
|
|
flexDirection: "row",
|
|
alignItems: "baseline",
|
|
justifyContent: "space-between",
|
|
gap: theme.spacing[2],
|
|
},
|
|
rowTitle: {
|
|
flex: 1,
|
|
minWidth: 0,
|
|
color: theme.colors.foreground,
|
|
fontSize: theme.fontSize.base,
|
|
},
|
|
rowMeta: {
|
|
color: theme.colors.foregroundMuted,
|
|
fontSize: theme.fontSize.sm,
|
|
},
|
|
rowPreview: {
|
|
color: theme.colors.foregroundMuted,
|
|
fontSize: theme.fontSize.base,
|
|
lineHeight: 20,
|
|
},
|
|
rowCwd: {
|
|
color: theme.colors.foregroundMuted,
|
|
fontSize: theme.fontSize.sm,
|
|
},
|
|
statusRow: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: theme.spacing[2],
|
|
paddingVertical: theme.spacing[2],
|
|
},
|
|
statusText: {
|
|
color: theme.colors.foregroundMuted,
|
|
fontSize: theme.fontSize.base,
|
|
},
|
|
emptyState: {
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
gap: theme.spacing[2],
|
|
paddingVertical: theme.spacing[8],
|
|
paddingHorizontal: theme.spacing[4],
|
|
},
|
|
emptyStateIcon: {
|
|
opacity: 0.6,
|
|
marginBottom: theme.spacing[1],
|
|
},
|
|
emptyStateTitle: {
|
|
color: theme.colors.foreground,
|
|
fontSize: theme.fontSize.base,
|
|
textAlign: "center",
|
|
},
|
|
refreshButton: {
|
|
padding: theme.spacing[2],
|
|
marginRight: theme.spacing[1],
|
|
borderRadius: theme.borderRadius.lg,
|
|
},
|
|
refreshButtonPressed: {
|
|
backgroundColor: theme.colors.surface2,
|
|
},
|
|
refreshIconSlot: {
|
|
width: 16,
|
|
height: 16,
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
},
|
|
}));
|