Skip to content

Commit b5338dc

Browse files
committed
refactor(cli): show /usage balance in dedicated view instead of system message
Replace inline system message with a UsageView component (bordered box, Esc to close), matching the MCP status pattern. This avoids render issues with addSessionSystemMessage and provides a cleaner UX.
1 parent 9af2be3 commit b5338dc

3 files changed

Lines changed: 69 additions & 45 deletions

File tree

packages/cli/src/tests/slash-commands.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ test("buildSlashCommands prefixes skills before built-ins", () => {
2727
"resume",
2828
"continue",
2929
"undo",
30+
"usage",
3031
"mcp",
3132
"raw",
3233
"exit",

packages/cli/src/ui/views/App.tsx

Lines changed: 9 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { findExpandedThinkingId } from "../core/thinking-state";
1313
import { WelcomeScreen } from "./WelcomeScreen";
1414
import { AskUserQuestionPrompt } from "./AskUserQuestionPrompt";
1515
import { McpStatusList } from "./McpStatusList";
16+
import { UsageView, type UsageData } from "./UsageView";
1617
import { ProcessStdoutView } from "./ProcessStdoutView";
1718
import {
1819
type AskUserQuestionAnswers,
@@ -51,7 +52,7 @@ import { SessionManager } from "@vegamo/deepcode-core";
5152
import { getCompactPromptTokenThreshold } from "@vegamo/deepcode-core";
5253
import { writeStdout, writeStdoutLine } from "../../utils/stdio-helpers";
5354

54-
type View = "chat" | "session-list" | "undo" | "mcp-status";
55+
type View = "chat" | "session-list" | "undo" | "usage" | "mcp-status";
5556

5657
const STATUS_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
5758

@@ -133,6 +134,7 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp
133134
const [resolvedSettings, setResolvedSettings] = useState(() => resolveCurrentSettings(projectRoot));
134135
const [nowTick, setNowTick] = useState(0);
135136
const [mcpStatuses, setMcpStatuses] = useState<ReturnType<typeof sessionManager.getMcpStatus>>([]);
137+
const [usageData, setUsageData] = useState<UsageData | null>(null);
136138
const [showProcessStdout, setShowProcessStdout] = useState(false);
137139

138140
rawModeRef.current = mode;
@@ -344,55 +346,15 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp
344346
setErrorLine(`Failed to fetch balance: HTTP ${response.status} ${response.statusText || ""}`.trim());
345347
return;
346348
}
347-
const data = (await response.json()) as {
348-
is_available: boolean;
349-
balance_infos?: Array<{
350-
currency: string;
351-
total_balance: string;
352-
granted_balance: string;
353-
topped_up_balance: string;
354-
}>;
355-
};
356-
357-
const statusIcon = data.is_available ? "🟢" : "🔴";
358-
const statusText = data.is_available ? "Available" : "Not available";
359-
let content = `/usage\n└ API status: ${statusIcon} ${statusText}`;
360-
361-
if (data.balance_infos && data.balance_infos.length > 0) {
362-
for (const info of data.balance_infos) {
363-
content += `\n ${info.currency}: total ${info.total_balance} (granted ${info.granted_balance}, topped up ${info.topped_up_balance})`;
364-
}
365-
} else {
366-
content += `\n No balance info returned.`;
367-
}
368-
369-
const now = new Date().toISOString();
370-
const activeSessionId = sessionManager.getActiveSessionId();
371-
if (activeSessionId) {
372-
sessionManager.addSessionSystemMessage(activeSessionId, content, true);
373-
} else {
374-
setMessages((prev) => [
375-
...prev,
376-
{
377-
id: crypto.randomUUID(),
378-
sessionId: "local",
379-
role: "system" as const,
380-
content,
381-
contentParams: null,
382-
messageParams: null,
383-
compacted: false,
384-
visible: true,
385-
createTime: now,
386-
updateTime: now,
387-
},
388-
]);
389-
}
349+
const data: UsageData = await response.json();
350+
setUsageData(data);
351+
navigateToSubView("usage");
390352
} catch (error) {
391353
setErrorLine(`Failed to fetch balance: ${error instanceof Error ? error.message : String(error)}`);
392354
} finally {
393355
setBusy(false);
394356
}
395-
}, [projectRoot, sessionManager]);
357+
}, [projectRoot, navigateToSubView]);
396358

397359
const handlePrompt = useCallback(
398360
async (submission: PromptSubmission) => {
@@ -1032,6 +994,8 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp
1032994
void sessionManager.reconnectMcpServer(name, latest.mcpServers?.[name]);
1033995
}}
1034996
/>
997+
) : view === "usage" ? (
998+
<UsageView data={usageData ?? { is_available: false, balance_infos: [] }} onCancel={() => setView("chat")} />
1035999
) : shouldShowQuestionPrompt && pendingQuestion && !busy ? (
10361000
<AskUserQuestionPrompt
10371001
questions={pendingQuestion.questions}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import React from "react";
2+
import { Box, Text, useInput } from "ink";
3+
4+
export type UsageData = {
5+
is_available: boolean;
6+
balance_infos: Array<{
7+
currency: string;
8+
total_balance: string;
9+
granted_balance: string;
10+
topped_up_balance: string;
11+
}>;
12+
};
13+
14+
type Props = {
15+
data: UsageData;
16+
onCancel: () => void;
17+
};
18+
19+
export function UsageView({ data, onCancel }: Props): React.ReactElement {
20+
useInput((_input, key) => {
21+
if (key.escape) {
22+
onCancel();
23+
}
24+
});
25+
26+
return (
27+
<Box flexDirection="column" marginLeft={1} paddingX={1} gap={1} borderStyle="round" borderDimColor>
28+
<Box flexDirection="column">
29+
<Text color="#229ac3" bold>
30+
/usage{" "}
31+
<Text color={data.is_available ? "green" : "red"}>
32+
{data.is_available ? "🟢 Available" : "🔴 Not available"}
33+
</Text>
34+
</Text>
35+
</Box>
36+
{data.balance_infos.length > 0 ? (
37+
<Box flexDirection="column">
38+
{data.balance_infos.map((info, i) => (
39+
<Text key={i}>
40+
<Text bold>{info.currency}</Text>
41+
{" total "}
42+
<Text bold>{info.total_balance}</Text>
43+
{" (granted "}
44+
{info.granted_balance}
45+
{", topped up "}
46+
{info.topped_up_balance}
47+
{")"}
48+
</Text>
49+
))}
50+
</Box>
51+
) : (
52+
<Box>
53+
<Text dimColor>No balance info returned.</Text>
54+
</Box>
55+
)}
56+
<Text dimColor>Esc to close</Text>
57+
</Box>
58+
);
59+
}

0 commit comments

Comments
 (0)