feat: add spice (#12)

This commit is contained in:
Charles Packer
2025-10-27 14:25:03 -07:00
committed by GitHub
parent 6f7b3bb08b
commit 938c5ae854
11 changed files with 215 additions and 34 deletions

View File

@@ -1,6 +1,6 @@
// Import useInput from vendored Ink for bracketed paste support
import { Box, Text, useInput } from "ink";
import { type ComponentType, memo, useMemo, useState } from "react";
import { memo, useMemo, useState } from "react";
import type { ApprovalContext } from "../../permissions/analyzer";
import { type AdvancedDiffSuccess, computeAdvancedDiff } from "../helpers/diff";
import { resolvePlaceholders } from "../helpers/pasteRegistry";

View File

@@ -0,0 +1,34 @@
export const shortAsciiLogo = `
██████ ██╗ ███████╗████████╗████████╗ █████╗
██ ██ ██║ ██╔════╝╚══██╔══╝╚══██╔══╝██╔══██╗
██ ▇▇ ██ ██║ █████╗ ██║ ██║ ███████║
██ ██ ██║ ██╔══╝ ██║ ██║ ██╔══██║
██████ ███████╗███████╗ ██║ ██║ ██║ ██║
╚═════╝ ╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝
`;
export const longAsciiLogo = `
██████ ██╗ ███████╗████████╗████████╗ █████╗ ██████╗ ██████╗ ██████╗ ███████╗
██ ██ ██║ ██╔════╝╚══██╔══╝╚══██╔══╝██╔══██╗ ██╔════╝██╔═══██╗██╔══██╗██╔════╝
██ ▇▇ ██ ██║ █████╗ ██║ ██║ ███████║ ██║ ██║ ██║██║ ██║█████╗
██ ██ ██║ ██╔══╝ ██║ ██║ ██╔══██║ ██║ ██║ ██║██║ ██║██╔══╝
██████ ███████╗███████╗ ██║ ██║ ██║ ██║ ╚██████╗╚██████╔╝██████╔╝███████╗
╚═════╝ ╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝
`;
export const tinyAsciiLogo = `
██████ ██╗ ███████╗████████╗████████╗ █████╗
██ ██ ██║ ██╔════╝╚══██╔══╝╚══██╔══╝██╔══██╗
██ ▇▇ ██ ██║ █████╗ ██║ ██║ ███████║
██ ██ ██║ ██╔══╝ ██║ ██║ ██╔══██║
██████ ███████╗███████╗ ██║ ██║ ██║ ██║
╚═════╝ ╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝
`;
export const asciiLogo = `
██████
██ ██
██ ▇▇ ██
██ ██
██████
`;

View File

@@ -1,5 +1,5 @@
import { Box, Text, useInput } from "ink";
import { type ComponentType, memo, useState } from "react";
import { memo, useState } from "react";
import { resolvePlaceholders } from "../helpers/pasteRegistry";
import { colors } from "./colors";
import { MarkdownDisplay } from "./MarkdownDisplay";

View File

@@ -1,4 +1,9 @@
import type { Letta } from "@letta-ai/letta-client";
import { Box, Text } from "ink";
import Link from "ink-link";
import { getVersion } from "../../version";
import { useTerminalWidth } from "../hooks/useTerminalWidth";
import { asciiLogo } from "./AsciiArt";
import { colors } from "./colors";
type LoadingState =
@@ -11,43 +16,128 @@ type LoadingState =
export function WelcomeScreen({
loadingState,
continueSession,
agentId,
agentState,
terminalWidth: frozenWidth,
}: {
loadingState: LoadingState;
continueSession?: boolean;
agentId?: string;
agentState?: Letta.AgentState | null;
terminalWidth?: number;
}) {
const getInitializingMessage = () => {
if (continueSession && agentId) {
return `Resuming agent ${agentId}...`;
const currentWidth = useTerminalWidth();
const terminalWidth = frozenWidth ?? currentWidth;
const cwd = process.cwd();
const version = getVersion();
const agentId = agentState?.id;
// Split logo into lines for side-by-side rendering
const logoLines = asciiLogo.trim().split("\n");
// Determine verbosity level based on terminal width
const isWide = terminalWidth >= 120;
const isMedium = terminalWidth >= 80;
const getMemoryBlocksText = () => {
if (!agentState?.memory?.blocks) {
return null;
}
return "Creating agent...";
const blocks = agentState.memory.blocks;
const count = blocks.length;
const labels = blocks
.map((b) => b.label)
.filter(Boolean)
.join(", ");
if (isWide && labels) {
return `attached ${count} memory block${count !== 1 ? "s" : ""} (${labels})`;
}
if (isMedium) {
return `attached ${count} memory block${count !== 1 ? "s" : ""}`;
}
return null;
};
const getReadyMessage = () => {
if (continueSession && agentId) {
return `Resumed agent (${agentId}). Ready to go!`;
const getAgentMessage = () => {
if (loadingState === "ready") {
const memoryText = getMemoryBlocksText();
const baseText =
continueSession && agentId
? "Resumed agent"
: agentId
? "Created a new agent"
: "Ready to go!";
if (memoryText) {
return `${baseText}, ${memoryText}`;
}
return baseText;
}
if (agentId) {
return `Created a new agent (${agentId}). Ready to go!`;
if (loadingState === "initializing") {
return continueSession ? "Resuming agent..." : "Creating agent...";
}
return "Ready to go!";
if (loadingState === "assembling") {
return "Assembling tools...";
}
if (loadingState === "upserting") {
return "Upserting tools...";
}
if (loadingState === "checking") {
return "Checking for pending approvals...";
}
return "";
};
const stateMessages: Record<LoadingState, string> = {
assembling: "Assembling tools...",
upserting: "Upserting tools...",
initializing: getInitializingMessage(),
checking: "Checking for pending approvals...",
ready: getReadyMessage(),
const getPathLine = () => {
if (isMedium) {
return `Running in ${cwd}`;
}
return cwd;
};
const getAgentLink = () => {
if (loadingState === "ready" && agentId) {
const url = `https://app.letta.com/projects/default-project/agents/${agentId}`;
if (isWide) {
return { text: url, url };
}
if (isMedium) {
return { text: agentId, url };
}
return { text: "Click to view in ADE", url };
}
return null;
};
const agentMessage = getAgentMessage();
const pathLine = getPathLine();
const agentLink = getAgentLink();
return (
<Box flexDirection="column">
<Text bold color={colors.welcome.accent}>
Letta Code
</Text>
<Text dimColor>{stateMessages[loadingState]}</Text>
<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}>
<Text bold color={colors.welcome.accent}>
Letta Code v{version}
</Text>
<Text dimColor>{pathLine}</Text>
{agentMessage && <Text dimColor>{agentMessage}</Text>}
{agentLink && (
<Link url={agentLink.url}>
<Text dimColor>{agentLink.text}</Text>
</Link>
)}
</Box>
</Box>
);
}