Add toast provider, agent details sheet, and remote-based grouping

This commit is contained in:
Mohamed Boudra
2026-01-25 09:41:17 +07:00
parent ccd175b4b7
commit aa979f5230
8 changed files with 731 additions and 25 deletions
@@ -0,0 +1,187 @@
import { useCallback, useMemo } from "react";
import { Pressable, Text, View } from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { Copy, Info } from "lucide-react-native";
import * as Clipboard from "expo-clipboard";
import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet";
import { Fonts } from "@/constants/theme";
import { useToast } from "@/contexts/toast-context";
export type AgentDetailsSheetProps = {
visible: boolean;
onClose: () => void;
agentId: string;
persistenceSessionId: string | null;
};
export function AgentDetailsSheet({
visible,
onClose,
agentId,
persistenceSessionId,
}: AgentDetailsSheetProps) {
const toast = useToast();
const details = useMemo(
() => [
{
label: "Agent ID",
value: agentId,
testID: "agent-details-agent-id",
copyLabel: "agent id",
},
{
label: "Persistence session ID",
value: persistenceSessionId,
testID: "agent-details-persistence-session-id",
copyLabel: "session id",
},
],
[agentId, persistenceSessionId]
);
const handleCopy = useCallback(
async (value: string | null, label: string) => {
if (!value) return;
try {
await Clipboard.setStringAsync(value);
toast.copied(label);
} catch {
toast.error("Copy failed");
}
},
[toast]
);
return (
<AdaptiveModalSheet
title="Agent Details"
visible={visible}
onClose={onClose}
testID="agent-details-sheet"
snapPoints={["45%", "70%"]}
>
{details.map((row) => (
<DetailRow
key={row.label}
label={row.label}
value={row.value}
onCopy={() => void handleCopy(row.value, row.copyLabel)}
testID={row.testID}
disabled={!row.value}
/>
))}
</AdaptiveModalSheet>
);
}
function DetailRow({
label,
value,
onCopy,
disabled,
testID,
}: {
label: string;
value: string | null;
onCopy: () => void;
disabled?: boolean;
testID: string;
}) {
const { theme } = useUnistyles();
return (
<Pressable
testID={testID}
accessibilityRole="button"
accessibilityLabel={`${label} copy`}
accessibilityHint={disabled ? "Not available" : "Copies to clipboard"}
disabled={disabled}
onPress={onCopy}
style={({ pressed }) => [
styles.row,
disabled ? styles.rowDisabled : null,
pressed && !disabled ? styles.rowPressed : null,
]}
>
<View style={styles.rowTop}>
<View style={styles.labelRow}>
<Info size={16} color={theme.colors.foregroundMuted} />
<Text style={styles.label}>{label}</Text>
</View>
<View style={styles.copyPill}>
<Copy size={14} color={theme.colors.foregroundMuted} />
<Text style={styles.copyText}>Copy</Text>
</View>
</View>
<Text
testID={`${testID}-value`}
style={[styles.value, !value ? styles.valueEmpty : null]}
numberOfLines={2}
ellipsizeMode="middle"
>
{value ?? "Not available"}
</Text>
</Pressable>
);
}
const styles = StyleSheet.create((theme) => ({
row: {
borderRadius: theme.borderRadius.lg,
borderWidth: theme.borderWidth[1],
borderColor: theme.colors.surface2,
backgroundColor: theme.colors.surface1,
paddingHorizontal: theme.spacing[5],
paddingVertical: theme.spacing[4],
gap: theme.spacing[3],
},
rowPressed: {
backgroundColor: theme.colors.surface2,
},
rowDisabled: {
opacity: 0.65,
},
rowTop: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
gap: theme.spacing[3],
},
labelRow: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[2],
flex: 1,
},
label: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.semibold,
flex: 1,
},
copyPill: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[2],
paddingHorizontal: theme.spacing[3],
paddingVertical: theme.spacing[2],
borderRadius: theme.borderRadius.full,
backgroundColor: theme.colors.surface2,
},
copyText: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.medium,
},
value: {
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
fontFamily: Fonts.mono,
},
valueEmpty: {
color: theme.colors.foregroundMuted,
fontFamily: Fonts.sans,
},
}));
@@ -17,7 +17,7 @@ import {
} from "react";
import { router, usePathname } from "expo-router";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { useQueryClient } from "@tanstack/react-query";
import { useQueries, useQueryClient, type UseQueryOptions } from "@tanstack/react-query";
import { ChevronDown, ChevronRight } from "lucide-react-native";
import { formatTimeAgo } from "@/utils/time";
import { shortenPath } from "@/utils/shorten-path";
@@ -32,6 +32,7 @@ import { type AggregatedAgent } from "@/hooks/use-aggregated-agents";
import { useSessionStore } from "@/stores/session-store";
import {
CHECKOUT_STATUS_STALE_TIME,
type CheckoutStatusPayload,
checkoutStatusQueryKey,
useCheckoutStatusCacheOnly,
} from "@/hooks/use-checkout-status-query";
@@ -210,10 +211,43 @@ export function GroupedAgentList({
});
}, []);
// Subscribe to checkout status cache entries so project grouping can react
// to remote URL updates (e.g. git worktrees in different directories).
const checkoutCacheQueries = useQueries({
queries: agents.map(
(agent): UseQueryOptions<CheckoutStatusPayload> => ({
queryKey: checkoutStatusQueryKey(agent.serverId, agent.id),
enabled: false,
staleTime: CHECKOUT_STATUS_STALE_TIME,
queryFn: async (): Promise<CheckoutStatusPayload> => {
throw new Error("checkout status cache-only query should not run");
},
})
),
});
const remoteUrlByAgentKey = useMemo(() => {
const result = new Map<string, string | null>();
for (let i = 0; i < agents.length; i++) {
const agent = agents[i];
if (!agent) {
continue;
}
const checkout = checkoutCacheQueries[i]?.data ?? null;
const remoteUrl = checkout?.remoteUrl ?? null;
result.set(`${agent.serverId}:${agent.id}`, remoteUrl);
}
return result;
}, [agents, checkoutCacheQueries]);
// Group agents
const { activeGroups, inactiveGroups } = useMemo(
() => groupAgents(agents),
[agents]
() =>
groupAgents(agents, {
getRemoteUrl: (agent) =>
remoteUrlByAgentKey.get(`${agent.serverId}:${agent.id}`) ?? null,
}),
[agents, remoteUrlByAgentKey]
);
// Build sections for SectionList