feat: add /new command (#374)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Charles Packer
2025-12-23 15:39:24 -08:00
committed by GitHub
parent e4c7f9dc62
commit 588d99265c
3 changed files with 211 additions and 1 deletions

View File

@@ -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" && (
<NewAgentDialog
onSubmit={handleCreateNewAgent}
onCancel={closeOverlay}
/>
)}
{/* Pin Dialog - for naming agent before pinning */}
{activeOverlay === "pin" && (
<PinDialog

View File

@@ -167,6 +167,13 @@ export const commands: Record<string, Command> = {
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: () => {

View File

@@ -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 (
<Box flexDirection="column" paddingY={1}>
<Box marginBottom={1}>
<Text color={colors.approval.header} bold>
Create new agent
</Text>
</Box>
<Box marginBottom={1}>
<Text dimColor>
Enter a name for your new agent, or press Enter for default.
</Text>
</Box>
<Box flexDirection="column" marginBottom={1}>
<Box marginBottom={1}>
<Text>Agent name:</Text>
</Box>
<Box>
<Text color={colors.approval.header}>&gt; </Text>
<PasteAwareTextInput
value={nameInput}
onChange={(val) => {
setNameInput(val);
setError("");
}}
onSubmit={handleNameSubmit}
placeholder={DEFAULT_AGENT_NAME}
/>
</Box>
</Box>
{error && (
<Box marginBottom={1}>
<Text color="red">{error}</Text>
</Box>
)}
<Box>
<Text dimColor>Press Enter to create Esc to cancel</Text>
</Box>
</Box>
);
}