import type { Letta } from "@letta-ai/letta-client"; import type { AgentState } from "@letta-ai/letta-client/resources/agents/agents"; import { Box, Text, useInput } from "ink"; import { useCallback, useEffect, useRef, useState } from "react"; import { getClient } from "../../agent/client"; import { useTerminalWidth } from "../hooks/useTerminalWidth"; import { colors } from "./colors"; interface ResumeSelectorProps { currentAgentId: string; onSelect: (agentId: string) => void; onCancel: () => void; } const DISPLAY_PAGE_SIZE = 5; // How many agents to show per page const FETCH_PAGE_SIZE = 20; // How many agents to fetch from server at once /** * Format a relative time string from a date */ function formatRelativeTime(dateStr: string | null | undefined): string { if (!dateStr) return "Never"; const date = new Date(dateStr); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); const diffWeeks = Math.floor(diffDays / 7); if (diffMins < 1) return "Just now"; if (diffMins < 60) return `${diffMins} minute${diffMins === 1 ? "" : "s"} ago`; if (diffHours < 24) return `${diffHours} hour${diffHours === 1 ? "" : "s"} ago`; if (diffDays < 7) return `${diffDays} day${diffDays === 1 ? "" : "s"} ago`; return `${diffWeeks} week${diffWeeks === 1 ? "" : "s"} ago`; } /** * Truncate agent ID with middle ellipsis if it exceeds available width * e.g., "agent-6b383e6f-f2df-43ed-ad88-8c832f1129d0" -> "agent-6b3...9d0" */ function truncateAgentId(id: string, availableWidth: number): string { if (id.length <= availableWidth) return id; if (availableWidth < 15) return id.slice(0, availableWidth); // Too narrow for ellipsis const prefixLen = Math.floor((availableWidth - 3) / 2); // -3 for "..." const suffixLen = availableWidth - 3 - prefixLen; return `${id.slice(0, prefixLen)}...${id.slice(-suffixLen)}`; } /** * Format model string to show provider/model-name */ function formatModel(agent: AgentState): string { // Prefer the new model field if (agent.model) { return agent.model; } // Fall back to llm_config if (agent.llm_config?.model) { const provider = agent.llm_config.model_endpoint_type || "unknown"; return `${provider}/${agent.llm_config.model}`; } return "unknown"; } export function ResumeSelector({ currentAgentId, onSelect, onCancel, }: ResumeSelectorProps) { const terminalWidth = useTerminalWidth(); const [allAgents, setAllAgents] = useState([]); // All fetched agents const [nextCursor, setNextCursor] = useState(null); const [currentPage, setCurrentPage] = useState(0); const [loading, setLoading] = useState(true); const [loadingMore, setLoadingMore] = useState(false); const [error, setError] = useState(null); const [selectedIndex, setSelectedIndex] = useState(0); const [searchInput, setSearchInput] = useState(""); // What user is typing const [activeQuery, setActiveQuery] = useState(""); // Submitted search query const [hasMore, setHasMore] = useState(true); const clientRef = useRef(null); // Fetch agents from the server const fetchAgents = useCallback( async (afterCursor?: string | null, query?: string) => { const client = clientRef.current || (await getClient()); clientRef.current = client; const agentList = await client.agents.list({ limit: FETCH_PAGE_SIZE, tags: ["origin:letta-code"], include: ["agent.blocks"], order: "desc", order_by: "last_run_completion", ...(afterCursor && { after: afterCursor }), ...(query && { query_text: query }), }); // Get cursor for next fetch (last item's ID if there are more) const cursor = agentList.items.length === FETCH_PAGE_SIZE ? (agentList.items[agentList.items.length - 1]?.id ?? null) : null; return { agents: agentList.items, nextCursor: cursor, }; }, [], ); // Fetch agents when activeQuery changes (initial load or search submitted) useEffect(() => { const doFetch = async () => { setLoading(true); try { const result = await fetchAgents(null, activeQuery || undefined); setAllAgents(result.agents); setNextCursor(result.nextCursor); setHasMore(result.nextCursor !== null); setCurrentPage(0); setSelectedIndex(0); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setLoading(false); } }; doFetch(); }, [fetchAgents, activeQuery]); // Submit search (called when Enter is pressed while typing search) const submitSearch = useCallback(() => { if (searchInput !== activeQuery) { setActiveQuery(searchInput); } }, [searchInput, activeQuery]); // Clear search const clearSearch = useCallback(() => { setSearchInput(""); if (activeQuery) { setActiveQuery(""); } }, [activeQuery]); // Fetch more agents when needed const fetchMoreAgents = useCallback(async () => { if (loadingMore || !hasMore || !nextCursor) return; setLoadingMore(true); try { const result = await fetchAgents(nextCursor, activeQuery || undefined); setAllAgents((prev) => [...prev, ...result.agents]); setNextCursor(result.nextCursor); setHasMore(result.nextCursor !== null); } catch (_err) { // Silently fail on pagination errors } finally { setLoadingMore(false); } }, [loadingMore, hasMore, nextCursor, fetchAgents, activeQuery]); // Calculate display pages from all fetched agents const totalDisplayPages = Math.ceil(allAgents.length / DISPLAY_PAGE_SIZE); const startIndex = currentPage * DISPLAY_PAGE_SIZE; const pageAgents = allAgents.slice( startIndex, startIndex + DISPLAY_PAGE_SIZE, ); const canGoNext = currentPage < totalDisplayPages - 1 || hasMore; useInput((input, key) => { if (loading || error) return; if (key.upArrow) { setSelectedIndex((prev) => Math.max(0, prev - 1)); } else if (key.downArrow) { setSelectedIndex((prev) => Math.min(pageAgents.length - 1, prev + 1)); } else if (key.return) { // If typing a search query, submit it; otherwise select agent if (searchInput && searchInput !== activeQuery) { submitSearch(); } else { const selectedAgent = pageAgents[selectedIndex]; if (selectedAgent?.id) { onSelect(selectedAgent.id); } } } else if (key.escape) { // If typing search, clear it first; otherwise cancel if (searchInput) { clearSearch(); } else { onCancel(); } } else if (key.backspace || key.delete) { setSearchInput((prev) => prev.slice(0, -1)); } else if (input === "j" || input === "J") { // Previous page (j = up/back) if (currentPage > 0) { setCurrentPage((prev) => prev - 1); setSelectedIndex(0); } } else if (input === "k" || input === "K") { // Next page (k = down/forward) if (canGoNext) { const nextPageIndex = currentPage + 1; const nextStartIndex = nextPageIndex * DISPLAY_PAGE_SIZE; // Fetch more if we need data for the next page if (nextStartIndex >= allAgents.length && hasMore) { fetchMoreAgents(); } // Navigate if we have the data if (nextStartIndex < allAgents.length) { setCurrentPage(nextPageIndex); setSelectedIndex(0); } } } else if (input === "/") { // Ignore "/" - just starts typing search } else if (input && !key.ctrl && !key.meta) { // Add regular characters to search input setSearchInput((prev) => prev + input); } }); // Always show the header, with contextual content below return ( Browsing Agents (sorting by last run) {/* Search input - show when typing or when there's an active search */} {(searchInput || activeQuery) && ( Search: {searchInput} {searchInput && searchInput !== activeQuery && ( (press Enter to search) )} {activeQuery && searchInput === activeQuery && ( (Esc to clear) )} )} {/* Error state */} {error && ( Error: {error} Press ESC to cancel )} {/* Loading state */} {loading && !error && ( Loading agents... )} {/* Empty state */} {!loading && !error && allAgents.length === 0 && ( {activeQuery ? "No matching agents found" : "No agents found"} Press ESC to cancel )} {/* Agent list - only show when loaded and have agents */} {!loading && !error && allAgents.length > 0 && ( {pageAgents.map((agent, index) => { const isSelected = index === selectedIndex; const isCurrent = agent.id === currentAgentId; const relativeTime = formatRelativeTime(agent.last_run_completion); const blockCount = agent.blocks?.length ?? 0; const modelStr = formatModel(agent); // Calculate available width for agent ID // Row format: "> Name · agent-id (current)" const nameLen = (agent.name || "Unnamed").length; const fixedChars = 2 + 3 + (isCurrent ? 10 : 0); // "> " + " · " + " (current)" const availableForId = Math.max( 15, terminalWidth - nameLen - fixedChars, ); const displayId = truncateAgentId(agent.id, availableForId); return ( {/* Row 1: Selection indicator, agent name, and ID */} {isSelected ? ">" : " "} {agent.name || "Unnamed"} · {displayId} {isCurrent && ( (current) )} {/* Row 2: Description */} {agent.description || "No description"} {/* Row 3: Metadata (dimmed) */} {relativeTime} · {blockCount} memory block {blockCount === 1 ? "" : "s"} · {modelStr} ); })} )} {/* Footer with pagination and controls - only show when loaded with agents */} {!loading && !error && allAgents.length > 0 && ( Page {currentPage + 1} {hasMore ? "+" : `/${totalDisplayPages || 1}`} {loadingMore && " (loading...)"} ↑↓ navigate · Enter to switch agents · J/K page · Type + Enter to search )} ); }