From 63299008d05c8c1dffde1d363e61796b02ec250f Mon Sep 17 00:00:00 2001 From: andrepimenta Date: Wed, 3 Dec 2025 00:29:08 +0000 Subject: [PATCH] Add subscription detection and usage badge to status bar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Detect user subscription type (Pro/Max) via CLI initialize request - Cache subscription type in globalState for persistence - Show clickable usage badge with chart icon in status bar - Plan users: show "Max Plan" badge, opens live usage view - API users: show cost badge, opens recent usage history - Badge opens terminal in editor with ccusage command 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/extension.ts | 86 ++++++++++++++++++++++++++++++++++++++++++++++++ src/script.ts | 63 +++++++++++++++++++++++++++++------ src/ui-styles.ts | 28 ++++++++++++++++ 3 files changed, 167 insertions(+), 10 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 517ce1e..42ece42 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -120,6 +120,8 @@ class ClaudeChatProvider { private _totalTokensInput: number = 0; private _totalTokensOutput: number = 0; private _requestCount: number = 0; + private _subscriptionType: string | undefined; // 'pro', 'max', or undefined for API users + private _accountInfoFetchedThisSession: boolean = false; // Track if we fetched account info this session private _currentSessionId: string | undefined; private _backupRepoPath: string | undefined; private _commits: Array<{ id: string, sha: string, message: string, timestamp: string }> = []; @@ -168,6 +170,9 @@ class ClaudeChatProvider { // Load saved model preference this._selectedModel = this._context.workspaceState.get('claude.selectedModel', 'default'); + // Load cached subscription type (will be refreshed on first message) + this._subscriptionType = this._context.globalState.get('claude.subscriptionType'); + // Resume session from latest conversation const latestConversation = this._getLatestConversation(); this._currentSessionId = latestConversation?.sessionId; @@ -255,6 +260,16 @@ class ClaudeChatProvider { model: this._selectedModel }); + // Send cached subscription type to webview (will be refreshed on first message) + if (this._subscriptionType) { + this._postMessage({ + type: 'accountInfo', + data: { + subscriptionType: this._subscriptionType + } + }); + } + // Send platform information to webview this._sendPlatformInfo(); @@ -311,6 +326,9 @@ class ClaudeChatProvider { case 'openModelTerminal': this._openModelTerminal(); return; + case 'viewUsage': + this._openUsageTerminal(message.usageType); + return; case 'executeSlashCommand': this._executeSlashCommand(message.command); return; @@ -597,6 +615,19 @@ class ClaudeChatProvider { // Send the message to Claude's stdin as JSON (stream-json input format) // Don't end stdin yet - we need to keep it open for permission responses if (claudeProcess.stdin) { + // First, send an initialize request to get account info (once per session) + if (!this._accountInfoFetchedThisSession) { + this._accountInfoFetchedThisSession = true; + const initRequest = { + type: 'control_request', + request_id: 'init-' + Date.now(), + request: { + subtype: 'initialize' + } + }; + claudeProcess.stdin.write(JSON.stringify(initRequest) + '\n'); + } + const userMessage = { type: 'user', session_id: this._currentSessionId || '', @@ -633,6 +664,12 @@ class ClaudeChatProvider { continue; } + // Handle control_response messages (responses to our initialize request) + if (jsonData.type === 'control_response') { + this._handleControlResponse(jsonData); + continue; + } + // Handle result message - end stdin when done if (jsonData.type === 'result') { if (claudeProcess.stdin && !claudeProcess.stdin.destroyed) { @@ -1404,6 +1441,38 @@ class ClaudeChatProvider { return false; } + /** + * Handle control_response messages from Claude CLI via stdio + * This is used to get account info from the initialize request + */ + private _handleControlResponse(controlResponse: any): void { + // Structure: controlResponse.response.response.account + // The outer response has subtype/request_id, inner response has the actual data + const innerResponse = controlResponse.response?.response; + + // Check if this is an initialize response with account info + if (innerResponse?.account) { + const account = innerResponse.account; + this._subscriptionType = account.subscriptionType; + + console.log('Account info received:', { + subscriptionType: account.subscriptionType, + email: account.email + }); + + // Save to globalState for persistence + this._context.globalState.update('claude.subscriptionType', this._subscriptionType); + + // Send subscription type to UI + this._postMessage({ + type: 'accountInfo', + data: { + subscriptionType: this._subscriptionType + } + }); + } + } + /** * Handle control_request messages from Claude CLI via stdio * This is the new permission flow that replaces the MCP file-based approach @@ -2636,6 +2705,23 @@ class ClaudeChatProvider { }); } + private _openUsageTerminal(usageType: string): void { + const terminal = vscode.window.createTerminal({ + name: 'Claude Usage', + location: vscode.TerminalLocation.Editor + }); + + if (usageType === 'plan') { + // Plan users get live usage view + terminal.sendText('npx -y ccusage blocks --live'); + } else { + // API users get recent usage history + terminal.sendText('npx -y ccusage blocks --recent --order desc'); + } + + terminal.show(); + } + private _executeSlashCommand(command: string): void { // Handle /compact in chat instead of spawning a terminal if (command === 'compact') { diff --git a/src/script.ts b/src/script.ts index ba846da..69e829c 100644 --- a/src/script.ts +++ b/src/script.ts @@ -890,6 +890,7 @@ const getScript = (isTelemetryEnabled: boolean) => `