From 588d99265cdc50ab6d2e1bffc4eb9a9c120f7894 Mon Sep 17 00:00:00 2001 From: Charles Packer Date: Tue, 23 Dec 2025 15:39:24 -0800 Subject: [PATCH] feat: add `/new` command (#374) Co-authored-by: Letta --- src/cli/App.tsx | 121 +++++++++++++++++++++++++- src/cli/commands/registry.ts | 7 ++ src/cli/components/NewAgentDialog.tsx | 84 ++++++++++++++++++ 3 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 src/cli/components/NewAgentDialog.tsx diff --git a/src/cli/App.tsx b/src/cli/App.tsx index d6ace01..42d3f6a 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -18,7 +18,7 @@ import { prefetchAvailableModelHandles } from "../agent/available-models"; import { getResumeData } from "../agent/check-approval"; import { getClient } from "../agent/client"; import { getCurrentAgentId, setCurrentAgentId } from "../agent/context"; -import type { AgentProvenance } from "../agent/create"; +import { type AgentProvenance, createAgent } from "../agent/create"; import { sendMessageStream } from "../agent/message"; import { SessionStats } from "../agent/stats"; import type { ApprovalContext } from "../permissions/analyzer"; @@ -62,6 +62,7 @@ import { McpSelector } from "./components/McpSelector"; import { MemoryViewer } from "./components/MemoryViewer"; import { MessageSearch } from "./components/MessageSearch"; import { ModelSelector } from "./components/ModelSelector"; +import { NewAgentDialog } from "./components/NewAgentDialog"; import { OAuthCodeDialog } from "./components/OAuthCodeDialog"; import { PinDialog, validateAgentName } from "./components/PinDialog"; import { PlanModeDialog } from "./components/PlanModeDialog"; @@ -436,6 +437,7 @@ export default function App({ | "feedback" | "memory" | "pin" + | "new" | "mcp" | "help" | "oauth" @@ -1666,6 +1668,18 @@ export default function App({ setCommandRunning(true); const inputCmd = "/pinned"; + const cmdId = uid("cmd"); + + // Show loading indicator while switching + buffersRef.current.byId.set(cmdId, { + kind: "command", + id: cmdId, + input: inputCmd, + output: "Switching agent...", + phase: "running", + }); + buffersRef.current.order.push(cmdId); + refreshDerived(); try { const client = await getClient(); @@ -1731,6 +1745,7 @@ export default function App({ hasBackfilledRef.current = true; } else { setStaticItems([successItem]); + setLines(toLines(buffersRef.current)); } } catch (error) { const errorDetails = formatErrorDetails(error, agentId); @@ -1752,6 +1767,96 @@ export default function App({ [refreshDerived, agentId, agentName, setCommandRunning], ); + // Handle creating a new agent and switching to it + const handleCreateNewAgent = useCallback( + async (name: string) => { + // Close dialog immediately + setActiveOverlay(null); + + // Lock input for async operation + setCommandRunning(true); + + const inputCmd = "/new"; + const cmdId = uid("cmd"); + + // Show "Creating..." status while we wait + buffersRef.current.byId.set(cmdId, { + kind: "command", + id: cmdId, + input: inputCmd, + output: `Creating agent "${name}"...`, + phase: "running", + }); + buffersRef.current.order.push(cmdId); + refreshDerived(); + + try { + // Create the new agent + const { agent } = await createAgent(name); + + // Update project settings with new agent + await updateProjectSettings({ lastAgent: agent.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); + + // Reset turn counter for memory reminders + turnCountRef.current = 0; + + // Update agent state + agentIdRef.current = agent.id; + setAgentId(agent.id); + setAgentState(agent); + setAgentName(agent.name); + setLlmConfig(agent.llm_config); + + // Build success message with hints + const agentUrl = `https://app.letta.com/projects/default-project/agents/${agent.id}`; + const successOutput = [ + `Created **${agent.name || agent.id}** (use /pin to save)`, + `⎿ ${agentUrl}`, + `⎿ Tip: use /init to initialize your agent's memory system!`, + ].join("\n"); + + const separator = { + kind: "separator" as const, + id: uid("sep"), + }; + const successItem: StaticItem = { + kind: "command", + id: uid("cmd"), + input: inputCmd, + output: successOutput, + phase: "finished", + success: true, + }; + + setStaticItems([separator, successItem]); + // Sync lines display after clearing buffers + setLines(toLines(buffersRef.current)); + } catch (error) { + const errorDetails = formatErrorDetails(error, agentId); + buffersRef.current.byId.set(cmdId, { + kind: "command", + id: cmdId, + input: inputCmd, + output: `Failed to create agent: ${errorDetails}`, + phase: "finished", + success: false, + }); + refreshDerived(); + } finally { + setCommandRunning(false); + } + }, + [refreshDerived, agentId, setCommandRunning], + ); + // Handle bash mode command submission // Uses the same shell runner as the Bash tool for consistency const handleBashSubmit = useCallback( @@ -2659,6 +2764,12 @@ export default function App({ return { submitted: true }; } + // Special handling for /new command - create new agent dialog + if (msg.trim() === "/new") { + setActiveOverlay("new"); + return { submitted: true }; + } + // Special handling for /pin command - pin current agent to project (or globally with -g) if (msg.trim() === "/pin" || msg.trim().startsWith("/pin ")) { const argsStr = msg.trim().slice(4).trim(); @@ -5024,6 +5135,14 @@ Plan file path: ${planFilePath}`; /> )} + {/* New Agent Dialog - for naming new agent before creation */} + {activeOverlay === "new" && ( + + )} + {/* Pin Dialog - for naming agent before pinning */} {activeOverlay === "pin" && ( = { return "Opening pinned agents..."; }, }, + "/new": { + desc: "Create a new agent and switch to it", + handler: () => { + // Handled specially in App.tsx + return "Creating new agent..."; + }, + }, "/subagents": { desc: "Manage custom subagents", handler: () => { diff --git a/src/cli/components/NewAgentDialog.tsx b/src/cli/components/NewAgentDialog.tsx new file mode 100644 index 0000000..6cfff3b --- /dev/null +++ b/src/cli/components/NewAgentDialog.tsx @@ -0,0 +1,84 @@ +import { Box, Text, useInput } from "ink"; +import { useState } from "react"; +import { DEFAULT_AGENT_NAME } from "../../constants"; +import { colors } from "./colors"; +import { PasteAwareTextInput } from "./PasteAwareTextInput"; +import { validateAgentName } from "./PinDialog"; + +interface NewAgentDialogProps { + onSubmit: (name: string) => void; + onCancel: () => void; +} + +export function NewAgentDialog({ onSubmit, onCancel }: NewAgentDialogProps) { + const [nameInput, setNameInput] = useState(""); + const [error, setError] = useState(""); + + useInput((_, key) => { + if (key.escape) { + onCancel(); + } + }); + + const handleNameSubmit = (text: string) => { + const trimmed = text.trim(); + + // Empty input = use default name + if (!trimmed) { + onSubmit(DEFAULT_AGENT_NAME); + return; + } + + const validationError = validateAgentName(trimmed); + if (validationError) { + setError(validationError); + return; + } + + onSubmit(trimmed); + }; + + return ( + + + + Create new agent + + + + + + Enter a name for your new agent, or press Enter for default. + + + + + + Agent name: + + + > + { + setNameInput(val); + setError(""); + }} + onSubmit={handleNameSubmit} + placeholder={DEFAULT_AGENT_NAME} + /> + + + + {error && ( + + {error} + + )} + + + Press Enter to create • Esc to cancel + + + ); +}