* fix(app): restore archived agents from History History navigation lost the explicit archived-agent intent during tab reconciliation, and workspace recovery stopped after restoring the workspace. Preserve the selected tab and recover its provider session as one action while serializing provider resume with timeline hydration. * fix(app): preserve archived agent recovery invariants * fix(server): preserve timeline hydration call shape * fix(app): retain agent tabs through lookup * fix(server): upgrade in-flight timeline broadcast
793 lines
22 KiB
TypeScript
793 lines
22 KiB
TypeScript
import {
|
|
View,
|
|
Text,
|
|
Pressable,
|
|
Modal,
|
|
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 { formatTimeAgo } from "@/utils/time";
|
|
import { type AggregatedAgent } from "@/hooks/use-aggregated-agents";
|
|
import { useSessionStore } from "@/stores/session-store";
|
|
import { Archive, ChevronRight } from "lucide-react-native";
|
|
import { getProviderIcon } from "@/components/provider-icons";
|
|
import { navigateToAgent } from "@/utils/navigate-to-agent";
|
|
import { useArchiveAgent } from "@/hooks/use-archive-agent";
|
|
|
|
interface AgentListProps {
|
|
agents: AggregatedAgent[];
|
|
showCheckoutInfo?: boolean;
|
|
isRefreshing?: boolean;
|
|
onRefresh?: () => void;
|
|
selectedAgentId?: string;
|
|
onAgentSelect?: () => void;
|
|
listFooterComponent?: ReactElement | null;
|
|
showAttentionIndicator?: boolean;
|
|
showHostColumn?: boolean;
|
|
}
|
|
|
|
type DateSectionKey = "today" | "yesterday" | "thisWeek" | "thisMonth" | "older";
|
|
|
|
const DATE_SECTION_ORDER = [
|
|
"today",
|
|
"yesterday",
|
|
"thisWeek",
|
|
"thisMonth",
|
|
"older",
|
|
] as const satisfies readonly DateSectionKey[];
|
|
|
|
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";
|
|
}) {
|
|
const badgeStyle = useMemo(
|
|
() => [
|
|
styles.badge,
|
|
tone === "warning" && styles.badgeWarning,
|
|
tone === "danger" && styles.badgeDanger,
|
|
],
|
|
[tone],
|
|
);
|
|
const badgeTextStyle = useMemo(
|
|
() => [
|
|
styles.badgeText,
|
|
tone === "warning" && styles.badgeTextWarning,
|
|
tone === "danger" && styles.badgeTextDanger,
|
|
],
|
|
[tone],
|
|
);
|
|
return (
|
|
<View style={badgeStyle}>
|
|
{icon}
|
|
<Text style={badgeTextStyle}>{label}</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function WorkspaceTitlePrefix({
|
|
visible,
|
|
workspaceName,
|
|
testID,
|
|
iconSize,
|
|
color,
|
|
}: {
|
|
visible: boolean;
|
|
workspaceName: string;
|
|
testID: string;
|
|
iconSize: number;
|
|
color: string;
|
|
}) {
|
|
if (!visible) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<Text style={styles.workspaceTitleText} numberOfLines={1} testID={testID}>
|
|
{workspaceName}
|
|
</Text>
|
|
<ChevronRight size={iconSize} color={color} />
|
|
</>
|
|
);
|
|
}
|
|
|
|
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,
|
|
isMobile,
|
|
selectedAgentId,
|
|
showAttentionIndicator,
|
|
showHostColumn,
|
|
onPress,
|
|
onLongPress,
|
|
}: {
|
|
agent: AggregatedAgent;
|
|
isMobile: boolean;
|
|
selectedAgentId?: string;
|
|
showAttentionIndicator: boolean;
|
|
showHostColumn: boolean;
|
|
onPress: (agent: AggregatedAgent) => void;
|
|
onLongPress: (agent: AggregatedAgent) => void;
|
|
}) {
|
|
const { theme } = useUnistyles();
|
|
const { t } = useTranslation();
|
|
const timeAgo = formatTimeAgo(agent.lastActivityAt);
|
|
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 = getProviderIcon(agent.provider);
|
|
const pendingPermissionCount = agent.pendingPermissionCount ?? 0;
|
|
|
|
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 sessionTitleStyle = useMemo(
|
|
() => [styles.sessionTitle, isSelected && styles.sessionTitleHighlighted],
|
|
[isSelected],
|
|
);
|
|
|
|
const archivedIcon = useMemo(
|
|
() => <Archive size={theme.fontSize.xs} color={theme.colors.foregroundMuted} />,
|
|
[theme.fontSize.xs, theme.colors.foregroundMuted],
|
|
);
|
|
const showDesktopAttention =
|
|
!isMobile && showAttentionIndicator && Boolean(agent.requiresAttention);
|
|
|
|
return (
|
|
<Pressable
|
|
style={pressableStyle}
|
|
onPress={handlePress}
|
|
onLongPress={handleLongPress}
|
|
testID={`agent-row-${agent.serverId}-${agent.id}`}
|
|
>
|
|
<View style={styles.rowContent}>
|
|
<View style={styles.rowTitleRow}>
|
|
<WorkspaceTitlePrefix
|
|
visible={!isMobile && Boolean(workspaceName)}
|
|
workspaceName={workspaceName}
|
|
testID={`agent-row-workspace-${agent.serverId}-${agent.id}`}
|
|
iconSize={theme.iconSize.xs}
|
|
color={theme.colors.foregroundMuted}
|
|
/>
|
|
<View style={styles.providerIconWrap}>
|
|
<ProviderIcon size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
|
</View>
|
|
<Text style={sessionTitleStyle} numberOfLines={1}>
|
|
{agent.title || t("agentList.fallbackTitle")}
|
|
</Text>
|
|
<SessionRowBadges
|
|
agent={agent}
|
|
archivedIcon={archivedIcon}
|
|
pendingPermissionCount={pendingPermissionCount}
|
|
showDesktopAttention={showDesktopAttention}
|
|
/>
|
|
</View>
|
|
{isMobile ? (
|
|
<View style={styles.rowMetaRow}>
|
|
<Text
|
|
style={styles.sessionMetaText}
|
|
numberOfLines={1}
|
|
testID={`agent-row-project-${agent.serverId}-${agent.id}`}
|
|
>
|
|
{projectName}
|
|
</Text>
|
|
<Text style={styles.sessionMetaSeparator}>·</Text>
|
|
<Text
|
|
style={styles.sessionMetaText}
|
|
numberOfLines={1}
|
|
testID={`agent-row-branch-${agent.serverId}-${agent.id}`}
|
|
>
|
|
{branch}
|
|
</Text>
|
|
<Text style={styles.sessionMetaSeparator}>·</Text>
|
|
<Text
|
|
style={styles.sessionMetaText}
|
|
numberOfLines={1}
|
|
testID={`agent-row-workspace-${agent.serverId}-${agent.id}`}
|
|
>
|
|
{workspaceName}
|
|
</Text>
|
|
<Text style={styles.sessionMetaSeparator}>·</Text>
|
|
<Text style={styles.sessionMetaText}>{timeAgo}</Text>
|
|
{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}>
|
|
<Text
|
|
style={styles.columnMeta}
|
|
numberOfLines={1}
|
|
testID={`agent-row-project-${agent.serverId}-${agent.id}`}
|
|
>
|
|
{projectName}
|
|
</Text>
|
|
{showHostColumn ? (
|
|
<Text style={styles.columnMetaHost} numberOfLines={1}>
|
|
{agent.serverLabel}
|
|
</Text>
|
|
) : null}
|
|
<Text
|
|
style={styles.columnMeta}
|
|
numberOfLines={1}
|
|
testID={`agent-row-branch-${agent.serverId}-${agent.id}`}
|
|
>
|
|
{branch}
|
|
</Text>
|
|
<Text style={styles.columnMetaFixed} numberOfLines={1}>
|
|
{timeAgo}
|
|
</Text>
|
|
</View>
|
|
) : null}
|
|
<SessionRowTrailingAttention
|
|
isMobile={isMobile}
|
|
showAttentionIndicator={showAttentionIndicator}
|
|
requiresAttention={agent.requiresAttention}
|
|
/>
|
|
</Pressable>
|
|
);
|
|
}
|
|
|
|
export function AgentList({
|
|
agents,
|
|
isRefreshing = false,
|
|
onRefresh,
|
|
selectedAgentId,
|
|
onAgentSelect,
|
|
listFooterComponent,
|
|
showAttentionIndicator = true,
|
|
showHostColumn = false,
|
|
}: 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}
|
|
isMobile={isMobile}
|
|
selectedAgentId={selectedAgentId}
|
|
showAttentionIndicator={showAttentionIndicator}
|
|
showHostColumn={showHostColumn}
|
|
onPress={handleAgentPress}
|
|
onLongPress={handleAgentLongPress}
|
|
/>
|
|
);
|
|
},
|
|
[
|
|
handleAgentLongPress,
|
|
handleAgentPress,
|
|
isMobile,
|
|
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.sm,
|
|
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",
|
|
},
|
|
providerIconWrap: {
|
|
width: theme.iconSize.md,
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
},
|
|
workspaceTitleText: {
|
|
flexShrink: 0,
|
|
maxWidth: 220,
|
|
fontSize: theme.fontSize.sm,
|
|
color: theme.colors.foregroundMuted,
|
|
},
|
|
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.sm,
|
|
fontWeight: "400",
|
|
color: theme.colors.foreground,
|
|
opacity: 0.86,
|
|
},
|
|
sessionTitleHighlighted: {
|
|
opacity: 1,
|
|
},
|
|
sessionMetaText: {
|
|
maxWidth: "100%",
|
|
fontSize: theme.fontSize.sm,
|
|
color: theme.colors.foregroundMuted,
|
|
},
|
|
sessionMetaSeparator: {
|
|
fontSize: theme.fontSize.sm,
|
|
color: theme.colors.foregroundMuted,
|
|
opacity: 0.7,
|
|
},
|
|
rowColumns: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
flexShrink: 0,
|
|
gap: theme.spacing[3],
|
|
},
|
|
columnMeta: {
|
|
fontSize: theme.fontSize.sm,
|
|
color: theme.colors.foregroundMuted,
|
|
flexShrink: 0,
|
|
width: 132,
|
|
},
|
|
columnMetaFixed: {
|
|
fontSize: theme.fontSize.sm,
|
|
color: theme.colors.foregroundMuted,
|
|
flexShrink: 0,
|
|
width: 72,
|
|
textAlign: "right" as const,
|
|
},
|
|
columnMetaHost: {
|
|
fontSize: theme.fontSize.sm,
|
|
color: theme.colors.foregroundMuted,
|
|
flexShrink: 0,
|
|
width: 120,
|
|
marginLeft: theme.spacing[4],
|
|
textAlign: "right" as const,
|
|
},
|
|
badge: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
flexShrink: 0,
|
|
gap: theme.spacing[1],
|
|
paddingHorizontal: theme.spacing[2],
|
|
paddingVertical: theme.spacing[1],
|
|
borderRadius: theme.borderRadius.full,
|
|
backgroundColor: theme.colors.surface2,
|
|
},
|
|
badgeWarning: {
|
|
backgroundColor: "rgba(245, 158, 11, 0.12)",
|
|
},
|
|
badgeDanger: {
|
|
backgroundColor: "rgba(239, 68, 68, 0.14)",
|
|
},
|
|
badgeText: {
|
|
fontSize: theme.fontSize.xs,
|
|
fontWeight: theme.fontWeight.medium,
|
|
color: theme.colors.foregroundMuted,
|
|
},
|
|
badgeTextWarning: {
|
|
color: theme.colors.palette.amber[500],
|
|
},
|
|
badgeTextDanger: {
|
|
color: theme.colors.palette.red[300],
|
|
},
|
|
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.lg,
|
|
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,
|
|
},
|
|
}));
|