Swap command (#84)

Co-authored-by: Shubham Naik <shub@memgpt.ai>
This commit is contained in:
Shubham Naik
2025-11-17 11:26:48 -08:00
committed by GitHub
parent 9212dfb52b
commit d3b9eb7245
3 changed files with 423 additions and 2 deletions

View File

@@ -0,0 +1,184 @@
import type { AgentState } from "@letta-ai/letta-client/resources/agents/agents";
import { Box, Text, useInput } from "ink";
import { useEffect, useState } from "react";
import { getClient } from "../../agent/client";
import { colors } from "./colors";
interface AgentSelectorProps {
currentAgentId: string;
onSelect: (agentId: string) => void;
onCancel: () => void;
}
export function AgentSelector({
currentAgentId,
onSelect,
onCancel,
}: AgentSelectorProps) {
const [agents, setAgents] = useState<AgentState[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedIndex, setSelectedIndex] = useState(0);
const [searchQuery, setSearchQuery] = useState("");
const [debouncedQuery, setDebouncedQuery] = useState("");
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);
}
};
fetchAgents();
}, []);
// Debounce search query (300ms delay)
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedQuery(searchQuery);
}, 300);
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
useEffect(() => {
setSelectedIndex(0);
}, []);
useInput((input, key) => {
if (loading || error) return;
if (key.upArrow) {
setSelectedIndex((prev) => Math.max(0, prev - 1));
} else if (key.downArrow) {
setSelectedIndex((prev) => Math.min(filteredAgents.length - 1, prev + 1));
} else if (key.return) {
const selectedAgent = filteredAgents[selectedIndex];
if (selectedAgent?.id) {
onSelect(selectedAgent.id);
}
} else if (key.escape) {
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 (loading) {
return (
<Box flexDirection="column">
<Text color={colors.selector.title}>Loading agents...</Text>
</Box>
);
}
if (error) {
return (
<Box flexDirection="column">
<Text color="red">Error loading agents: {error}</Text>
<Text dimColor>Press ESC to cancel</Text>
</Box>
);
}
if (agents.length === 0) {
return (
<Box flexDirection="column">
<Text color={colors.selector.title}>No agents found</Text>
<Text dimColor>Press ESC to cancel</Text>
</Box>
);
}
return (
<Box flexDirection="column" gap={1}>
<Box>
<Text bold color={colors.selector.title}>
Select Agent ( to navigate, Enter to select, ESC to cancel)
</Text>
</Box>
<Box>
<Text dimColor>Search: </Text>
<Text>{searchQuery || "_"}</Text>
</Box>
{filteredAgents.length === 0 && (
<Box>
<Text dimColor>No agents match your search</Text>
</Box>
)}
{filteredAgents.length > 0 && (
<Box>
<Text dimColor>
Showing {filteredAgents.length}
{matchingAgents.length > 10 ? ` of ${matchingAgents.length}` : ""}
{debouncedQuery ? " matching" : ""} agents
</Text>
</Box>
)}
<Box flexDirection="column">
{filteredAgents.map((agent, index) => {
const isSelected = index === selectedIndex;
const isCurrent = agent.id === currentAgentId;
const lastInteractedAt = agent.last_run_completion
? new Date(agent.last_run_completion).toLocaleString()
: "Never";
return (
<Box key={agent.id} flexDirection="row" gap={1}>
<Text
color={isSelected ? colors.selector.itemHighlighted : undefined}
>
{isSelected ? "" : " "}
</Text>
<Box flexDirection="row" gap={2}>
<Text
bold={isSelected}
color={
isSelected ? colors.selector.itemHighlighted : undefined
}
wrap="truncate-end"
>
{agent.name || "Unnamed"}
{isCurrent && (
<Text color={colors.selector.itemCurrent}> (current)</Text>
)}
</Text>
<Text dimColor wrap="truncate-end">
{agent.id}
</Text>
<Text dimColor wrap="truncate-end">
{lastInteractedAt}
</Text>
</Box>
</Box>
);
})}
</Box>
</Box>
);
}