feat: add usage command (#281)

This commit is contained in:
cthomas
2025-12-17 19:48:04 -08:00
committed by GitHub
parent fb6fecddf8
commit b9e52d20e8
5 changed files with 145 additions and 48 deletions

View File

@@ -59,7 +59,7 @@ import { ProfileSelector } from "./components/ProfileSelector";
import { QuestionDialog } from "./components/QuestionDialog";
import { ReasoningMessage } from "./components/ReasoningMessageRich";
import { ResumeSelector } from "./components/ResumeSelector";
import { SessionStats as SessionStatsComponent } from "./components/SessionStats";
import { formatUsageStats } from "./components/SessionStats";
import { StatusMessage } from "./components/StatusMessage";
import { SubagentGroupDisplay } from "./components/SubagentGroupDisplay";
import { SubagentGroupStatic } from "./components/SubagentGroupStatic";
@@ -460,9 +460,6 @@ export default function App({
// Track if we've sent the session context for this CLI session
const hasSentSessionContextRef = useRef(false);
// Show exit stats on exit
const [showExitStats, setShowExitStats] = useState(false);
// Static items (things that are done rendering and can be frozen)
const [staticItems, setStaticItems] = useState<StaticItem[]>([]);
@@ -1333,8 +1330,7 @@ export default function App({
const handleExit = useCallback(() => {
saveLastAgentBeforeExit();
setShowExitStats(true);
// Give React time to render the stats, then exit
// Give React time to render the goodbye message, then exit
setTimeout(() => {
process.exit(0);
}, 100);
@@ -1674,7 +1670,99 @@ export default function App({
return { submitted: true };
}
// Special handling for /exit command - show stats and exit
// Special handling for /usage command - show session stats
if (trimmed === "/usage") {
const cmdId = uid("cmd");
buffersRef.current.byId.set(cmdId, {
kind: "command",
id: cmdId,
input: trimmed,
output: "Fetching usage statistics...",
phase: "running",
});
buffersRef.current.order.push(cmdId);
refreshDerived();
// Fetch balance and display stats asynchronously
(async () => {
try {
const stats = sessionStatsRef.current.getSnapshot();
// Try to fetch balance info (only works for Letta Cloud)
// Silently skip if endpoint not available (not deployed yet or self-hosted)
let balance:
| {
total_balance: number;
monthly_credit_balance: number;
purchased_credit_balance: number;
billing_tier: string;
}
| undefined;
try {
const settings = settingsManager.getSettings();
const baseURL =
process.env.LETTA_BASE_URL ||
settings.env?.LETTA_BASE_URL ||
"https://api.letta.com";
const apiKey =
process.env.LETTA_API_KEY || settings.env?.LETTA_API_KEY;
const balanceResponse = await fetch(
`${baseURL}/v1/metadata/balance`,
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
"X-Letta-Source": "letta-code",
},
},
);
if (balanceResponse.ok) {
balance = (await balanceResponse.json()) as {
total_balance: number;
monthly_credit_balance: number;
purchased_credit_balance: number;
billing_tier: string;
};
}
} catch {
// Silently skip balance info if endpoint not available
}
const output = formatUsageStats({
stats,
balance,
});
buffersRef.current.byId.set(cmdId, {
kind: "command",
id: cmdId,
input: trimmed,
output,
phase: "finished",
success: true,
dimOutput: true,
});
refreshDerived();
} catch (error) {
buffersRef.current.byId.set(cmdId, {
kind: "command",
id: cmdId,
input: trimmed,
output: `Error fetching usage: ${error instanceof Error ? error.message : String(error)}`,
phase: "finished",
success: false,
});
refreshDerived();
}
})();
return { submitted: true };
}
// Special handling for /exit command - exit without stats
if (trimmed === "/exit") {
const cmdId = uid("cmd");
buffersRef.current.byId.set(cmdId, {
@@ -3986,21 +4074,9 @@ Plan file path: ${planFilePath}`;
{/* Ensure 1 blank line above input when there are no live items */}
{liveItems.length === 0 && <Box height={1} />}
{/* Show exit stats when exiting */}
{showExitStats && (
<SessionStatsComponent
stats={sessionStatsRef.current.getSnapshot()}
agentId={agentId}
/>
)}
{/* Input row - always mounted to preserve state */}
<Input
visible={
!showExitStats &&
pendingApprovals.length === 0 &&
!anySelectorOpen
}
visible={pendingApprovals.length === 0 && !anySelectorOpen}
streaming={
streaming && !abortControllerRef.current?.signal.aborted
}

View File

@@ -25,9 +25,9 @@ export const commands: Record<string, Command> = {
},
},
"/exit": {
desc: "Exit and show session stats",
desc: "Exit this session",
handler: () => {
// Handled specially in App.tsx to show stats
// Handled specially in App.tsx
return "Exiting...";
},
},
@@ -180,6 +180,13 @@ export const commands: Record<string, Command> = {
return "Opening memory viewer...";
},
},
"/usage": {
desc: "Show session usage statistics and balance",
handler: () => {
// Handled specially in App.tsx to display usage stats
return "Fetching usage statistics...";
},
},
};
/**

View File

@@ -12,6 +12,7 @@ type CommandLine = {
output: string;
phase?: "running" | "finished";
success?: boolean;
dimOutput?: boolean;
};
/**
@@ -63,7 +64,7 @@ export const CommandMessage = memo(({ line }: { line: CommandLine }) => {
<Text>{" ⎿ "}</Text>
</Box>
<Box flexGrow={1} width={Math.max(0, columns - 5)}>
<MarkdownDisplay text={line.output} />
<MarkdownDisplay text={line.output} dimColor={line.dimOutput} />
</Box>
</Box>
)}

View File

@@ -1,12 +1,6 @@
import { Box, Text } from "ink";
import type { SessionStatsSnapshot } from "../../agent/stats";
interface SessionStatsProps {
stats: SessionStatsSnapshot;
agentId?: string;
}
function formatDuration(ms: number): string {
export function formatDuration(ms: number): string {
if (ms < 1000) {
return `${Math.round(ms)}ms`;
}
@@ -25,25 +19,43 @@ function formatDuration(ms: number): string {
return `${(ms / 1000).toFixed(1)}s`;
}
function formatNumber(n: number): string {
export function formatNumber(n: number): string {
return n.toLocaleString();
}
export function SessionStats({ stats, agentId }: SessionStatsProps) {
const wallDuration = formatDuration(stats.totalWallMs);
const apiDuration = formatDuration(stats.totalApiMs);
const steps = stats.usage.stepCount;
const inputTokens = formatNumber(stats.usage.promptTokens);
const outputTokens = formatNumber(stats.usage.completionTokens);
return (
<Box flexDirection="column" paddingTop={1}>
<Text dimColor>Total duration (API): {apiDuration}</Text>
<Text dimColor>Total duration (wall): {wallDuration}</Text>
<Text dimColor>
Usage: {steps} steps · {inputTokens} input · {outputTokens} output
</Text>
{agentId && <Text dimColor>Agent ID: {agentId}</Text>}
</Box>
);
interface BalanceInfo {
total_balance: number;
monthly_credit_balance: number;
purchased_credit_balance: number;
billing_tier: string;
}
interface FormatUsageStatsOptions {
stats: SessionStatsSnapshot;
balance?: BalanceInfo;
}
/**
* Format usage statistics as markdown text for display in CommandMessage
*/
export function formatUsageStats({
stats,
balance,
}: FormatUsageStatsOptions): string {
const outputLines = [
`Total duration (API): ${formatDuration(stats.totalApiMs)}`,
`Total duration (wall): ${formatDuration(stats.totalWallMs)}`,
`Session usage: ${stats.usage.stepCount} steps, ${formatNumber(stats.usage.promptTokens)} input, ${formatNumber(stats.usage.completionTokens)} output`,
"",
];
if (balance) {
outputLines.push(
`Available credits: $${balance.total_balance.toFixed(2)} Plan: [${balance.billing_tier}]`,
` Monthly credits: $${balance.monthly_credit_balance.toFixed(2)}`,
` Purchased credits: $${balance.purchased_credit_balance.toFixed(2)}`,
);
}
return outputLines.join("\n");
}

View File

@@ -44,6 +44,7 @@ export type Line =
output: string;
phase?: "running" | "finished";
success?: boolean;
dimOutput?: boolean;
}
| {
kind: "status";