import type { Letta } from "@letta-ai/letta-client"; import type { MessageSearchResponse } from "@letta-ai/letta-client/resources/messages"; import { Box, Text, useInput } from "ink"; import Link from "ink-link"; import { useCallback, useEffect, useRef, useState } from "react"; import { getClient } from "../../agent/client"; import { useTerminalWidth } from "../hooks/useTerminalWidth"; import { colors } from "./colors"; interface MessageSearchProps { onClose: () => void; initialQuery?: string; } const DISPLAY_PAGE_SIZE = 5; const SEARCH_LIMIT = 100; // Max results from API type SearchMode = "hybrid" | "vector" | "fts"; const SEARCH_MODES: SearchMode[] = ["hybrid", "vector", "fts"]; /** * Format a relative time string from a date */ function formatRelativeTime(dateStr: string | null | undefined): string { if (!dateStr) return ""; 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}m ago`; if (diffHours < 24) return `${diffHours}h ago`; if (diffDays < 7) return `${diffDays}d ago`; return `${diffWeeks}w ago`; } /** * Format a timestamp in local timezone */ function formatLocalTime(dateStr: string | null | undefined): string { if (!dateStr) return ""; const date = new Date(dateStr); // Format: "Dec 15, 6:30 PM" or "Dec 15, 2024, 6:30 PM" depending on year const now = new Date(); const sameYear = date.getFullYear() === now.getFullYear(); const options: Intl.DateTimeFormatOptions = { month: "short", day: "numeric", ...(sameYear ? {} : { year: "numeric" }), hour: "numeric", minute: "2-digit", }; return date.toLocaleString(undefined, options); } /** * Truncate text to fit width, adding ellipsis if needed */ function truncateText(text: string, maxWidth: number): string { if (text.length <= maxWidth) return text; return `${text.slice(0, maxWidth - 3)}...`; } /** * Get display text from a message */ function getMessageText(msg: MessageSearchResponse[number]): string { // Assistant message content if ("content" in msg) { const content = msg.content; if (typeof content === "string") return content; if (Array.isArray(content)) { const textPart = content.find( (c) => typeof c === "object" && c && "text" in c, ); if (textPart && typeof textPart === "object" && "text" in textPart) { return String(textPart.text); } } } // Text field (user messages, etc) if ("text" in msg && typeof msg.text === "string") { return msg.text; } // Reasoning messages if ("reasoning" in msg && typeof msg.reasoning === "string") { return msg.reasoning; } // Tool call messages if ("tool_call" in msg && msg.tool_call) { const tc = msg.tool_call as { name?: string; arguments?: string }; return `Tool: ${tc.name || "unknown"}`; } // Tool return messages - show tool name and preview of return if ("tool_return" in msg) { const toolName = "name" in msg ? (msg.name as string) : "tool"; const returnValue = msg.tool_return as string; // Truncate long return values const preview = returnValue?.slice(0, 100) || ""; return `${toolName}: ${preview}`; } return `[${msg.message_type || "unknown"}]`; } export function MessageSearch({ onClose, initialQuery }: MessageSearchProps) { const terminalWidth = useTerminalWidth(); const [searchInput, setSearchInput] = useState(initialQuery ?? ""); const [activeQuery, setActiveQuery] = useState(initialQuery ?? ""); const [searchMode, setSearchMode] = useState("hybrid"); const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [currentPage, setCurrentPage] = useState(0); const [selectedIndex, setSelectedIndex] = useState(0); const clientRef = useRef(null); // Execute search const executeSearch = useCallback(async (query: string, mode: SearchMode) => { if (!query.trim()) return; setLoading(true); setError(null); try { const client = clientRef.current || (await getClient()); clientRef.current = client; // Direct API call since client.messages.search doesn't exist yet in SDK const searchResults = await client.post( "/v1/messages/search", { body: { query: query.trim(), search_mode: mode, limit: SEARCH_LIMIT, }, }, ); setResults(searchResults); setCurrentPage(0); setSelectedIndex(0); } catch (err) { setError(err instanceof Error ? err.message : String(err)); setResults([]); } finally { setLoading(false); } }, []); // Submit search const submitSearch = useCallback(() => { if (searchInput.trim() && searchInput !== activeQuery) { setActiveQuery(searchInput); executeSearch(searchInput, searchMode); } }, [searchInput, activeQuery, searchMode, executeSearch]); // Clear search const clearSearch = useCallback(() => { setSearchInput(""); setActiveQuery(""); setResults([]); setCurrentPage(0); setSelectedIndex(0); }, []); // Cycle search mode const cycleSearchMode = useCallback(() => { setSearchMode((current) => { const currentIndex = SEARCH_MODES.indexOf(current); const nextIndex = (currentIndex + 1) % SEARCH_MODES.length; return SEARCH_MODES[nextIndex] as SearchMode; }); }, []); // Re-run search when mode changes (if there's an active query) useEffect(() => { if (activeQuery) { executeSearch(activeQuery, searchMode); } }, [searchMode, activeQuery, executeSearch]); // Calculate pagination const totalPages = Math.ceil(results.length / DISPLAY_PAGE_SIZE); const startIndex = currentPage * DISPLAY_PAGE_SIZE; const pageResults = results.slice(startIndex, startIndex + DISPLAY_PAGE_SIZE); useInput((input, key) => { // CTRL-C: immediately close (bypasses search clearing) if (key.ctrl && input === "c") { onClose(); return; } if (key.escape) { if (searchInput || activeQuery) { clearSearch(); } else { onClose(); } } else if (key.return) { submitSearch(); } else if (key.backspace || key.delete) { setSearchInput((prev) => prev.slice(0, -1)); } else if (key.tab) { // Tab cycles search mode cycleSearchMode(); } else if (key.upArrow) { setSelectedIndex((prev) => Math.max(0, prev - 1)); } else if (key.downArrow) { setSelectedIndex((prev) => Math.min(pageResults.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 (input && !key.ctrl && !key.meta) { setSearchInput((prev) => prev + input); } }); return ( Search messages across all agents {/* Search input and mode */} Search: {searchInput ? ( <> {searchInput} {searchInput !== activeQuery && ( (press Enter to search) )} ) : ( (type your query) )} Mode: {SEARCH_MODES.map((mode, i) => ( {i > 0 && · } {mode} ))} (Tab to change) {/* Error state */} {error && ( Error: {error} )} {/* Loading state */} {loading && ( Searching... )} {/* No results */} {!loading && activeQuery && results.length === 0 && ( No results found for "{activeQuery}" )} {/* Results list */} {!loading && results.length > 0 && ( {pageResults.map( (msg: MessageSearchResponse[number], index: number) => { const isSelected = index === selectedIndex; const messageText = getMessageText(msg); // All messages have a date field const msgWithDate = msg as { date?: string; created_at?: string; agent_id?: string; conversation_id?: string; }; const timestamp = msgWithDate.date ? formatRelativeTime(msgWithDate.date) : ""; const msgType = (msg.message_type || "unknown").replace( "_message", "", ); const agentId = msgWithDate.agent_id || "unknown"; const conversationId = msgWithDate.conversation_id; const createdAt = formatLocalTime(msgWithDate.created_at); // Calculate available width for message text const metaWidth = timestamp.length + msgType.length + 10; // padding const availableWidth = Math.max( 20, terminalWidth - metaWidth - 4, ); const displayText = truncateText( messageText.replace(/\n/g, " "), availableWidth, ); // Use message id + index for guaranteed uniqueness (search can return same message multiple times) const msgId = "message_id" in msg ? String(msg.message_id) : "result"; const uniqueKey = `${msgId}-${startIndex + index}`; return ( {isSelected ? ">" : " "} {displayText} {msgType} {timestamp && ` · ${timestamp}`} {agentId && ( <> · view message · agent: {agentId} )} {createdAt && · {createdAt}} ); }, )} )} {/* Footer */} {results.length > 0 && ( Page {currentPage + 1}/{totalPages || 1} ({results.length}{" "} results) )} Type + Enter to search · Tab mode · J/K page · Esc close ); }