import { Box, useInput } from "ink";
import { useState } from "react";
import { DEFAULT_AGENT_NAME } from "../../constants";
import { colors } from "./colors";
import { PasteAwareTextInput } from "./PasteAwareTextInput";
import { Text } from "./Text";
interface PinDialogProps {
currentName: string;
local: boolean;
onSubmit: (newName: string | null) => void; // null means keep current name
onCancel: () => void;
}
/**
* Validate agent name against backend rules.
* Matches: Unicode letters, digits, underscores, spaces, hyphens, apostrophes
* Blocks: / \ : * ? " < > |
*/
export function validateAgentName(name: string): string | null {
if (!name || !name.trim()) {
return "Name cannot be empty";
}
const trimmed = name.trim();
// Match backend regex: ^[\w '-]+$ with unicode
// \w matches Unicode letters, digits, and underscores
// We also allow spaces, hyphens, and apostrophes
const validPattern = /^[\w '-]+$/u;
if (!validPattern.test(trimmed)) {
return "Name contains invalid characters. Only letters, digits, spaces, hyphens, underscores, and apostrophes are allowed.";
}
if (trimmed.length > 100) {
return "Name is too long (max 100 characters)";
}
return null;
}
/**
* Check if the name is the default Letta Code agent name.
*/
export function isDefaultAgentName(name: string): boolean {
return name === DEFAULT_AGENT_NAME;
}
export function PinDialog({
currentName,
local,
onSubmit,
onCancel,
}: PinDialogProps) {
const isDefault = isDefaultAgentName(currentName);
const [mode, setMode] = useState<"choose" | "input">(
isDefault ? "input" : "choose",
);
const [nameInput, setNameInput] = useState("");
const [selectedOption, setSelectedOption] = useState(0);
const [error, setError] = useState("");
const scopeText = local ? "to this project" : "globally";
useInput((input, key) => {
// CTRL-C: immediately cancel (bypasses mode transitions)
if (key.ctrl && input === "c") {
onCancel();
return;
}
if (key.escape) {
if (mode === "input" && !isDefault) {
// Go back to choose mode
setMode("choose");
setError("");
} else {
onCancel();
}
return;
}
if (mode === "choose") {
if (input === "j" || key.downArrow) {
setSelectedOption((prev) => Math.min(prev + 1, 1));
} else if (input === "k" || key.upArrow) {
setSelectedOption((prev) => Math.max(prev - 1, 0));
} else if (key.return) {
if (selectedOption === 0) {
// Keep current name
onSubmit(null);
} else {
// Change name
setMode("input");
}
}
}
});
const handleNameSubmit = (text: string) => {
const trimmed = text.trim();
const validationError = validateAgentName(trimmed);
if (validationError) {
setError(validationError);
return;
}
onSubmit(trimmed);
};
// Input-only mode for default names
if (isDefault || mode === "input") {
return (
{isDefault ? "Name your agent" : "Rename your agent"}
{isDefault && (
Give your agent a memorable name before pinning {scopeText}.
)}
Enter a name:
>
{
setNameInput(val);
setError("");
}}
onSubmit={handleNameSubmit}
placeholder={isDefault ? "e.g., my-coding-agent" : currentName}
/>
{error && (
{error}
)}
Press Enter to confirm {!isDefault && "• Esc to go back"}
{isDefault && "• Esc to cancel"}
);
}
// Choice mode for custom names
return (
Pin agent {scopeText}
Would you like to keep the current name or change it?
{[
{ label: `Keep name "${currentName}"`, value: "keep" },
{ label: "Change name", value: "change" },
].map((option, index) => {
const isSelected = index === selectedOption;
return (
{isSelected ? ">" : " "}
{index + 1}. {option.label}
);
})}
↑↓/jk to select • Enter to confirm • Esc to cancel
);
}