Add subscription detection and usage badge to status bar

- 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 <noreply@anthropic.com>
This commit is contained in:
andrepimenta
2025-12-03 00:29:08 +00:00
parent 14ac46018f
commit 63299008d0
3 changed files with 167 additions and 10 deletions

View File

@@ -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') {

View File

@@ -890,6 +890,7 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
let isProcessing = false;
let requestStartTime = null;
let requestTimer = null;
let subscriptionType = null; // 'pro', 'max', or null for API users
// Send usage statistics
function sendStats(eventName) {
@@ -909,6 +910,15 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
statusDiv.className = \`status \${state}\`;
}
function updateStatusHtml(html, state = 'ready') {
statusTextDiv.innerHTML = html;
statusDiv.className = \`status \${state}\`;
}
function viewUsage(usageType) {
vscode.postMessage({ type: 'viewUsage', usageType: usageType });
}
function updateStatusWithTotals() {
if (isProcessing) {
// While processing, show tokens and elapsed time
@@ -926,14 +936,29 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
updateStatus(statusText, 'processing');
} else {
// When ready, show full info
const costStr = totalCost > 0 ? \`$\${totalCost.toFixed(4)}\` : '$0.00';
// Show plan type for subscription users, cost for API users
let usageStr;
const usageIcon = \`<svg class="usage-icon" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1" y="8" width="3" height="6" rx="0.5" fill="currentColor" opacity="0.5"/>
<rect x="5.5" y="5" width="3" height="9" rx="0.5" fill="currentColor" opacity="0.7"/>
<rect x="10" y="2" width="3" height="12" rx="0.5" fill="currentColor"/>
</svg>\`;
if (subscriptionType) {
// Extract just the plan type (e.g., "Claude Max" -> "Max", "pro" -> "Pro")
let planName = subscriptionType.replace(/^claude\\s*/i, '').trim();
planName = planName.charAt(0).toUpperCase() + planName.slice(1);
usageStr = \`<a href="#" onclick="event.preventDefault(); viewUsage('plan');" class="usage-badge" title="View live usage">\${planName} Plan\${usageIcon}</a>\`;
} else {
const costStr = totalCost > 0 ? \`$\${totalCost.toFixed(4)}\` : '$0.00';
usageStr = \`<a href="#" onclick="event.preventDefault(); viewUsage('api');" class="usage-badge" title="View usage">\${costStr}\${usageIcon}</a>\`;
}
const totalTokens = totalTokensInput + totalTokensOutput;
const tokensStr = totalTokens > 0 ?
const tokensStr = totalTokens > 0 ?
\`\${totalTokens.toLocaleString()} tokens\` : '0 tokens';
const requestStr = requestCount > 0 ? \`\${requestCount} requests\` : '';
const statusText = \`Ready • \${costStr}\${tokensStr}\${requestStr ? \`\${requestStr}\` : ''}\`;
updateStatus(statusText, 'ready');
const statusText = \`Ready • \${tokensStr}\${requestStr ? \`\${requestStr}\` : ''}\${usageStr}\`;
updateStatusHtml(statusText, 'ready');
}
}
@@ -2165,17 +2190,25 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
totalTokensInput = message.data.totalTokensInput || 0;
totalTokensOutput = message.data.totalTokensOutput || 0;
requestCount = message.data.requestCount || 0;
// Update status bar with new totals
updateStatusWithTotals();
// Show current request info if available
if (message.data.currentCost || message.data.currentDuration) {
// Show current request info if available (only for API users)
if (!subscriptionType && (message.data.currentCost || message.data.currentDuration)) {
const currentCostStr = message.data.currentCost ? \`$\${message.data.currentCost.toFixed(4)}\` : 'N/A';
const currentDurationStr = message.data.currentDuration ? \`\${message.data.currentDuration}ms\` : 'N/A';
addMessage(\`Request completed - Cost: \${currentCostStr}, Duration: \${currentDurationStr}\`, 'system');
}
break;
case 'accountInfo':
// Store subscription type to determine cost vs plan display
subscriptionType = message.data.subscriptionType || null;
console.log('Account info received:', subscriptionType);
// Update status bar to reflect plan type
updateStatusWithTotals();
break;
case 'sessionResumed':
console.log('Session resumed:', message.data);
@@ -2919,9 +2952,19 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
const date = new Date(conv.startTime).toLocaleDateString();
const time = new Date(conv.startTime).toLocaleTimeString();
// Show plan type or cost based on subscription
let usageStr;
if (subscriptionType) {
let planName = subscriptionType.replace(/^claude\\s*/i, '').trim();
planName = planName.charAt(0).toUpperCase() + planName.slice(1);
usageStr = planName;
} else {
usageStr = \`$\${conv.totalCost.toFixed(3)}\`;
}
item.innerHTML = \`
<div class="conversation-title">\${conv.firstUserMessage.substring(0, 60)}\${conv.firstUserMessage.length > 60 ? '...' : ''}</div>
<div class="conversation-meta">\${date} at \${time}\${conv.messageCount} messages • $\${conv.totalCost.toFixed(3)}</div>
<div class="conversation-meta">\${date} at \${time}\${conv.messageCount} messages • \${usageStr}</div>
<div class="conversation-preview">Last: \${conv.lastUserMessage.substring(0, 80)}\${conv.lastUserMessage.length > 80 ? '...' : ''}</div>
\`;

View File

@@ -2432,6 +2432,34 @@ const styles = `
flex: 1;
}
.status-text .usage-badge {
display: inline-flex;
align-items: center;
gap: 4px;
color: inherit;
text-decoration: none;
background: rgba(255, 255, 255, 0.08);
padding: 2px 8px 2px 8px;
border-radius: 10px;
cursor: pointer;
transition: background 0.15s, transform 0.1s;
}
.status-text .usage-badge:hover {
background: rgba(255, 255, 255, 0.15);
transform: translateY(-1px);
}
.status-text .usage-badge:active {
transform: translateY(0);
}
.status-text .usage-icon {
width: 12px;
height: 12px;
flex-shrink: 0;
}
pre {
white-space: pre-wrap;
word-wrap: break-word;