219 lines
6.2 KiB
TypeScript
219 lines
6.2 KiB
TypeScript
import { homedir } from "node:os";
|
|
import type { Letta } from "@letta-ai/letta-client";
|
|
import { Box, Text } from "ink";
|
|
|
|
import type { AgentProvenance } from "../../agent/create";
|
|
import { isProjectBlock } from "../../agent/memory";
|
|
import { settingsManager } from "../../settings-manager";
|
|
import { getVersion } from "../../version";
|
|
import { useTerminalWidth } from "../hooks/useTerminalWidth";
|
|
import { asciiLogo } from "./AsciiArt";
|
|
import { colors } from "./colors";
|
|
|
|
/**
|
|
* Convert absolute path to use ~ for home directory
|
|
*/
|
|
function toTildePath(absolutePath: string): string {
|
|
const home = homedir();
|
|
if (absolutePath.startsWith(home)) {
|
|
return `~${absolutePath.slice(home.length)}`;
|
|
}
|
|
return absolutePath;
|
|
}
|
|
|
|
/**
|
|
* Determine the auth method used
|
|
*/
|
|
function getAuthMethod(): "url" | "api-key" | "oauth" {
|
|
// Check if custom URL is being used
|
|
if (process.env.LETTA_BASE_URL) {
|
|
return "url";
|
|
}
|
|
// Check if API key from env
|
|
if (process.env.LETTA_API_KEY) {
|
|
return "api-key";
|
|
}
|
|
// Check settings for refresh token (OAuth)
|
|
const settings = settingsManager.getSettings();
|
|
if (settings.refreshToken) {
|
|
return "oauth";
|
|
}
|
|
// Check if API key stored in settings
|
|
if (settings.env?.LETTA_API_KEY) {
|
|
return "api-key";
|
|
}
|
|
return "oauth"; // default
|
|
}
|
|
|
|
type LoadingState =
|
|
| "assembling"
|
|
| "upserting"
|
|
| "updating_tools"
|
|
| "importing"
|
|
| "initializing"
|
|
| "checking"
|
|
| "ready";
|
|
|
|
/**
|
|
* Generate status hints based on session type and block provenance.
|
|
* Pure function - no React dependencies.
|
|
*/
|
|
export function getAgentStatusHints(
|
|
continueSession: boolean,
|
|
agentState?: Letta.AgentState | null,
|
|
agentProvenance?: AgentProvenance | null,
|
|
): string[] {
|
|
const hints: string[] = [];
|
|
|
|
// For resumed agents, show memory blocks and --new hint
|
|
if (continueSession) {
|
|
if (agentState?.memory?.blocks) {
|
|
const blocks = agentState.memory.blocks;
|
|
const count = blocks.length;
|
|
const labels = blocks
|
|
.map((b) => b.label)
|
|
.filter(Boolean)
|
|
.join(", ");
|
|
if (labels) {
|
|
hints.push(
|
|
`→ Attached ${count} memory block${count !== 1 ? "s" : ""}: ${labels}`,
|
|
);
|
|
}
|
|
}
|
|
hints.push("→ To create a new agent, use --new");
|
|
return hints;
|
|
}
|
|
|
|
// For new agents with provenance, show block sources
|
|
if (agentProvenance) {
|
|
// Blocks reused from existing storage
|
|
const reusedGlobalBlocks = agentProvenance.blocks
|
|
.filter((b) => b.source === "global")
|
|
.map((b) => b.label);
|
|
const reusedProjectBlocks = agentProvenance.blocks
|
|
.filter((b) => b.source === "project")
|
|
.map((b) => b.label);
|
|
|
|
// New blocks - categorize by where they'll be stored
|
|
// (project blocks → .letta/, others → ~/.letta/)
|
|
const newBlocks = agentProvenance.blocks.filter((b) => b.source === "new");
|
|
const newGlobalBlocks = newBlocks
|
|
.filter((b) => !isProjectBlock(b.label))
|
|
.map((b) => b.label);
|
|
const newProjectBlocks = newBlocks
|
|
.filter((b) => isProjectBlock(b.label))
|
|
.map((b) => b.label);
|
|
|
|
if (reusedGlobalBlocks.length > 0) {
|
|
hints.push(
|
|
`→ Reusing from global (~/.letta/): ${reusedGlobalBlocks.join(", ")}`,
|
|
);
|
|
}
|
|
if (newGlobalBlocks.length > 0) {
|
|
hints.push(
|
|
`→ Created in global (~/.letta/): ${newGlobalBlocks.join(", ")}`,
|
|
);
|
|
}
|
|
if (reusedProjectBlocks.length > 0) {
|
|
hints.push(
|
|
`→ Reusing from project (.letta/): ${reusedProjectBlocks.join(", ")}`,
|
|
);
|
|
}
|
|
if (newProjectBlocks.length > 0) {
|
|
hints.push(
|
|
`→ Created in project (.letta/): ${newProjectBlocks.join(", ")}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
return hints;
|
|
}
|
|
|
|
export function WelcomeScreen({
|
|
loadingState,
|
|
continueSession,
|
|
agentState,
|
|
agentProvenance: _agentProvenance,
|
|
}: {
|
|
loadingState: LoadingState;
|
|
continueSession?: boolean;
|
|
agentState?: Letta.AgentState | null;
|
|
agentProvenance?: AgentProvenance | null;
|
|
}) {
|
|
// Keep hook call for potential future responsive behavior
|
|
useTerminalWidth();
|
|
const cwd = process.cwd();
|
|
const version = getVersion();
|
|
|
|
const logoLines = asciiLogo.trim().split("\n");
|
|
const tildePath = toTildePath(cwd);
|
|
|
|
// Get model from agent state - just the last part (after last /)
|
|
const fullModel = agentState?.model || agentState?.llm_config?.model;
|
|
const model = fullModel?.split("/").pop();
|
|
|
|
// Get auth method
|
|
const authMethod = getAuthMethod();
|
|
const authDisplay =
|
|
authMethod === "url"
|
|
? process.env.LETTA_BASE_URL || "Custom URL"
|
|
: authMethod === "api-key"
|
|
? "API key auth"
|
|
: "OAuth";
|
|
|
|
return (
|
|
<Box flexDirection="row" marginTop={1}>
|
|
{/* Left column: Logo */}
|
|
<Box flexDirection="column" paddingLeft={1} paddingRight={2}>
|
|
{logoLines.map((line, idx) => (
|
|
// biome-ignore lint/suspicious/noArrayIndexKey: Logo lines are static and never reorder
|
|
<Text key={idx} bold color={colors.welcome.accent}>
|
|
{idx === 0 ? ` ${line}` : line}
|
|
</Text>
|
|
))}
|
|
</Box>
|
|
|
|
{/* Right column: Text info */}
|
|
<Box flexDirection="column" marginTop={0}>
|
|
{/* Row 1: Letta Code + version */}
|
|
<Box>
|
|
<Text bold>Letta Code</Text>
|
|
<Text color="gray"> v{version}</Text>
|
|
</Box>
|
|
{/* Row 2: model · auth (or just auth while loading) */}
|
|
<Text color="gray">
|
|
{model ? `${model} · ${authDisplay}` : authDisplay}
|
|
</Text>
|
|
{/* Row 3: loading status, then path once ready */}
|
|
<Text color="gray">
|
|
{loadingState === "ready"
|
|
? tildePath
|
|
: getLoadingMessage(loadingState, !!continueSession)}
|
|
</Text>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
function getLoadingMessage(
|
|
loadingState: LoadingState,
|
|
continueSession: boolean,
|
|
): string {
|
|
switch (loadingState) {
|
|
case "initializing":
|
|
return continueSession ? "Resuming agent..." : "Creating agent...";
|
|
case "assembling":
|
|
return "Assembling tools...";
|
|
case "upserting":
|
|
return "Upserting tools...";
|
|
case "updating_tools":
|
|
return "Updating tools...";
|
|
case "importing":
|
|
return "Importing agent...";
|
|
case "checking":
|
|
return "Checking for pending approvals...";
|
|
default:
|
|
return "Loading...";
|
|
}
|
|
}
|