feat: add /new command (#374)
Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
121
src/cli/App.tsx
121
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" && (
|
||||
<NewAgentDialog
|
||||
onSubmit={handleCreateNewAgent}
|
||||
onCancel={closeOverlay}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Pin Dialog - for naming agent before pinning */}
|
||||
{activeOverlay === "pin" && (
|
||||
<PinDialog
|
||||
|
||||
@@ -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: () => {
|
||||
|
||||
84
src/cli/components/NewAgentDialog.tsx
Normal file
84
src/cli/components/NewAgentDialog.tsx
Normal 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}>> </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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user