Skip to content

Commit 9af2be3

Browse files
committed
feat(cli): add /usage slash command for DeepSeek API balance
- Add 'usage' to SlashCommandKind type and BUILTIN_SLASH_COMMANDS - Handle /usage in PromptInput (command type + handleSlashSelection) - Create handleUsage callback in App: detects DeepSeek via baseURL, fetches GET /user/balance, displays result as system message - Detect slash commands in -p/--prompt startup path - Show provider mismatch error when baseURL is not api.deepseek.com
1 parent ed2aa23 commit 9af2be3

3 files changed

Lines changed: 97 additions & 2 deletions

File tree

packages/cli/src/ui/core/slash-commands.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export type SlashCommandKind =
99
| "resume"
1010
| "continue"
1111
| "undo"
12+
| "usage"
1213
| "mcp"
1314
| "raw"
1415
| "exit";
@@ -65,6 +66,12 @@ export const BUILTIN_SLASH_COMMANDS: SlashCommandItem[] = [
6566
label: "/undo",
6667
description: "Restore code and/or conversation to a previous point",
6768
},
69+
{
70+
kind: "usage",
71+
name: "usage",
72+
label: "/usage",
73+
description: "Show DeepSeek API balance / credits",
74+
},
6875
{
6976
kind: "mcp",
7077
name: "mcp",

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

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { resolveCurrentSettings, writeModelConfigSelection } from "@vegamo/deepc
3535
import { useStatusLine } from "../hooks";
3636
import type { SessionInfo } from "../statusline";
3737
import { isCollapsedThinking } from "../core/thinking-state";
38+
import { BUILTIN_SLASH_COMMANDS, findExactSlashCommand } from "../core/slash-commands";
3839
import { ANSI_CLEAR_SCREEN } from "../constants";
3940
import type {
4041
LlmStreamProgress,
@@ -321,6 +322,78 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp
321322
[exit, sessionManager]
322323
);
323324

325+
const handleUsage = useCallback(async () => {
326+
const settings = resolveCurrentSettings(projectRoot);
327+
const baseURL = settings.baseURL.toLowerCase();
328+
if (!baseURL.includes("api.deepseek.com")) {
329+
setErrorLine(`/usage is only compatible with DeepSeek API. Current base URL: ${settings.baseURL}`);
330+
return;
331+
}
332+
if (!settings.apiKey) {
333+
setErrorLine("No API key configured. Set DEEPCODE_API_KEY env var or apiKey in settings.json.");
334+
return;
335+
}
336+
337+
setBusy(true);
338+
setErrorLine(null);
339+
try {
340+
const response = await fetch("https://api.deepseek.com/user/balance", {
341+
headers: { Authorization: `Bearer ${settings.apiKey}` },
342+
});
343+
if (!response.ok) {
344+
setErrorLine(`Failed to fetch balance: HTTP ${response.status} ${response.statusText || ""}`.trim());
345+
return;
346+
}
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+
}
390+
} catch (error) {
391+
setErrorLine(`Failed to fetch balance: ${error instanceof Error ? error.message : String(error)}`);
392+
} finally {
393+
setBusy(false);
394+
}
395+
}, [projectRoot, sessionManager]);
396+
324397
const handlePrompt = useCallback(
325398
async (submission: PromptSubmission) => {
326399
if (submission.command === "exit") {
@@ -361,6 +434,10 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp
361434
navigateToSubView("mcp-status");
362435
return;
363436
}
437+
if (submission.command === "usage") {
438+
void handleUsage();
439+
return;
440+
}
364441

365442
const prompt: UserPromptContent = {
366443
text: submission.text,
@@ -420,6 +497,7 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp
420497
sessionManager,
421498
pendingPermissionReply,
422499
handleExit,
500+
handleUsage,
423501
onRestart,
424502
refreshSkills,
425503
refreshSessionsList,
@@ -551,10 +629,15 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp
551629
// Step 2: Submit prompt if provided
552630
if (initialPrompt && initialPrompt.trim()) {
553631
initialPromptSubmittedRef.current = true;
632+
const trimmed = initialPrompt.trim();
633+
const slashToken = trimmed.split(/\s+/, 1)[0];
634+
const match = findExactSlashCommand(BUILTIN_SLASH_COMMANDS, slashToken);
635+
const command = match && match.kind !== "skill" ? (match.kind as PromptSubmission["command"]) : undefined;
554636
handleSubmit({
555-
text: initialPrompt,
637+
text: trimmed,
556638
imageUrls: [],
557639
selectedSkills: undefined,
640+
command,
558641
});
559642
}
560643
}

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export type PromptSubmission = {
7272
selectedSkills?: SkillInfo[];
7373
permissions?: UserToolPermission[];
7474
alwaysAllows?: PermissionScope[];
75-
command?: "new" | "resume" | "continue" | "undo" | "mcp" | "exit";
75+
command?: "new" | "resume" | "continue" | "undo" | "usage" | "mcp" | "exit";
7676
};
7777

7878
export type PromptDraft = {
@@ -708,6 +708,11 @@ export const PromptInput = React.memo(function PromptInput({
708708
resetPromptInput();
709709
return;
710710
}
711+
if (item.kind === "usage") {
712+
onSubmit({ text: "/usage", imageUrls: [], command: "usage" });
713+
resetPromptInput();
714+
return;
715+
}
711716
if (item.kind === "mcp") {
712717
onSubmit({ text: "/mcp", imageUrls: [], command: "mcp" });
713718
resetPromptInput();

0 commit comments

Comments
 (0)