Files
letta-code/src/cli/components/ResumeSelector.tsx
2025-12-29 21:19:57 -08:00

786 lines
25 KiB
TypeScript

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 { getModelDisplayName } from "../../agent/model";
import { settingsManager } from "../../settings-manager";
import { useTerminalWidth } from "../hooks/useTerminalWidth";
import { colors } from "./colors";
interface ResumeSelectorProps {
currentAgentId: string;
onSelect: (agentId: string) => void;
onCancel: () => void;
}
type TabId = "pinned" | "letta-code" | "all";
interface PinnedAgentData {
agentId: string;
agent: AgentState | null;
error: string | null;
isLocal: boolean;
}
const TABS: { id: TabId; label: string }[] = [
{ id: "pinned", label: "Pinned" },
{ id: "letta-code", label: "Letta Code" },
{ id: "all", label: "All" },
];
const TAB_DESCRIPTIONS: Record<TabId, string> = {
pinned: "Save agents for easy access by pinning them with /pin",
"letta-code": "Displaying agents created inside of Letta Code",
all: "Displaying all available agents",
};
const TAB_EMPTY_STATES: Record<TabId, string> = {
pinned: "No pinned agents, use /pin to save",
"letta-code": "No agents with tag 'origin:letta-code'",
all: "No agents found",
};
const DISPLAY_PAGE_SIZE = 5;
const FETCH_PAGE_SIZE = 20;
/**
* 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
*/
function truncateAgentId(id: string, availableWidth: number): string {
if (id.length <= availableWidth) return id;
if (availableWidth < 15) return id.slice(0, availableWidth);
const prefixLen = Math.floor((availableWidth - 3) / 2);
const suffixLen = availableWidth - 3 - prefixLen;
return `${id.slice(0, prefixLen)}...${id.slice(-suffixLen)}`;
}
/**
* Format model string to show friendly display name (e.g., "Sonnet 4.5")
*/
function formatModel(agent: AgentState): string {
// Build handle from agent config
let handle: string | null = null;
if (agent.model) {
handle = agent.model;
} else if (agent.llm_config?.model) {
const provider = agent.llm_config.model_endpoint_type || "unknown";
handle = `${provider}/${agent.llm_config.model}`;
}
if (handle) {
// Try to get friendly display name
const displayName = getModelDisplayName(handle);
if (displayName) return displayName;
// Fallback to handle
return handle;
}
return "unknown";
}
export function ResumeSelector({
currentAgentId,
onSelect,
onCancel,
}: ResumeSelectorProps) {
const terminalWidth = useTerminalWidth();
const clientRef = useRef<Letta | null>(null);
// Tab state
const [activeTab, setActiveTab] = useState<TabId>("pinned");
// Pinned tab state
const [pinnedAgents, setPinnedAgents] = useState<PinnedAgentData[]>([]);
const [pinnedLoading, setPinnedLoading] = useState(true);
const [pinnedSelectedIndex, setPinnedSelectedIndex] = useState(0);
const [pinnedPage, setPinnedPage] = useState(0);
// Letta Code tab state (cached separately)
const [lettaCodeAgents, setLettaCodeAgents] = useState<AgentState[]>([]);
const [lettaCodeCursor, setLettaCodeCursor] = useState<string | null>(null);
const [lettaCodeLoading, setLettaCodeLoading] = useState(false);
const [lettaCodeLoadingMore, setLettaCodeLoadingMore] = useState(false);
const [lettaCodeHasMore, setLettaCodeHasMore] = useState(true);
const [lettaCodeSelectedIndex, setLettaCodeSelectedIndex] = useState(0);
const [lettaCodePage, setLettaCodePage] = useState(0);
const [lettaCodeError, setLettaCodeError] = useState<string | null>(null);
const [lettaCodeLoaded, setLettaCodeLoaded] = useState(false);
const [lettaCodeQuery, setLettaCodeQuery] = useState<string>(""); // Query used to load current data
// All tab state (cached separately)
const [allAgents, setAllAgents] = useState<AgentState[]>([]);
const [allCursor, setAllCursor] = useState<string | null>(null);
const [allLoading, setAllLoading] = useState(false);
const [allLoadingMore, setAllLoadingMore] = useState(false);
const [allHasMore, setAllHasMore] = useState(true);
const [allSelectedIndex, setAllSelectedIndex] = useState(0);
const [allPage, setAllPage] = useState(0);
const [allError, setAllError] = useState<string | null>(null);
const [allLoaded, setAllLoaded] = useState(false);
const [allQuery, setAllQuery] = useState<string>(""); // Query used to load current data
// Search state (shared across list tabs)
const [searchInput, setSearchInput] = useState("");
const [activeQuery, setActiveQuery] = useState("");
// Load pinned agents
const loadPinnedAgents = useCallback(async () => {
setPinnedLoading(true);
try {
const mergedPinned = settingsManager.getMergedPinnedAgents();
if (mergedPinned.length === 0) {
setPinnedAgents([]);
setPinnedLoading(false);
return;
}
const client = clientRef.current || (await getClient());
clientRef.current = client;
const pinnedData = await Promise.all(
mergedPinned.map(async ({ agentId, isLocal }) => {
try {
const agent = await client.agents.retrieve(agentId, {
include: ["agent.blocks"],
});
return { agentId, agent, error: null, isLocal };
} catch {
return { agentId, agent: null, error: "Agent not found", isLocal };
}
}),
);
setPinnedAgents(pinnedData);
} catch {
setPinnedAgents([]);
} finally {
setPinnedLoading(false);
}
}, []);
// Fetch agents for list tabs (Letta Code / All)
const fetchListAgents = useCallback(
async (
filterLettaCode: boolean,
afterCursor?: string | null,
query?: string,
) => {
const client = clientRef.current || (await getClient());
clientRef.current = client;
const agentList = await client.agents.list({
limit: FETCH_PAGE_SIZE,
...(filterLettaCode && { tags: ["origin:letta-code"] }),
include: ["agent.blocks"],
order: "desc",
order_by: "last_run_completion",
...(afterCursor && { after: afterCursor }),
...(query && { query_text: query }),
});
const cursor =
agentList.items.length === FETCH_PAGE_SIZE
? (agentList.items[agentList.items.length - 1]?.id ?? null)
: null;
return { agents: agentList.items, nextCursor: cursor };
},
[],
);
// Load Letta Code agents
const loadLettaCodeAgents = useCallback(
async (query?: string) => {
setLettaCodeLoading(true);
setLettaCodeError(null);
try {
const result = await fetchListAgents(true, null, query);
setLettaCodeAgents(result.agents);
setLettaCodeCursor(result.nextCursor);
setLettaCodeHasMore(result.nextCursor !== null);
setLettaCodePage(0);
setLettaCodeSelectedIndex(0);
setLettaCodeLoaded(true);
setLettaCodeQuery(query || ""); // Track query used for this load
} catch (err) {
setLettaCodeError(err instanceof Error ? err.message : String(err));
} finally {
setLettaCodeLoading(false);
}
},
[fetchListAgents],
);
// Load All agents
const loadAllAgents = useCallback(
async (query?: string) => {
setAllLoading(true);
setAllError(null);
try {
const result = await fetchListAgents(false, null, query);
setAllAgents(result.agents);
setAllCursor(result.nextCursor);
setAllHasMore(result.nextCursor !== null);
setAllPage(0);
setAllSelectedIndex(0);
setAllLoaded(true);
setAllQuery(query || ""); // Track query used for this load
} catch (err) {
setAllError(err instanceof Error ? err.message : String(err));
} finally {
setAllLoading(false);
}
},
[fetchListAgents],
);
// Load pinned agents on mount
useEffect(() => {
loadPinnedAgents();
}, [loadPinnedAgents]);
// Load tab data when switching tabs (only if not already loaded)
useEffect(() => {
if (activeTab === "letta-code" && !lettaCodeLoaded && !lettaCodeLoading) {
loadLettaCodeAgents();
} else if (activeTab === "all" && !allLoaded && !allLoading) {
loadAllAgents();
}
}, [
activeTab,
lettaCodeLoaded,
lettaCodeLoading,
loadLettaCodeAgents,
allLoaded,
allLoading,
loadAllAgents,
]);
// Reload current tab when search query changes (only if query differs from cached)
useEffect(() => {
if (activeTab === "letta-code" && activeQuery !== lettaCodeQuery) {
loadLettaCodeAgents(activeQuery || undefined);
} else if (activeTab === "all" && activeQuery !== allQuery) {
loadAllAgents(activeQuery || undefined);
}
}, [
activeQuery,
activeTab,
lettaCodeQuery,
allQuery,
loadLettaCodeAgents,
loadAllAgents,
]);
// Fetch more Letta Code agents
const fetchMoreLettaCodeAgents = useCallback(async () => {
if (lettaCodeLoadingMore || !lettaCodeHasMore || !lettaCodeCursor) return;
setLettaCodeLoadingMore(true);
try {
const result = await fetchListAgents(
true,
lettaCodeCursor,
activeQuery || undefined,
);
setLettaCodeAgents((prev) => [...prev, ...result.agents]);
setLettaCodeCursor(result.nextCursor);
setLettaCodeHasMore(result.nextCursor !== null);
} catch {
// Silently fail on pagination errors
} finally {
setLettaCodeLoadingMore(false);
}
}, [
lettaCodeLoadingMore,
lettaCodeHasMore,
lettaCodeCursor,
fetchListAgents,
activeQuery,
]);
// Fetch more All agents
const fetchMoreAllAgents = useCallback(async () => {
if (allLoadingMore || !allHasMore || !allCursor) return;
setAllLoadingMore(true);
try {
const result = await fetchListAgents(
false,
allCursor,
activeQuery || undefined,
);
setAllAgents((prev) => [...prev, ...result.agents]);
setAllCursor(result.nextCursor);
setAllHasMore(result.nextCursor !== null);
} catch {
// Silently fail on pagination errors
} finally {
setAllLoadingMore(false);
}
}, [allLoadingMore, allHasMore, allCursor, fetchListAgents, activeQuery]);
// Pagination calculations - Pinned
const pinnedTotalPages = Math.ceil(pinnedAgents.length / DISPLAY_PAGE_SIZE);
const pinnedStartIndex = pinnedPage * DISPLAY_PAGE_SIZE;
const pinnedPageAgents = pinnedAgents.slice(
pinnedStartIndex,
pinnedStartIndex + DISPLAY_PAGE_SIZE,
);
// Pagination calculations - Letta Code
const lettaCodeTotalPages = Math.ceil(
lettaCodeAgents.length / DISPLAY_PAGE_SIZE,
);
const lettaCodeStartIndex = lettaCodePage * DISPLAY_PAGE_SIZE;
const lettaCodePageAgents = lettaCodeAgents.slice(
lettaCodeStartIndex,
lettaCodeStartIndex + DISPLAY_PAGE_SIZE,
);
const lettaCodeCanGoNext =
lettaCodePage < lettaCodeTotalPages - 1 || lettaCodeHasMore;
// Pagination calculations - All
const allTotalPages = Math.ceil(allAgents.length / DISPLAY_PAGE_SIZE);
const allStartIndex = allPage * DISPLAY_PAGE_SIZE;
const allPageAgents = allAgents.slice(
allStartIndex,
allStartIndex + DISPLAY_PAGE_SIZE,
);
const allCanGoNext = allPage < allTotalPages - 1 || allHasMore;
// Current tab's state (computed)
const currentLoading =
activeTab === "pinned"
? pinnedLoading
: activeTab === "letta-code"
? lettaCodeLoading
: allLoading;
const currentError =
activeTab === "letta-code"
? lettaCodeError
: activeTab === "all"
? allError
: null;
const currentAgents =
activeTab === "pinned"
? pinnedPageAgents.map((p) => p.agent).filter(Boolean)
: activeTab === "letta-code"
? lettaCodePageAgents
: allPageAgents;
const setCurrentSelectedIndex =
activeTab === "pinned"
? setPinnedSelectedIndex
: activeTab === "letta-code"
? setLettaCodeSelectedIndex
: setAllSelectedIndex;
// Submit search
const submitSearch = useCallback(() => {
if (searchInput !== activeQuery) {
setActiveQuery(searchInput);
}
}, [searchInput, activeQuery]);
// Clear search (effect will handle reload when query changes)
const clearSearch = useCallback(() => {
setSearchInput("");
if (activeQuery) {
setActiveQuery("");
}
}, [activeQuery]);
useInput((input, key) => {
// CTRL-C: immediately cancel
if (key.ctrl && input === "c") {
onCancel();
return;
}
// Tab key cycles through tabs
if (key.tab) {
const currentIndex = TABS.findIndex((t) => t.id === activeTab);
const nextIndex = (currentIndex + 1) % TABS.length;
setActiveTab(TABS[nextIndex]?.id ?? "pinned");
return;
}
if (currentLoading) return;
if (key.upArrow) {
setCurrentSelectedIndex((prev: number) => Math.max(0, prev - 1));
} else if (key.downArrow) {
setCurrentSelectedIndex((prev: number) =>
Math.min((currentAgents as AgentState[]).length - 1, prev + 1),
);
} else if (key.return) {
// If typing a search query (list tabs only), submit it
if (
activeTab !== "pinned" &&
searchInput &&
searchInput !== activeQuery
) {
submitSearch();
return;
}
// Select agent
if (activeTab === "pinned") {
const selected = pinnedPageAgents[pinnedSelectedIndex];
if (selected?.agent) {
onSelect(selected.agentId);
}
} else if (activeTab === "letta-code") {
const selected = lettaCodePageAgents[lettaCodeSelectedIndex];
if (selected?.id) {
onSelect(selected.id);
}
} else {
const selected = allPageAgents[allSelectedIndex];
if (selected?.id) {
onSelect(selected.id);
}
}
} else if (key.escape) {
// If typing search (list tabs), clear it first
if (activeTab !== "pinned" && searchInput) {
clearSearch();
return;
}
onCancel();
} else if (key.backspace || key.delete) {
if (activeTab !== "pinned") {
setSearchInput((prev) => prev.slice(0, -1));
}
} else if (input === "j" || input === "J") {
// Previous page
if (activeTab === "pinned") {
if (pinnedPage > 0) {
setPinnedPage((prev) => prev - 1);
setPinnedSelectedIndex(0);
}
} else if (activeTab === "letta-code") {
if (lettaCodePage > 0) {
setLettaCodePage((prev) => prev - 1);
setLettaCodeSelectedIndex(0);
}
} else {
if (allPage > 0) {
setAllPage((prev) => prev - 1);
setAllSelectedIndex(0);
}
}
} else if (input === "k" || input === "K") {
// Next page
if (activeTab === "pinned") {
if (pinnedPage < pinnedTotalPages - 1) {
setPinnedPage((prev) => prev + 1);
setPinnedSelectedIndex(0);
}
} else if (activeTab === "letta-code" && lettaCodeCanGoNext) {
const nextPageIndex = lettaCodePage + 1;
const nextStartIndex = nextPageIndex * DISPLAY_PAGE_SIZE;
if (nextStartIndex >= lettaCodeAgents.length && lettaCodeHasMore) {
fetchMoreLettaCodeAgents();
}
if (nextStartIndex < lettaCodeAgents.length) {
setLettaCodePage(nextPageIndex);
setLettaCodeSelectedIndex(0);
}
} else if (activeTab === "all" && allCanGoNext) {
const nextPageIndex = allPage + 1;
const nextStartIndex = nextPageIndex * DISPLAY_PAGE_SIZE;
if (nextStartIndex >= allAgents.length && allHasMore) {
fetchMoreAllAgents();
}
if (nextStartIndex < allAgents.length) {
setAllPage(nextPageIndex);
setAllSelectedIndex(0);
}
}
} else if (activeTab === "pinned" && (input === "d" || input === "D")) {
// Unpin from all (pinned tab only)
const selected = pinnedPageAgents[pinnedSelectedIndex];
if (selected) {
settingsManager.unpinBoth(selected.agentId);
loadPinnedAgents();
}
} else if (activeTab === "pinned" && (input === "p" || input === "P")) {
// Unpin from current scope (pinned tab only)
const selected = pinnedPageAgents[pinnedSelectedIndex];
if (selected) {
if (selected.isLocal) {
settingsManager.unpinLocal(selected.agentId);
} else {
settingsManager.unpinGlobal(selected.agentId);
}
loadPinnedAgents();
}
} else if (activeTab !== "pinned" && input && !key.ctrl && !key.meta) {
// Type to search (list tabs only)
setSearchInput((prev) => prev + input);
}
});
// Render tab bar
const renderTabBar = () => (
<Box flexDirection="row" gap={1}>
{TABS.map((tab) => {
const isActive = tab.id === activeTab;
return (
<Text
key={tab.id}
color={isActive ? colors.selector.itemHighlighted : undefined}
bold={isActive}
>
[{tab.label}]
</Text>
);
})}
</Box>
);
// Render agent item (shared between tabs)
const renderAgentItem = (
agent: AgentState,
_index: number,
isSelected: boolean,
extra?: { isLocal?: boolean },
) => {
const isCurrent = agent.id === currentAgentId;
const relativeTime = formatRelativeTime(agent.last_run_completion);
const blockCount = agent.blocks?.length ?? 0;
const modelStr = formatModel(agent);
const nameLen = (agent.name || "Unnamed").length;
const fixedChars = 2 + 3 + (isCurrent ? 10 : 0);
const availableForId = Math.max(15, terminalWidth - nameLen - fixedChars);
const displayId = truncateAgentId(agent.id, availableForId);
return (
<Box key={agent.id} flexDirection="column" marginBottom={1}>
<Box flexDirection="row">
<Text
color={isSelected ? colors.selector.itemHighlighted : undefined}
>
{isSelected ? ">" : " "}
</Text>
<Text> </Text>
<Text
bold={isSelected}
color={isSelected ? colors.selector.itemHighlighted : undefined}
>
{agent.name || "Unnamed"}
</Text>
<Text dimColor>
{" · "}
{extra?.isLocal !== undefined
? `${extra.isLocal ? "project" : "global"} · `
: ""}
{displayId}
</Text>
{isCurrent && (
<Text color={colors.selector.itemCurrent}> (current)</Text>
)}
</Box>
<Box flexDirection="row" marginLeft={2}>
<Text dimColor italic>
{agent.description || "No description"}
</Text>
</Box>
<Box flexDirection="row" marginLeft={2}>
<Text dimColor>
{relativeTime} · {blockCount} memory block
{blockCount === 1 ? "" : "s"} · {modelStr}
</Text>
</Box>
</Box>
);
};
// Render pinned agent item (may have error)
const renderPinnedItem = (
data: PinnedAgentData,
index: number,
isSelected: boolean,
) => {
if (data.agent) {
return renderAgentItem(data.agent, index, isSelected, {
isLocal: data.isLocal,
});
}
// Error state for missing agent
return (
<Box key={data.agentId} flexDirection="column" marginBottom={1}>
<Box flexDirection="row">
<Text
color={isSelected ? colors.selector.itemHighlighted : undefined}
>
{isSelected ? ">" : " "}
</Text>
<Text> </Text>
<Text
bold={isSelected}
color={isSelected ? colors.selector.itemHighlighted : undefined}
>
{data.agentId.slice(0, 12)}
</Text>
<Text dimColor> · {data.isLocal ? "project" : "global"}</Text>
</Box>
<Box flexDirection="row" marginLeft={2}>
<Text color="red" italic>
{data.error}
</Text>
</Box>
</Box>
);
};
return (
<Box flexDirection="column">
{/* Header */}
<Box flexDirection="column" gap={1} marginBottom={1}>
<Text bold color={colors.selector.title}>
Browsing Agents
</Text>
<Box flexDirection="column">
{renderTabBar()}
<Text dimColor>{TAB_DESCRIPTIONS[activeTab]}</Text>
</Box>
</Box>
{/* Search input - list tabs only */}
{activeTab !== "pinned" && (searchInput || activeQuery) && (
<Box marginBottom={1}>
<Text dimColor>Search: </Text>
<Text>{searchInput}</Text>
{searchInput && searchInput !== activeQuery && (
<Text dimColor> (press Enter to search)</Text>
)}
{activeQuery && searchInput === activeQuery && (
<Text dimColor> (Esc to clear)</Text>
)}
</Box>
)}
{/* Error state - list tabs */}
{activeTab !== "pinned" && currentError && (
<Box flexDirection="column">
<Text color="red">Error: {currentError}</Text>
<Text dimColor>Press ESC to cancel</Text>
</Box>
)}
{/* Loading state */}
{currentLoading && (
<Box>
<Text dimColor>Loading agents...</Text>
</Box>
)}
{/* Empty state */}
{!currentLoading &&
((activeTab === "pinned" && pinnedAgents.length === 0) ||
(activeTab === "letta-code" &&
!lettaCodeError &&
lettaCodeAgents.length === 0) ||
(activeTab === "all" && !allError && allAgents.length === 0)) && (
<Box flexDirection="column">
<Text dimColor>{TAB_EMPTY_STATES[activeTab]}</Text>
<Text dimColor>Press ESC to cancel</Text>
</Box>
)}
{/* Pinned tab content */}
{activeTab === "pinned" && !pinnedLoading && pinnedAgents.length > 0 && (
<Box flexDirection="column">
{pinnedPageAgents.map((data, index) =>
renderPinnedItem(data, index, index === pinnedSelectedIndex),
)}
</Box>
)}
{/* Letta Code tab content */}
{activeTab === "letta-code" &&
!lettaCodeLoading &&
!lettaCodeError &&
lettaCodeAgents.length > 0 && (
<Box flexDirection="column">
{lettaCodePageAgents.map((agent, index) =>
renderAgentItem(agent, index, index === lettaCodeSelectedIndex),
)}
</Box>
)}
{/* All tab content */}
{activeTab === "all" &&
!allLoading &&
!allError &&
allAgents.length > 0 && (
<Box flexDirection="column">
{allPageAgents.map((agent, index) =>
renderAgentItem(agent, index, index === allSelectedIndex),
)}
</Box>
)}
{/* Footer */}
{!currentLoading &&
((activeTab === "pinned" && pinnedAgents.length > 0) ||
(activeTab === "letta-code" &&
!lettaCodeError &&
lettaCodeAgents.length > 0) ||
(activeTab === "all" && !allError && allAgents.length > 0)) && (
<Box flexDirection="column">
<Box>
<Text dimColor>
{activeTab === "pinned"
? `Page ${pinnedPage + 1}/${pinnedTotalPages || 1}`
: activeTab === "letta-code"
? `Page ${lettaCodePage + 1}${lettaCodeHasMore ? "+" : `/${lettaCodeTotalPages || 1}`}${lettaCodeLoadingMore ? " (loading...)" : ""}`
: `Page ${allPage + 1}${allHasMore ? "+" : `/${allTotalPages || 1}`}${allLoadingMore ? " (loading...)" : ""}`}
</Text>
</Box>
<Box>
<Text dimColor>
Tab switch · navigate · Enter select · J/K page
{activeTab === "pinned"
? " · P unpin · D unpin all"
: " · Type to search"}
</Text>
</Box>
</Box>
)}
</Box>
);
}