mirror of
https://github.com/andrepimenta/claude-code-chat.git
synced 2025-12-08 03:47:56 +00:00
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:
@@ -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') {
|
||||
|
||||
@@ -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>
|
||||
\`;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user