fix: Fixed status line and updated welcome screen (#156)
This commit is contained in:
111
src/cli/App.tsx
111
src/cli/App.tsx
@@ -201,74 +201,6 @@ function getSkillUnloadReminder(): string {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Generate status lines based on agent provenance
|
||||
function generateStatusLines(
|
||||
continueSession: boolean,
|
||||
agentProvenance: AgentProvenance | null,
|
||||
agentState?: AgentState | null,
|
||||
): string[] {
|
||||
const lines: string[] = [];
|
||||
|
||||
// For resumed agents
|
||||
if (continueSession) {
|
||||
lines.push(`Resumed existing agent (${agentState?.id})`);
|
||||
|
||||
// Show attached blocks if available
|
||||
if (agentState?.memory?.blocks) {
|
||||
const labels = agentState.memory.blocks
|
||||
.map((b) => b.label)
|
||||
.filter(Boolean)
|
||||
.join(", ");
|
||||
if (labels) {
|
||||
lines.push(` → Memory blocks: ${labels}`);
|
||||
}
|
||||
}
|
||||
|
||||
lines.push(" → To create a new agent, use --new");
|
||||
return lines;
|
||||
}
|
||||
|
||||
// For new agents with provenance
|
||||
if (agentProvenance) {
|
||||
if (agentProvenance.freshBlocks) {
|
||||
lines.push(`Created new agent (${agentState?.id})`);
|
||||
const allLabels = agentProvenance.blocks.map((b) => b.label).join(", ");
|
||||
if (allLabels) {
|
||||
lines.push(` → Created new memory blocks: ${allLabels}`);
|
||||
}
|
||||
} else {
|
||||
lines.push(`Created new agent (${agentState?.id})`);
|
||||
|
||||
// Group blocks by source
|
||||
const globalBlocks = agentProvenance.blocks
|
||||
.filter((b) => b.source === "global")
|
||||
.map((b) => b.label);
|
||||
const projectBlocks = agentProvenance.blocks
|
||||
.filter((b) => b.source === "project")
|
||||
.map((b) => b.label);
|
||||
const newBlocks = agentProvenance.blocks
|
||||
.filter((b) => b.source === "new")
|
||||
.map((b) => b.label);
|
||||
|
||||
if (globalBlocks.length > 0) {
|
||||
lines.push(
|
||||
` → Reusing from global (~/.letta/): ${globalBlocks.join(", ")}`,
|
||||
);
|
||||
}
|
||||
if (projectBlocks.length > 0) {
|
||||
lines.push(
|
||||
` → Reusing from project (.letta/): ${projectBlocks.join(", ")}`,
|
||||
);
|
||||
}
|
||||
if (newBlocks.length > 0) {
|
||||
lines.push(` → Created new blocks: ${newBlocks.join(", ")}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
// Items that have finished rendering and no longer change
|
||||
type StaticItem =
|
||||
| {
|
||||
@@ -277,6 +209,7 @@ type StaticItem =
|
||||
snapshot: {
|
||||
continueSession: boolean;
|
||||
agentState?: AgentState | null;
|
||||
agentProvenance?: AgentProvenance | null;
|
||||
terminalWidth: number;
|
||||
};
|
||||
}
|
||||
@@ -576,6 +509,7 @@ export default function App({
|
||||
snapshot: {
|
||||
continueSession,
|
||||
agentState,
|
||||
agentProvenance,
|
||||
terminalWidth: columns,
|
||||
},
|
||||
},
|
||||
@@ -604,22 +538,6 @@ export default function App({
|
||||
// Insert at the beginning of the order array
|
||||
buffersRef.current.order.unshift(backfillStatusId);
|
||||
|
||||
// Inject provenance status line at the END of the backfilled history
|
||||
const statusLines = generateStatusLines(
|
||||
continueSession,
|
||||
agentProvenance,
|
||||
agentState,
|
||||
);
|
||||
if (statusLines.length > 0) {
|
||||
const statusId = `status-${Date.now().toString(36)}`;
|
||||
buffersRef.current.byId.set(statusId, {
|
||||
kind: "status",
|
||||
id: statusId,
|
||||
lines: statusLines,
|
||||
});
|
||||
buffersRef.current.order.push(statusId);
|
||||
}
|
||||
|
||||
refreshDerived();
|
||||
commitEligibleLines(buffersRef.current);
|
||||
}
|
||||
@@ -2834,12 +2752,18 @@ Plan file path: ${planFilePath}`;
|
||||
}, [lines, tokenStreamingEnabled]);
|
||||
|
||||
// Commit welcome snapshot once when ready for fresh sessions (no history)
|
||||
// Wait for agentProvenance to be available for new agents (continueSession=false)
|
||||
useEffect(() => {
|
||||
if (
|
||||
loadingState === "ready" &&
|
||||
!welcomeCommittedRef.current &&
|
||||
messageHistory.length === 0
|
||||
) {
|
||||
// For new agents, wait until provenance is available
|
||||
// For resumed agents, provenance stays null (that's expected)
|
||||
if (!continueSession && !agentProvenance) {
|
||||
return; // Wait for provenance to be set
|
||||
}
|
||||
welcomeCommittedRef.current = true;
|
||||
setStaticItems((prev) => [
|
||||
...prev,
|
||||
@@ -2849,27 +2773,11 @@ Plan file path: ${planFilePath}`;
|
||||
snapshot: {
|
||||
continueSession,
|
||||
agentState,
|
||||
agentProvenance,
|
||||
terminalWidth: columns,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// Inject status line for fresh sessions
|
||||
const statusLines = generateStatusLines(
|
||||
continueSession,
|
||||
agentProvenance,
|
||||
agentState,
|
||||
);
|
||||
if (statusLines.length > 0) {
|
||||
const statusId = `status-${Date.now().toString(36)}`;
|
||||
buffersRef.current.byId.set(statusId, {
|
||||
kind: "status",
|
||||
id: statusId,
|
||||
lines: statusLines,
|
||||
});
|
||||
buffersRef.current.order.push(statusId);
|
||||
refreshDerived();
|
||||
}
|
||||
}
|
||||
}, [
|
||||
loadingState,
|
||||
@@ -2878,7 +2786,6 @@ Plan file path: ${planFilePath}`;
|
||||
columns,
|
||||
agentProvenance,
|
||||
agentState,
|
||||
refreshDerived,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Letta } from "@letta-ai/letta-client";
|
||||
import { Box, Text } from "ink";
|
||||
import Link from "ink-link";
|
||||
import type { AgentProvenance } from "../../agent/create";
|
||||
import { getVersion } from "../../version";
|
||||
import { useTerminalWidth } from "../hooks/useTerminalWidth";
|
||||
import { asciiLogo } from "./AsciiArt";
|
||||
@@ -15,15 +16,92 @@ type LoadingState =
|
||||
| "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/skills → .letta/, others → ~/.letta/)
|
||||
const newBlocks = agentProvenance.blocks.filter((b) => b.source === "new");
|
||||
const newGlobalBlocks = newBlocks
|
||||
.filter((b) => b.label !== "project" && b.label !== "skills")
|
||||
.map((b) => b.label);
|
||||
const newProjectBlocks = newBlocks
|
||||
.filter((b) => b.label === "project" || b.label === "skills")
|
||||
.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,
|
||||
terminalWidth: frozenWidth,
|
||||
}: {
|
||||
loadingState: LoadingState;
|
||||
continueSession?: boolean;
|
||||
agentState?: Letta.AgentState | null;
|
||||
agentProvenance?: AgentProvenance | null;
|
||||
terminalWidth?: number;
|
||||
}) {
|
||||
const currentWidth = useTerminalWidth();
|
||||
@@ -32,94 +110,20 @@ export function WelcomeScreen({
|
||||
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;
|
||||
}
|
||||
|
||||
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 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 (loadingState === "initializing") {
|
||||
return continueSession ? "Resuming agent..." : "Creating agent...";
|
||||
}
|
||||
if (loadingState === "assembling") {
|
||||
return "Assembling tools...";
|
||||
}
|
||||
if (loadingState === "upserting") {
|
||||
return "Upserting tools...";
|
||||
}
|
||||
if (loadingState === "linking") {
|
||||
return "Attaching Letta Code tools...";
|
||||
}
|
||||
if (loadingState === "unlinking") {
|
||||
return "Removing Letta Code tools...";
|
||||
}
|
||||
if (loadingState === "checking") {
|
||||
return "Checking for pending approvals...";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const getPathLine = () => {
|
||||
if (isMedium) {
|
||||
return `Running in ${cwd}`;
|
||||
}
|
||||
return cwd;
|
||||
};
|
||||
|
||||
const getAgentLink = () => {
|
||||
if (loadingState === "ready" && agentId) {
|
||||
const url = `https://app.letta.com/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();
|
||||
const statusMessage = getStatusMessage(
|
||||
loadingState,
|
||||
!!continueSession,
|
||||
agentId,
|
||||
);
|
||||
const pathLine = isMedium ? `Running in ${cwd}` : cwd;
|
||||
const agentUrl = agentId ? `https://app.letta.com/agents/${agentId}` : null;
|
||||
const hints =
|
||||
loadingState === "ready"
|
||||
? getAgentStatusHints(!!continueSession, agentState, agentProvenance)
|
||||
: [];
|
||||
|
||||
return (
|
||||
<Box flexDirection="row" marginTop={1}>
|
||||
@@ -139,13 +143,55 @@ export function WelcomeScreen({
|
||||
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>
|
||||
{statusMessage && (
|
||||
<Text dimColor>
|
||||
{statusMessage}
|
||||
{loadingState === "ready" && agentUrl && (
|
||||
<>
|
||||
{": "}
|
||||
<Link url={agentUrl}>
|
||||
<Text dimColor>{agentUrl}</Text>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
{hints.map((hint, idx) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: Hint lines are static and never reorder
|
||||
<Text key={idx} dimColor>
|
||||
{hint}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function getStatusMessage(
|
||||
loadingState: LoadingState,
|
||||
continueSession: boolean,
|
||||
agentId?: string,
|
||||
): string {
|
||||
switch (loadingState) {
|
||||
case "ready":
|
||||
return continueSession && agentId
|
||||
? "Resumed agent"
|
||||
: agentId
|
||||
? "Created a new agent"
|
||||
: "Ready to go!";
|
||||
case "initializing":
|
||||
return continueSession ? "Resuming agent..." : "Creating agent...";
|
||||
case "assembling":
|
||||
return "Assembling tools...";
|
||||
case "upserting":
|
||||
return "Upserting tools...";
|
||||
case "linking":
|
||||
return "Attaching Letta Code tools...";
|
||||
case "unlinking":
|
||||
return "Removing Letta Code tools...";
|
||||
case "checking":
|
||||
return "Checking for pending approvals...";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user