Files
letta-code/src/cli/components/WelcomeScreen.tsx
2026-02-02 17:24:46 -08:00

167 lines
4.9 KiB
TypeScript

import { homedir } from "node:os";
import type { Letta } from "@letta-ai/letta-client";
import { Box } from "ink";
import { useEffect, useState } from "react";
import type { AgentProvenance } from "../../agent/create";
import { getModelDisplayName } from "../../agent/model";
import { settingsManager } from "../../settings-manager";
import { getVersion } from "../../version";
import { useTerminalWidth } from "../hooks/useTerminalWidth";
import { AnimatedLogo } from "./AnimatedLogo";
import { colors } from "./colors";
import { Text } from "./Text";
/**
* 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;
}
/**
* Synchronously determine auth method from env vars (for initial render).
* Returns null if we need to check keychain/settings asynchronously.
*/
function getInitialAuthMethod(): "url" | "api-key" | null {
if (process.env.LETTA_BASE_URL) return "url";
if (process.env.LETTA_API_KEY) return "api-key";
return null; // Need async check for keychain/settings
}
/**
* Determine the auth method used (async for keychain access)
*/
async function getAuthMethod(): Promise<"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) from keychain tokens
const settings = await settingsManager.getSettingsWithSecureTokens();
if (settings.refreshToken) {
return "oauth";
}
// Check if API key stored in settings or keychain
if (settings.env?.LETTA_API_KEY) {
return "api-key";
}
return "oauth"; // default
}
type LoadingState =
| "loading_profiles"
| "assembling"
| "importing"
| "initializing"
| "checking"
| "selecting_global"
| "ready";
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 tildePath = toTildePath(cwd);
// Get model display name (pretty name if available, otherwise last part of handle)
// Build full model handle from llm_config (model_endpoint_type/model) like App.tsx does
const llmConfig = agentState?.llm_config;
const fullModel =
llmConfig?.model_endpoint_type && llmConfig?.model
? `${llmConfig.model_endpoint_type}/${llmConfig.model}`
: (llmConfig?.model ?? null);
const model = fullModel
? (getModelDisplayName(fullModel) ?? fullModel.split("/").pop())
: undefined;
// Get auth method - use sync check for env vars, async only for keychain
const initialAuth = getInitialAuthMethod();
const [authMethod, setAuthMethod] = useState<"url" | "api-key" | "oauth">(
initialAuth ?? "oauth",
);
useEffect(() => {
// Only run async check if env vars didn't determine auth method
if (!initialAuth) {
getAuthMethod().then(setAuthMethod);
}
}, [initialAuth]);
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}>
<AnimatedLogo
color={colors.welcome.accent}
animate={loadingState !== "ready"}
/>
</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 "loading_profiles":
return "Loading pinned agents...";
case "initializing":
return continueSession ? "Resuming agent..." : "Creating agent...";
case "assembling":
return "Assembling tools...";
case "importing":
return "Importing agent...";
case "checking":
return "Checking for pending approvals...";
default:
return "Loading...";
}
}