Add toast provider, agent details sheet, and remote-based grouping
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user