History was a date-grouped list you scrolled. Finding a session from last week meant eyeballing every row. This adds a search field to the existing filter rail, next to the host picker. Search runs on the daemon, not the client. The daemon already holds every persisted agent in memory, so it ranks the whole history and the client never has to warn that it only looked at the pages it had loaded. A searched response is one ranked page with no cursor. The global top K is always contained in the union of each host's own top K, so merging per-host pages and slicing back to the limit is the exact global answer — no k-way merge, and no offset into a ranking that is recomputed per request, which would duplicate and skip rows as history changes underneath it. Past result 200 of a relevance order the answer is a narrower query, so the footer says that instead of offering a "Load more" that cannot deliver. Matching is the app's existing picker matcher, moved to the protocol package so app and daemon share one implementation instead of drifting into two "why didn't this show up?" rule sets. Typo tolerance is opt-in, so the comboboxes keep exact narrowing; the budget is per token, and four-character tokens get transpositions only, because at that length a free substitution turns "main" into "mail", "maid" and "rain". Ranked rows carry the spans they matched on, so results can mark them. The subsequence and typo tiers match characters the eye would never find unaided, and an unexplained row reads as a bug. The ranker computes the spans anyway; sending them keeps the client from deriving a second opinion that could disagree with the ranking it is explaining. Hosts that fail or are offline are now named above the list. Without that, "No sessions match" is a claim the app has no basis for. Gated on server_info.features.agentHistorySearch, checked once against the connected hosts. Reachability is reported by the banner instead, so one offline host in the registry no longer hides the field.
837 lines
23 KiB
TypeScript
837 lines
23 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";
|
|
import { HighlightedText } from "@/components/ui/highlighted-text";
|
|
import type { AgentSearchMatch } from "@getpaseo/protocol/messages";
|
|
import type { MatchRange } from "@getpaseo/protocol/search/text-match";
|
|
|
|
interface AgentListProps {
|
|
agents: AggregatedAgent[];
|
|
showCheckoutInfo?: boolean;
|
|
isRefreshing?: boolean;
|
|
onRefresh?: () => void;
|
|
selectedAgentId?: string;
|
|
onAgentSelect?: () => void;
|
|
listFooterComponent?: ReactElement | null;
|
|
showAttentionIndicator?: boolean;
|
|
showHostColumn?: boolean;
|
|
/**
|
|
* Where a search matched each row, keyed by `serverId:agentId`. Rows mark the
|
|
* spans so the list can explain why a result is in it — the subsequence and
|
|
* typo tiers match characters the eye would not find on its own.
|
|
*/
|
|
searchMatchesByAgentKey?: Record<string, AgentSearchMatch[]>;
|
|
/**
|
|
* Renders one flat list in the given order instead of grouping by day. Day
|
|
* headings claim the list is chronological, which is a lie once the caller
|
|
* has ordered it by something else — relevance, for instance.
|
|
*/
|
|
flat?: 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,
|
|
ranges,
|
|
testID,
|
|
iconSize,
|
|
color,
|
|
}: {
|
|
visible: boolean;
|
|
workspaceName: string;
|
|
ranges?: readonly MatchRange[];
|
|
testID: string;
|
|
iconSize: number;
|
|
color: string;
|
|
}) {
|
|
if (!visible) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<HighlightedText
|
|
text={workspaceName}
|
|
ranges={ranges}
|
|
style={styles.workspaceTitleText}
|
|
numberOfLines={1}
|
|
testID={testID}
|
|
/>
|
|
<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,
|
|
searchMatches,
|
|
isMobile,
|
|
selectedAgentId,
|
|
showAttentionIndicator,
|
|
showHostColumn,
|
|
onPress,
|
|
onLongPress,
|
|
}: {
|
|
agent: AggregatedAgent;
|
|
searchMatches?: readonly AgentSearchMatch[];
|
|
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 rangesFor = useCallback(
|
|
(field: AgentSearchMatch["field"]) =>
|
|
searchMatches?.find((match) => match.field === field)?.ranges,
|
|
[searchMatches],
|
|
);
|
|
|
|
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}
|
|
ranges={rangesFor("workspace")}
|
|
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>
|
|
<HighlightedText
|
|
text={agent.title || t("agentList.fallbackTitle")}
|
|
ranges={agent.title ? rangesFor("title") : undefined}
|
|
style={sessionTitleStyle}
|
|
numberOfLines={1}
|
|
/>
|
|
<SessionRowBadges
|
|
agent={agent}
|
|
archivedIcon={archivedIcon}
|
|
pendingPermissionCount={pendingPermissionCount}
|
|
showDesktopAttention={showDesktopAttention}
|
|
/>
|
|
</View>
|
|
{isMobile ? (
|
|
<View style={styles.rowMetaRow}>
|
|
<HighlightedText
|
|
text={projectName}
|
|
ranges={rangesFor("project")}
|
|
style={styles.sessionMetaText}
|
|
numberOfLines={1}
|
|
testID={`agent-row-project-${agent.serverId}-${agent.id}`}
|
|
/>
|
|
<Text style={styles.sessionMetaSeparator}>·</Text>
|
|
<HighlightedText
|
|
text={branch}
|
|
ranges={rangesFor("branch")}
|
|
style={styles.sessionMetaText}
|
|
numberOfLines={1}
|
|
testID={`agent-row-branch-${agent.serverId}-${agent.id}`}
|
|
/>
|
|
<Text style={styles.sessionMetaSeparator}>·</Text>
|
|
<HighlightedText
|
|
text={workspaceName}
|
|
ranges={rangesFor("workspace")}
|
|
style={styles.sessionMetaText}
|
|
numberOfLines={1}
|
|
testID={`agent-row-workspace-${agent.serverId}-${agent.id}`}
|
|
/>
|
|
<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}>
|
|
<HighlightedText
|
|
text={projectName}
|
|
ranges={rangesFor("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={rangesFor("branch")}
|
|
style={styles.columnMeta}
|
|
numberOfLines={1}
|
|
testID={`agent-row-branch-${agent.serverId}-${agent.id}`}
|
|
/>
|
|
<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,
|
|
searchMatchesByAgentKey,
|
|
flat = 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[] => {
|
|
if (flat) {
|
|
return agents.map((agent) => ({
|
|
type: "agent" as const,
|
|
key: `${agent.serverId}:${agent.id}`,
|
|
agent,
|
|
}));
|
|
}
|
|
|
|
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, flat]);
|
|
|
|
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}
|
|
searchMatches={searchMatchesByAgentKey?.[item.key]}
|
|
isMobile={isMobile}
|
|
selectedAgentId={selectedAgentId}
|
|
showAttentionIndicator={showAttentionIndicator}
|
|
showHostColumn={showHostColumn}
|
|
onPress={handleAgentPress}
|
|
onLongPress={handleAgentLongPress}
|
|
/>
|
|
);
|
|
},
|
|
[
|
|
handleAgentLongPress,
|
|
handleAgentPress,
|
|
isMobile,
|
|
searchMatchesByAgentKey,
|
|
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,
|
|
},
|
|
}));
|