diff --git a/bun.lock b/bun.lock
index f41c82d..5f16797 100644
--- a/bun.lock
+++ b/bun.lock
@@ -1,11 +1,10 @@
{
"lockfileVersion": 1,
- "configVersion": 0,
"workspaces": {
"": {
"name": "@letta-ai/letta-code",
"dependencies": {
- "@letta-ai/letta-client": "^1.7.2",
+ "@letta-ai/letta-client": "1.7.5",
"glob": "^13.0.0",
"ink-link": "^5.0.0",
"open": "^10.2.0",
@@ -91,7 +90,7 @@
"@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="],
- "@letta-ai/letta-client": ["@letta-ai/letta-client@1.7.2", "", {}, "sha512-88XLWwacqTcqL1uZ8QCKmxPIzTlgHAPVbyxVrNLMKeDdqp6Vs7y1WIRFBdEvGy2WJmUgOt5HTUJSnJZevhPX7A=="],
+ "@letta-ai/letta-client": ["@letta-ai/letta-client@1.7.5", "", {}, "sha512-fyzJ9Bj+8Jf/LGDsPoijwKkddXJl3lII8FDUNkQipV6MQS6vgR+7vrL0QtwMgpwXZr1f47MNb5+Y0O1/TDDsJA=="],
"@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="],
diff --git a/package-lock.json b/package-lock.json
index 14a0450..93f1ee3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,7 +10,7 @@
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
- "@letta-ai/letta-client": "^1.7.2",
+ "@letta-ai/letta-client": "^1.7.4",
"glob": "^13.0.0",
"ink-link": "^5.0.0",
"open": "^10.2.0",
@@ -550,9 +550,9 @@
}
},
"node_modules/@letta-ai/letta-client": {
- "version": "1.7.2",
- "resolved": "https://registry.npmjs.org/@letta-ai/letta-client/-/letta-client-1.7.2.tgz",
- "integrity": "sha512-88XLWwacqTcqL1uZ8QCKmxPIzTlgHAPVbyxVrNLMKeDdqp6Vs7y1WIRFBdEvGy2WJmUgOt5HTUJSnJZevhPX7A==",
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/@letta-ai/letta-client/-/letta-client-1.7.4.tgz",
+ "integrity": "sha512-TLaXDUDCuW6f0LVEOX7wskjlw5MBqkIozz/JPnVhuK2Rn8ZKns5qVQeZUYG5a536+nxmoGT9ZgkGkXAVrN9WKQ==",
"license": "Apache-2.0"
},
"node_modules/@types/bun": {
diff --git a/package.json b/package.json
index 9078c15..c381c09 100644
--- a/package.json
+++ b/package.json
@@ -30,7 +30,7 @@
"access": "public"
},
"dependencies": {
- "@letta-ai/letta-client": "^1.7.2",
+ "@letta-ai/letta-client": "1.7.5",
"glob": "^13.0.0",
"ink-link": "^5.0.0",
"open": "^10.2.0",
diff --git a/src/agent/message.ts b/src/agent/message.ts
index f964430..6b7bb51 100644
--- a/src/agent/message.ts
+++ b/src/agent/message.ts
@@ -81,7 +81,7 @@ export async function sendMessageStream(
conversationId,
{
messages: messages,
- streaming: true,
+ stream: true,
stream_tokens: opts.streamTokens ?? true,
background: opts.background ?? true,
client_tools: getClientToolsFromRegistry(),
diff --git a/src/cli/App.tsx b/src/cli/App.tsx
index 248b4ff..c31bab5 100644
--- a/src/cli/App.tsx
+++ b/src/cli/App.tsx
@@ -3670,7 +3670,10 @@ export default function App({
}, [processConversation]);
const handleAgentSelect = useCallback(
- async (targetAgentId: string, _opts?: { profileName?: string }) => {
+ async (
+ targetAgentId: string,
+ opts?: { profileName?: string; conversationId?: string },
+ ) => {
// Close selector immediately
setActiveOverlay(null);
@@ -3733,9 +3736,8 @@ export default function App({
// Fetch new agent
const agent = await client.agents.retrieve(targetAgentId);
- // Use the agent's default conversation when switching agents
- // User can /new to start a fresh conversation if needed
- const targetConversationId = "default";
+ // Use specified conversation or default to the agent's default conversation
+ const targetConversationId = opts?.conversationId ?? "default";
// Update project settings with new agent
await updateProjectSettings({ lastAgent: targetAgentId });
@@ -3768,13 +3770,20 @@ export default function App({
setLlmConfig(agent.llm_config);
setConversationId(targetConversationId);
- // Build success message - resumed default conversation
+ // Build success message
const agentLabel = agent.name || targetAgentId;
- const successOutput = [
- `Resumed the default conversation with **${agentLabel}**.`,
- `⎿ Type /resume to browse all conversations`,
- `⎿ Type /new to start a new conversation`,
- ].join("\n");
+ const isSpecificConv =
+ opts?.conversationId && opts.conversationId !== "default";
+ const successOutput = isSpecificConv
+ ? [
+ `Switched to **${agentLabel}**`,
+ `⎿ Conversation: ${opts.conversationId}`,
+ ].join("\n")
+ : [
+ `Resumed the default conversation with **${agentLabel}**.`,
+ `⎿ Type /resume to browse all conversations`,
+ `⎿ Type /new to start a new conversation`,
+ ].join("\n");
const successItem: StaticItem = {
kind: "command",
id: uid("cmd"),
@@ -8693,7 +8702,9 @@ Plan file path: ${planFilePath}`;
) : item.kind === "status" ? (
) : item.kind === "separator" ? (
- {"─".repeat(columns)}
+
+ {"─".repeat(columns)}
+
) : item.kind === "command" ? (
) : item.kind === "bash_command" ? (
@@ -9298,6 +9309,185 @@ Plan file path: ${planFilePath}`;
{
+ closeOverlay();
+
+ // Different agent: use handleAgentSelect (which supports optional conversationId)
+ if (targetAgentId !== agentId) {
+ await handleAgentSelect(targetAgentId, {
+ conversationId: targetConvId,
+ });
+ return;
+ }
+
+ // Normalize undefined/null to "default"
+ const actualTargetConv = targetConvId || "default";
+
+ // Same agent, same conversation: nothing to do
+ if (actualTargetConv === conversationId) {
+ return;
+ }
+
+ // Same agent, different conversation: switch conversation
+ // (Reuses ConversationSelector's onSelect logic pattern)
+ if (isAgentBusy()) {
+ setQueuedOverlayAction({
+ type: "switch_conversation",
+ conversationId: actualTargetConv,
+ });
+ const cmdId = uid("cmd");
+ buffersRef.current.byId.set(cmdId, {
+ kind: "command",
+ id: cmdId,
+ input: "/search",
+ output: `Conversation switch queued – will switch after current task completes`,
+ phase: "finished",
+ success: true,
+ });
+ buffersRef.current.order.push(cmdId);
+ refreshDerived();
+ return;
+ }
+
+ setCommandRunning(true);
+ const cmdId = uid("cmd");
+ buffersRef.current.byId.set(cmdId, {
+ kind: "command",
+ id: cmdId,
+ input: "/search",
+ output: "Switching conversation...",
+ phase: "running",
+ });
+ buffersRef.current.order.push(cmdId);
+ refreshDerived();
+
+ try {
+ if (agentState) {
+ const client = await getClient();
+ const resumeData = await getResumeData(
+ client,
+ agentState,
+ actualTargetConv,
+ );
+
+ setConversationId(actualTargetConv);
+ settingsManager.setLocalLastSession(
+ { agentId, conversationId: actualTargetConv },
+ process.cwd(),
+ );
+ settingsManager.setGlobalLastSession({
+ agentId,
+ conversationId: actualTargetConv,
+ });
+
+ // Clear current transcript and static items
+ buffersRef.current.byId.clear();
+ buffersRef.current.order = [];
+ buffersRef.current.tokenCount = 0;
+ emittedIdsRef.current.clear();
+ setStaticItems([]);
+ setStaticRenderEpoch((e) => e + 1);
+
+ const currentAgentName =
+ agentState.name || "Unnamed Agent";
+ const successOutput = [
+ `Switched to conversation with "${currentAgentName}"`,
+ `⎿ Conversation: ${actualTargetConv}`,
+ ].join("\n");
+ const successItem: StaticItem = {
+ kind: "command",
+ id: uid("cmd"),
+ input: "/search",
+ output: successOutput,
+ phase: "finished",
+ success: true,
+ };
+
+ // Backfill message history
+ if (resumeData.messageHistory.length > 0) {
+ hasBackfilledRef.current = false;
+ backfillBuffers(
+ buffersRef.current,
+ resumeData.messageHistory,
+ );
+ const backfilledItems: StaticItem[] = [];
+ for (const id of buffersRef.current.order) {
+ const ln = buffersRef.current.byId.get(id);
+ if (!ln) continue;
+ emittedIdsRef.current.add(id);
+ backfilledItems.push({ ...ln } as StaticItem);
+ }
+ const separator = {
+ kind: "separator" as const,
+ id: uid("sep"),
+ };
+ setStaticItems([
+ separator,
+ ...backfilledItems,
+ successItem,
+ ]);
+ setLines(toLines(buffersRef.current));
+ hasBackfilledRef.current = true;
+ } else {
+ const separator = {
+ kind: "separator" as const,
+ id: uid("sep"),
+ };
+ setStaticItems([separator, successItem]);
+ setLines(toLines(buffersRef.current));
+ }
+
+ // Restore pending approvals if any
+ if (resumeData.pendingApprovals.length > 0) {
+ setPendingApprovals(resumeData.pendingApprovals);
+ try {
+ const contexts = await Promise.all(
+ resumeData.pendingApprovals.map(
+ async (approval) => {
+ const parsedArgs = safeJsonParseOr<
+ Record
+ >(approval.toolArgs, {});
+ return await analyzeToolApproval(
+ approval.toolName,
+ parsedArgs,
+ );
+ },
+ ),
+ );
+ setApprovalContexts(contexts);
+ } catch {
+ // If analysis fails, leave context as null
+ }
+ }
+ }
+ } catch (error) {
+ let errorMsg = "Unknown error";
+ if (error instanceof APIError) {
+ if (error.status === 404) {
+ errorMsg = "Conversation not found";
+ } else if (error.status === 422) {
+ errorMsg = "Invalid conversation ID";
+ } else {
+ errorMsg = error.message;
+ }
+ } else if (error instanceof Error) {
+ errorMsg = error.message;
+ }
+ buffersRef.current.byId.set(cmdId, {
+ kind: "command",
+ id: cmdId,
+ input: "/search",
+ output: `Failed: ${errorMsg}`,
+ phase: "finished",
+ success: false,
+ });
+ refreshDerived();
+ } finally {
+ setCommandRunning(false);
+ }
+ }}
/>
)}
diff --git a/src/cli/components/MessageSearch.tsx b/src/cli/components/MessageSearch.tsx
index 5389bef..5148a6b 100644
--- a/src/cli/components/MessageSearch.tsx
+++ b/src/cli/components/MessageSearch.tsx
@@ -1,43 +1,33 @@
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";
+// Horizontal line character (matches approval dialogs)
+const SOLID_LINE = "─";
+
interface MessageSearchProps {
onClose: () => void;
initialQuery?: string;
+ /** Current agent ID for "current agent" filter */
+ agentId?: string;
+ /** Current conversation ID for "current conv" filter */
+ conversationId?: string;
+ /** Callback when user wants to open a conversation */
+ onOpenConversation?: (agentId: string, conversationId?: string) => void;
}
-const DISPLAY_PAGE_SIZE = 5;
+const VISIBLE_ITEMS = 5;
const SEARCH_LIMIT = 100; // Max results from API
type SearchMode = "hybrid" | "vector" | "fts";
-const SEARCH_MODES: SearchMode[] = ["hybrid", "vector", "fts"];
+const SEARCH_MODES: SearchMode[] = ["fts", "vector", "hybrid"]; // Display order (hybrid is default)
-/**
- * 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`;
-}
+type SearchRange = "all" | "agent" | "conv";
+const SEARCH_RANGES: SearchRange[] = ["all", "agent", "conv"];
/**
* Format a timestamp in local timezone
@@ -110,89 +100,234 @@ function getMessageText(msg: MessageSearchResponse[number]): string {
return `[${msg.message_type || "unknown"}]`;
}
-export function MessageSearch({ onClose, initialQuery }: MessageSearchProps) {
+export function MessageSearch({
+ onClose,
+ initialQuery,
+ agentId,
+ conversationId,
+ onOpenConversation,
+}: MessageSearchProps) {
const terminalWidth = useTerminalWidth();
const [searchInput, setSearchInput] = useState(initialQuery ?? "");
const [activeQuery, setActiveQuery] = useState(initialQuery ?? "");
const [searchMode, setSearchMode] = useState("hybrid");
+ const [searchRange, setSearchRange] = useState("all");
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 [expandedMessage, setExpandedMessage] = useState<
+ MessageSearchResponse[number] | null
+ >(null);
const clientRef = useRef(null);
+ // Cache results per query+mode+range combination to avoid re-fetching
+ const resultsCache = useRef