Skip to content

Commit 33b26dc

Browse files
committed
feat: show context and token usage in the status bar
Add a status bar item with active context usage and a tooltip with prompt, completion, cached, and reasoning tokens. Clicking the item opens the Deep Code view.
1 parent 3d69200 commit 33b26dc

3 files changed

Lines changed: 113 additions & 11 deletions

File tree

‎packages/vscode-ide-companion/src/extension.ts‎

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,13 @@ import {
2222
type ResolvedDeepcodingSettings,
2323
setShellIfWindows,
2424
} from "@vegamo/deepcode-core";
25-
import { collectPromptImageUrls, getNonce, isAllowedExternalUrl } from "./utils.js";
25+
import {
26+
collectPromptImageUrls,
27+
formatContextStatus,
28+
formatTokenCount,
29+
getNonce,
30+
isAllowedExternalUrl,
31+
} from "./utils.js";
2632
import { handleWebviewMessage, postUndoState, releaseQueueEdit } from "./provider.js";
2733
import { createSharpLoader } from "./sharp-loader.js";
2834
import { prepareToolDiffContent } from "./tool-diff.js";
@@ -34,6 +40,24 @@ type ReasoningMessageParams = {
3440
reasoning_content?: string;
3541
};
3642

43+
type TokenUsage = {
44+
prompt_tokens?: number;
45+
completion_tokens?: number;
46+
prompt_cache_hit_tokens?: number;
47+
prompt_tokens_details?: { cached_tokens?: number };
48+
completion_tokens_details?: { reasoning_tokens?: number };
49+
};
50+
51+
type TokenTelemetry = {
52+
model: string;
53+
thinkingEnabled: boolean;
54+
reasoningEffort: ReasoningEffort;
55+
activeTokens: number;
56+
contextWindow: number;
57+
compactPromptTokenThreshold: number;
58+
usage: unknown | null;
59+
};
60+
3761
export class DeepCodeViewProvider implements vscode.WebviewViewProvider {
3862
public static readonly viewType = "deepcode.chatView";
3963

@@ -42,6 +66,7 @@ export class DeepCodeViewProvider implements vscode.WebviewViewProvider {
4266
private readonly md: MarkdownIt;
4367
private readonly sessionManager: SessionManager;
4468
private unsubscribeRuntime?: () => void;
69+
private readonly statusBar: vscode.StatusBarItem;
4570

4671
constructor(context: vscode.ExtensionContext) {
4772
this.context = context;
@@ -50,6 +75,12 @@ export class DeepCodeViewProvider implements vscode.WebviewViewProvider {
5075
linkify: false,
5176
breaks: true,
5277
});
78+
this.statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
79+
this.statusBar.name = "Deep Code";
80+
this.statusBar.command = "deepcode.openView";
81+
this.statusBar.text = "$(comment-discussion) Deep Code";
82+
this.statusBar.tooltip = "Open Deep Code";
83+
this.statusBar.show();
5384
this.sessionManager = new SessionManager({
5485
projectRoot: this.getWorkspaceRoot(),
5586
loadSharp: createSharpLoader({
@@ -103,6 +134,8 @@ export class DeepCodeViewProvider implements vscode.WebviewViewProvider {
103134
});
104135
},
105136
onSessionEntryUpdated: (entry) => {
137+
const tokenTelemetry = this.buildTokenTelemetry(entry);
138+
this.updateStatusBar(tokenTelemetry);
106139
if (!this.webviewView) {
107140
return;
108141
}
@@ -114,7 +147,7 @@ export class DeepCodeViewProvider implements vscode.WebviewViewProvider {
114147
pluginRateLimitedTool: entry.pluginRateLimitedTool ?? null,
115148
askPermissions: entry.askPermissions,
116149
processes: this.serializeProcesses(entry.processes),
117-
tokenTelemetry: this.buildTokenTelemetry(entry),
150+
tokenTelemetry,
118151
});
119152
},
120153
onLlmStreamProgress: (progress: LlmStreamProgress) => {
@@ -143,6 +176,7 @@ export class DeepCodeViewProvider implements vscode.WebviewViewProvider {
143176
}
144177

145178
dispose(): void {
179+
this.statusBar.dispose();
146180
this.unsubscribeRuntime?.();
147181
releaseQueueEdit(this.sessionManager);
148182
this.sessionManager.dispose();
@@ -157,6 +191,7 @@ export class DeepCodeViewProvider implements vscode.WebviewViewProvider {
157191
if (this.webviewView === webviewView) this.webviewView = undefined;
158192
releaseQueueEdit(this.sessionManager);
159193
});
194+
this.refreshStatusBar();
160195

161196
webviewView.webview.options = {
162197
enableScripts: true,
@@ -191,6 +226,7 @@ export class DeepCodeViewProvider implements vscode.WebviewViewProvider {
191226
renderMarkdown: (text) => this.md.render(text),
192227
copyToClipboard: (text) => void vscode.env.clipboard.writeText(text),
193228
});
229+
this.refreshStatusBar();
194230

195231
if (!handled) {
196232
// unrecognized message type — no-op
@@ -512,15 +548,7 @@ export class DeepCodeViewProvider implements vscode.WebviewViewProvider {
512548
};
513549
}
514550

515-
private buildTokenTelemetry(session: SessionEntry | null): {
516-
model: string;
517-
thinkingEnabled: boolean;
518-
reasoningEffort: ReasoningEffort;
519-
activeTokens: number;
520-
contextWindow: number;
521-
compactPromptTokenThreshold: number;
522-
usage: unknown | null;
523-
} {
551+
private buildTokenTelemetry(session: SessionEntry | null): TokenTelemetry {
524552
const settings = this.resolveCurrentSettings();
525553
return {
526554
model: settings.model,
@@ -533,6 +561,38 @@ export class DeepCodeViewProvider implements vscode.WebviewViewProvider {
533561
};
534562
}
535563

564+
private refreshStatusBar(): void {
565+
const sessionId = this.sessionManager.getActiveSessionId();
566+
const session = sessionId ? this.sessionManager.getSession(sessionId) : null;
567+
this.updateStatusBar(this.buildTokenTelemetry(session));
568+
}
569+
570+
private updateStatusBar(telemetry: TokenTelemetry): void {
571+
const activeTokens = Number.isFinite(telemetry.activeTokens) ? Math.max(0, telemetry.activeTokens) : 0;
572+
const contextWindow = Number.isFinite(telemetry.contextWindow) ? Math.max(0, telemetry.contextWindow) : 0;
573+
const usage = (telemetry.usage ?? {}) as TokenUsage;
574+
const promptTokens = Number(usage.prompt_tokens ?? 0);
575+
const completionTokens = Number(usage.completion_tokens ?? 0);
576+
const cachedTokens = Number(usage.prompt_tokens_details?.cached_tokens ?? usage.prompt_cache_hit_tokens ?? 0);
577+
const reasoningTokens = Number(usage.completion_tokens_details?.reasoning_tokens ?? 0);
578+
579+
if (activeTokens === 0 && promptTokens === 0 && completionTokens === 0) {
580+
this.statusBar.text = "$(comment-discussion) Deep Code";
581+
this.statusBar.tooltip = "Open Deep Code";
582+
return;
583+
}
584+
585+
this.statusBar.text = `$(comment-discussion) ${formatContextStatus(activeTokens, contextWindow)}`;
586+
const tooltip = new vscode.MarkdownString();
587+
tooltip.appendMarkdown(`**Deep Code** · ${telemetry.model}\n\n`);
588+
tooltip.appendMarkdown(`Context: ${formatContextStatus(activeTokens, contextWindow)}\n\n`);
589+
tooltip.appendMarkdown(`Prompt: ${formatTokenCount(promptTokens)} \n`);
590+
tooltip.appendMarkdown(`Completion: ${formatTokenCount(completionTokens)} \n`);
591+
tooltip.appendMarkdown(`Cached: ${formatTokenCount(cachedTokens)} \n`);
592+
tooltip.appendMarkdown(`Reasoning: ${formatTokenCount(reasoningTokens)}`);
593+
this.statusBar.tooltip = tooltip;
594+
}
595+
536596
private async initializeMcpServers(): Promise<void> {
537597
try {
538598
await this.sessionManager.initMcpServers(this.resolveCurrentSettings().mcpServers);

‎packages/vscode-ide-companion/src/tests/extension-utils.test.ts‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import {
44
collectPromptImageUrls,
55
PLUS_PACKAGES_URL,
66
VALID_PERMISSION_SCOPES,
7+
formatContextStatus,
8+
formatTokenCount,
79
getNonce,
810
isAllowedExternalUrl,
911
parsePermissionScopes,
@@ -163,3 +165,22 @@ test("attachment discovery includes queue, drafts and legacy images but ignores
163165
["file:///legacy.png", "file:///pending.png", "data:image/png;base64,abc"]
164166
);
165167
});
168+
169+
// --- token formatting ---
170+
171+
test("formatTokenCount compacts thousands and millions", () => {
172+
assert.equal(formatTokenCount(0), "0");
173+
assert.equal(formatTokenCount(-1), "0");
174+
assert.equal(formatTokenCount(Number.NaN), "0");
175+
assert.equal(formatTokenCount(999), "999");
176+
assert.equal(formatTokenCount(1_000), "1.0k");
177+
assert.equal(formatTokenCount(20_770), "20.8k");
178+
assert.equal(formatTokenCount(1_048_576), "1.0M");
179+
});
180+
181+
test("formatContextStatus renders used tokens, window, and percentage", () => {
182+
assert.equal(formatContextStatus(20_770, 1_048_576), "20.8k / 1.0M (2%)");
183+
assert.equal(formatContextStatus(500_000, 1_000_000), "500.0k / 1.0M (50%)");
184+
assert.equal(formatContextStatus(0, 0), "0 / 0 (0%)");
185+
assert.equal(formatContextStatus(Number.NaN, 1_000_000), "0 / 1.0M (0%)");
186+
});

‎packages/vscode-ide-companion/src/utils.ts‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,24 @@ export function collectPromptImageUrls(value: unknown): string[] {
8484
visit(value);
8585
return [...urls];
8686
}
87+
88+
export function formatTokenCount(value: number): string {
89+
const number = Number(value);
90+
if (!Number.isFinite(number) || number <= 0) {
91+
return "0";
92+
}
93+
if (number >= 1_000_000) {
94+
return `${(number / 1_000_000).toFixed(1)}M`;
95+
}
96+
if (number >= 1_000) {
97+
return `${(number / 1_000).toFixed(1)}k`;
98+
}
99+
return String(Math.round(number));
100+
}
101+
102+
export function formatContextStatus(activeTokens: number, contextWindow: number): string {
103+
const active = Number.isFinite(activeTokens) && activeTokens > 0 ? activeTokens : 0;
104+
const window = Number.isFinite(contextWindow) && contextWindow > 0 ? contextWindow : 0;
105+
const percent = window > 0 ? Math.round((active / window) * 100) : 0;
106+
return `${formatTokenCount(active)} / ${formatTokenCount(window)} (${percent}%)`;
107+
}

0 commit comments

Comments
 (0)