mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-05 07:54:38 +00:00
The app no longer locks to portrait. Phones and tablets on Android and iOS follow the device rotation and respect the system rotation lock, so Android tablets fill the display in landscape instead of letterboxing. Modals pass all orientations explicitly because React Native presents iPhone modals in portrait by default. Closes #1669 Co-authored-by: yz <43207690+yzim@users.noreply.github.com> Co-authored-by: magiclaw <930198090@qq.com>
753 lines
22 KiB
TypeScript
753 lines
22 KiB
TypeScript
import { Modal } from "@/components/ui/modal";
|
|
import {
|
|
View,
|
|
Text,
|
|
Pressable,
|
|
RefreshControl,
|
|
FlatList,
|
|
type ListRenderItem,
|
|
type PressableStateCallbackType,
|
|
} from "react-native";
|
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
import { useCallback, useMemo, useState, type ReactElement } from "react";
|
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
|
import type { TFunction } from "i18next";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
|
import { useTimeAgo } from "@/hooks/use-time-ago";
|
|
import { type AggregatedAgent } from "@/hooks/use-aggregated-agents";
|
|
import { useSessionStore } from "@/stores/session-store";
|
|
import { Archive, ChevronRight } from "lucide-react-native";
|
|
import { useProviderIcon } from "@/components/provider-icons";
|
|
import { navigateToAgent } from "@/utils/navigate-to-agent";
|
|
import { useArchiveAgent } from "@/hooks/use-archive-agent";
|
|
import { HighlightedText } from "@/components/ui/highlighted-text";
|
|
import { StatusBadge, type StatusBadgeVariant } from "@/components/ui/status-badge";
|
|
import { findHighlightRanges } from "@/components/ui/highlighted-text-segments";
|
|
|
|
interface AgentListProps {
|
|
agents: AggregatedAgent[];
|
|
showCheckoutInfo?: boolean;
|
|
isRefreshing?: boolean;
|
|
onRefresh?: () => void;
|
|
selectedAgentId?: string;
|
|
onAgentSelect?: () => void;
|
|
listFooterComponent?: ReactElement | null;
|
|
showAttentionIndicator?: boolean;
|
|
showHostColumn?: boolean;
|
|
search?: string;
|
|
}
|
|
|
|
type DateSectionKey = "today" | "yesterday" | "thisWeek" | "thisMonth" | "older";
|
|
|
|
const DATE_SECTION_ORDER = [
|
|
"today",
|
|
"yesterday",
|
|
"thisWeek",
|
|
"thisMonth",
|
|
"older",
|
|
] as const satisfies readonly DateSectionKey[];
|
|
|
|
const HISTORY_MATCH_OPTIONS = { typoTolerant: true } as const;
|
|
|
|
type FlatListItem =
|
|
| { type: "header"; key: string; section: DateSectionKey }
|
|
| { type: "agent"; key: string; agent: AggregatedAgent };
|
|
|
|
function deriveDateSectionKey(lastActivityAt: Date): DateSectionKey {
|
|
const now = new Date();
|
|
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
const yesterdayStart = new Date(todayStart.getTime() - 24 * 60 * 60 * 1000);
|
|
const activityStart = new Date(
|
|
lastActivityAt.getFullYear(),
|
|
lastActivityAt.getMonth(),
|
|
lastActivityAt.getDate(),
|
|
);
|
|
|
|
if (activityStart.getTime() >= todayStart.getTime()) {
|
|
return "today";
|
|
}
|
|
if (activityStart.getTime() >= yesterdayStart.getTime()) {
|
|
return "yesterday";
|
|
}
|
|
|
|
const diffTime = todayStart.getTime() - activityStart.getTime();
|
|
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
|
if (diffDays <= 7) {
|
|
return "thisWeek";
|
|
}
|
|
if (diffDays <= 30) {
|
|
return "thisMonth";
|
|
}
|
|
return "older";
|
|
}
|
|
|
|
function formatDateSectionLabel(t: TFunction, section: DateSectionKey): string {
|
|
switch (section) {
|
|
case "today":
|
|
return t("agentList.dateSections.today");
|
|
case "yesterday":
|
|
return t("agentList.dateSections.yesterday");
|
|
case "thisWeek":
|
|
return t("agentList.dateSections.thisWeek");
|
|
case "thisMonth":
|
|
return t("agentList.dateSections.thisMonth");
|
|
case "older":
|
|
return t("agentList.dateSections.older");
|
|
}
|
|
}
|
|
|
|
function SessionBadge({
|
|
label,
|
|
icon,
|
|
tone = "neutral",
|
|
}: {
|
|
label: string;
|
|
icon?: ReactElement;
|
|
tone?: "neutral" | "warning" | "danger";
|
|
}) {
|
|
let variant: StatusBadgeVariant = "muted";
|
|
if (tone === "warning") variant = "warning";
|
|
else if (tone === "danger") variant = "error";
|
|
return <StatusBadge label={label} variant={variant} leading={icon} />;
|
|
}
|
|
|
|
function SessionRowBadges({
|
|
agent,
|
|
archivedIcon,
|
|
pendingPermissionCount,
|
|
showDesktopAttention,
|
|
}: {
|
|
agent: AggregatedAgent;
|
|
archivedIcon: ReactElement;
|
|
pendingPermissionCount: number;
|
|
showDesktopAttention: boolean;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
return (
|
|
<>
|
|
{agent.archivedAt ? (
|
|
<SessionBadge label={t("agentList.badges.archived")} icon={archivedIcon} />
|
|
) : null}
|
|
{pendingPermissionCount > 0 ? (
|
|
<SessionBadge
|
|
label={t("agentList.badges.pending", { count: pendingPermissionCount })}
|
|
tone="warning"
|
|
/>
|
|
) : null}
|
|
{showDesktopAttention ? (
|
|
<SessionBadge label={t("agentList.badges.attention")} tone="danger" />
|
|
) : null}
|
|
</>
|
|
);
|
|
}
|
|
|
|
function SessionRowTrailingAttention({
|
|
isMobile,
|
|
showAttentionIndicator,
|
|
requiresAttention,
|
|
}: {
|
|
isMobile: boolean;
|
|
showAttentionIndicator: boolean;
|
|
requiresAttention: boolean | undefined;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
if (!isMobile || !showAttentionIndicator || !requiresAttention) {
|
|
return null;
|
|
}
|
|
return (
|
|
<View style={styles.rowTrailing}>
|
|
<SessionBadge label={t("agentList.badges.attention")} tone="danger" />
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function SessionRow({
|
|
agent,
|
|
search,
|
|
isMobile,
|
|
selectedAgentId,
|
|
showAttentionIndicator,
|
|
showHostColumn,
|
|
onPress,
|
|
onLongPress,
|
|
}: {
|
|
agent: AggregatedAgent;
|
|
search?: string;
|
|
isMobile: boolean;
|
|
selectedAgentId?: string;
|
|
showAttentionIndicator: boolean;
|
|
showHostColumn: boolean;
|
|
onPress: (agent: AggregatedAgent) => void;
|
|
onLongPress: (agent: AggregatedAgent) => void;
|
|
}) {
|
|
const { theme } = useUnistyles();
|
|
const { t } = useTranslation();
|
|
const agentKey = `${agent.serverId}:${agent.id}`;
|
|
const isSelected = selectedAgentId === agentKey;
|
|
const projectName = agent.projectPlacement?.projectName ?? "";
|
|
const branch = agent.projectPlacement?.checkout.currentBranch ?? "";
|
|
const workspaceName = agent.projectPlacement?.workspaceName ?? "";
|
|
const ProviderIcon = useProviderIcon(agent.provider, agent.serverId);
|
|
const pendingPermissionCount = agent.pendingPermissionCount ?? 0;
|
|
const ranges = useMemo(
|
|
() => ({
|
|
// History search matches typo-tolerantly on the daemon, so the marks do too.
|
|
workspace: findHighlightRanges(search ?? "", workspaceName, HISTORY_MATCH_OPTIONS),
|
|
title: findHighlightRanges(search ?? "", agent.title ?? "", HISTORY_MATCH_OPTIONS),
|
|
branch: findHighlightRanges(search ?? "", branch, HISTORY_MATCH_OPTIONS),
|
|
project: findHighlightRanges(search ?? "", projectName, HISTORY_MATCH_OPTIONS),
|
|
}),
|
|
[search, workspaceName, agent.title, branch, projectName],
|
|
);
|
|
|
|
const pressableStyle = useCallback(
|
|
({ pressed, hovered = false }: PressableStateCallbackType & { hovered?: boolean }) => [
|
|
styles.row,
|
|
isSelected && styles.rowSelected,
|
|
Boolean(hovered) && styles.rowHovered,
|
|
pressed && styles.rowPressed,
|
|
],
|
|
[isSelected],
|
|
);
|
|
|
|
const handlePress = useCallback(() => onPress(agent), [onPress, agent]);
|
|
const handleLongPress = useCallback(() => onLongPress(agent), [onLongPress, agent]);
|
|
|
|
const archivedIcon = useMemo(
|
|
() => <Archive size={theme.fontSize.sm} color={theme.colors.foregroundMuted} />,
|
|
[theme.fontSize.sm, theme.colors.foregroundMuted],
|
|
);
|
|
const showDesktopAttention =
|
|
!isMobile && showAttentionIndicator && Boolean(agent.requiresAttention);
|
|
|
|
const agentTitle = (
|
|
<View style={styles.agentTitleRow}>
|
|
<View style={styles.providerIconWrap}>
|
|
<ProviderIcon size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
|
</View>
|
|
<HighlightedText
|
|
text={agent.title || t("agentList.fallbackTitle")}
|
|
ranges={ranges.title}
|
|
style={styles.sessionTitle}
|
|
numberOfLines={1}
|
|
testID={`agent-row-title-${agent.serverId}-${agent.id}`}
|
|
/>
|
|
</View>
|
|
);
|
|
|
|
return (
|
|
<Pressable
|
|
style={pressableStyle}
|
|
onPress={handlePress}
|
|
onLongPress={handleLongPress}
|
|
accessibilityRole="button"
|
|
testID={`agent-row-${agent.serverId}-${agent.id}`}
|
|
>
|
|
<View style={styles.rowContent}>
|
|
<View style={styles.rowTitleRow}>
|
|
<HighlightedText
|
|
text={workspaceName || projectName}
|
|
ranges={workspaceName ? ranges.workspace : ranges.project}
|
|
style={styles.workspaceTitleText}
|
|
numberOfLines={1}
|
|
testID={`agent-row-workspace-${agent.serverId}-${agent.id}`}
|
|
/>
|
|
{!isMobile ? (
|
|
<>
|
|
<ChevronRight size={theme.iconSize.xs} color={theme.colors.foregroundMuted} />
|
|
{agentTitle}
|
|
</>
|
|
) : null}
|
|
<SessionRowBadges
|
|
agent={agent}
|
|
archivedIcon={archivedIcon}
|
|
pendingPermissionCount={pendingPermissionCount}
|
|
showDesktopAttention={showDesktopAttention}
|
|
/>
|
|
</View>
|
|
{isMobile ? agentTitle : null}
|
|
{isMobile ? (
|
|
<View style={styles.rowMetaRow}>
|
|
<HighlightedText
|
|
text={projectName}
|
|
ranges={ranges.project}
|
|
style={styles.sessionMetaText}
|
|
numberOfLines={1}
|
|
testID={`agent-row-project-${agent.serverId}-${agent.id}`}
|
|
/>
|
|
<Text style={styles.sessionMetaSeparator}>·</Text>
|
|
<HighlightedText
|
|
text={branch}
|
|
ranges={ranges.branch}
|
|
style={styles.sessionMetaText}
|
|
numberOfLines={1}
|
|
testID={`agent-row-branch-${agent.serverId}-${agent.id}`}
|
|
/>
|
|
<Text style={styles.sessionMetaSeparator}>·</Text>
|
|
<AgentActivityTime date={agent.lastActivityAt} isMobile />
|
|
{showHostColumn && agent.serverLabel ? (
|
|
<>
|
|
<Text style={styles.sessionMetaSeparator}>·</Text>
|
|
<Text style={styles.sessionMetaText} numberOfLines={1}>
|
|
{agent.serverLabel}
|
|
</Text>
|
|
</>
|
|
) : null}
|
|
</View>
|
|
) : null}
|
|
</View>
|
|
{!isMobile ? (
|
|
<View style={styles.rowColumns}>
|
|
<HighlightedText
|
|
text={projectName}
|
|
ranges={ranges.project}
|
|
style={styles.columnMeta}
|
|
numberOfLines={1}
|
|
testID={`agent-row-project-${agent.serverId}-${agent.id}`}
|
|
/>
|
|
{showHostColumn ? (
|
|
<Text style={styles.columnMetaHost} numberOfLines={1}>
|
|
{agent.serverLabel}
|
|
</Text>
|
|
) : null}
|
|
<HighlightedText
|
|
text={branch}
|
|
ranges={ranges.branch}
|
|
style={styles.columnMeta}
|
|
numberOfLines={1}
|
|
testID={`agent-row-branch-${agent.serverId}-${agent.id}`}
|
|
/>
|
|
<AgentActivityTime date={agent.lastActivityAt} isMobile={false} />
|
|
</View>
|
|
) : null}
|
|
<SessionRowTrailingAttention
|
|
isMobile={isMobile}
|
|
showAttentionIndicator={showAttentionIndicator}
|
|
requiresAttention={agent.requiresAttention}
|
|
/>
|
|
</Pressable>
|
|
);
|
|
}
|
|
|
|
function AgentActivityTime({ date, isMobile }: { date: Date; isMobile: boolean }) {
|
|
const label = useTimeAgo(date);
|
|
return (
|
|
<Text style={isMobile ? styles.sessionMetaText : styles.columnMetaFixed} numberOfLines={1}>
|
|
{label}
|
|
</Text>
|
|
);
|
|
}
|
|
|
|
export function AgentList({
|
|
agents,
|
|
isRefreshing = false,
|
|
onRefresh,
|
|
selectedAgentId,
|
|
onAgentSelect,
|
|
listFooterComponent,
|
|
showAttentionIndicator = true,
|
|
showHostColumn = false,
|
|
search,
|
|
}: AgentListProps) {
|
|
const { theme } = useUnistyles();
|
|
const { t } = useTranslation();
|
|
const insets = useSafeAreaInsets();
|
|
const [actionAgent, setActionAgent] = useState<AggregatedAgent | null>(null);
|
|
const isMobile = useIsCompactFormFactor();
|
|
const { archiveAgent } = useArchiveAgent();
|
|
|
|
const actionClient = useSessionStore((state) =>
|
|
actionAgent?.serverId ? (state.sessions[actionAgent.serverId]?.client ?? null) : null,
|
|
);
|
|
|
|
const isActionSheetVisible = actionAgent !== null;
|
|
const isActionDaemonUnavailable = Boolean(actionAgent?.serverId && !actionClient);
|
|
|
|
const handleAgentPress = useCallback(
|
|
(agent: AggregatedAgent) => {
|
|
if (isActionSheetVisible) {
|
|
return;
|
|
}
|
|
|
|
const serverId = agent.serverId;
|
|
const agentId = agent.id;
|
|
|
|
onAgentSelect?.();
|
|
navigateToAgent({
|
|
serverId,
|
|
agentId,
|
|
workspaceId: agent.workspaceId,
|
|
pin: true,
|
|
});
|
|
},
|
|
[isActionSheetVisible, onAgentSelect],
|
|
);
|
|
|
|
const handleAgentLongPress = useCallback(
|
|
(agent: AggregatedAgent) => {
|
|
const isRunning = agent.status === "running";
|
|
if (isRunning) {
|
|
setActionAgent(agent);
|
|
return;
|
|
}
|
|
|
|
const client = useSessionStore.getState().sessions[agent.serverId]?.client ?? null;
|
|
if (!client) {
|
|
setActionAgent(agent);
|
|
return;
|
|
}
|
|
void archiveAgent({ serverId: agent.serverId, agentId: agent.id }).catch(() => {});
|
|
},
|
|
[archiveAgent],
|
|
);
|
|
|
|
const handleCloseActionSheet = useCallback(() => {
|
|
setActionAgent(null);
|
|
}, []);
|
|
|
|
const handleArchiveAgent = useCallback(() => {
|
|
if (!actionAgent || !actionClient) {
|
|
return;
|
|
}
|
|
// Timeout errors are swallowed — the daemon will still process the archive
|
|
void archiveAgent({ serverId: actionAgent.serverId, agentId: actionAgent.id }).catch(() => {});
|
|
setActionAgent(null);
|
|
}, [actionAgent, actionClient, archiveAgent]);
|
|
|
|
const flatItems = useMemo((): FlatListItem[] => {
|
|
const buckets = new Map<DateSectionKey, AggregatedAgent[]>();
|
|
for (const agent of agents) {
|
|
const section = deriveDateSectionKey(agent.lastActivityAt);
|
|
const existing = buckets.get(section) ?? [];
|
|
existing.push(agent);
|
|
buckets.set(section, existing);
|
|
}
|
|
|
|
const result: FlatListItem[] = [];
|
|
for (const section of DATE_SECTION_ORDER) {
|
|
const data = buckets.get(section);
|
|
if (!data || data.length === 0) {
|
|
continue;
|
|
}
|
|
result.push({ type: "header", key: `header:${section}`, section });
|
|
for (const agent of data) {
|
|
result.push({ type: "agent", key: `${agent.serverId}:${agent.id}`, agent });
|
|
}
|
|
}
|
|
return result;
|
|
}, [agents]);
|
|
|
|
const renderItem: ListRenderItem<FlatListItem> = useCallback(
|
|
({ item }) => {
|
|
if (item.type === "header") {
|
|
return (
|
|
<View style={styles.sectionHeading}>
|
|
<Text style={styles.sectionTitle}>{formatDateSectionLabel(t, item.section)}</Text>
|
|
</View>
|
|
);
|
|
}
|
|
return (
|
|
<SessionRow
|
|
agent={item.agent}
|
|
search={search}
|
|
isMobile={isMobile}
|
|
selectedAgentId={selectedAgentId}
|
|
showAttentionIndicator={showAttentionIndicator}
|
|
showHostColumn={showHostColumn}
|
|
onPress={handleAgentPress}
|
|
onLongPress={handleAgentLongPress}
|
|
/>
|
|
);
|
|
},
|
|
[
|
|
handleAgentLongPress,
|
|
handleAgentPress,
|
|
isMobile,
|
|
search,
|
|
selectedAgentId,
|
|
showAttentionIndicator,
|
|
showHostColumn,
|
|
t,
|
|
],
|
|
);
|
|
|
|
const keyExtractor = useCallback((item: FlatListItem) => item.key, []);
|
|
|
|
const refreshColors = useMemo(
|
|
() => [theme.colors.foregroundMuted],
|
|
[theme.colors.foregroundMuted],
|
|
);
|
|
const sheetContainerStyle = useMemo(
|
|
() => [styles.sheetContainer, { paddingBottom: Math.max(insets.bottom, theme.spacing[6]) }],
|
|
[insets.bottom, theme.spacing],
|
|
);
|
|
const sheetArchiveTextStyle = useMemo(
|
|
() => [styles.sheetArchiveText, isActionDaemonUnavailable && styles.sheetArchiveTextDisabled],
|
|
[isActionDaemonUnavailable],
|
|
);
|
|
|
|
const refreshControl = useMemo(
|
|
() =>
|
|
onRefresh ? (
|
|
<RefreshControl
|
|
refreshing={isRefreshing}
|
|
onRefresh={onRefresh}
|
|
tintColor={theme.colors.foregroundMuted}
|
|
colors={refreshColors}
|
|
/>
|
|
) : undefined,
|
|
[onRefresh, isRefreshing, theme.colors.foregroundMuted, refreshColors],
|
|
);
|
|
|
|
return (
|
|
<>
|
|
<FlatList
|
|
data={flatItems}
|
|
style={styles.list}
|
|
contentContainerStyle={styles.listContent}
|
|
keyExtractor={keyExtractor}
|
|
renderItem={renderItem}
|
|
showsVerticalScrollIndicator={false}
|
|
keyboardShouldPersistTaps="handled"
|
|
ListFooterComponent={listFooterComponent}
|
|
refreshControl={refreshControl}
|
|
/>
|
|
|
|
<Modal
|
|
visible={isActionSheetVisible}
|
|
animationType="fade"
|
|
transparent
|
|
onRequestClose={handleCloseActionSheet}
|
|
>
|
|
<View style={styles.sheetOverlay}>
|
|
<Pressable style={styles.sheetBackdrop} onPress={handleCloseActionSheet} />
|
|
<View style={sheetContainerStyle}>
|
|
<View style={styles.sheetHandle} />
|
|
<Text style={styles.sheetTitle}>
|
|
{isActionDaemonUnavailable
|
|
? t("agentList.archiveSheet.hostOffline")
|
|
: t("agentList.archiveSheet.runningAgent")}
|
|
</Text>
|
|
<View style={styles.sheetButtonRow}>
|
|
<Pressable
|
|
style={[styles.sheetButton, styles.sheetCancelButton]}
|
|
onPress={handleCloseActionSheet}
|
|
testID="agent-action-cancel"
|
|
>
|
|
<Text style={styles.sheetCancelText}>{t("common.actions.cancel")}</Text>
|
|
</Pressable>
|
|
<Pressable
|
|
disabled={isActionDaemonUnavailable}
|
|
style={[styles.sheetButton, styles.sheetArchiveButton]}
|
|
onPress={handleArchiveAgent}
|
|
testID="agent-action-archive"
|
|
>
|
|
<Text style={sheetArchiveTextStyle}>{t("agentList.archiveSheet.archive")}</Text>
|
|
</Pressable>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</Modal>
|
|
</>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create((theme) => ({
|
|
list: {
|
|
flex: 1,
|
|
minHeight: 0,
|
|
},
|
|
listContent: {
|
|
paddingHorizontal: {
|
|
xs: theme.spacing[3],
|
|
md: theme.spacing[6],
|
|
},
|
|
paddingTop: theme.spacing[4],
|
|
paddingBottom: theme.spacing[6],
|
|
gap: theme.spacing[1],
|
|
},
|
|
sectionHeading: {
|
|
marginTop: theme.spacing[2],
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: theme.spacing[3],
|
|
paddingHorizontal: theme.spacing[3],
|
|
marginBottom: theme.spacing[2],
|
|
},
|
|
sectionTitle: {
|
|
fontSize: theme.fontSize.base,
|
|
fontWeight: theme.fontWeight.medium,
|
|
color: theme.colors.foregroundMuted,
|
|
},
|
|
row: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
paddingVertical: theme.spacing[2],
|
|
paddingHorizontal: theme.spacing[3],
|
|
borderRadius: {
|
|
xs: theme.borderRadius.lg,
|
|
md: 0,
|
|
},
|
|
marginBottom: {
|
|
xs: theme.spacing[1],
|
|
md: 0,
|
|
},
|
|
},
|
|
rowContent: {
|
|
flex: 1,
|
|
minWidth: 0,
|
|
overflow: "hidden",
|
|
},
|
|
rowTitleRow: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
flexWrap: "nowrap",
|
|
gap: theme.spacing[2],
|
|
overflow: "hidden",
|
|
},
|
|
agentTitleRow: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: theme.spacing[2],
|
|
flexShrink: 1,
|
|
minWidth: 0,
|
|
},
|
|
providerIconWrap: {
|
|
flexShrink: 0,
|
|
},
|
|
workspaceTitleText: {
|
|
flexShrink: { xs: 1, md: 0 },
|
|
maxWidth: { xs: "100%", md: 320 },
|
|
fontSize: theme.fontSize.base,
|
|
color: theme.colors.foreground,
|
|
},
|
|
rowMetaRow: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
flexWrap: "wrap",
|
|
gap: theme.spacing[1],
|
|
marginTop: 2,
|
|
},
|
|
rowTrailing: {
|
|
marginLeft: theme.spacing[2],
|
|
},
|
|
rowSelected: {
|
|
backgroundColor: theme.colors.surface2,
|
|
},
|
|
rowHovered: {
|
|
backgroundColor: theme.colors.surface1,
|
|
},
|
|
rowPressed: {
|
|
backgroundColor: theme.colors.surface2,
|
|
},
|
|
sessionTitle: {
|
|
flexShrink: 1,
|
|
minWidth: 0,
|
|
fontSize: theme.fontSize.base,
|
|
fontWeight: "400",
|
|
color: theme.colors.foregroundMuted,
|
|
},
|
|
sessionMetaText: {
|
|
maxWidth: "100%",
|
|
fontSize: theme.fontSize.base,
|
|
color: theme.colors.foregroundMuted,
|
|
},
|
|
sessionMetaSeparator: {
|
|
fontSize: theme.fontSize.base,
|
|
color: theme.colors.foregroundMuted,
|
|
opacity: 0.7,
|
|
},
|
|
rowColumns: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
flexShrink: 0,
|
|
gap: theme.spacing[3],
|
|
},
|
|
columnMeta: {
|
|
fontSize: theme.fontSize.base,
|
|
color: theme.colors.foregroundMuted,
|
|
flexShrink: 0,
|
|
width: 132,
|
|
},
|
|
columnMetaFixed: {
|
|
fontSize: theme.fontSize.base,
|
|
color: theme.colors.foregroundMuted,
|
|
flexShrink: 0,
|
|
width: 72,
|
|
textAlign: "right" as const,
|
|
},
|
|
columnMetaHost: {
|
|
fontSize: theme.fontSize.base,
|
|
color: theme.colors.foregroundMuted,
|
|
flexShrink: 0,
|
|
width: 120,
|
|
marginLeft: theme.spacing[4],
|
|
textAlign: "right" as const,
|
|
},
|
|
sheetOverlay: {
|
|
flex: 1,
|
|
justifyContent: "flex-end",
|
|
},
|
|
sheetBackdrop: {
|
|
position: "absolute",
|
|
top: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
left: 0,
|
|
backgroundColor: "rgba(0,0,0,0.35)",
|
|
},
|
|
sheetContainer: {
|
|
backgroundColor: theme.colors.surface2,
|
|
borderTopLeftRadius: theme.borderRadius["2xl"],
|
|
borderTopRightRadius: theme.borderRadius["2xl"],
|
|
paddingHorizontal: theme.spacing[6],
|
|
paddingTop: theme.spacing[4],
|
|
gap: theme.spacing[4],
|
|
},
|
|
sheetHandle: {
|
|
alignSelf: "center",
|
|
width: 40,
|
|
height: 4,
|
|
borderRadius: theme.borderRadius.full,
|
|
backgroundColor: theme.colors.foregroundMuted,
|
|
opacity: 0.3,
|
|
},
|
|
sheetTitle: {
|
|
fontSize: theme.fontSize.base,
|
|
fontWeight: theme.fontWeight.semibold,
|
|
color: theme.colors.foreground,
|
|
textAlign: "center",
|
|
},
|
|
sheetButtonRow: {
|
|
flexDirection: "row",
|
|
gap: theme.spacing[3],
|
|
},
|
|
sheetButton: {
|
|
flex: 1,
|
|
borderRadius: theme.borderRadius.lg,
|
|
paddingVertical: theme.spacing[4],
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
},
|
|
sheetArchiveButton: {
|
|
backgroundColor: theme.colors.primary,
|
|
},
|
|
sheetArchiveText: {
|
|
color: theme.colors.primaryForeground,
|
|
fontWeight: theme.fontWeight.semibold,
|
|
fontSize: theme.fontSize.base,
|
|
},
|
|
sheetArchiveTextDisabled: {
|
|
opacity: 0.5,
|
|
},
|
|
sheetCancelButton: {
|
|
backgroundColor: theme.colors.surface1,
|
|
},
|
|
sheetCancelText: {
|
|
color: theme.colors.foreground,
|
|
fontWeight: theme.fontWeight.semibold,
|
|
fontSize: theme.fontSize.base,
|
|
},
|
|
}));
|