diff --git a/src/cli/App.tsx b/src/cli/App.tsx index 896a57f..0250ce0 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -72,7 +72,6 @@ import { NewAgentDialog } from "./components/NewAgentDialog"; import { OAuthCodeDialog } from "./components/OAuthCodeDialog"; import { PinDialog, validateAgentName } from "./components/PinDialog"; import { PlanModeDialog } from "./components/PlanModeDialog"; -import { ProfileSelector } from "./components/ProfileSelector"; import { QuestionDialog } from "./components/QuestionDialog"; import { ReasoningMessage } from "./components/ReasoningMessageRich"; import { ResumeSelector } from "./components/ResumeSelector"; @@ -478,7 +477,6 @@ export default function App({ | "system" | "agent" | "resume" - | "profile" | "search" | "subagent" | "feedback" @@ -2945,8 +2943,14 @@ export default function App({ return { submitted: true }; } - // Special handling for /agents command - show agent selector (/resume is hidden alias) - if (msg.trim() === "/agents" || msg.trim() === "/resume") { + // Special handling for /agents command - show agent browser + // /resume, /pinned, /profiles are hidden aliases + if ( + msg.trim() === "/agents" || + msg.trim() === "/resume" || + msg.trim() === "/pinned" || + msg.trim() === "/profiles" + ) { setActiveOverlay("resume"); return { submitted: true }; } @@ -2972,9 +2976,9 @@ export default function App({ setAgentName, }; - // /profile - open profile selector + // /profile - open agent browser (now points to /agents) if (!subcommand) { - setActiveOverlay("profile"); + setActiveOverlay("resume"); return { submitted: true }; } @@ -3033,12 +3037,6 @@ export default function App({ return { submitted: true }; } - // Special handling for /profiles and /pinned commands - open pinned agents selector - if (msg.trim() === "/profiles" || msg.trim() === "/pinned") { - setActiveOverlay("profile"); - return { submitted: true }; - } - // Special handling for /new command - create new agent dialog if (msg.trim() === "/new") { setActiveOverlay("new"); @@ -5508,33 +5506,6 @@ Plan file path: ${planFilePath}`; /> )} - {/* Profile Selector - conditionally mounted as overlay */} - {activeOverlay === "profile" && ( - { - closeOverlay(); - await handleAgentSelect(id); - }} - onUnpin={(unpinAgentId) => { - closeOverlay(); - settingsManager.unpinBoth(unpinAgentId); - const cmdId = uid("cmd"); - buffersRef.current.byId.set(cmdId, { - kind: "command", - id: cmdId, - input: "/pinned", - output: `Unpinned agent ${unpinAgentId.slice(0, 12)}`, - phase: "finished", - success: true, - }); - buffersRef.current.order.push(cmdId); - refreshDerived(); - }} - onCancel={closeOverlay} - /> - )} - {/* Message Search - conditionally mounted as overlay */} {activeOverlay === "search" && ( diff --git a/src/cli/commands/registry.ts b/src/cli/commands/registry.ts index 1be79ad..3bbc742 100644 --- a/src/cli/commands/registry.ts +++ b/src/cli/commands/registry.ts @@ -12,12 +12,12 @@ interface Command { export const commands: Record = { // === Page 1: Most commonly used (order 10-19) === - "/pinned": { - desc: "Browse pinned agents", + "/agents": { + desc: "Browse agents (pinned, Letta Code, all)", order: 10, handler: () => { - // Handled specially in App.tsx to open pinned agents selector - return "Opening pinned agents..."; + // Handled specially in App.tsx to open agent browser + return "Opening agent browser..."; }, }, "/model": { @@ -85,14 +85,6 @@ export const commands: Record = { return "Creating new agent..."; }, }, - "/agents": { - desc: "Browse all agents", - order: 21, - handler: () => { - // Handled specially in App.tsx to show agent selector - return "Opening agent selector..."; - }, - }, "/pin": { desc: "Pin current agent globally, or use -l for local only", order: 22, @@ -336,6 +328,20 @@ export const commands: Record = { return "Opening agent selector..."; }, }, + "/pinned": { + desc: "Browse pinned agents", + hidden: true, // Alias for /agents (opens to Pinned tab) + handler: () => { + return "Opening agent browser..."; + }, + }, + "/profiles": { + desc: "Browse pinned agents", + hidden: true, // Alias for /agents (opens to Pinned tab) + handler: () => { + return "Opening agent browser..."; + }, + }, }; /** diff --git a/src/cli/components/ResumeSelector.tsx b/src/cli/components/ResumeSelector.tsx index e1a86bd..ee067d9 100644 --- a/src/cli/components/ResumeSelector.tsx +++ b/src/cli/components/ResumeSelector.tsx @@ -3,6 +3,8 @@ 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"; @@ -12,8 +14,35 @@ interface ResumeSelectorProps { 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 +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 = { + 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 = { + 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 @@ -40,28 +69,34 @@ function formatRelativeTime(dateStr: string | null | undefined): string { /** * 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 "..." + 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 provider/model-name + * Format model string to show friendly display name (e.g., "Sonnet 4.5") */ function formatModel(agent: AgentState): string { - // Prefer the new model field + // Build handle from agent config + let handle: string | null = null; if (agent.model) { - return agent.model; - } - // Fall back to llm_config - if (agent.llm_config?.model) { + handle = agent.model; + } else if (agent.llm_config?.model) { const provider = agent.llm_config.model_endpoint_type || "unknown"; - return `${provider}/${agent.llm_config.model}`; + 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"; } @@ -72,22 +107,88 @@ export function ResumeSelector({ 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 [filterLettaCode, setFilterLettaCode] = useState(true); // Filter to only letta-code agents const clientRef = useRef(null); - // Fetch agents from the server - const fetchAgents = useCallback( - async (afterCursor?: string | null, query?: string) => { + // Tab state + const [activeTab, setActiveTab] = useState("pinned"); + + // Pinned tab state + const [pinnedAgents, setPinnedAgents] = useState([]); + 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([]); + const [lettaCodeCursor, setLettaCodeCursor] = useState(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(null); + const [lettaCodeLoaded, setLettaCodeLoaded] = useState(false); + const [lettaCodeQuery, setLettaCodeQuery] = useState(""); // Query used to load current data + + // All tab state (cached separately) + const [allAgents, setAllAgents] = useState([]); + const [allCursor, setAllCursor] = useState(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(null); + const [allLoaded, setAllLoaded] = useState(false); + const [allQuery, setAllQuery] = useState(""); // 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; @@ -101,48 +202,211 @@ export function ResumeSelector({ ...(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, - }; + return { agents: agentList.items, nextCursor: cursor }; }, - [filterLettaCode], + [], ); - // Fetch agents when activeQuery changes (initial load or search submitted) - useEffect(() => { - const doFetch = async () => { - setLoading(true); + // Load Letta Code agents + const loadLettaCodeAgents = useCallback( + async (query?: string) => { + setLettaCodeLoading(true); + setLettaCodeError(null); try { - const result = await fetchAgents(null, activeQuery || undefined); - setAllAgents(result.agents); - setNextCursor(result.nextCursor); - setHasMore(result.nextCursor !== null); - setCurrentPage(0); - setSelectedIndex(0); + 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) { - setError(err instanceof Error ? err.message : String(err)); + setLettaCodeError(err instanceof Error ? err.message : String(err)); } finally { - setLoading(false); + setLettaCodeLoading(false); } - }; - doFetch(); - }, [fetchAgents, activeQuery]); + }, + [fetchListAgents], + ); - // Submit search (called when Enter is pressed while typing search) + // 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 + // Clear search (effect will handle reload when query changes) const clearSearch = useCallback(() => { setSearchInput(""); if (activeQuery) { @@ -150,109 +414,267 @@ export function ResumeSelector({ } }, [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; + // 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) { - setSelectedIndex((prev) => Math.max(0, prev - 1)); + setCurrentSelectedIndex((prev: number) => Math.max(0, prev - 1)); } else if (key.downArrow) { - setSelectedIndex((prev) => Math.min(pageAgents.length - 1, prev + 1)); + setCurrentSelectedIndex((prev: number) => + Math.min((currentAgents as AgentState[]).length - 1, prev + 1), + ); } else if (key.return) { - // If typing a search query, submit it; otherwise select agent - if (searchInput && searchInput !== activeQuery) { + // 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 selectedAgent = pageAgents[selectedIndex]; - if (selectedAgent?.id) { - onSelect(selectedAgent.id); + const selected = allPageAgents[allSelectedIndex]; + if (selected?.id) { + onSelect(selected.id); } } } else if (key.escape) { - // If typing search, clear it first; otherwise cancel - if (searchInput) { + // If typing search (list tabs), clear it first + if (activeTab !== "pinned" && searchInput) { clearSearch(); - } else { - onCancel(); + return; } + onCancel(); } else if (key.backspace || key.delete) { - setSearchInput((prev) => prev.slice(0, -1)); + if (activeTab !== "pinned") { + 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); + // 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 (k = down/forward) - if (canGoNext) { - const nextPageIndex = currentPage + 1; + // 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; - // Fetch more if we need data for the next page - if (nextStartIndex >= allAgents.length && hasMore) { - fetchMoreAgents(); + 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(); } - // Navigate if we have the data if (nextStartIndex < allAgents.length) { - setCurrentPage(nextPageIndex); - setSelectedIndex(0); + setAllPage(nextPageIndex); + setAllSelectedIndex(0); } } - } else if (input === "/") { - // Ignore "/" - just starts typing search - } else if (input === "a" || input === "A") { - // Toggle filter between letta-code agents and all agents - setFilterLettaCode((prev) => !prev); - } else if (input && !key.ctrl && !key.meta) { - // Add regular characters to search input + } 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); } }); - // Always show the header, with contextual content below + // Render tab bar + const renderTabBar = () => ( + + {TABS.map((tab) => { + const isActive = tab.id === activeTab; + return ( + + [{tab.label}] + + ); + })} + + ); + + // 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 ( + + + + {isSelected ? ">" : " "} + + + + {agent.name || "Unnamed"} + + + {" · "} + {extra?.isLocal !== undefined + ? `${extra.isLocal ? "project" : "global"} · ` + : ""} + {displayId} + + {isCurrent && ( + (current) + )} + + + + {agent.description || "No description"} + + + + + {relativeTime} · {blockCount} memory block + {blockCount === 1 ? "" : "s"} · {modelStr} + + + + ); + }; + + // 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 ( + + + + {isSelected ? ">" : " "} + + + + {data.agentId.slice(0, 12)} + + · {data.isLocal ? "project" : "global"} + + + + {data.error} + + + + ); + }; + return ( - - + + {/* Header */} + - Browsing Agents (sorting by last run) - - - {filterLettaCode - ? "Displaying agents created in Letta Code (press A to show all)" - : "Displaying all agents (press A to filter to Letta Code)"} + Browsing Agents + + {renderTabBar()} + {TAB_DESCRIPTIONS[activeTab]} + - {/* Search input - show when typing or when there's an active search */} - {(searchInput || activeQuery) && ( - + {/* Search input - list tabs only */} + {activeTab !== "pinned" && (searchInput || activeQuery) && ( + Search: {searchInput} {searchInput && searchInput !== activeQuery && ( @@ -264,113 +686,94 @@ export function ResumeSelector({ )} - {/* Error state */} - {error && ( + {/* Error state - list tabs */} + {activeTab !== "pinned" && currentError && ( - Error: {error} + Error: {currentError} Press ESC to cancel )} {/* Loading state */} - {loading && !error && ( + {currentLoading && ( 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 select · J/K page · Type to search - + {!currentLoading && + ((activeTab === "pinned" && pinnedAgents.length === 0) || + (activeTab === "letta-code" && + !lettaCodeError && + lettaCodeAgents.length === 0) || + (activeTab === "all" && !allError && allAgents.length === 0)) && ( + + {TAB_EMPTY_STATES[activeTab]} + Press ESC to cancel + )} + + {/* Pinned tab content */} + {activeTab === "pinned" && !pinnedLoading && pinnedAgents.length > 0 && ( + + {pinnedPageAgents.map((data, index) => + renderPinnedItem(data, index, index === pinnedSelectedIndex), + )} )} + + {/* Letta Code tab content */} + {activeTab === "letta-code" && + !lettaCodeLoading && + !lettaCodeError && + lettaCodeAgents.length > 0 && ( + + {lettaCodePageAgents.map((agent, index) => + renderAgentItem(agent, index, index === lettaCodeSelectedIndex), + )} + + )} + + {/* All tab content */} + {activeTab === "all" && + !allLoading && + !allError && + allAgents.length > 0 && ( + + {allPageAgents.map((agent, index) => + renderAgentItem(agent, index, index === allSelectedIndex), + )} + + )} + + {/* Footer */} + {!currentLoading && + ((activeTab === "pinned" && pinnedAgents.length > 0) || + (activeTab === "letta-code" && + !lettaCodeError && + lettaCodeAgents.length > 0) || + (activeTab === "all" && !allError && allAgents.length > 0)) && ( + + + + {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...)" : ""}`} + + + + + Tab switch · ↑↓ navigate · Enter select · J/K page + {activeTab === "pinned" + ? " · P unpin · D unpin all" + : " · Type to search"} + + + + )} ); }