197 lines
5.9 KiB
TypeScript
197 lines
5.9 KiB
TypeScript
import { useCallback, useEffect, useMemo } from "react";
|
|
import type { ReactNode } from "react";
|
|
import { ActivityIndicator, Pressable, Text, View } from "react-native";
|
|
import { useRouter } from "expo-router";
|
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
|
import { BackHeader } from "@/components/headers/back-header";
|
|
import { useSessionDirectory } from "@/hooks/use-session-directory";
|
|
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
|
import type { Agent } from "@/contexts/session-context";
|
|
|
|
type AgentMatch = {
|
|
serverId: string;
|
|
serverLabel: string;
|
|
agent: Agent;
|
|
};
|
|
|
|
export function LegacyAgentIdScreen({ agentId }: { agentId: string }) {
|
|
const router = useRouter();
|
|
const { theme } = useUnistyles();
|
|
const { connectionStates } = useDaemonConnections();
|
|
const sessionDirectory = useSessionDirectory();
|
|
const resolvedAgentId = typeof agentId === "string" ? agentId.trim() : undefined;
|
|
|
|
const matches = useMemo<AgentMatch[]>(() => {
|
|
if (!resolvedAgentId) {
|
|
return [];
|
|
}
|
|
|
|
const results: AgentMatch[] = [];
|
|
sessionDirectory.forEach((session, serverId) => {
|
|
if (!session) {
|
|
return;
|
|
}
|
|
const agent = session.agents.get(resolvedAgentId);
|
|
if (!agent) {
|
|
return;
|
|
}
|
|
const serverLabel = connectionStates.get(serverId)?.daemon.label ?? serverId;
|
|
results.push({ serverId, serverLabel, agent });
|
|
});
|
|
|
|
return results;
|
|
}, [resolvedAgentId, sessionDirectory, connectionStates]);
|
|
|
|
const hasSessions = sessionDirectory.size > 0;
|
|
const isRedirecting = Boolean(resolvedAgentId && matches.length === 1);
|
|
|
|
useEffect(() => {
|
|
if (!isRedirecting) {
|
|
return;
|
|
}
|
|
const match = matches[0];
|
|
router.replace(`/agent/${match.serverId}/${match.agent.id}` as any);
|
|
}, [isRedirecting, matches, router]);
|
|
|
|
const handleGoDraft = useCallback(() => {
|
|
router.replace("/agent" as any);
|
|
}, [router]);
|
|
|
|
const handleSelectMatch = useCallback(
|
|
(match: AgentMatch) => {
|
|
router.replace(`/agent/${match.serverId}/${match.agent.id}` as any);
|
|
},
|
|
[router]
|
|
);
|
|
|
|
let body: ReactNode = null;
|
|
|
|
if (!resolvedAgentId) {
|
|
body = (
|
|
<View style={styles.centerState}>
|
|
<Text style={styles.title}>Missing agent</Text>
|
|
<Text style={styles.subtitle}>
|
|
This link is missing an agent id. Go back to the Agents screen to pick one.
|
|
</Text>
|
|
<Pressable style={styles.primaryButton} onPress={handleGoDraft}>
|
|
<Text style={styles.primaryButtonText}>New agent</Text>
|
|
</Pressable>
|
|
</View>
|
|
);
|
|
} else if (!hasSessions || isRedirecting) {
|
|
body = (
|
|
<View style={styles.centerState}>
|
|
<ActivityIndicator size="large" color={theme.colors.primary} />
|
|
<Text style={styles.subtitle}>
|
|
{isRedirecting ? "Opening your agent..." : "Looking for your agent..."}
|
|
</Text>
|
|
</View>
|
|
);
|
|
} else if (matches.length === 0) {
|
|
body = (
|
|
<View style={styles.centerState}>
|
|
<Text style={styles.title}>Agent not found</Text>
|
|
<Text style={styles.subtitle}>
|
|
We could not find {resolvedAgentId} on any host right now. Hosts reconnect automatically—open it
|
|
again from the Agents screen after it comes back online.
|
|
</Text>
|
|
<Pressable style={styles.primaryButton} onPress={handleGoDraft}>
|
|
<Text style={styles.primaryButtonText}>New agent</Text>
|
|
</Pressable>
|
|
</View>
|
|
);
|
|
} else if (matches.length > 1) {
|
|
body = (
|
|
<View style={styles.centerState}>
|
|
<Text style={styles.title}>Pick a host</Text>
|
|
<Text style={styles.subtitle}>
|
|
Multiple hosts have an agent with this id. Choose the one you intended to open.
|
|
</Text>
|
|
<View style={styles.matchList}>
|
|
{matches.map((match) => (
|
|
<Pressable
|
|
key={`${match.serverId}:${match.agent.id}`}
|
|
style={styles.matchButton}
|
|
onPress={() => handleSelectMatch(match)}
|
|
>
|
|
<Text style={styles.matchLabel}>{match.serverLabel}</Text>
|
|
<Text style={styles.matchMeta} numberOfLines={1}>
|
|
{match.agent.cwd}
|
|
</Text>
|
|
</Pressable>
|
|
))}
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<BackHeader title="Agent" onBack={handleGoDraft} />
|
|
<View style={styles.content}>{body}</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create((theme) => ({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: theme.colors.surface0,
|
|
},
|
|
content: {
|
|
flex: 1,
|
|
paddingHorizontal: theme.spacing[6],
|
|
paddingVertical: theme.spacing[8],
|
|
},
|
|
centerState: {
|
|
flex: 1,
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
gap: theme.spacing[4],
|
|
},
|
|
title: {
|
|
fontSize: theme.fontSize.xl,
|
|
fontWeight: theme.fontWeight.semibold,
|
|
color: theme.colors.foreground,
|
|
textAlign: "center",
|
|
},
|
|
subtitle: {
|
|
fontSize: theme.fontSize.sm,
|
|
color: theme.colors.foregroundMuted,
|
|
textAlign: "center",
|
|
},
|
|
primaryButton: {
|
|
marginTop: theme.spacing[2],
|
|
paddingHorizontal: theme.spacing[4],
|
|
paddingVertical: theme.spacing[3],
|
|
borderRadius: theme.borderRadius.lg,
|
|
backgroundColor: theme.colors.primary,
|
|
},
|
|
primaryButtonText: {
|
|
color: theme.colors.primaryForeground,
|
|
fontSize: theme.fontSize.sm,
|
|
fontWeight: theme.fontWeight.semibold,
|
|
},
|
|
matchList: {
|
|
width: "100%",
|
|
gap: theme.spacing[3],
|
|
},
|
|
matchButton: {
|
|
width: "100%",
|
|
paddingHorizontal: theme.spacing[4],
|
|
paddingVertical: theme.spacing[3],
|
|
borderRadius: theme.borderRadius.lg,
|
|
backgroundColor: theme.colors.surface2,
|
|
},
|
|
matchLabel: {
|
|
fontSize: theme.fontSize.base,
|
|
fontWeight: theme.fontWeight.semibold,
|
|
color: theme.colors.foreground,
|
|
},
|
|
matchMeta: {
|
|
marginTop: theme.spacing[1],
|
|
fontSize: theme.fontSize.sm,
|
|
color: theme.colors.foregroundMuted,
|
|
},
|
|
}));
|