feat: add spice (#12)
This commit is contained in:
13
bun.lock
13
bun.lock
@@ -3,6 +3,9 @@
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "letta-code",
|
||||
"dependencies": {
|
||||
"ink-link": "^5.0.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@letta-ai/letta-client": "1.0.0-alpha.2",
|
||||
"@types/bun": "latest",
|
||||
@@ -141,6 +144,8 @@
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, ""],
|
||||
|
||||
"has-flag": ["has-flag@5.0.1", "", {}, "sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, ""],
|
||||
|
||||
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, ""],
|
||||
@@ -157,6 +162,8 @@
|
||||
|
||||
"ink": ["ink@5.2.1", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.1.3", "ansi-escapes": "^7.0.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^4.0.0", "code-excerpt": "^4.0.0", "es-toolkit": "^1.22.0", "indent-string": "^5.0.0", "is-in-ci": "^1.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.29.0", "scheduler": "^0.23.0", "signal-exit": "^3.0.7", "slice-ansi": "^7.1.0", "stack-utils": "^2.0.6", "string-width": "^7.2.0", "type-fest": "^4.27.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=18.0.0", "react": ">=18.0.0", "react-devtools-core": "^4.19.1" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-BqcUyWrG9zq5HIwW6JcfFHsIYebJkWWb4fczNah1goUO0vv5vneIlfwuS85twyJ5hYR/y18FlAYUxrO9ChIWVg=="],
|
||||
|
||||
"ink-link": ["ink-link@5.0.0", "", { "dependencies": { "terminal-link": "^5.0.0" }, "peerDependencies": { "ink": ">=6" } }, "sha512-TFDXc/0mwUW7LMjsr0/LeLxPVV5BnHDuDQff9RCgP4rb3R+V/4dIwGBZbCevcJZtQnVcW+Iz1LUrUbpq+UDwYA=="],
|
||||
|
||||
"ink-spinner": ["ink-spinner@5.0.0", "", { "dependencies": { "cli-spinners": "^2.7.0" }, "peerDependencies": { "ink": ">=4.0.0", "react": ">=18.0.0" } }, "sha512-EYEasbEjkqLGyPOUc8hBJZNuC5GvXGMLu0w5gdTNskPc7Izc5vO3tdQEYnzvshucyGCBXc86ig0ujXPMWaQCdA=="],
|
||||
|
||||
"ink-text-input": ["ink-text-input@5.0.1", "", { "dependencies": { "chalk": "^5.2.0", "type-fest": "^3.6.1" }, "peerDependencies": { "ink": "^4.0.0", "react": "^18.0.0" } }, "sha512-crnsYJalG4EhneOFnr/q+Kzw1RgmXI2KsBaLFE6mpiIKxAtJLUnvygOF2IUKO8z4nwkSkveGRBMd81RoYdRSag=="],
|
||||
@@ -251,6 +258,12 @@
|
||||
|
||||
"strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, ""],
|
||||
|
||||
"supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
|
||||
|
||||
"supports-hyperlinks": ["supports-hyperlinks@4.3.0", "", { "dependencies": { "has-flag": "^5.0.1", "supports-color": "^10.0.0" } }, "sha512-i6sWEzuwadSlcr2mOnb0ktlIl+K5FVxsPXmoPfknDd2gyw4ZBIAZ5coc0NQzYqDdEYXMHy8NaY9rWwa1Q1myiQ=="],
|
||||
|
||||
"terminal-link": ["terminal-link@5.0.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "supports-hyperlinks": "^4.1.0" } }, "sha512-qFAy10MTMwjzjU8U16YS4YoZD+NQLHzLssFMNqgravjbvIPNiqkGFR4yjhJfmY9R5OFU7+yHxc6y+uGHkKwLRA=="],
|
||||
|
||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, ""],
|
||||
|
||||
"tr46": ["tr46@0.0.3", "", {}, ""],
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {},
|
||||
"dependencies": {
|
||||
"ink-link": "^5.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@vscode/ripgrep": "^1.17.0"
|
||||
},
|
||||
|
||||
@@ -78,12 +78,17 @@ type StaticItem =
|
||||
| {
|
||||
kind: "welcome";
|
||||
id: string;
|
||||
snapshot: { continueSession: boolean; agentId?: string };
|
||||
snapshot: {
|
||||
continueSession: boolean;
|
||||
agentState?: Letta.AgentState | null;
|
||||
terminalWidth: number;
|
||||
};
|
||||
}
|
||||
| Line;
|
||||
|
||||
export default function App({
|
||||
agentId,
|
||||
agentState,
|
||||
loadingState = "ready",
|
||||
continueSession = false,
|
||||
startupApproval = null,
|
||||
@@ -91,6 +96,7 @@ export default function App({
|
||||
tokenStreaming = true,
|
||||
}: {
|
||||
agentId: string;
|
||||
agentState?: Letta.AgentState | null;
|
||||
loadingState?:
|
||||
| "assembling"
|
||||
| "upserting"
|
||||
@@ -310,7 +316,8 @@ export default function App({
|
||||
id: `welcome-${Date.now().toString(36)}`,
|
||||
snapshot: {
|
||||
continueSession,
|
||||
agentId: agentId !== "loading" ? agentId : undefined,
|
||||
agentState,
|
||||
terminalWidth: columns,
|
||||
},
|
||||
},
|
||||
]);
|
||||
@@ -325,8 +332,9 @@ export default function App({
|
||||
messageHistory,
|
||||
refreshDerived,
|
||||
commitEligibleLines,
|
||||
agentId,
|
||||
continueSession,
|
||||
columns,
|
||||
agentState,
|
||||
]);
|
||||
|
||||
// Fetch llmConfig when agent is ready
|
||||
@@ -1119,12 +1127,19 @@ export default function App({
|
||||
id: `welcome-${Date.now().toString(36)}`,
|
||||
snapshot: {
|
||||
continueSession,
|
||||
agentId: agentId !== "loading" ? agentId : undefined,
|
||||
agentState,
|
||||
terminalWidth: columns,
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
}, [loadingState, continueSession, agentId, messageHistory.length]);
|
||||
}, [
|
||||
loadingState,
|
||||
continueSession,
|
||||
messageHistory.length,
|
||||
columns,
|
||||
agentState,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
@@ -1160,7 +1175,7 @@ export default function App({
|
||||
<WelcomeScreen
|
||||
loadingState={loadingState}
|
||||
continueSession={continueSession}
|
||||
agentId={agentId !== "loading" ? agentId : undefined}
|
||||
agentState={agentState}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
34
src/cli/components/AsciiArt.ts
Normal file
34
src/cli/components/AsciiArt.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
export const shortAsciiLogo = `
|
||||
██████ ██╗ ███████╗████████╗████████╗ █████╗
|
||||
██ ██ ██║ ██╔════╝╚══██╔══╝╚══██╔══╝██╔══██╗
|
||||
██ ▇▇ ██ ██║ █████╗ ██║ ██║ ███████║
|
||||
██ ██ ██║ ██╔══╝ ██║ ██║ ██╔══██║
|
||||
██████ ███████╗███████╗ ██║ ██║ ██║ ██║
|
||||
╚═════╝ ╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝
|
||||
`;
|
||||
|
||||
export const longAsciiLogo = `
|
||||
██████ ██╗ ███████╗████████╗████████╗ █████╗ ██████╗ ██████╗ ██████╗ ███████╗
|
||||
██ ██ ██║ ██╔════╝╚══██╔══╝╚══██╔══╝██╔══██╗ ██╔════╝██╔═══██╗██╔══██╗██╔════╝
|
||||
██ ▇▇ ██ ██║ █████╗ ██║ ██║ ███████║ ██║ ██║ ██║██║ ██║█████╗
|
||||
██ ██ ██║ ██╔══╝ ██║ ██║ ██╔══██║ ██║ ██║ ██║██║ ██║██╔══╝
|
||||
██████ ███████╗███████╗ ██║ ██║ ██║ ██║ ╚██████╗╚██████╔╝██████╔╝███████╗
|
||||
╚═════╝ ╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝
|
||||
`;
|
||||
|
||||
export const tinyAsciiLogo = `
|
||||
██████ ██╗ ███████╗████████╗████████╗ █████╗
|
||||
██ ██ ██║ ██╔════╝╚══██╔══╝╚══██╔══╝██╔══██╗
|
||||
██ ▇▇ ██ ██║ █████╗ ██║ ██║ ███████║
|
||||
██ ██ ██║ ██╔══╝ ██║ ██║ ██╔══██║
|
||||
██████ ███████╗███████╗ ██║ ██║ ██║ ██║
|
||||
╚═════╝ ╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝
|
||||
`;
|
||||
|
||||
export const asciiLogo = `
|
||||
██████
|
||||
██ ██
|
||||
██ ▇▇ ██
|
||||
██ ██
|
||||
██████
|
||||
`;
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
12
src/cli/helpers/asciiUtils.ts
Normal file
12
src/cli/helpers/asciiUtils.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Calculates the maximum width of a multi-line ASCII art string.
|
||||
* @param asciiArt The ASCII art string.
|
||||
* @returns The length of the longest line in the ASCII art.
|
||||
*/
|
||||
export function getAsciiArtWidth(asciiArt: string): number {
|
||||
if (!asciiArt) {
|
||||
return 0;
|
||||
}
|
||||
const lines = asciiArt.split("\n");
|
||||
return Math.max(...lines.map((line) => line.length));
|
||||
}
|
||||
10
src/index.ts
10
src/index.ts
@@ -91,6 +91,13 @@ async function main() {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Handle version flag
|
||||
if (values.version) {
|
||||
const { getVersion } = await import("./version");
|
||||
console.log(`${getVersion()} (Letta Code)`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const shouldContinue = (values.continue as boolean | undefined) ?? false;
|
||||
const specifiedAgentId = (values.agent as string | undefined) ?? null;
|
||||
const isHeadless = values.prompt || values.run || !process.stdin.isTTY;
|
||||
@@ -176,6 +183,7 @@ async function main() {
|
||||
"assembling" | "upserting" | "initializing" | "checking" | "ready"
|
||||
>("assembling");
|
||||
const [agentId, setAgentId] = useState<string | null>(null);
|
||||
const [agentState, setAgentState] = useState<Letta.AgentState | null>(null);
|
||||
const [resumeData, setResumeData] = useState<ResumeData | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -232,6 +240,7 @@ async function main() {
|
||||
}
|
||||
|
||||
setAgentId(agent.id);
|
||||
setAgentState(agent);
|
||||
setLoadingState("ready");
|
||||
}
|
||||
|
||||
@@ -253,6 +262,7 @@ async function main() {
|
||||
|
||||
return React.createElement(App, {
|
||||
agentId,
|
||||
agentState,
|
||||
loadingState,
|
||||
continueSession: isResumingSession,
|
||||
startupApproval: resumeData?.pendingApproval ?? null,
|
||||
|
||||
@@ -26,7 +26,7 @@ describe("Bash background tools", () => {
|
||||
// Extract bash_id from the response text
|
||||
const match = startResult.content[0].text.match(/bash_(\d+)/);
|
||||
expect(match).toBeDefined();
|
||||
const bashId = `bash_${match![1]}`;
|
||||
const bashId = `bash_${match?.[1]}`;
|
||||
|
||||
// Wait for command to complete
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
@@ -52,7 +52,7 @@ describe("Bash background tools", () => {
|
||||
});
|
||||
|
||||
const match = startResult.content[0].text.match(/bash_(\d+)/);
|
||||
const bashId = `bash_${match![1]}`;
|
||||
const bashId = `bash_${match?.[1]}`;
|
||||
|
||||
// Kill it (KillBash uses shell_id parameter)
|
||||
const killResult = await kill_bash({ shell_id: bashId });
|
||||
|
||||
5
src/version.ts
Normal file
5
src/version.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import packageJson from "../package.json";
|
||||
|
||||
export function getVersion(): string {
|
||||
return packageJson.version;
|
||||
}
|
||||
Reference in New Issue
Block a user