mirror of
https://github.com/getpaseo/paseo.git
synced 2026-08-14 12:23:16 +00:00
Adds client-side i18n support, language settings, translated UI copy, and locale parity coverage.
673 lines
20 KiB
TypeScript
673 lines
20 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 { i18n } from "@/i18n/i18next";
|
|
import {
|
|
aggregateSessionEntries,
|
|
ALL_FILTER_VALUE,
|
|
buildProviderLabelMap,
|
|
collectErroredProviderLabels,
|
|
computeEmptyState,
|
|
getPromptPreview,
|
|
getSessionTitle,
|
|
PER_PROVIDER_LIMIT,
|
|
resolveProvidersToFetch,
|
|
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;
|
|
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,
|
|
onClose,
|
|
onImportedAgent,
|
|
onImported,
|
|
}: ImportSessionSheetProps) {
|
|
const { t } = useTranslation();
|
|
const queryClient = useQueryClient();
|
|
const { theme } = useUnistyles();
|
|
|
|
const { entries: snapshotEntries, supportsSnapshot } = useProvidersSnapshot(serverId, {
|
|
cwd,
|
|
enabled: visible,
|
|
});
|
|
|
|
const providersToFetch = useMemo(
|
|
() => resolveProvidersToFetch(supportsSnapshot, snapshotEntries),
|
|
[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,
|
|
});
|
|
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 = !supportsSnapshot;
|
|
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.sm,
|
|
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.xs,
|
|
},
|
|
rowPreview: {
|
|
color: theme.colors.foregroundMuted,
|
|
fontSize: theme.fontSize.sm,
|
|
lineHeight: 20,
|
|
},
|
|
rowCwd: {
|
|
color: theme.colors.foregroundMuted,
|
|
fontSize: theme.fontSize.xs,
|
|
},
|
|
statusRow: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: theme.spacing[2],
|
|
paddingVertical: theme.spacing[2],
|
|
},
|
|
statusText: {
|
|
color: theme.colors.foregroundMuted,
|
|
fontSize: theme.fontSize.sm,
|
|
},
|
|
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",
|
|
},
|
|
}));
|