diff --git a/src/cli/App.tsx b/src/cli/App.tsx
index 2bbe277..803014f 100644
--- a/src/cli/App.tsx
+++ b/src/cli/App.tsx
@@ -91,7 +91,7 @@ import { FeedbackDialog } from "./components/FeedbackDialog";
import { HelpDialog } from "./components/HelpDialog";
import { Input } from "./components/InputRich";
import { McpSelector } from "./components/McpSelector";
-import { MemoryViewer } from "./components/MemoryViewer";
+import { MemoryTabViewer } from "./components/MemoryTabViewer";
import { MessageSearch } from "./components/MessageSearch";
import { ModelSelector } from "./components/ModelSelector";
import { NewAgentDialog } from "./components/NewAgentDialog";
@@ -99,7 +99,7 @@ import { PendingApprovalStub } from "./components/PendingApprovalStub";
import { PinDialog, validateAgentName } from "./components/PinDialog";
// QuestionDialog removed - now using InlineQuestionApproval
import { ReasoningMessage } from "./components/ReasoningMessageRich";
-import { ResumeSelector } from "./components/ResumeSelector";
+
import { formatUsageStats } from "./components/SessionStats";
// InlinePlanApproval kept for easy rollback if needed
// import { InlinePlanApproval } from "./components/InlinePlanApproval";
@@ -7393,23 +7393,14 @@ Plan file path: ${planFilePath}`;
/>
)}
- {/* Agent Selector - conditionally mounted as overlay */}
- {activeOverlay === "agent" && (
-
- )}
-
{/* Subagent Manager - for managing custom subagents */}
{activeOverlay === "subagent" && (
)}
- {/* Resume Selector - conditionally mounted as overlay */}
+ {/* Agent Selector - for browsing/selecting agents */}
{activeOverlay === "resume" && (
- {
closeOverlay();
@@ -7709,10 +7700,9 @@ Plan file path: ${planFilePath}`;
{/* Memory Viewer - conditionally mounted as overlay */}
{activeOverlay === "memory" && (
-
diff --git a/src/cli/components/AgentInfoBar.tsx b/src/cli/components/AgentInfoBar.tsx
index ba10434..ee2f571 100644
--- a/src/cli/components/AgentInfoBar.tsx
+++ b/src/cli/components/AgentInfoBar.tsx
@@ -3,6 +3,7 @@ import Link from "ink-link";
import { memo, useMemo } from "react";
import { DEFAULT_AGENT_NAME } from "../../constants";
import { settingsManager } from "../../settings-manager";
+import { getVersion } from "../../version";
import { colors } from "./colors";
interface AgentInfoBarProps {
@@ -37,14 +38,34 @@ export const AgentInfoBar = memo(function AgentInfoBar({
}
return (
-
+
+ {/* Blank line after commands */}
+
+
+ {/* Discord/version info */}
- {agentName || "Unnamed"}
+
+ {" "}Having issues? Report bugs with /feedback or{" "}
+
+ join our Discord ↗
+
+
+
+
+
+ {" "}Version: Letta Code v{getVersion()}
+
+
+
+ {/* Blank line before agent info */}
+
+
+ {/* Agent name and links */}
+
+ {" "}
+
+ {agentName || "Unnamed"}
+
{isPinned ? (
(pinned ✓)
) : agentName === DEFAULT_AGENT_NAME || !agentName ? (
@@ -55,21 +76,21 @@ export const AgentInfoBar = memo(function AgentInfoBar({
· {agentId}
+ {" "}
{isCloudUser && (
- Open in ADE ↗
+ Open in ADE ↗
)}
-
-
+ {isCloudUser && {" · "}}
{isCloudUser && (
- View usage ↗
+ View usage ↗
)}
- {!isCloudUser && · {serverUrl}}
+ {!isCloudUser && {serverUrl}}
);
diff --git a/src/cli/components/AgentSelector.tsx b/src/cli/components/AgentSelector.tsx
index 31742e4..d8e125a 100644
--- a/src/cli/components/AgentSelector.tsx
+++ b/src/cli/components/AgentSelector.tsx
@@ -1,190 +1,818 @@
+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 { useEffect, useState } from "react";
+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";
+import { MarkdownDisplay } from "./MarkdownDisplay";
+
+// Horizontal line character (matches approval dialogs)
+const SOLID_LINE = "─";
interface AgentSelectorProps {
currentAgentId: string;
onSelect: (agentId: string) => void;
onCancel: () => void;
+ /** The command that triggered this selector (e.g., "/agents" or "/resume") */
+ command?: string;
+}
+
+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
+ */
+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 AgentSelector({
currentAgentId,
onSelect,
onCancel,
+ command = "/agents",
}: AgentSelectorProps) {
- const [agents, setAgents] = useState([]);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
- const [selectedIndex, setSelectedIndex] = useState(0);
- const [searchQuery, setSearchQuery] = useState("");
- const [debouncedQuery, setDebouncedQuery] = useState("");
+ const terminalWidth = useTerminalWidth();
+ const clientRef = useRef(null);
- useEffect(() => {
- const fetchAgents = async () => {
- try {
- const client = await getClient();
- const agentList = await client.agents.list();
- setAgents(agentList.items);
- setLoading(false);
- } catch (err) {
- setError(err instanceof Error ? err.message : String(err));
- setLoading(false);
+ // 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;
}
- };
- fetchAgents();
+
+ 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);
+ }
}, []);
- // Debounce search query (300ms delay)
+ // 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(() => {
- const timer = setTimeout(() => {
- setDebouncedQuery(searchQuery);
- }, 300);
+ loadPinnedAgents();
+ }, [loadPinnedAgents]);
- return () => clearTimeout(timer);
- }, [searchQuery]);
-
- // Filter agents based on debounced search query
- const matchingAgents = agents.filter((agent) => {
- if (!debouncedQuery) return true;
- const query = debouncedQuery.toLowerCase();
- const name = (agent.name || "").toLowerCase();
- const id = (agent.id || "").toLowerCase();
- return name.includes(query) || id.includes(query);
- });
-
- const filteredAgents = matchingAgents.slice(0, 10);
-
- // Reset selected index when filtered list changes
+ // Load tab data when switching tabs (only if not already loaded)
useEffect(() => {
- setSelectedIndex(0);
- }, []);
+ 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 (works even during loading/error)
+ // CTRL-C: immediately cancel
if (key.ctrl && input === "c") {
onCancel();
return;
}
- 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;
+
+ // For pinned tab, use pinnedPageAgents.length to include "not found" entries
+ // For other tabs, use currentAgents.length
+ const maxIndex =
+ activeTab === "pinned"
+ ? pinnedPageAgents.length - 1
+ : (currentAgents as AgentState[]).length - 1;
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(filteredAgents.length - 1, prev + 1));
+ setCurrentSelectedIndex((prev: number) => Math.min(maxIndex, prev + 1));
} else if (key.return) {
- const selectedAgent = filteredAgents[selectedIndex];
- if (selectedAgent?.id) {
- onSelect(selectedAgent.id);
+ // 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) {
- setSearchQuery((prev) => prev.slice(0, -1));
- } else if (input && !key.ctrl && !key.meta) {
- // Add regular characters to search query
- setSearchQuery((prev) => prev + input);
+ if (activeTab !== "pinned") {
+ setSearchInput((prev) => prev.slice(0, -1));
+ }
+ } else if (key.leftArrow) {
+ // 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 (key.rightArrow) {
+ // 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);
+ }
+ }
+ // NOTE: "D" for unpin all disabled - too destructive without confirmation
+ // } else if (activeTab === "pinned" && (input === "d" || input === "D")) {
+ // 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);
}
});
- if (loading) {
+ // Render tab bar
+ const renderTabBar = () => (
+
+ {TABS.map((tab) => {
+ const isActive = tab.id === activeTab;
+ // Always use same width (with padding) to prevent jitter when switching tabs
+ 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 (
-
- Loading agents...
-
- );
- }
-
- if (error) {
- return (
-
- Error loading agents: {error}
- Press ESC to cancel
-
- );
- }
-
- if (agents.length === 0) {
- return (
-
- No agents found
- Press ESC to cancel
-
- );
- }
-
- return (
-
-
-
- Select Agent (↑↓ to navigate, Enter to select, ESC to cancel)
-
-
-
-
- Search:
- {searchQuery || "_"}
-
-
- {filteredAgents.length === 0 && (
-
- No agents match your search
-
- )}
-
- {filteredAgents.length > 0 && (
-
+
+
+
+ {isSelected ? ">" : " "}
+
+
+
+ {agent.name || "Unnamed"}
+
- Showing {filteredAgents.length}
- {matchingAgents.length > 10 ? ` of ${matchingAgents.length}` : ""}
- {debouncedQuery ? " matching" : ""} agents
+ {" · "}
+ {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}
+
+
+
+ );
+ };
+
+ // Calculate horizontal line width
+ const solidLine = SOLID_LINE.repeat(Math.max(terminalWidth, 10));
+
+ return (
+
+ {/* Command header */}
+ {`> ${command}`}
+ {solidLine}
+
+
+
+ {/* Header */}
+
+
+ Swap to a different agent
+
+
+ {renderTabBar()}
+ {TAB_DESCRIPTIONS[activeTab]}
+
+
+
+ {/* Search input - list tabs only */}
+ {activeTab !== "pinned" && (searchInput || activeQuery) && (
+
+ Search:
+ {searchInput}
+ {searchInput && searchInput !== activeQuery && (
+ (press Enter to search)
+ )}
+ {activeQuery && searchInput === activeQuery && (
+ (Esc to clear)
+ )}
+
)}
-
- {filteredAgents.map((agent, index) => {
- const isSelected = index === selectedIndex;
- const isCurrent = agent.id === currentAgentId;
+ {/* Error state - list tabs */}
+ {activeTab !== "pinned" && currentError && (
+
+ Error: {currentError}
+ Press ESC to cancel
+
+ )}
- const lastInteractedAt = agent.last_run_completion
- ? new Date(agent.last_run_completion).toLocaleString()
- : "Never";
+ {/* Loading state */}
+ {currentLoading && (
+
+ {" "}Loading agents...
+
+ )}
+
+ {/* Empty state */}
+ {!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)) &&
+ (() => {
+ const footerWidth = Math.max(0, terminalWidth - 2);
+ const pageText =
+ 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...)" : ""}`;
+ const hintsText = `Enter select · ↑↓ navigate · ←→ page · Tab switch${activeTab === "pinned" ? " · P unpin" : " · Type to search"} · Esc cancel`;
return (
-
-
- {isSelected ? "›" : " "}
-
-
-
- {agent.name || "Unnamed"}
- {isCurrent && (
- (current)
- )}
-
-
- {agent.id}
-
-
- {lastInteractedAt}
-
+
+
+
+
+
+
+
+
+
+
+
+
);
- })}
-
+ })()}
);
}
diff --git a/src/cli/components/Autocomplete.tsx b/src/cli/components/Autocomplete.tsx
index b093d0f..99311de 100644
--- a/src/cli/components/Autocomplete.tsx
+++ b/src/cli/components/Autocomplete.tsx
@@ -3,8 +3,8 @@ import type { ReactNode } from "react";
import { colors } from "./colors";
interface AutocompleteBoxProps {
- /** Header text shown at top of autocomplete */
- header: ReactNode;
+ /** Optional header text shown at top of autocomplete */
+ header?: ReactNode;
children: ReactNode;
}
@@ -14,13 +14,8 @@ interface AutocompleteBoxProps {
*/
export function AutocompleteBox({ header, children }: AutocompleteBoxProps) {
return (
-
- {header}
+
+ {header && {header}}
{children}
);
@@ -35,7 +30,8 @@ interface AutocompleteItemProps {
/**
* Shared item component for autocomplete lists.
- * Handles selection indicator and styling.
+ * Handles selection styling (color-based, no arrow indicator).
+ * 2-char gutter aligns with input box prompt.
*/
export function AutocompleteItem({
selected,
@@ -46,7 +42,7 @@ export function AutocompleteItem({
color={selected ? colors.command.selected : undefined}
bold={selected}
>
- {selected ? "▶ " : " "}
+ {" "}
{children}
);
diff --git a/src/cli/components/ConversationSelector.tsx b/src/cli/components/ConversationSelector.tsx
index 5a5c825..0dbd3e1 100644
--- a/src/cli/components/ConversationSelector.tsx
+++ b/src/cli/components/ConversationSelector.tsx
@@ -4,7 +4,12 @@ import type { Conversation } from "@letta-ai/letta-client/resources/conversation
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";
+import { MarkdownDisplay } from "./MarkdownDisplay";
+
+// Horizontal line character (matches approval dialogs)
+const SOLID_LINE = "─";
interface ConversationSelectorProps {
agentId: string;
@@ -333,13 +338,13 @@ export function ConversationSelector({
} else if (input === "n" || input === "N") {
// New conversation
onNewConversation();
- } else if (input === "j" || input === "J") {
+ } else if (key.leftArrow) {
// Previous page
if (page > 0) {
setPage((prev) => prev - 1);
setSelectedIndex(0);
}
- } else if (input === "k" || input === "K") {
+ } else if (key.rightArrow) {
// Next page
if (canGoNext) {
const nextPageIndex = page + 1;
@@ -376,14 +381,19 @@ export function ConversationSelector({
const createdTime = formatRelativeTime(conv.created_at);
// Build preview content: (1) summary if exists, (2) preview lines, (3) message count fallback
+ // Uses L-bracket indentation style for visual hierarchy
const renderPreview = () => {
+ const bracket = {"⎿ "};
+ const indent = " "; // Same width as "⎿ " for alignment
+
// Priority 1: Summary
if (conv.summary) {
return (
+ {bracket}
- {conv.summary.length > 60
- ? `${conv.summary.slice(0, 57)}...`
+ {conv.summary.length > 57
+ ? `${conv.summary.slice(0, 54)}...`
: conv.summary}
@@ -400,6 +410,7 @@ export function ConversationSelector({
flexDirection="row"
marginLeft={2}
>
+ {idx === 0 ? bracket : {indent}}
{line.role === "assistant" ? "👾 " : "👤 "}
@@ -416,6 +427,7 @@ export function ConversationSelector({
if (messageCount > 0) {
return (
+ {bracket}
{messageCount} message{messageCount === 1 ? "" : "s"} (no
in-context user/agent messages)
@@ -426,6 +438,7 @@ export function ConversationSelector({
return (
+ {bracket}
No in-context messages
@@ -462,14 +475,22 @@ export function ConversationSelector({
);
};
+ const terminalWidth = useTerminalWidth();
+ const solidLine = SOLID_LINE.repeat(Math.max(terminalWidth, 10));
+
return (
- {/* Header */}
-
+ {/* Command header */}
+ {"> /resume"}
+ {solidLine}
+
+
+
+ {/* Title */}
+
- Resume Conversation
+ Resume a previous conversation
- Select a conversation to resume or start a new one
{/* Error state */}
@@ -505,22 +526,32 @@ export function ConversationSelector({
)}
{/* Footer */}
- {!loading && !error && conversations.length > 0 && (
-
-
-
- Page {page + 1}
- {hasMore ? "+" : `/${totalPages || 1}`}
- {loadingMore ? " (loading...)" : ""}
-
-
-
-
- ↑↓ navigate · Enter select · J/K page · N new · ESC cancel
-
-
-
- )}
+ {!loading &&
+ !error &&
+ conversations.length > 0 &&
+ (() => {
+ const footerWidth = Math.max(0, terminalWidth - 2);
+ const pageText = `Page ${page + 1}${hasMore ? "+" : `/${totalPages || 1}`}${loadingMore ? " (loading...)" : ""}`;
+ const hintsText =
+ "Enter select · ↑↓ navigate · ←→ page · N new · Esc cancel";
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ })()}
);
}
diff --git a/src/cli/components/InputRich.tsx b/src/cli/components/InputRich.tsx
index 21dd46e..3d14cd6 100644
--- a/src/cli/components/InputRich.tsx
+++ b/src/cli/components/InputRich.tsx
@@ -51,6 +51,7 @@ const InputFooter = memo(function InputFooter({
agentName,
currentModel,
isOpenAICodexProvider,
+ isAutocompleteActive,
}: {
ctrlCPressed: boolean;
escapePressed: boolean;
@@ -61,7 +62,13 @@ const InputFooter = memo(function InputFooter({
agentName: string | null | undefined;
currentModel: string | null | undefined;
isOpenAICodexProvider: boolean;
+ isAutocompleteActive: boolean;
}) {
+ // Hide footer when autocomplete is showing
+ if (isAutocompleteActive) {
+ return null;
+ }
+
return (
{ctrlCPressed ? (
@@ -841,6 +848,7 @@ export function Input({
isOpenAICodexProvider={
currentModelProvider === OPENAI_CODEX_PROVIDER_NAME
}
+ isAutocompleteActive={isAutocompleteActive}
/>
diff --git a/src/cli/components/McpSelector.tsx b/src/cli/components/McpSelector.tsx
index 4d027dd..6643e03 100644
--- a/src/cli/components/McpSelector.tsx
+++ b/src/cli/components/McpSelector.tsx
@@ -10,6 +10,9 @@ import { getClient } from "../../agent/client";
import { useTerminalWidth } from "../hooks/useTerminalWidth";
import { colors } from "./colors";
+// Horizontal line character (matches approval dialogs)
+const SOLID_LINE = "─";
+
interface McpSelectorProps {
agentId: string;
onAdd: () => void;
@@ -67,6 +70,7 @@ export const McpSelector = memo(function McpSelector({
onCancel,
}: McpSelectorProps) {
const terminalWidth = useTerminalWidth();
+ const solidLine = SOLID_LINE.repeat(Math.max(terminalWidth, 10));
const [servers, setServers] = useState([]);
const [loading, setLoading] = useState(true);
const [selectedIndex, setSelectedIndex] = useState(0);
@@ -444,8 +448,15 @@ export const McpSelector = memo(function McpSelector({
);
return (
-
-
+
+ {/* Command header */}
+ {"> /mcp"}
+ {solidLine}
+
+
+
+ {/* Title */}
+
Tools for {viewingServer.server_name}
@@ -455,12 +466,11 @@ export const McpSelector = memo(function McpSelector({
{toolsLoading && (
+ {" "}
{tools.length > 0 ? "Refreshing tools..." : "Loading tools..."}
{tools.length === 0 && (
-
- This may take a moment on first load
-
+ {" "}This may take a moment on first load
)}
)}
@@ -468,9 +478,12 @@ export const McpSelector = memo(function McpSelector({
{/* Error state */}
{!toolsLoading && toolsError && (
- {toolsError}
+
+ {" "}
+ {toolsError}
+
- R refresh from server · Esc back
+ {" "}R refresh from server · Esc back
)}
@@ -478,10 +491,12 @@ export const McpSelector = memo(function McpSelector({
{/* Empty state */}
{!toolsLoading && !toolsError && tools.length === 0 && (
- No tools available for this server.
- Press R to sync tools from the MCP server.
+ {" "}No tools available for this server.
+
+ {" "}Press R to sync tools from the MCP server.
+
- R refresh · Esc back
+ {" "}R refresh · Esc back
)}
@@ -505,9 +520,8 @@ export const McpSelector = memo(function McpSelector({
isSelected ? colors.selector.itemHighlighted : undefined
}
>
- {isSelected ? ">" : " "}
+ {isSelected ? "> " : " "}
-
{/* Row 2: Description */}
-
-
- {truncateText(toolDesc, terminalWidth - 4)}
+
+
+ {" "}
+ {truncateText(toolDesc, terminalWidth - 6)}
@@ -546,20 +561,17 @@ export const McpSelector = memo(function McpSelector({
).length;
return (
-
-
- {toolsTotalPages > 1 &&
- `Page ${toolsPage + 1}/${toolsTotalPages} · `}
- {attachedFromThisServer}/{tools.length} attached from server
- · {attachedToolIds.size} total on agent
-
-
-
-
- ↑↓ navigate · Space/Enter toggle · A attach all · D detach
- all · R refresh · Esc back
-
-
+
+ {" "}
+ {toolsTotalPages > 1 &&
+ `Page ${toolsPage + 1}/${toolsTotalPages} · `}
+ {attachedFromThisServer}/{tools.length} attached from server ·{" "}
+ {attachedToolIds.size} total on agent
+
+
+ {" "}Space/Enter toggle · ↑↓ navigate · A attach all · D
+ detach all · R refresh · Esc back
+
);
})()}
@@ -571,15 +583,24 @@ export const McpSelector = memo(function McpSelector({
if (mode === "confirming-delete" && selectedServer) {
const options = ["Yes, delete", "No, cancel"];
return (
-
-
+
+ {/* Command header */}
+ {"> /mcp"}
+ {solidLine}
+
+
+
+ {/* Title */}
+
- Delete MCP Server
+ Delete MCP server?
-
- Delete "{selectedServer.server_name}"?
-
+
+
+ {" "}Delete "{selectedServer.server_name}"?
+
+
{options.map((option, index) => {
const isSelected = index === deleteConfirmIndex;
@@ -591,7 +612,8 @@ export const McpSelector = memo(function McpSelector({
}
bold={isSelected}
>
- {isSelected ? ">" : " "} {option}
+ {isSelected ? "> " : " "}
+ {option}
);
@@ -603,26 +625,35 @@ export const McpSelector = memo(function McpSelector({
// Main browsing UI
return (
-
-
+
+ {/* Command header */}
+ {"> /mcp"}
+ {solidLine}
+
+
+
+ {/* Title */}
+
- MCP Servers
+ Manage MCP servers
{/* Loading state */}
{loading && (
- Loading MCP servers...
+ {" "}Loading MCP servers...
)}
{/* Error state */}
{!loading && error && (
- Error: {error}
+
+ {" "}Error: {error}
+
- R refresh · Esc close
+ {" "}R refresh · Esc cancel
)}
@@ -630,10 +661,10 @@ export const McpSelector = memo(function McpSelector({
{/* Empty state */}
{!loading && !error && servers.length === 0 && (
- No MCP servers configured.
- Press A to add a new server.
+ {" "}No MCP servers configured.
+ {" "}Press A to add a new server.
- A add · Esc close
+ {" "}A add · Esc cancel
)}
@@ -649,7 +680,7 @@ export const McpSelector = memo(function McpSelector({
// Calculate available width for target display
const nameLen = server.server_name.length;
const typeLen = serverType.length;
- const fixedChars = 2 + 3 + 3 + typeLen; // "> " + " · " + " · " + type
+ const fixedChars = 4 + 3 + 3 + typeLen; // " > " + " · " + " · " + type
const availableForTarget = Math.max(
20,
terminalWidth - nameLen - fixedChars,
@@ -662,16 +693,15 @@ export const McpSelector = memo(function McpSelector({
flexDirection="column"
marginBottom={1}
>
- {/* Row 1: Selection indicator, name, type, and ID */}
+ {/* Row 1: Selection indicator, name, type, and target */}
- {isSelected ? ">" : " "}
+ {isSelected ? "> " : " "}
-
{/* Row 2: Server ID if available */}
{server.id && (
-
-
- ID: {server.id}
+
+
+ {" "}ID: {server.id}
)}
@@ -703,18 +733,14 @@ export const McpSelector = memo(function McpSelector({
{!loading && !error && servers.length > 0 && (
{totalPages > 1 && (
-
-
- Page {currentPage + 1}/{totalPages}
-
-
- )}
-
- ↑↓ navigate · Enter view tools · A add · D delete · R refresh ·
- Esc close
+ {" "}Page {currentPage + 1}/{totalPages}
-
+ )}
+
+ {" "}Enter view tools · ↑↓ navigate · A add · D delete · R refresh
+ · Esc cancel
+
)}
diff --git a/src/cli/components/MemoryTabViewer.tsx b/src/cli/components/MemoryTabViewer.tsx
new file mode 100644
index 0000000..9e0a8ae
--- /dev/null
+++ b/src/cli/components/MemoryTabViewer.tsx
@@ -0,0 +1,216 @@
+import type { Block } from "@letta-ai/letta-client/resources/agents/blocks";
+import { Box, Text, useInput } from "ink";
+import Link from "ink-link";
+import { useState } from "react";
+import { useTerminalWidth } from "../hooks/useTerminalWidth";
+import { colors } from "./colors";
+import { MarkdownDisplay } from "./MarkdownDisplay";
+
+// Horizontal line character (matches approval dialogs)
+const SOLID_LINE = "─";
+
+const VISIBLE_LINES = 12; // Visible lines for value content
+
+interface MemoryTabViewerProps {
+ blocks: Block[];
+ agentId: string;
+ onClose: () => void;
+ conversationId?: string;
+}
+
+/**
+ * Format character count as "current / limit"
+ */
+function formatCharCount(current: number, limit: number | null): string {
+ if (limit === null || limit === undefined) {
+ return `${current.toLocaleString()} chars`;
+ }
+ return `${current.toLocaleString()} / ${limit.toLocaleString()} chars`;
+}
+
+export function MemoryTabViewer({
+ blocks,
+ agentId,
+ onClose,
+ conversationId,
+}: MemoryTabViewerProps) {
+ const terminalWidth = useTerminalWidth();
+ const solidLine = SOLID_LINE.repeat(Math.max(terminalWidth, 10));
+ const adeUrl = `https://app.letta.com/agents/${agentId}?view=memory${conversationId ? `&conversation=${conversationId}` : ""}`;
+
+ const [selectedTabIndex, setSelectedTabIndex] = useState(0);
+ const [scrollOffset, setScrollOffset] = useState(0);
+
+ // Get current block
+ const currentBlock = blocks[selectedTabIndex];
+ const valueLines = currentBlock?.value?.split("\n") || [];
+ const maxScrollOffset = Math.max(0, valueLines.length - VISIBLE_LINES);
+
+ // Reset scroll when switching tabs
+ const switchTab = (newIndex: number) => {
+ setSelectedTabIndex(newIndex);
+ setScrollOffset(0);
+ };
+
+ useInput((input, key) => {
+ // CTRL-C: immediately close
+ if (key.ctrl && input === "c") {
+ onClose();
+ return;
+ }
+
+ // ESC: close
+ if (key.escape) {
+ onClose();
+ return;
+ }
+
+ // Tab or left/right to switch tabs
+ if (key.tab) {
+ const nextIndex = (selectedTabIndex + 1) % blocks.length;
+ switchTab(nextIndex);
+ return;
+ }
+
+ if (key.leftArrow) {
+ const prevIndex =
+ selectedTabIndex === 0 ? blocks.length - 1 : selectedTabIndex - 1;
+ switchTab(prevIndex);
+ return;
+ }
+
+ if (key.rightArrow) {
+ const nextIndex = (selectedTabIndex + 1) % blocks.length;
+ switchTab(nextIndex);
+ return;
+ }
+
+ // Up/down to scroll content
+ if (key.upArrow) {
+ setScrollOffset((prev) => Math.max(prev - 1, 0));
+ } else if (key.downArrow) {
+ setScrollOffset((prev) => Math.min(prev + 1, maxScrollOffset));
+ }
+ });
+
+ // Render tab bar
+ const renderTabBar = () => (
+
+ {blocks.map((block, index) => {
+ const isActive = index === selectedTabIndex;
+ return (
+
+ {` ${block.label} `}
+
+ );
+ })}
+
+ );
+
+ // Empty state
+ if (blocks.length === 0) {
+ return (
+
+ {"> /memory"}
+ {solidLine}
+
+
+
+
+
+ View your agent's memory
+
+
+ {" "}No memory blocks attached to this agent.
+
+ {" "}Esc cancel
+
+
+ );
+ }
+
+ const charCount = (currentBlock?.value || "").length;
+ const visibleValueLines = valueLines.slice(
+ scrollOffset,
+ scrollOffset + VISIBLE_LINES,
+ );
+ const canScrollDown = scrollOffset < maxScrollOffset;
+ const barColor = colors.selector.itemHighlighted;
+
+ return (
+
+ {/* Command header */}
+ {"> /memory"}
+ {solidLine}
+
+
+
+ {/* Title */}
+
+
+ View your agent's memory
+
+
+
+ {/* Tab bar */}
+
+ {renderTabBar()}
+ {currentBlock?.description && (
+
+
+
+
+ )}
+
+
+ {/* Content area */}
+
+ {/* Value content with left border */}
+
+ {visibleValueLines.join("\n") || "(empty)"}
+
+
+ {/* Scroll down indicator or phantom row */}
+ {canScrollDown ? (
+
+ {" "}↓ {maxScrollOffset - scrollOffset} more line
+ {maxScrollOffset - scrollOffset !== 1 ? "s" : ""} below
+
+ ) : maxScrollOffset > 0 ? (
+
+ ) : null}
+
+
+ {/* Footer */}
+
+
+ {" "}
+ {formatCharCount(charCount, currentBlock?.limit ?? null)}
+ {currentBlock?.read_only ? " · read-only" : " · read/write"}
+
+
+ {" "}←→/Tab switch · ↑↓ scroll ·
+
+ Edit in ADE
+
+ · Esc cancel
+
+
+
+ );
+}
diff --git a/src/cli/components/MemoryViewer.tsx b/src/cli/components/MemoryViewer.tsx
deleted file mode 100644
index b973bf5..0000000
--- a/src/cli/components/MemoryViewer.tsx
+++ /dev/null
@@ -1,316 +0,0 @@
-import type { Block } from "@letta-ai/letta-client/resources/agents/blocks";
-import { Box, Text, useInput } from "ink";
-import Link from "ink-link";
-import { useState } from "react";
-import { colors } from "./colors";
-
-const PAGE_SIZE = 3; // Show 3 memory blocks per page
-const PREVIEW_LINES = 3; // Show 3 lines of content preview
-const DETAIL_DESCRIPTION_LINES = 3; // Max lines for description in detail view
-const DETAIL_VALUE_LINES = 12; // Visible lines for value content in detail view
-
-interface MemoryViewerProps {
- blocks: Block[];
- agentId: string;
- agentName: string | null;
- onClose: () => void;
- conversationId?: string;
-}
-
-/**
- * Truncate text to a certain number of lines
- */
-function truncateToLines(text: string, maxLines: number): string[] {
- const lines = text.split("\n").slice(0, maxLines);
- return lines;
-}
-
-/**
- * Format character count as "current / limit"
- */
-function formatCharCount(current: number, limit: number | null): string {
- if (limit === null || limit === undefined) {
- return `${current.toLocaleString()} chars`;
- }
- return `${current.toLocaleString()} / ${limit.toLocaleString()} chars`;
-}
-
-export function MemoryViewer({
- blocks,
- agentId,
- agentName,
- onClose,
- conversationId,
-}: MemoryViewerProps) {
- // Construct ADE URL for this agent's memory
- const adeUrl = `https://app.letta.com/agents/${agentId}?view=memory${conversationId ? `&conversation=${conversationId}` : ""}`;
- const [selectedIndex, setSelectedIndex] = useState(0);
- const [currentPage, setCurrentPage] = useState(0);
-
- // Detail view state
- const [detailBlockIndex, setDetailBlockIndex] = useState(null);
- const [scrollOffset, setScrollOffset] = useState(0);
-
- const totalPages = Math.ceil(blocks.length / PAGE_SIZE);
- const startIndex = currentPage * PAGE_SIZE;
- const visibleBlocks = blocks.slice(startIndex, startIndex + PAGE_SIZE);
-
- // Navigation within page and across pages
- const navigateUp = () => {
- if (selectedIndex > 0) {
- setSelectedIndex(selectedIndex - 1);
- } else if (currentPage > 0) {
- setCurrentPage(currentPage - 1);
- setSelectedIndex(PAGE_SIZE - 1);
- }
- };
-
- const navigateDown = () => {
- if (selectedIndex < visibleBlocks.length - 1) {
- setSelectedIndex(selectedIndex + 1);
- } else if (currentPage < totalPages - 1) {
- setCurrentPage(currentPage + 1);
- setSelectedIndex(0);
- }
- };
-
- // Get the block being viewed in detail
- const detailBlock =
- detailBlockIndex !== null ? blocks[detailBlockIndex] : null;
- const detailValueLines = detailBlock?.value?.split("\n") || [];
- const maxScrollOffset = Math.max(
- 0,
- detailValueLines.length - DETAIL_VALUE_LINES,
- );
-
- useInput((input, key) => {
- // CTRL-C: immediately close the entire viewer
- if (key.ctrl && input === "c") {
- onClose();
- return;
- }
-
- // ESC: exit detail view or close entirely
- if (key.escape) {
- if (detailBlockIndex !== null) {
- setDetailBlockIndex(null);
- setScrollOffset(0);
- } else {
- onClose();
- }
- return;
- }
-
- // Enter: open detail view for selected block
- if (key.return && detailBlockIndex === null) {
- const globalIndex = currentPage * PAGE_SIZE + selectedIndex;
- if (globalIndex < blocks.length) {
- setDetailBlockIndex(globalIndex);
- setScrollOffset(0);
- }
- return;
- }
-
- // j/k vim-style navigation (list or scroll)
- if (input === "j" || key.downArrow) {
- if (detailBlockIndex !== null) {
- // Scroll down in detail view
- setScrollOffset((prev) => Math.min(prev + 1, maxScrollOffset));
- } else {
- navigateDown();
- }
- } else if (input === "k" || key.upArrow) {
- if (detailBlockIndex !== null) {
- // Scroll up in detail view
- setScrollOffset((prev) => Math.max(prev - 1, 0));
- } else {
- navigateUp();
- }
- }
- });
-
- if (blocks.length === 0) {
- return (
-
-
- Memory Blocks
-
- No memory blocks attached to this agent.
- Press ESC to close
-
- );
- }
-
- // Detail view for a single block
- if (detailBlock) {
- const charCount = (detailBlock.value || "").length;
- const descriptionLines = truncateToLines(
- detailBlock.description || "",
- DETAIL_DESCRIPTION_LINES,
- );
- const visibleValueLines = detailValueLines.slice(
- scrollOffset,
- scrollOffset + DETAIL_VALUE_LINES,
- );
- const canScrollUp = scrollOffset > 0;
- const canScrollDown = scrollOffset < maxScrollOffset;
- const barColor = colors.selector.itemHighlighted;
-
- return (
-
- {/* Header */}
-
-
- Viewing the
-
- {detailBlock.label}
-
- block
- {detailBlock.read_only && (read-only)}
-
-
- {formatCharCount(charCount, detailBlock.limit ?? null)}
-
-
-
- View/edit in the ADE
-
- ↑↓/jk to scroll • ESC to go back
-
- {/* Description (up to 3 lines) */}
- {descriptionLines.length > 0 && (
-
- {descriptionLines.map((line) => (
-
- {line}
-
- ))}
-
- )}
-
- {/* Scrollable value content */}
-
- {/* Scroll up indicator */}
- {canScrollUp && (
-
- ↑ {scrollOffset} more line{scrollOffset !== 1 ? "s" : ""} above
-
- )}
-
- {/* Value content with left border */}
-
- {visibleValueLines.join("\n")}
-
-
- {/* Scroll down indicator */}
- {canScrollDown && (
-
- ↓ {maxScrollOffset - scrollOffset} more line
- {maxScrollOffset - scrollOffset !== 1 ? "s" : ""} below
-
- )}
-
-
- );
- }
-
- return (
-
- {/* Header */}
-
-
- Memory Blocks ({blocks.length} attached to {agentName || "agent"})
-
- {totalPages > 1 && (
-
- Page {currentPage + 1}/{totalPages}
-
- )}
-
-
- View/edit in the ADE
-
- ↑↓/jk to navigate • Enter to view • ESC to close
-
- {/* Block list */}
-
- {visibleBlocks.map((block, index) => {
- const isSelected = index === selectedIndex;
- const contentLines = truncateToLines(
- block.value || "",
- PREVIEW_LINES,
- );
- const charCount = (block.value || "").length;
-
- const barColor = isSelected
- ? colors.selector.itemHighlighted
- : colors.command.border;
- const hasEllipsis =
- (block.value || "").split("\n").length > PREVIEW_LINES;
-
- // Build content preview text
- const previewText = contentLines
- .map((line) =>
- line.length > 80 ? `${line.slice(0, 80)}...` : line,
- )
- .join("\n");
-
- return (
-
- {/* Header row: label + char count */}
-
-
-
- {block.label}
-
- {block.read_only && (read-only)}
-
-
- {formatCharCount(charCount, block.limit ?? null)}
-
-
-
- {/* Description (if available) */}
- {block.description && (
-
- {block.description.length > 60
- ? `${block.description.slice(0, 60)}...`
- : block.description}
-
- )}
-
- {/* Content preview */}
- {previewText}
-
- {/* Ellipsis if content is truncated */}
- {hasEllipsis && ...}
-
- );
- })}
-
-
- );
-}
diff --git a/src/cli/components/ModelSelector.tsx b/src/cli/components/ModelSelector.tsx
index df4e565..fe224e0 100644
--- a/src/cli/components/ModelSelector.tsx
+++ b/src/cli/components/ModelSelector.tsx
@@ -7,9 +7,13 @@ import {
getAvailableModelsCacheInfo,
} from "../../agent/available-models";
import { models } from "../../agent/model";
+import { useTerminalWidth } from "../hooks/useTerminalWidth";
import { colors } from "./colors";
-const PAGE_SIZE = 10;
+// Horizontal line character (matches approval dialogs)
+const SOLID_LINE = "─";
+
+const VISIBLE_ITEMS = 8;
type ModelCategory = "supported" | "all";
const MODEL_CATEGORIES: ModelCategory[] = ["supported", "all"];
@@ -41,9 +45,10 @@ export function ModelSelector({
filterProvider,
forceRefresh: forceRefreshOnMount,
}: ModelSelectorProps) {
+ const terminalWidth = useTerminalWidth();
+ const solidLine = SOLID_LINE.repeat(Math.max(terminalWidth, 10));
const typedModels = models as UiModel[];
const [category, setCategory] = useState("supported");
- const [currentPage, setCurrentPage] = useState(0);
const [selectedIndex, setSelectedIndex] = useState(0);
// undefined: not loaded yet (show spinner)
@@ -153,18 +158,27 @@ export function ModelSelector({
}));
}, [category, supportedModels, otherModelHandles]);
- // Pagination
- const totalPages = useMemo(
- () => Math.max(1, Math.ceil(currentList.length / PAGE_SIZE)),
- [currentList.length],
- );
+ // Show 1 fewer item in "all" category because Search line takes space
+ const visibleCount = category === "all" ? VISIBLE_ITEMS - 1 : VISIBLE_ITEMS;
+
+ // Scrolling - keep selectedIndex in view
+ const startIndex = useMemo(() => {
+ // Keep selected item in the visible window
+ if (selectedIndex < visibleCount) return 0;
+ return Math.min(
+ selectedIndex - visibleCount + 1,
+ Math.max(0, currentList.length - visibleCount),
+ );
+ }, [selectedIndex, currentList.length, visibleCount]);
const visibleModels = useMemo(() => {
- const start = currentPage * PAGE_SIZE;
- return currentList.slice(start, start + PAGE_SIZE);
- }, [currentList, currentPage]);
+ return currentList.slice(startIndex, startIndex + visibleCount);
+ }, [currentList, startIndex, visibleCount]);
- // Reset page and selection when category changes
+ const showScrollDown = startIndex + visibleCount < currentList.length;
+ const itemsBelow = currentList.length - startIndex - visibleCount;
+
+ // Reset selection when category changes
const cycleCategory = useCallback(() => {
setCategory((current) => {
const idx = MODEL_CATEGORIES.indexOf(current);
@@ -172,7 +186,6 @@ export function ModelSelector({
(idx + 1) % MODEL_CATEGORIES.length
] as ModelCategory;
});
- setCurrentPage(0);
setSelectedIndex(0);
setSearchQuery("");
}, []);
@@ -180,21 +193,21 @@ export function ModelSelector({
// Set initial selection to current model on mount
const initializedRef = useRef(false);
useEffect(() => {
- if (!initializedRef.current && visibleModels.length > 0) {
- const index = visibleModels.findIndex((m) => m.id === currentModelId);
+ if (!initializedRef.current && currentList.length > 0) {
+ const index = currentList.findIndex((m) => m.id === currentModelId);
if (index >= 0) {
setSelectedIndex(index);
}
initializedRef.current = true;
}
- }, [visibleModels, currentModelId]);
+ }, [currentList, currentModelId]);
// Clamp selectedIndex when list changes
useEffect(() => {
- if (selectedIndex >= visibleModels.length && visibleModels.length > 0) {
- setSelectedIndex(visibleModels.length - 1);
+ if (selectedIndex >= currentList.length && currentList.length > 0) {
+ setSelectedIndex(currentList.length - 1);
}
- }, [selectedIndex, visibleModels.length]);
+ }, [selectedIndex, currentList.length]);
useInput(
(input, key) => {
@@ -208,7 +221,6 @@ export function ModelSelector({
if (key.escape) {
if (searchQuery) {
setSearchQuery("");
- setCurrentPage(0);
setSelectedIndex(0);
} else {
onCancel();
@@ -231,50 +243,28 @@ export function ModelSelector({
if (key.backspace || key.delete) {
if (searchQuery) {
setSearchQuery((prev) => prev.slice(0, -1));
- setCurrentPage(0);
setSelectedIndex(0);
}
return;
}
// Disable other inputs while loading
- if (isLoading || refreshing || visibleModels.length === 0) {
+ if (isLoading || refreshing || currentList.length === 0) {
return;
}
if (key.upArrow) {
setSelectedIndex((prev) => Math.max(0, prev - 1));
} else if (key.downArrow) {
- setSelectedIndex((prev) =>
- Math.min(visibleModels.length - 1, prev + 1),
- );
- } else if (input === "j" || input === "J") {
- // Previous page
- if (currentPage > 0) {
- setCurrentPage((prev) => prev - 1);
- setSelectedIndex(0);
- }
- } else if (input === "k" || input === "K") {
- // Next page
- if (currentPage < totalPages - 1) {
- setCurrentPage((prev) => prev + 1);
- setSelectedIndex(0);
- }
- } else if (key.leftArrow && currentPage > 0) {
- setCurrentPage((prev) => prev - 1);
- setSelectedIndex(0);
- } else if (key.rightArrow && currentPage < totalPages - 1) {
- setCurrentPage((prev) => prev + 1);
- setSelectedIndex(0);
+ setSelectedIndex((prev) => Math.min(currentList.length - 1, prev + 1));
} else if (key.return) {
- const selectedModel = visibleModels[selectedIndex];
+ const selectedModel = currentList[selectedIndex];
if (selectedModel) {
onSelect(selectedModel.id);
}
} else if (category === "all" && input && input.length === 1) {
// Capture text input for search (only in "all" category)
setSearchQuery((prev) => prev + input);
- setCurrentPage(0);
setSelectedIndex(0);
}
},
@@ -284,65 +274,68 @@ export function ModelSelector({
const getCategoryLabel = (cat: ModelCategory) => {
if (cat === "supported") return `Recommended (${supportedModels.length})`;
- return `All Available Models (${otherModelHandles.length})`;
+ return `All Available (${otherModelHandles.length})`;
};
+ // Render tab bar (matches AgentSelector style)
+ const renderTabBar = () => (
+
+ {MODEL_CATEGORIES.map((cat) => {
+ const isActive = cat === category;
+ return (
+
+ {` ${getCategoryLabel(cat)} `}
+
+ );
+ })}
+
+ );
+
return (
-
-
+
+ {/* Command header */}
+ {"> /model"}
+ {solidLine}
+
+
+
+ {/* Title and tabs */}
+
- Select Model (↑↓ navigate, ←→/jk page, Tab category, Enter select, ESC
- cancel)
+ Swap your agent's model
{!isLoading && !refreshing && (
-
- Category:
- {MODEL_CATEGORIES.map((cat, i) => (
-
- {i > 0 && · }
-
- {getCategoryLabel(cat)}
-
-
- ))}
- (Tab to switch)
-
- )}
- {!isLoading && !refreshing && (
-
-
- Page {currentPage + 1}/{totalPages}
- {isCached ? " · cached" : ""} · 'r' to refresh
-
+
+ {renderTabBar()}
{category === "all" && (
- Search: {searchQuery || "(type to search)"}
+ Search: {searchQuery || "(type to filter)"}
)}
)}
+ {/* Loading states */}
{isLoading && (
-
+
Loading available models...
)}
{refreshing && (
-
+
Refreshing models...
)}
{error && (
-
+
Warning: Could not fetch available models. Showing all models.
@@ -350,7 +343,7 @@ export function ModelSelector({
)}
{!isLoading && !refreshing && visibleModels.length === 0 && (
-
+
{category === "supported"
? "No supported models available."
@@ -359,40 +352,61 @@ export function ModelSelector({
)}
+ {/* Model list */}
{visibleModels.map((model, index) => {
- const isSelected = index === selectedIndex;
+ const actualIndex = startIndex + index;
+ const isSelected = actualIndex === selectedIndex;
const isCurrent = model.id === currentModelId;
return (
-
+
- {isSelected ? "›" : " "}
+ {isSelected ? "> " : " "}
-
-
- {model.label}
- {isCurrent && (current)}
-
- {model.description && (
- {model.description}
- )}
-
+
+ {model.label}
+ {isCurrent && (current)}
+
+ {model.description && (
+ · {model.description}
+ )}
);
})}
+ {showScrollDown ? (
+
+ {" "}↓ {itemsBelow} more below
+
+ ) : currentList.length > visibleCount ? (
+
+ ) : null}
+
+ {/* Footer */}
+ {!isLoading && !refreshing && currentList.length > 0 && (
+
+
+ {" "}
+ {currentList.length} models{isCached ? " · cached" : ""} · R to
+ refresh
+
+
+ {" "}Enter select · ↑↓ navigate · Tab switch · Esc cancel
+
+
+ )}
);
}
diff --git a/src/cli/components/ProfileSelector.tsx b/src/cli/components/ProfileSelector.tsx
deleted file mode 100644
index a3c503a..0000000
--- a/src/cli/components/ProfileSelector.tsx
+++ /dev/null
@@ -1,384 +0,0 @@
-import type { AgentState } from "@letta-ai/letta-client/resources/agents/agents";
-import { Box, Text, useInput } from "ink";
-import { memo, useCallback, useEffect, useState } from "react";
-import { getClient } from "../../agent/client";
-import { settingsManager } from "../../settings-manager";
-import { useTerminalWidth } from "../hooks/useTerminalWidth";
-import { colors } from "./colors";
-
-interface ProfileSelectorProps {
- currentAgentId: string;
- onSelect: (agentId: string) => void;
- onUnpin: (agentId: string) => void;
- onCancel: () => void;
-}
-
-interface ProfileData {
- name: string;
- agentId: string;
- agent: AgentState | null;
- error: string | null;
- isLocal: boolean; // true = project-level pin, false = global pin
-}
-
-const DISPLAY_PAGE_SIZE = 5;
-
-/**
- * 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 provider/model-name
- */
-function formatModel(agent: AgentState): string {
- if (agent.model) {
- return agent.model;
- }
- if (agent.llm_config?.model) {
- const provider = agent.llm_config.model_endpoint_type || "unknown";
- return `${provider}/${agent.llm_config.model}`;
- }
- return "unknown";
-}
-
-type Mode = "browsing" | "confirming-delete";
-
-export const ProfileSelector = memo(function ProfileSelector({
- currentAgentId,
- onSelect,
- onUnpin,
- onCancel,
-}: ProfileSelectorProps) {
- const terminalWidth = useTerminalWidth();
- const [profiles, setProfiles] = useState([]);
- const [loading, setLoading] = useState(true);
- const [selectedIndex, setSelectedIndex] = useState(0);
- const [currentPage, setCurrentPage] = useState(0);
- const [mode, setMode] = useState("browsing");
- const [deleteConfirmIndex, setDeleteConfirmIndex] = useState(0);
-
- // Load pinned agents and fetch agent data
- const loadProfiles = useCallback(async () => {
- setLoading(true);
- try {
- const mergedPinned = settingsManager.getMergedPinnedAgents();
-
- if (mergedPinned.length === 0) {
- setProfiles([]);
- setLoading(false);
- return;
- }
-
- const client = await getClient();
-
- // Fetch agent data for each pinned agent
- const profileDataPromises = mergedPinned.map(
- async ({ agentId, isLocal }) => {
- try {
- const agent = await client.agents.retrieve(agentId, {
- include: ["agent.blocks"],
- });
- // Use agent name from server
- return { name: agent.name, agentId, agent, error: null, isLocal };
- } catch (_err) {
- return {
- name: agentId.slice(0, 12),
- agentId,
- agent: null,
- error: "Agent not found",
- isLocal,
- };
- }
- },
- );
-
- const profileData = await Promise.all(profileDataPromises);
- setProfiles(profileData);
- } catch (_err) {
- setProfiles([]);
- } finally {
- setLoading(false);
- }
- }, []);
-
- useEffect(() => {
- loadProfiles();
- }, [loadProfiles]);
-
- // Pagination
- const totalPages = Math.ceil(profiles.length / DISPLAY_PAGE_SIZE);
- const startIndex = currentPage * DISPLAY_PAGE_SIZE;
- const pageProfiles = profiles.slice(
- startIndex,
- startIndex + DISPLAY_PAGE_SIZE,
- );
-
- // Get currently selected profile
- const selectedProfile = pageProfiles[selectedIndex];
-
- useInput((input, key) => {
- // CTRL-C: immediately cancel (works even during loading)
- if (key.ctrl && input === "c") {
- onCancel();
- return;
- }
-
- if (loading) return;
-
- // Handle delete confirmation mode
- if (mode === "confirming-delete") {
- if (key.upArrow || key.downArrow) {
- setDeleteConfirmIndex((prev) => (prev === 0 ? 1 : 0));
- } else if (key.return) {
- if (deleteConfirmIndex === 0 && selectedProfile) {
- // Yes - unpin (onUnpin closes the selector)
- onUnpin(selectedProfile.agentId);
- return;
- } else {
- // No - cancel
- setMode("browsing");
- }
- } else if (key.escape) {
- setMode("browsing");
- }
- return;
- }
-
- // Browsing mode
- if (key.upArrow) {
- setSelectedIndex((prev) => Math.max(0, prev - 1));
- } else if (key.downArrow) {
- setSelectedIndex((prev) => Math.min(pageProfiles.length - 1, prev + 1));
- } else if (key.return) {
- if (selectedProfile?.agent) {
- onSelect(selectedProfile.agentId);
- }
- } else if (key.escape) {
- onCancel();
- } else if (input === "d" || input === "D") {
- if (selectedProfile) {
- setMode("confirming-delete");
- setDeleteConfirmIndex(1); // Default to "No"
- }
- } else if (input === "j" || input === "J") {
- // Previous page
- if (currentPage > 0) {
- setCurrentPage((prev) => prev - 1);
- setSelectedIndex(0);
- }
- } else if (input === "k" || input === "K") {
- // Next page
- if (currentPage < totalPages - 1) {
- setCurrentPage((prev) => prev + 1);
- setSelectedIndex(0);
- }
- } else if (input === "p" || input === "P") {
- if (selectedProfile) {
- // Unpin from current scope
- if (selectedProfile.isLocal) {
- settingsManager.unpinLocal(selectedProfile.agentId);
- } else {
- settingsManager.unpinGlobal(selectedProfile.agentId);
- }
- } else {
- // No profiles - pin the current agent
- settingsManager.pinLocal(currentAgentId);
- }
- // Reload profiles to reflect change
- loadProfiles();
- }
- });
-
- // Unpin confirmation UI
- if (mode === "confirming-delete" && selectedProfile) {
- const options = ["Yes, unpin", "No, cancel"];
- return (
-
-
-
- Unpin Agent
-
-
-
- Unpin "{selectedProfile.name}" from all locations?
-
-
- {options.map((option, index) => {
- const isSelected = index === deleteConfirmIndex;
- return (
-
-
- {isSelected ? ">" : " "} {option}
-
-
- );
- })}
-
-
- );
- }
-
- // Main browsing UI
- return (
-
-
-
- Pinned Agents
-
-
-
- {/* Loading state */}
- {loading && (
-
- Loading pinned agents...
-
- )}
-
- {/* Empty state */}
- {!loading && profiles.length === 0 && (
-
- No agents pinned.
- Press P to pin the current agent.
-
- Esc to close
-
-
- )}
-
- {/* Profile list */}
- {!loading && profiles.length > 0 && (
-
- {pageProfiles.map((profile, index) => {
- const isSelected = index === selectedIndex;
- const isCurrent = profile.agentId === currentAgentId;
- const hasAgent = profile.agent !== null;
-
- // Calculate available width for agent ID
- const nameLen = profile.name.length;
- const fixedChars = 2 + 3 + (isCurrent ? 10 : 0); // "> " + " · " + " (current)"
- const availableForId = Math.max(
- 15,
- terminalWidth - nameLen - fixedChars,
- );
- const displayId = truncateAgentId(profile.agentId, availableForId);
-
- return (
-
- {/* Row 1: Selection indicator, profile name, and ID */}
-
-
- {isSelected ? ">" : " "}
-
-
-
- {profile.name}
-
-
- {" "}
- · {profile.isLocal ? "project" : "global"} · {displayId}
-
- {isCurrent && (
- (current)
- )}
-
- {/* Row 2: Description or error */}
-
- {hasAgent ? (
-
- {profile.agent?.description || "No description"}
-
- ) : (
-
- {profile.error}
-
- )}
-
- {/* Row 3: Metadata (only if agent exists) */}
- {hasAgent && profile.agent && (
-
-
- {formatRelativeTime(profile.agent.last_run_completion)} ·{" "}
- {profile.agent.blocks?.length ?? 0} memory block
- {(profile.agent.blocks?.length ?? 0) === 1 ? "" : "s"} ·{" "}
- {formatModel(profile.agent)}
-
-
- )}
-
- );
- })}
-
- )}
-
- {/* Footer with pagination and controls */}
- {!loading && profiles.length > 0 && (
-
- {totalPages > 1 && (
-
-
- Page {currentPage + 1}/{totalPages}
-
-
- )}
-
-
- ↑↓ navigate · Enter load · P unpin · D unpin all · Esc close
-
-
-
- )}
-
- {/* Footer for empty state already handled above */}
-
- );
-});
-
-ProfileSelector.displayName = "ProfileSelector";
diff --git a/src/cli/components/ResumeSelector.tsx b/src/cli/components/ResumeSelector.tsx
deleted file mode 100644
index f329658..0000000
--- a/src/cli/components/ResumeSelector.tsx
+++ /dev/null
@@ -1,789 +0,0 @@
-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 = {
- 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
- */
-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(null);
-
- // 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;
-
- 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;
-
- // For pinned tab, use pinnedPageAgents.length to include "not found" entries
- // For other tabs, use currentAgents.length
- const maxIndex =
- activeTab === "pinned"
- ? pinnedPageAgents.length - 1
- : (currentAgents as AgentState[]).length - 1;
-
- if (key.upArrow) {
- setCurrentSelectedIndex((prev: number) => Math.max(0, prev - 1));
- } else if (key.downArrow) {
- setCurrentSelectedIndex((prev: number) => Math.min(maxIndex, 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);
- }
- }
- // NOTE: "D" for unpin all disabled - too destructive without confirmation
- // } else if (activeTab === "pinned" && (input === "d" || input === "D")) {
- // 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 = () => (
-
- {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
-
-
- {renderTabBar()}
- {TAB_DESCRIPTIONS[activeTab]}
-
-
-
- {/* Search input - list tabs only */}
- {activeTab !== "pinned" && (searchInput || activeQuery) && (
-
- Search:
- {searchInput}
- {searchInput && searchInput !== activeQuery && (
- (press Enter to search)
- )}
- {activeQuery && searchInput === activeQuery && (
- (Esc to clear)
- )}
-
- )}
-
- {/* Error state - list tabs */}
- {activeTab !== "pinned" && currentError && (
-
- Error: {currentError}
- Press ESC to cancel
-
- )}
-
- {/* Loading state */}
- {currentLoading && (
-
- Loading agents...
-
- )}
-
- {/* Empty state */}
- {!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" : " · Type to search"}
-
-
-
- )}
-
- );
-}
diff --git a/src/cli/components/SlashCommandAutocomplete.tsx b/src/cli/components/SlashCommandAutocomplete.tsx
index f1509c4..66d97da 100644
--- a/src/cli/components/SlashCommandAutocomplete.tsx
+++ b/src/cli/components/SlashCommandAutocomplete.tsx
@@ -1,12 +1,9 @@
import { Text } from "ink";
-import Link from "ink-link";
import { useEffect, useMemo, useState } from "react";
import { settingsManager } from "../../settings-manager";
-import { getVersion } from "../../version";
import { commands } from "../commands/registry";
import { useAutocompleteNavigation } from "../hooks/useAutocompleteNavigation";
import { AutocompleteBox, AutocompleteItem } from "./Autocomplete";
-import { colors } from "./colors";
import type { AutocompleteProps, CommandMatch } from "./types/autocomplete";
const VISIBLE_COMMANDS = 8; // Number of commands visible at once
@@ -179,12 +176,10 @@ export function SlashCommandAutocomplete({
startIndex,
startIndex + VISIBLE_COMMANDS,
);
- const showScrollUp = startIndex > 0;
const showScrollDown = startIndex + VISIBLE_COMMANDS < totalMatches;
return (
-
- {showScrollUp && ↑ {startIndex} more above}
+
{visibleMatches.map((item, idx) => {
const actualIndex = startIndex + idx;
return (
@@ -197,20 +192,13 @@ export function SlashCommandAutocomplete({
);
})}
- {showScrollDown && (
+ {showScrollDown ? (
- {" "}
- ↓ {totalMatches - startIndex - VISIBLE_COMMANDS} more below
+ {" "}↓ {totalMatches - startIndex - VISIBLE_COMMANDS} more below
- )}
-
-
- Having issues? Report bugs with /feedback or{" "}
-
- join our Discord ↗
-
-
- Version: Letta Code v{getVersion()}
+ ) : needsScrolling ? (
+
+ ) : null}
);
}
diff --git a/src/cli/components/SystemPromptSelector.tsx b/src/cli/components/SystemPromptSelector.tsx
index 774efe8..627f399 100644
--- a/src/cli/components/SystemPromptSelector.tsx
+++ b/src/cli/components/SystemPromptSelector.tsx
@@ -2,8 +2,12 @@
import { Box, Text, useInput } from "ink";
import { useMemo, useState } from "react";
import { SYSTEM_PROMPTS } from "../../agent/promptAssets";
+import { useTerminalWidth } from "../hooks/useTerminalWidth";
import { colors } from "./colors";
+// Horizontal line character (matches approval dialogs)
+const SOLID_LINE = "─";
+
interface SystemPromptSelectorProps {
currentPromptId?: string;
onSelect: (promptId: string) => void;
@@ -15,6 +19,8 @@ export function SystemPromptSelector({
onSelect,
onCancel,
}: SystemPromptSelectorProps) {
+ const terminalWidth = useTerminalWidth();
+ const solidLine = SOLID_LINE.repeat(Math.max(terminalWidth, 10));
const [showAll, setShowAll] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(0);
@@ -61,10 +67,17 @@ export function SystemPromptSelector({
});
return (
-
-
+
+ {/* Command header */}
+ {"> /prompt"}
+ {solidLine}
+
+
+
+ {/* Title */}
+
- Select System Prompt (↑↓ to navigate, Enter to select, ESC to cancel)
+ Swap your agent's system prompt
@@ -74,31 +87,27 @@ export function SystemPromptSelector({
const isCurrent = prompt.id === currentPromptId;
return (
-
+
- {isSelected ? "›" : " "}
+ {isSelected ? "> " : " "}
-
-
- {prompt.label}
- {isCurrent && (
- (current)
- )}
-
- {prompt.description}
-
+
+ {prompt.label}
+ {isCurrent && (
+ (current)
+ )}
+
+ · {prompt.description}
);
})}
{hasShowAllOption && (
-
+
- {selectedIndex === visiblePrompts.length ? "›" : " "}
+ {selectedIndex === visiblePrompts.length ? "> " : " "}
Show all prompts
)}
+
+ {/* Footer */}
+
+ {" "}Enter select · ↑↓ navigate · Esc cancel
+
);
}
diff --git a/src/cli/components/ToolsetSelector.tsx b/src/cli/components/ToolsetSelector.tsx
index 84f51aa..16fef93 100644
--- a/src/cli/components/ToolsetSelector.tsx
+++ b/src/cli/components/ToolsetSelector.tsx
@@ -1,8 +1,12 @@
// Import useInput from vendored Ink for bracketed paste support
import { Box, Text, useInput } from "ink";
import { useMemo, useState } from "react";
+import { useTerminalWidth } from "../hooks/useTerminalWidth";
import { colors } from "./colors";
+// Horizontal line character (matches approval dialogs)
+const SOLID_LINE = "─";
+
type ToolsetId =
| "codex"
| "codex_snake"
@@ -120,6 +124,8 @@ export function ToolsetSelector({
onSelect,
onCancel,
}: ToolsetSelectorProps) {
+ const terminalWidth = useTerminalWidth();
+ const solidLine = SOLID_LINE.repeat(Math.max(terminalWidth, 10));
const [showAll, setShowAll] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(0);
@@ -166,10 +172,17 @@ export function ToolsetSelector({
});
return (
-
-
+
+ {/* Command header */}
+ {"> /toolset"}
+ {solidLine}
+
+
+
+ {/* Title */}
+
- Select Toolset (↑↓ to navigate, Enter to select, ESC to cancel)
+ Swap your agent's toolset
@@ -179,40 +192,36 @@ export function ToolsetSelector({
const isCurrent = toolset.id === currentToolset;
return (
-
-
+
+
- {isSelected ? "›" : " "}
+ {isSelected ? "> " : " "}
+
+
+ {toolset.label}
+ {isCurrent && (
+ (current)
+ )}
-
-
-
- {toolset.label}
- {isCurrent && (
-
- {" "}
- (current)
-
- )}
-
-
- {toolset.description}
-
+
+ {" "}
+ {toolset.description}
+
);
})}
{hasShowAllOption && (
-
+
- {selectedIndex === visibleToolsets.length ? "›" : " "}
+ {selectedIndex === visibleToolsets.length ? "> " : " "}
Show all toolsets
)}
+
+ {/* Footer */}
+
+ {" "}Enter select · ↑↓ navigate · Esc cancel
+
);
}