Update files
This commit is contained in:
@@ -5,15 +5,23 @@ import {
|
|||||||
Modal,
|
Modal,
|
||||||
RefreshControl,
|
RefreshControl,
|
||||||
FlatList,
|
FlatList,
|
||||||
|
type ViewToken,
|
||||||
type ListRenderItem,
|
type ListRenderItem,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { useCallback, useState, type ReactElement } from "react";
|
import { useCallback, useMemo, useRef, useState, type ReactElement } from "react";
|
||||||
import { router, usePathname } from "expo-router";
|
import { router, usePathname } from "expo-router";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { formatTimeAgo } from "@/utils/time";
|
import { formatTimeAgo } from "@/utils/time";
|
||||||
import { shortenPath } from "@/utils/shorten-path";
|
import { shortenPath } from "@/utils/shorten-path";
|
||||||
import { type AggregatedAgent } from "@/hooks/use-aggregated-agents";
|
import { type AggregatedAgent } from "@/hooks/use-aggregated-agents";
|
||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
|
import {
|
||||||
|
CHECKOUT_STATUS_STALE_TIME,
|
||||||
|
checkoutStatusQueryKey,
|
||||||
|
type CheckoutStatusPayload,
|
||||||
|
useCheckoutStatusCacheOnly,
|
||||||
|
} from "@/hooks/use-checkout-status-query";
|
||||||
import {
|
import {
|
||||||
buildAgentNavigationKey,
|
buildAgentNavigationKey,
|
||||||
startNavigationTiming,
|
startNavigationTiming,
|
||||||
@@ -38,6 +46,7 @@ export function AgentList({
|
|||||||
}: AgentListProps) {
|
}: AgentListProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const [actionAgent, setActionAgent] = useState<AggregatedAgent | null>(null);
|
const [actionAgent, setActionAgent] = useState<AggregatedAgent | null>(null);
|
||||||
|
|
||||||
// Get the methods for the specific server
|
// Get the methods for the specific server
|
||||||
@@ -102,18 +111,98 @@ export function AgentList({
|
|||||||
setActionAgent(null);
|
setActionAgent(null);
|
||||||
}, [actionAgent, deleteAgent]);
|
}, [actionAgent, deleteAgent]);
|
||||||
|
|
||||||
const renderAgentItem = useCallback<ListRenderItem<AggregatedAgent>>(
|
const deriveBranchLabel = useCallback((checkout: CheckoutStatusPayload | null): string | null => {
|
||||||
({ item: agent }) => {
|
if (!checkout || !checkout.isGit) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const currentBranch: string | null = checkout.currentBranch ?? null;
|
||||||
|
const baseRef: string | null = checkout.baseRef ?? null;
|
||||||
|
if (!currentBranch) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (baseRef && currentBranch === baseRef) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return currentBranch;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const deriveProjectPath = useCallback(
|
||||||
|
(agent: AggregatedAgent, checkout: CheckoutStatusPayload | null): string => {
|
||||||
|
const basePath = checkout?.isGit ? (checkout.repoRoot ?? agent.cwd) : agent.cwd;
|
||||||
|
const worktreeMarker = ".paseo/worktrees/";
|
||||||
|
const idx = basePath.indexOf(worktreeMarker);
|
||||||
|
if (idx !== -1) {
|
||||||
|
const afterMarker = basePath.slice(idx + worktreeMarker.length);
|
||||||
|
const slashIdx = afterMarker.indexOf("/");
|
||||||
|
if (slashIdx !== -1) {
|
||||||
|
return afterMarker.slice(slashIdx + 1);
|
||||||
|
}
|
||||||
|
return afterMarker;
|
||||||
|
}
|
||||||
|
return basePath;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const viewabilityConfig = useMemo(
|
||||||
|
() => ({ itemVisiblePercentThreshold: 30 }),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onViewableItemsChanged = useRef(
|
||||||
|
({ viewableItems }: { viewableItems: Array<ViewToken> }) => {
|
||||||
|
for (const token of viewableItems) {
|
||||||
|
const agent = token.item as AggregatedAgent | undefined;
|
||||||
|
if (!agent) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = useSessionStore.getState().sessions[agent.serverId];
|
||||||
|
const client = session?.client ?? null;
|
||||||
|
const isConnected = session?.connection.isConnected ?? false;
|
||||||
|
if (!client || !isConnected) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryKey = checkoutStatusQueryKey(agent.serverId, agent.id);
|
||||||
|
const queryState = queryClient.getQueryState(queryKey);
|
||||||
|
const isFetching = queryState?.fetchStatus === "fetching";
|
||||||
|
const isFresh =
|
||||||
|
typeof queryState?.dataUpdatedAt === "number" &&
|
||||||
|
Date.now() - queryState.dataUpdatedAt < CHECKOUT_STATUS_STALE_TIME;
|
||||||
|
if (isFetching || isFresh) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
void queryClient.prefetchQuery({
|
||||||
|
queryKey,
|
||||||
|
queryFn: async () => await client.getCheckoutStatus(agent.id),
|
||||||
|
staleTime: CHECKOUT_STATUS_STALE_TIME,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const AgentListRow = useCallback(
|
||||||
|
({ agent }: { agent: AggregatedAgent }) => {
|
||||||
const timeAgo = formatTimeAgo(agent.lastActivityAt);
|
const timeAgo = formatTimeAgo(agent.lastActivityAt);
|
||||||
const isRunning = agent.status === "running";
|
|
||||||
const agentKey = `${agent.serverId}:${agent.id}`;
|
const agentKey = `${agent.serverId}:${agent.id}`;
|
||||||
const isSelected = selectedAgentId === agentKey;
|
const isSelected = selectedAgentId === agentKey;
|
||||||
|
const isRunning = agent.status === "running";
|
||||||
const statusColor = isRunning
|
const statusColor = isRunning
|
||||||
? theme.colors.palette.blue[500]
|
? theme.colors.palette.blue[500]
|
||||||
: agent.requiresAttention
|
: agent.requiresAttention
|
||||||
? theme.colors.success
|
? theme.colors.success
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
const checkoutQuery = useCheckoutStatusCacheOnly({
|
||||||
|
serverId: agent.serverId,
|
||||||
|
agentId: agent.id,
|
||||||
|
});
|
||||||
|
const checkout = checkoutQuery.data ?? null;
|
||||||
|
const projectPath = deriveProjectPath(agent, checkout);
|
||||||
|
const branchLabel = deriveBranchLabel(checkout);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
style={({ pressed, hovered }) => [
|
style={({ pressed, hovered }) => [
|
||||||
@@ -145,14 +234,28 @@ export function AgentList({
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Text style={styles.secondaryRow} numberOfLines={1}>
|
<Text style={styles.secondaryRow} numberOfLines={1}>
|
||||||
{shortenPath(agent.cwd)} · {timeAgo}
|
{shortenPath(projectPath)}
|
||||||
|
{branchLabel ? ` · ${branchLabel}` : ""} · {timeAgo}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[handleAgentLongPress, handleAgentPress, selectedAgentId]
|
[
|
||||||
|
deriveBranchLabel,
|
||||||
|
deriveProjectPath,
|
||||||
|
handleAgentLongPress,
|
||||||
|
handleAgentPress,
|
||||||
|
selectedAgentId,
|
||||||
|
theme.colors.palette.blue,
|
||||||
|
theme.colors.success,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderAgentItem = useCallback<ListRenderItem<AggregatedAgent>>(
|
||||||
|
({ item: agent }) => <AgentListRow agent={agent} />,
|
||||||
|
[AgentListRow]
|
||||||
);
|
);
|
||||||
|
|
||||||
const keyExtractor = useCallback(
|
const keyExtractor = useCallback(
|
||||||
@@ -177,6 +280,8 @@ export function AgentList({
|
|||||||
updateCellsBatchingPeriod={16}
|
updateCellsBatchingPeriod={16}
|
||||||
removeClippedSubviews={true}
|
removeClippedSubviews={true}
|
||||||
ListFooterComponent={listFooterComponent}
|
ListFooterComponent={listFooterComponent}
|
||||||
|
onViewableItemsChanged={onViewableItemsChanged.current}
|
||||||
|
viewabilityConfig={viewabilityConfig}
|
||||||
refreshControl={
|
refreshControl={
|
||||||
onRefresh ? (
|
onRefresh ? (
|
||||||
<RefreshControl
|
<RefreshControl
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import {
|
|||||||
nextCheckoutStatusRefetchDecision,
|
nextCheckoutStatusRefetchDecision,
|
||||||
} from "./checkout-status-revalidation";
|
} from "./checkout-status-revalidation";
|
||||||
|
|
||||||
const CHECKOUT_STATUS_STALE_TIME = 15_000;
|
export const CHECKOUT_STATUS_STALE_TIME = 15_000;
|
||||||
|
|
||||||
function checkoutStatusQueryKey(serverId: string, agentId: string) {
|
export function checkoutStatusQueryKey(serverId: string, agentId: string) {
|
||||||
return ["checkoutStatus", serverId, agentId] as const;
|
return ["checkoutStatus", serverId, agentId] as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,6 +22,13 @@ interface UseCheckoutStatusQueryOptions {
|
|||||||
|
|
||||||
export type CheckoutStatusPayload = CheckoutStatusResponse["payload"];
|
export type CheckoutStatusPayload = CheckoutStatusResponse["payload"];
|
||||||
|
|
||||||
|
function fetchCheckoutStatus(
|
||||||
|
client: { getCheckoutStatus: (agentId: string) => Promise<CheckoutStatusPayload> },
|
||||||
|
agentId: string
|
||||||
|
): Promise<CheckoutStatusPayload> {
|
||||||
|
return client.getCheckoutStatus(agentId);
|
||||||
|
}
|
||||||
|
|
||||||
export function useCheckoutStatusQuery({ serverId, agentId }: UseCheckoutStatusQueryOptions) {
|
export function useCheckoutStatusQuery({ serverId, agentId }: UseCheckoutStatusQueryOptions) {
|
||||||
const client = useSessionStore(
|
const client = useSessionStore(
|
||||||
(state) => state.sessions[serverId]?.client ?? null
|
(state) => state.sessions[serverId]?.client ?? null
|
||||||
@@ -42,7 +49,7 @@ export function useCheckoutStatusQuery({ serverId, agentId }: UseCheckoutStatusQ
|
|||||||
if (!client) {
|
if (!client) {
|
||||||
throw new Error("Daemon client not available");
|
throw new Error("Daemon client not available");
|
||||||
}
|
}
|
||||||
return await client.getCheckoutStatus(agentId);
|
return await fetchCheckoutStatus(client, agentId);
|
||||||
},
|
},
|
||||||
enabled: !!client && isConnected && !!agentId,
|
enabled: !!client && isConnected && !!agentId,
|
||||||
staleTime: CHECKOUT_STATUS_STALE_TIME,
|
staleTime: CHECKOUT_STATUS_STALE_TIME,
|
||||||
@@ -73,3 +80,26 @@ export function useCheckoutStatusQuery({ serverId, agentId }: UseCheckoutStatusQ
|
|||||||
refresh: query.refetch,
|
refresh: query.refetch,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to checkout status updates from the React Query cache without
|
||||||
|
* initiating a fetch. Useful for list rows where a parent component prefetches
|
||||||
|
* only the visible agents.
|
||||||
|
*/
|
||||||
|
export function useCheckoutStatusCacheOnly({ serverId, agentId }: UseCheckoutStatusQueryOptions) {
|
||||||
|
const client = useSessionStore(
|
||||||
|
(state) => state.sessions[serverId]?.client ?? null
|
||||||
|
);
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: checkoutStatusQueryKey(serverId, agentId),
|
||||||
|
queryFn: async () => {
|
||||||
|
if (!client) {
|
||||||
|
throw new Error("Daemon client not available");
|
||||||
|
}
|
||||||
|
return await fetchCheckoutStatus(client, agentId);
|
||||||
|
},
|
||||||
|
enabled: false,
|
||||||
|
staleTime: CHECKOUT_STATUS_STALE_TIME,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user