refactor: use conversations (#475)
Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
615
src/cli/App.tsx
615
src/cli/App.tsx
@@ -81,6 +81,7 @@ import { ApprovalPreview } from "./components/ApprovalPreview";
|
||||
import { AssistantMessage } from "./components/AssistantMessageRich";
|
||||
import { BashCommandMessage } from "./components/BashCommandMessage";
|
||||
import { CommandMessage } from "./components/CommandMessage";
|
||||
import { ConversationSelector } from "./components/ConversationSelector";
|
||||
import { colors } from "./components/colors";
|
||||
// EnterPlanModeDialog removed - now using InlineEnterPlanModeApproval
|
||||
import { ErrorMessage } from "./components/ErrorMessageRich";
|
||||
@@ -530,16 +531,19 @@ type StaticItem =
|
||||
export default function App({
|
||||
agentId: initialAgentId,
|
||||
agentState: initialAgentState,
|
||||
conversationId: initialConversationId,
|
||||
loadingState = "ready",
|
||||
continueSession = false,
|
||||
startupApproval = null,
|
||||
startupApprovals = [],
|
||||
messageHistory = [],
|
||||
resumedExistingConversation = false,
|
||||
tokenStreaming = false,
|
||||
agentProvenance = null,
|
||||
}: {
|
||||
agentId: string;
|
||||
agentState?: AgentState | null;
|
||||
conversationId: string; // Required: created at startup
|
||||
loadingState?:
|
||||
| "assembling"
|
||||
| "importing"
|
||||
@@ -550,6 +554,7 @@ export default function App({
|
||||
startupApproval?: ApprovalRequest | null; // Deprecated: use startupApprovals
|
||||
startupApprovals?: ApprovalRequest[];
|
||||
messageHistory?: Message[];
|
||||
resumedExistingConversation?: boolean; // True if we explicitly resumed via --resume
|
||||
tokenStreaming?: boolean;
|
||||
agentProvenance?: AgentProvenance | null;
|
||||
}) {
|
||||
@@ -562,6 +567,9 @@ export default function App({
|
||||
const [agentId, setAgentId] = useState(initialAgentId);
|
||||
const [agentState, setAgentState] = useState(initialAgentState);
|
||||
|
||||
// Track current conversation (always created fresh on startup)
|
||||
const [conversationId, setConversationId] = useState(initialConversationId);
|
||||
|
||||
// Keep a ref to the current agentId for use in callbacks that need the latest value
|
||||
const agentIdRef = useRef(agentId);
|
||||
useEffect(() => {
|
||||
@@ -569,11 +577,18 @@ export default function App({
|
||||
telemetry.setCurrentAgentId(agentId);
|
||||
}, [agentId]);
|
||||
|
||||
// Keep a ref to the current conversationId for use in callbacks
|
||||
const conversationIdRef = useRef(conversationId);
|
||||
useEffect(() => {
|
||||
conversationIdRef.current = conversationId;
|
||||
}, [conversationId]);
|
||||
|
||||
const resumeKey = useSuspend();
|
||||
|
||||
// Track previous prop values to detect actual prop changes (not internal state changes)
|
||||
const prevInitialAgentIdRef = useRef(initialAgentId);
|
||||
const prevInitialAgentStateRef = useRef(initialAgentState);
|
||||
const prevInitialConversationIdRef = useRef(initialConversationId);
|
||||
|
||||
// Sync with prop changes (e.g., when parent updates from "loading" to actual ID)
|
||||
// Only sync when the PROP actually changes, not when internal state changes
|
||||
@@ -592,6 +607,14 @@ export default function App({
|
||||
}
|
||||
}, [initialAgentState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialConversationId !== prevInitialConversationIdRef.current) {
|
||||
prevInitialConversationIdRef.current = initialConversationId;
|
||||
conversationIdRef.current = initialConversationId;
|
||||
setConversationId(initialConversationId);
|
||||
}
|
||||
}, [initialConversationId]);
|
||||
|
||||
// Set agent context for tools (especially Task tool)
|
||||
useEffect(() => {
|
||||
if (agentId) {
|
||||
@@ -793,6 +816,7 @@ export default function App({
|
||||
| "system"
|
||||
| "agent"
|
||||
| "resume"
|
||||
| "conversations"
|
||||
| "search"
|
||||
| "subagent"
|
||||
| "feedback"
|
||||
@@ -1299,10 +1323,6 @@ export default function App({
|
||||
|
||||
// Add combined status at the END so user sees it without scrolling
|
||||
const statusId = `status-resumed-${Date.now().toString(36)}`;
|
||||
const cwd = process.cwd();
|
||||
const shortCwd = cwd.startsWith(process.env.HOME || "")
|
||||
? `~${cwd.slice((process.env.HOME || "").length)}`
|
||||
: cwd;
|
||||
|
||||
// Check if agent is pinned (locally or globally)
|
||||
const isPinned = agentState?.id
|
||||
@@ -1312,23 +1332,32 @@ export default function App({
|
||||
|
||||
// Build status message
|
||||
const agentName = agentState?.name || "Unnamed Agent";
|
||||
const headerMessage = `Connecting to **${agentName}** (last used in ${shortCwd})`;
|
||||
const isResumingConversation =
|
||||
resumedExistingConversation || messageHistory.length > 0;
|
||||
if (process.env.DEBUG) {
|
||||
console.log(
|
||||
`[DEBUG] Header: resumedExistingConversation=${resumedExistingConversation}, messageHistory.length=${messageHistory.length}`,
|
||||
);
|
||||
}
|
||||
const headerMessage = isResumingConversation
|
||||
? `Resuming conversation with **${agentName}**`
|
||||
: `Starting new conversation with **${agentName}**`;
|
||||
|
||||
// Command hints - for pinned agents show /memory, for unpinned show /pin
|
||||
const commandHints = isPinned
|
||||
? [
|
||||
"→ **/agents** list all agents",
|
||||
"→ **/resume** resume a previous conversation",
|
||||
"→ **/memory** view your agent's memory blocks",
|
||||
"→ **/init** initialize your agent's memory",
|
||||
"→ **/remember** teach your agent",
|
||||
"→ **/agents** list agents",
|
||||
"→ **/ade** open in the browser (web UI)",
|
||||
]
|
||||
: [
|
||||
"→ **/agents** list all agents",
|
||||
"→ **/resume** resume a previous conversation",
|
||||
"→ **/pin** save + name your agent",
|
||||
"→ **/init** initialize your agent's memory",
|
||||
"→ **/remember** teach your agent",
|
||||
"→ **/agents** list agents",
|
||||
"→ **/ade** open in the browser (web UI)",
|
||||
];
|
||||
|
||||
const statusLines = [headerMessage, ...commandHints];
|
||||
@@ -1351,6 +1380,7 @@ export default function App({
|
||||
columns,
|
||||
agentState,
|
||||
agentProvenance,
|
||||
resumedExistingConversation,
|
||||
]);
|
||||
|
||||
// Fetch llmConfig when agent is ready
|
||||
@@ -1595,13 +1625,16 @@ export default function App({
|
||||
return;
|
||||
}
|
||||
|
||||
// Stream one turn - use ref to always get the latest agentId
|
||||
// Stream one turn - use ref to always get the latest conversationId
|
||||
// Wrap in try-catch to handle pre-stream desync errors (when sendMessageStream
|
||||
// throws before streaming begins, e.g., retry after LLM error when backend
|
||||
// already cleared the approval)
|
||||
let stream: Awaited<ReturnType<typeof sendMessageStream>>;
|
||||
try {
|
||||
stream = await sendMessageStream(agentIdRef.current, currentInput);
|
||||
stream = await sendMessageStream(
|
||||
conversationIdRef.current,
|
||||
currentInput,
|
||||
);
|
||||
} catch (preStreamError) {
|
||||
// Check if this is a pre-stream approval desync error
|
||||
const hasApprovalInPayload = currentInput.some(
|
||||
@@ -2829,7 +2862,7 @@ export default function App({
|
||||
buffersRef.current.byId.set(cmdId, {
|
||||
kind: "command",
|
||||
id: cmdId,
|
||||
input: "/pinned",
|
||||
input: "/agents",
|
||||
output: `Already on "${label}"`,
|
||||
phase: "finished",
|
||||
success: true,
|
||||
@@ -2842,7 +2875,7 @@ export default function App({
|
||||
// Lock input for async operation (set before any await to prevent queue processing)
|
||||
setCommandRunning(true);
|
||||
|
||||
const inputCmd = "/pinned";
|
||||
const inputCmd = "/agents";
|
||||
const cmdId = uid("cmd");
|
||||
|
||||
// Show loading indicator while switching
|
||||
@@ -2861,13 +2894,26 @@ export default function App({
|
||||
// Fetch new agent
|
||||
const agent = await client.agents.retrieve(targetAgentId);
|
||||
|
||||
// Fetch agent's message history
|
||||
const messagesPage = await client.agents.messages.list(targetAgentId);
|
||||
const messages = messagesPage.items;
|
||||
// Always create a new conversation when switching agents
|
||||
// User can /resume to get back to a previous conversation if needed
|
||||
const newConversation = await client.conversations.create({
|
||||
agent_id: targetAgentId,
|
||||
});
|
||||
const targetConversationId = newConversation.id;
|
||||
|
||||
// Update project settings with new agent
|
||||
await updateProjectSettings({ lastAgent: targetAgentId });
|
||||
|
||||
// Save the session (agent + conversation) to settings
|
||||
settingsManager.setLocalLastSession(
|
||||
{ agentId: targetAgentId, conversationId: targetConversationId },
|
||||
process.cwd(),
|
||||
);
|
||||
settingsManager.setGlobalLastSession({
|
||||
agentId: targetAgentId,
|
||||
conversationId: targetConversationId,
|
||||
});
|
||||
|
||||
// Clear current transcript and static items
|
||||
buffersRef.current.byId.clear();
|
||||
buffersRef.current.order = [];
|
||||
@@ -2885,10 +2931,14 @@ export default function App({
|
||||
setAgentState(agent);
|
||||
setAgentName(agent.name);
|
||||
setLlmConfig(agent.llm_config);
|
||||
setConversationId(targetConversationId);
|
||||
|
||||
// Build success command
|
||||
const agentUrl = `https://app.letta.com/projects/default-project/agents/${targetAgentId}`;
|
||||
const successOutput = `Resumed "${agent.name || targetAgentId}"\n⎿ ${agentUrl}`;
|
||||
// Build success message - always a new conversation
|
||||
const agentLabel = agent.name || targetAgentId;
|
||||
const successOutput = [
|
||||
`Started a new conversation with **${agentLabel}**.`,
|
||||
`⎿ Type /resume to resume a previous conversation`,
|
||||
].join("\n");
|
||||
const successItem: StaticItem = {
|
||||
kind: "command",
|
||||
id: uid("cmd"),
|
||||
@@ -2898,30 +2948,13 @@ export default function App({
|
||||
success: true,
|
||||
};
|
||||
|
||||
// Backfill message history with visual separator, then success command at end
|
||||
if (messages.length > 0) {
|
||||
hasBackfilledRef.current = false;
|
||||
backfillBuffers(buffersRef.current, messages);
|
||||
// Collect backfilled items
|
||||
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);
|
||||
}
|
||||
// Add separator before backfilled messages, then success at end
|
||||
const separator = {
|
||||
kind: "separator" as const,
|
||||
id: uid("sep"),
|
||||
};
|
||||
setStaticItems([separator, ...backfilledItems, successItem]);
|
||||
setLines(toLines(buffersRef.current));
|
||||
hasBackfilledRef.current = true;
|
||||
} else {
|
||||
setStaticItems([successItem]);
|
||||
setLines(toLines(buffersRef.current));
|
||||
}
|
||||
// Add separator for visual spacing, then success message
|
||||
const separator = {
|
||||
kind: "separator" as const,
|
||||
id: uid("sep"),
|
||||
};
|
||||
setStaticItems([separator, successItem]);
|
||||
setLines(toLines(buffersRef.current));
|
||||
} catch (error) {
|
||||
const errorDetails = formatErrorDetails(error, agentId);
|
||||
const errorCmdId = uid("cmd");
|
||||
@@ -3886,14 +3919,14 @@ export default function App({
|
||||
return { submitted: true };
|
||||
}
|
||||
|
||||
// Special handling for /clear command - reset conversation
|
||||
// Special handling for /clear command - start new conversation
|
||||
if (msg.trim() === "/clear") {
|
||||
const cmdId = uid("cmd");
|
||||
buffersRef.current.byId.set(cmdId, {
|
||||
kind: "command",
|
||||
id: cmdId,
|
||||
input: msg,
|
||||
output: "Clearing conversation...",
|
||||
output: "Starting new conversation...",
|
||||
phase: "running",
|
||||
});
|
||||
buffersRef.current.order.push(cmdId);
|
||||
@@ -3903,16 +3936,24 @@ export default function App({
|
||||
|
||||
try {
|
||||
const client = await getClient();
|
||||
await client.agents.messages.reset(agentId, {
|
||||
add_default_initial_messages: false,
|
||||
|
||||
// Create a new conversation for the current agent
|
||||
const conversation = await client.conversations.create({
|
||||
agent_id: agentId,
|
||||
});
|
||||
|
||||
// Clear local buffers and static items
|
||||
// buffersRef.current.byId.clear();
|
||||
// buffersRef.current.order = [];
|
||||
// buffersRef.current.tokenCount = 0;
|
||||
// emittedIdsRef.current.clear();
|
||||
// setStaticItems([]);
|
||||
// Update conversationId state
|
||||
setConversationId(conversation.id);
|
||||
|
||||
// Save the new session to settings
|
||||
settingsManager.setLocalLastSession(
|
||||
{ agentId, conversationId: conversation.id },
|
||||
process.cwd(),
|
||||
);
|
||||
settingsManager.setGlobalLastSession({
|
||||
agentId,
|
||||
conversationId: conversation.id,
|
||||
});
|
||||
|
||||
// Reset turn counter for memory reminders
|
||||
turnCountRef.current = 0;
|
||||
@@ -3922,7 +3963,75 @@ export default function App({
|
||||
kind: "command",
|
||||
id: cmdId,
|
||||
input: msg,
|
||||
output: "Conversation cleared",
|
||||
output: "Started new conversation",
|
||||
phase: "finished",
|
||||
success: true,
|
||||
});
|
||||
buffersRef.current.order.push(cmdId);
|
||||
refreshDerived();
|
||||
} catch (error) {
|
||||
const errorDetails = formatErrorDetails(error, agentId);
|
||||
buffersRef.current.byId.set(cmdId, {
|
||||
kind: "command",
|
||||
id: cmdId,
|
||||
input: msg,
|
||||
output: `Failed: ${errorDetails}`,
|
||||
phase: "finished",
|
||||
success: false,
|
||||
});
|
||||
refreshDerived();
|
||||
} finally {
|
||||
setCommandRunning(false);
|
||||
}
|
||||
return { submitted: true };
|
||||
}
|
||||
|
||||
// Special handling for /clear-messages command - reset all agent messages (destructive)
|
||||
if (msg.trim() === "/clear-messages") {
|
||||
const cmdId = uid("cmd");
|
||||
buffersRef.current.byId.set(cmdId, {
|
||||
kind: "command",
|
||||
id: cmdId,
|
||||
input: msg,
|
||||
output: "Resetting agent messages...",
|
||||
phase: "running",
|
||||
});
|
||||
buffersRef.current.order.push(cmdId);
|
||||
refreshDerived();
|
||||
|
||||
setCommandRunning(true);
|
||||
|
||||
try {
|
||||
const client = await getClient();
|
||||
|
||||
// Reset all messages on the agent (destructive operation)
|
||||
await client.agents.messages.reset(agentId, {
|
||||
add_default_initial_messages: false,
|
||||
});
|
||||
|
||||
// Also create a new conversation since messages were cleared
|
||||
const conversation = await client.conversations.create({
|
||||
agent_id: agentId,
|
||||
});
|
||||
setConversationId(conversation.id);
|
||||
settingsManager.setLocalLastSession(
|
||||
{ agentId, conversationId: conversation.id },
|
||||
process.cwd(),
|
||||
);
|
||||
settingsManager.setGlobalLastSession({
|
||||
agentId,
|
||||
conversationId: conversation.id,
|
||||
});
|
||||
|
||||
// Reset turn counter for memory reminders
|
||||
turnCountRef.current = 0;
|
||||
|
||||
// Update command with success
|
||||
buffersRef.current.byId.set(cmdId, {
|
||||
kind: "command",
|
||||
id: cmdId,
|
||||
input: msg,
|
||||
output: "All agent messages reset",
|
||||
phase: "finished",
|
||||
success: true,
|
||||
});
|
||||
@@ -4177,10 +4286,9 @@ export default function App({
|
||||
}
|
||||
|
||||
// Special handling for /agents command - show agent browser
|
||||
// /resume, /pinned, /profiles are hidden aliases
|
||||
// /pinned, /profiles are hidden aliases
|
||||
if (
|
||||
msg.trim() === "/agents" ||
|
||||
msg.trim() === "/resume" ||
|
||||
msg.trim() === "/pinned" ||
|
||||
msg.trim() === "/profiles"
|
||||
) {
|
||||
@@ -4188,6 +4296,147 @@ export default function App({
|
||||
return { submitted: true };
|
||||
}
|
||||
|
||||
// Special handling for /resume command - show conversation selector or switch directly
|
||||
if (msg.trim().startsWith("/resume")) {
|
||||
const parts = msg.trim().split(/\s+/);
|
||||
const targetConvId = parts[1]; // Optional conversation ID
|
||||
|
||||
if (targetConvId) {
|
||||
// Direct switch to specified conversation
|
||||
if (targetConvId === conversationId) {
|
||||
const cmdId = uid("cmd");
|
||||
buffersRef.current.byId.set(cmdId, {
|
||||
kind: "command",
|
||||
id: cmdId,
|
||||
input: msg.trim(),
|
||||
output: "Already on this conversation",
|
||||
phase: "finished",
|
||||
success: true,
|
||||
});
|
||||
buffersRef.current.order.push(cmdId);
|
||||
refreshDerived();
|
||||
return { submitted: true };
|
||||
}
|
||||
|
||||
// Lock input and show loading
|
||||
setCommandRunning(true);
|
||||
const cmdId = uid("cmd");
|
||||
buffersRef.current.byId.set(cmdId, {
|
||||
kind: "command",
|
||||
id: cmdId,
|
||||
input: msg.trim(),
|
||||
output: "Switching conversation...",
|
||||
phase: "running",
|
||||
});
|
||||
buffersRef.current.order.push(cmdId);
|
||||
refreshDerived();
|
||||
|
||||
try {
|
||||
// Update conversation ID and settings
|
||||
setConversationId(targetConvId);
|
||||
settingsManager.setLocalLastSession(
|
||||
{ agentId, conversationId: targetConvId },
|
||||
process.cwd(),
|
||||
);
|
||||
settingsManager.setGlobalLastSession({
|
||||
agentId,
|
||||
conversationId: targetConvId,
|
||||
});
|
||||
|
||||
// Fetch message history for the selected conversation
|
||||
if (agentState) {
|
||||
const client = await getClient();
|
||||
const resumeData = await getResumeData(
|
||||
client,
|
||||
agentState,
|
||||
targetConvId,
|
||||
);
|
||||
|
||||
// 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);
|
||||
|
||||
// Build success message
|
||||
const currentAgentName = agentState.name || "Unnamed Agent";
|
||||
const successLines =
|
||||
resumeData.messageHistory.length > 0
|
||||
? [
|
||||
`Resumed conversation with "${currentAgentName}"`,
|
||||
`⎿ Agent: ${agentId}`,
|
||||
`⎿ Conversation: ${targetConvId}`,
|
||||
]
|
||||
: [
|
||||
`Switched to conversation with "${currentAgentName}"`,
|
||||
`⎿ Agent: ${agentId}`,
|
||||
`⎿ Conversation: ${targetConvId} (empty)`,
|
||||
];
|
||||
const successOutput = successLines.join("\n");
|
||||
const successItem: StaticItem = {
|
||||
kind: "command",
|
||||
id: uid("cmd"),
|
||||
input: msg.trim(),
|
||||
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));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const errorCmdId = uid("cmd");
|
||||
buffersRef.current.byId.set(errorCmdId, {
|
||||
kind: "command",
|
||||
id: errorCmdId,
|
||||
input: msg.trim(),
|
||||
output: `Failed to switch conversation: ${error instanceof Error ? error.message : String(error)}`,
|
||||
phase: "finished",
|
||||
success: false,
|
||||
});
|
||||
buffersRef.current.order.push(errorCmdId);
|
||||
refreshDerived();
|
||||
} finally {
|
||||
setCommandRunning(false);
|
||||
}
|
||||
return { submitted: true };
|
||||
}
|
||||
|
||||
// No conversation ID provided - show selector
|
||||
setActiveOverlay("conversations");
|
||||
return { submitted: true };
|
||||
}
|
||||
|
||||
// Special handling for /search command - show message search
|
||||
if (msg.trim() === "/search") {
|
||||
setActiveOverlay("search");
|
||||
@@ -6737,12 +6986,6 @@ Plan file path: ${planFilePath}`;
|
||||
// Add status line showing agent info
|
||||
const statusId = `status-agent-${Date.now().toString(36)}`;
|
||||
|
||||
// Get short path for display
|
||||
const cwd = process.cwd();
|
||||
const shortCwd = cwd.startsWith(process.env.HOME || "")
|
||||
? `~${cwd.slice((process.env.HOME || "").length)}`
|
||||
: cwd;
|
||||
|
||||
// Check if agent is pinned (locally or globally)
|
||||
const isPinned = agentState?.id
|
||||
? settingsManager.getLocalPinnedAgents().includes(agentState.id) ||
|
||||
@@ -6751,25 +6994,27 @@ Plan file path: ${planFilePath}`;
|
||||
|
||||
// Build status message based on session type
|
||||
const agentName = agentState?.name || "Unnamed Agent";
|
||||
const headerMessage = continueSession
|
||||
? `Connecting to **${agentName}** (last used in ${shortCwd})`
|
||||
: "Creating a new agent";
|
||||
const headerMessage = resumedExistingConversation
|
||||
? `Resuming (empty) conversation with **${agentName}**`
|
||||
: continueSession
|
||||
? `Starting new conversation with **${agentName}**`
|
||||
: "Creating a new agent";
|
||||
|
||||
// Command hints - for pinned agents show /memory, for unpinned show /pin
|
||||
const commandHints = isPinned
|
||||
? [
|
||||
"→ **/agents** list all agents",
|
||||
"→ **/resume** resume a previous conversation",
|
||||
"→ **/memory** view your agent's memory blocks",
|
||||
"→ **/init** initialize your agent's memory",
|
||||
"→ **/remember** teach your agent",
|
||||
"→ **/agents** list agents",
|
||||
"→ **/ade** open in the browser (web UI)",
|
||||
]
|
||||
: [
|
||||
"→ **/agents** list all agents",
|
||||
"→ **/resume** resume a previous conversation",
|
||||
"→ **/pin** save + name your agent",
|
||||
"→ **/init** initialize your agent's memory",
|
||||
"→ **/remember** teach your agent",
|
||||
"→ **/agents** list agents",
|
||||
"→ **/ade** open in the browser (web UI)",
|
||||
];
|
||||
|
||||
const statusLines = [headerMessage, ...commandHints];
|
||||
@@ -6786,6 +7031,7 @@ Plan file path: ${planFilePath}`;
|
||||
}, [
|
||||
loadingState,
|
||||
continueSession,
|
||||
resumedExistingConversation,
|
||||
messageHistory.length,
|
||||
commitEligibleLines,
|
||||
columns,
|
||||
@@ -7345,6 +7591,237 @@ Plan file path: ${planFilePath}`;
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Conversation Selector - for resuming conversations */}
|
||||
{activeOverlay === "conversations" && (
|
||||
<ConversationSelector
|
||||
agentId={agentId}
|
||||
currentConversationId={conversationId}
|
||||
onSelect={async (convId) => {
|
||||
closeOverlay();
|
||||
|
||||
// Skip if already on this conversation
|
||||
if (convId === conversationId) {
|
||||
const cmdId = uid("cmd");
|
||||
buffersRef.current.byId.set(cmdId, {
|
||||
kind: "command",
|
||||
id: cmdId,
|
||||
input: "/resume",
|
||||
output: "Already on this conversation",
|
||||
phase: "finished",
|
||||
success: true,
|
||||
});
|
||||
buffersRef.current.order.push(cmdId);
|
||||
refreshDerived();
|
||||
return;
|
||||
}
|
||||
|
||||
// Lock input for async operation
|
||||
setCommandRunning(true);
|
||||
|
||||
const inputCmd = "/resume";
|
||||
const cmdId = uid("cmd");
|
||||
|
||||
// Show loading indicator while switching
|
||||
buffersRef.current.byId.set(cmdId, {
|
||||
kind: "command",
|
||||
id: cmdId,
|
||||
input: inputCmd,
|
||||
output: "Switching conversation...",
|
||||
phase: "running",
|
||||
});
|
||||
buffersRef.current.order.push(cmdId);
|
||||
refreshDerived();
|
||||
|
||||
try {
|
||||
// Update conversation ID and settings
|
||||
setConversationId(convId);
|
||||
settingsManager.setLocalLastSession(
|
||||
{ agentId, conversationId: convId },
|
||||
process.cwd(),
|
||||
);
|
||||
settingsManager.setGlobalLastSession({
|
||||
agentId,
|
||||
conversationId: convId,
|
||||
});
|
||||
|
||||
// Fetch message history for the selected conversation
|
||||
if (agentState) {
|
||||
const client = await getClient();
|
||||
const resumeData = await getResumeData(
|
||||
client,
|
||||
agentState,
|
||||
convId,
|
||||
);
|
||||
|
||||
// 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);
|
||||
|
||||
// Build success command with agent + conversation info
|
||||
const currentAgentName =
|
||||
agentState.name || "Unnamed Agent";
|
||||
const successLines =
|
||||
resumeData.messageHistory.length > 0
|
||||
? [
|
||||
`Resumed conversation with "${currentAgentName}"`,
|
||||
`⎿ Agent: ${agentId}`,
|
||||
`⎿ Conversation: ${convId}`,
|
||||
]
|
||||
: [
|
||||
`Switched to conversation with "${currentAgentName}"`,
|
||||
`⎿ Agent: ${agentId}`,
|
||||
`⎿ Conversation: ${convId} (empty)`,
|
||||
];
|
||||
const successOutput = successLines.join("\n");
|
||||
const successItem: StaticItem = {
|
||||
kind: "command",
|
||||
id: uid("cmd"),
|
||||
input: inputCmd,
|
||||
output: successOutput,
|
||||
phase: "finished",
|
||||
success: true,
|
||||
};
|
||||
|
||||
// Backfill message history with visual separator
|
||||
if (resumeData.messageHistory.length > 0) {
|
||||
hasBackfilledRef.current = false;
|
||||
backfillBuffers(
|
||||
buffersRef.current,
|
||||
resumeData.messageHistory,
|
||||
);
|
||||
// Collect backfilled items
|
||||
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);
|
||||
}
|
||||
// Add separator before backfilled messages, then success at end
|
||||
const separator = {
|
||||
kind: "separator" as const,
|
||||
id: uid("sep"),
|
||||
};
|
||||
setStaticItems([
|
||||
separator,
|
||||
...backfilledItems,
|
||||
successItem,
|
||||
]);
|
||||
setLines(toLines(buffersRef.current));
|
||||
hasBackfilledRef.current = true;
|
||||
} else {
|
||||
// Add separator for visual spacing even without backfill
|
||||
const separator = {
|
||||
kind: "separator" as const,
|
||||
id: uid("sep"),
|
||||
};
|
||||
setStaticItems([separator, successItem]);
|
||||
setLines(toLines(buffersRef.current));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const errorCmdId = uid("cmd");
|
||||
buffersRef.current.byId.set(errorCmdId, {
|
||||
kind: "command",
|
||||
id: errorCmdId,
|
||||
input: inputCmd,
|
||||
output: `Failed to switch conversation: ${error instanceof Error ? error.message : String(error)}`,
|
||||
phase: "finished",
|
||||
success: false,
|
||||
});
|
||||
buffersRef.current.order.push(errorCmdId);
|
||||
refreshDerived();
|
||||
} finally {
|
||||
setCommandRunning(false);
|
||||
}
|
||||
}}
|
||||
onNewConversation={async () => {
|
||||
closeOverlay();
|
||||
|
||||
// Lock input for async operation
|
||||
setCommandRunning(true);
|
||||
|
||||
const inputCmd = "/resume";
|
||||
const cmdId = uid("cmd");
|
||||
|
||||
// Show loading indicator
|
||||
buffersRef.current.byId.set(cmdId, {
|
||||
kind: "command",
|
||||
id: cmdId,
|
||||
input: inputCmd,
|
||||
output: "Creating new conversation...",
|
||||
phase: "running",
|
||||
});
|
||||
buffersRef.current.order.push(cmdId);
|
||||
refreshDerived();
|
||||
|
||||
try {
|
||||
// Create a new conversation
|
||||
const client = await getClient();
|
||||
const conversation = await client.conversations.create({
|
||||
agent_id: agentId,
|
||||
});
|
||||
setConversationId(conversation.id);
|
||||
settingsManager.setLocalLastSession(
|
||||
{ agentId, conversationId: conversation.id },
|
||||
process.cwd(),
|
||||
);
|
||||
settingsManager.setGlobalLastSession({
|
||||
agentId,
|
||||
conversationId: conversation.id,
|
||||
});
|
||||
|
||||
// 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);
|
||||
|
||||
// Build success command with agent + conversation info
|
||||
const currentAgentName =
|
||||
agentState?.name || "Unnamed Agent";
|
||||
const shortConvId = conversation.id.slice(0, 20);
|
||||
const successLines = [
|
||||
`Started new conversation with "${currentAgentName}"`,
|
||||
`⎿ Agent: ${agentId}`,
|
||||
`⎿ Conversation: ${shortConvId}... (new)`,
|
||||
];
|
||||
const successItem: StaticItem = {
|
||||
kind: "command",
|
||||
id: uid("cmd"),
|
||||
input: inputCmd,
|
||||
output: successLines.join("\n"),
|
||||
phase: "finished",
|
||||
success: true,
|
||||
};
|
||||
setStaticItems([successItem]);
|
||||
setLines(toLines(buffersRef.current));
|
||||
} catch (error) {
|
||||
const errorCmdId = uid("cmd");
|
||||
buffersRef.current.byId.set(errorCmdId, {
|
||||
kind: "command",
|
||||
id: errorCmdId,
|
||||
input: inputCmd,
|
||||
output: `Failed to create conversation: ${error instanceof Error ? error.message : String(error)}`,
|
||||
phase: "finished",
|
||||
success: false,
|
||||
});
|
||||
buffersRef.current.order.push(errorCmdId);
|
||||
refreshDerived();
|
||||
} finally {
|
||||
setCommandRunning(false);
|
||||
}
|
||||
}}
|
||||
onCancel={closeOverlay}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Message Search - conditionally mounted as overlay */}
|
||||
{activeOverlay === "search" && (
|
||||
<MessageSearch onClose={closeOverlay} />
|
||||
|
||||
Reference in New Issue
Block a user