Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/cli/src/tests/slash-commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ test("buildSlashCommands prefixes skills before built-ins", () => {
"resume",
"continue",
"undo",
"usage",
"mcp",
"raw",
"exit",
Expand Down
7 changes: 7 additions & 0 deletions packages/cli/src/ui/core/slash-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type SlashCommandKind =
| "resume"
| "continue"
| "undo"
| "usage"
| "mcp"
| "raw"
| "exit";
Expand Down Expand Up @@ -65,6 +66,12 @@ export const BUILTIN_SLASH_COMMANDS: SlashCommandItem[] = [
label: "/undo",
description: "Restore code and/or conversation to a previous point",
},
{
kind: "usage",
name: "usage",
label: "/usage",
description: "Show DeepSeek API balance / credits",
},
{
kind: "mcp",
name: "mcp",
Expand Down
51 changes: 49 additions & 2 deletions packages/cli/src/ui/views/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { findExpandedThinkingId } from "../core/thinking-state";
import { WelcomeScreen } from "./WelcomeScreen";
import { AskUserQuestionPrompt } from "./AskUserQuestionPrompt";
import { McpStatusList } from "./McpStatusList";
import { UsageView, type UsageData } from "./UsageView";
import { ProcessStdoutView } from "./ProcessStdoutView";
import {
type AskUserQuestionAnswers,
Expand All @@ -35,6 +36,7 @@ import { resolveCurrentSettings, writeModelConfigSelection } from "@vegamo/deepc
import { useStatusLine } from "../hooks";
import type { SessionInfo } from "../statusline";
import { isCollapsedThinking } from "../core/thinking-state";
import { BUILTIN_SLASH_COMMANDS, findExactSlashCommand } from "../core/slash-commands";
import { ANSI_CLEAR_SCREEN } from "../constants";
import type {
LlmStreamProgress,
Expand All @@ -50,7 +52,7 @@ import { SessionManager } from "@vegamo/deepcode-core";
import { getCompactPromptTokenThreshold } from "@vegamo/deepcode-core";
import { writeStdout, writeStdoutLine } from "../../utils/stdio-helpers";

type View = "chat" | "session-list" | "undo" | "mcp-status";
type View = "chat" | "session-list" | "undo" | "usage" | "mcp-status";

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

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

rawModeRef.current = mode;
Expand Down Expand Up @@ -321,6 +324,38 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp
[exit, sessionManager]
);

const handleUsage = useCallback(async () => {
const settings = resolveCurrentSettings(projectRoot);
const baseURL = settings.baseURL.toLowerCase();
if (!baseURL.includes("api.deepseek.com")) {
setErrorLine(`/usage is only compatible with DeepSeek API. Current base URL: ${settings.baseURL}`);
return;
}
if (!settings.apiKey) {
setErrorLine("No API key configured. Set DEEPCODE_API_KEY env var or apiKey in settings.json.");
return;
}

setBusy(true);
setErrorLine(null);
try {
const response = await fetch("https://api.deepseek.com/user/balance", {
headers: { Authorization: `Bearer ${settings.apiKey}` },
});
if (!response.ok) {
setErrorLine(`Failed to fetch balance: HTTP ${response.status} ${response.statusText || ""}`.trim());
return;
}
const data: UsageData = await response.json();
setUsageData(data);
navigateToSubView("usage");
} catch (error) {
setErrorLine(`Failed to fetch balance: ${error instanceof Error ? error.message : String(error)}`);
} finally {
setBusy(false);
}
}, [projectRoot, navigateToSubView]);

const handlePrompt = useCallback(
async (submission: PromptSubmission) => {
if (submission.command === "exit") {
Expand Down Expand Up @@ -361,6 +396,10 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp
navigateToSubView("mcp-status");
return;
}
if (submission.command === "usage") {
void handleUsage();
return;
}

const prompt: UserPromptContent = {
text: submission.text,
Expand Down Expand Up @@ -420,6 +459,7 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp
sessionManager,
pendingPermissionReply,
handleExit,
handleUsage,
onRestart,
refreshSkills,
refreshSessionsList,
Expand Down Expand Up @@ -551,10 +591,15 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp
// Step 2: Submit prompt if provided
if (initialPrompt && initialPrompt.trim()) {
initialPromptSubmittedRef.current = true;
const trimmed = initialPrompt.trim();
const slashToken = trimmed.split(/\s+/, 1)[0];
const match = findExactSlashCommand(BUILTIN_SLASH_COMMANDS, slashToken);
const command = match && match.kind !== "skill" ? (match.kind as PromptSubmission["command"]) : undefined;
handleSubmit({
text: initialPrompt,
text: trimmed,
imageUrls: [],
selectedSkills: undefined,
command,
});
}
}
Expand Down Expand Up @@ -949,6 +994,8 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp
void sessionManager.reconnectMcpServer(name, latest.mcpServers?.[name]);
}}
/>
) : view === "usage" ? (
<UsageView data={usageData ?? { is_available: false, balance_infos: [] }} onCancel={() => setView("chat")} />
) : shouldShowQuestionPrompt && pendingQuestion && !busy ? (
<AskUserQuestionPrompt
questions={pendingQuestion.questions}
Expand Down
7 changes: 6 additions & 1 deletion packages/cli/src/ui/views/PromptInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export type PromptSubmission = {
selectedSkills?: SkillInfo[];
permissions?: UserToolPermission[];
alwaysAllows?: PermissionScope[];
command?: "new" | "resume" | "continue" | "undo" | "mcp" | "exit";
command?: "new" | "resume" | "continue" | "undo" | "usage" | "mcp" | "exit";
};

export type PromptDraft = {
Expand Down Expand Up @@ -708,6 +708,11 @@ export const PromptInput = React.memo(function PromptInput({
resetPromptInput();
return;
}
if (item.kind === "usage") {
onSubmit({ text: "/usage", imageUrls: [], command: "usage" });
resetPromptInput();
return;
}
if (item.kind === "mcp") {
onSubmit({ text: "/mcp", imageUrls: [], command: "mcp" });
resetPromptInput();
Expand Down
59 changes: 59 additions & 0 deletions packages/cli/src/ui/views/UsageView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from "react";
import { Box, Text, useInput } from "ink";

export type UsageData = {
is_available: boolean;
balance_infos: Array<{
currency: string;
total_balance: string;
granted_balance: string;
topped_up_balance: string;
}>;
};

type Props = {
data: UsageData;
onCancel: () => void;
};

export function UsageView({ data, onCancel }: Props): React.ReactElement {
useInput((_input, key) => {
if (key.escape) {
onCancel();
}
});

return (
<Box flexDirection="column" marginLeft={1} paddingX={1} gap={1} borderStyle="round" borderDimColor>
<Box flexDirection="column">
<Text color="#229ac3" bold>
/usage{" "}
<Text color={data.is_available ? "green" : "red"}>
{data.is_available ? "🟢 Available" : "🔴 Not available"}
</Text>
</Text>
</Box>
{data.balance_infos.length > 0 ? (
<Box flexDirection="column">
{data.balance_infos.map((info, i) => (
<Text key={i}>
<Text bold>{info.currency}</Text>
{" total "}
<Text bold>{info.total_balance}</Text>
{" (granted "}
{info.granted_balance}
{", topped up "}
{info.topped_up_balance}
{")"}
</Text>
))}
</Box>
) : (
<Box>
<Text dimColor>No balance info returned.</Text>
</Box>
)}
<Text dimColor>Esc to close</Text>
</Box>
);
}