mirror of
https://github.com/andrepimenta/claude-code-chat.git
synced 2025-12-10 23:09:50 +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 _totalTokensInput: number = 0;
|
||||||
private _totalTokensOutput: number = 0;
|
private _totalTokensOutput: number = 0;
|
||||||
private _requestCount: 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 _currentSessionId: string | undefined;
|
||||||
private _backupRepoPath: string | undefined;
|
private _backupRepoPath: string | undefined;
|
||||||
private _commits: Array<{ id: string, sha: string, message: string, timestamp: string }> = [];
|
private _commits: Array<{ id: string, sha: string, message: string, timestamp: string }> = [];
|
||||||
@@ -168,6 +170,9 @@ class ClaudeChatProvider {
|
|||||||
// Load saved model preference
|
// Load saved model preference
|
||||||
this._selectedModel = this._context.workspaceState.get('claude.selectedModel', 'default');
|
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
|
// Resume session from latest conversation
|
||||||
const latestConversation = this._getLatestConversation();
|
const latestConversation = this._getLatestConversation();
|
||||||
this._currentSessionId = latestConversation?.sessionId;
|
this._currentSessionId = latestConversation?.sessionId;
|
||||||
@@ -255,6 +260,16 @@ class ClaudeChatProvider {
|
|||||||
model: this._selectedModel
|
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
|
// Send platform information to webview
|
||||||
this._sendPlatformInfo();
|
this._sendPlatformInfo();
|
||||||
|
|
||||||
@@ -311,6 +326,9 @@ class ClaudeChatProvider {
|
|||||||
case 'openModelTerminal':
|
case 'openModelTerminal':
|
||||||
this._openModelTerminal();
|
this._openModelTerminal();
|
||||||
return;
|
return;
|
||||||
|
case 'viewUsage':
|
||||||
|
this._openUsageTerminal(message.usageType);
|
||||||
|
return;
|
||||||
case 'executeSlashCommand':
|
case 'executeSlashCommand':
|
||||||
this._executeSlashCommand(message.command);
|
this._executeSlashCommand(message.command);
|
||||||
return;
|
return;
|
||||||
@@ -597,6 +615,19 @@ class ClaudeChatProvider {
|
|||||||
// Send the message to Claude's stdin as JSON (stream-json input format)
|
// 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
|
// Don't end stdin yet - we need to keep it open for permission responses
|
||||||
if (claudeProcess.stdin) {
|
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 = {
|
const userMessage = {
|
||||||
type: 'user',
|
type: 'user',
|
||||||
session_id: this._currentSessionId || '',
|
session_id: this._currentSessionId || '',
|
||||||
@@ -633,6 +664,12 @@ class ClaudeChatProvider {
|
|||||||
continue;
|
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
|
// Handle result message - end stdin when done
|
||||||
if (jsonData.type === 'result') {
|
if (jsonData.type === 'result') {
|
||||||
if (claudeProcess.stdin && !claudeProcess.stdin.destroyed) {
|
if (claudeProcess.stdin && !claudeProcess.stdin.destroyed) {
|
||||||
@@ -1404,6 +1441,38 @@ class ClaudeChatProvider {
|
|||||||
return false;
|
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
|
* Handle control_request messages from Claude CLI via stdio
|
||||||
* This is the new permission flow that replaces the MCP file-based approach
|
* 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 {
|
private _executeSlashCommand(command: string): void {
|
||||||
// Handle /compact in chat instead of spawning a terminal
|
// Handle /compact in chat instead of spawning a terminal
|
||||||
if (command === 'compact') {
|
if (command === 'compact') {
|
||||||
|
|||||||
@@ -890,6 +890,7 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
|
|||||||
let isProcessing = false;
|
let isProcessing = false;
|
||||||
let requestStartTime = null;
|
let requestStartTime = null;
|
||||||
let requestTimer = null;
|
let requestTimer = null;
|
||||||
|
let subscriptionType = null; // 'pro', 'max', or null for API users
|
||||||
|
|
||||||
// Send usage statistics
|
// Send usage statistics
|
||||||
function sendStats(eventName) {
|
function sendStats(eventName) {
|
||||||
@@ -909,6 +910,15 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
|
|||||||
statusDiv.className = \`status \${state}\`;
|
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() {
|
function updateStatusWithTotals() {
|
||||||
if (isProcessing) {
|
if (isProcessing) {
|
||||||
// While processing, show tokens and elapsed time
|
// While processing, show tokens and elapsed time
|
||||||
@@ -926,14 +936,29 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
|
|||||||
updateStatus(statusText, 'processing');
|
updateStatus(statusText, 'processing');
|
||||||
} else {
|
} else {
|
||||||
// When ready, show full info
|
// When ready, show full info
|
||||||
|
// 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';
|
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 totalTokens = totalTokensInput + totalTokensOutput;
|
||||||
const tokensStr = totalTokens > 0 ?
|
const tokensStr = totalTokens > 0 ?
|
||||||
\`\${totalTokens.toLocaleString()} tokens\` : '0 tokens';
|
\`\${totalTokens.toLocaleString()} tokens\` : '0 tokens';
|
||||||
const requestStr = requestCount > 0 ? \`\${requestCount} requests\` : '';
|
const requestStr = requestCount > 0 ? \`\${requestCount} requests\` : '';
|
||||||
|
|
||||||
const statusText = \`Ready • \${costStr} • \${tokensStr}\${requestStr ? \` • \${requestStr}\` : ''}\`;
|
const statusText = \`Ready • \${tokensStr}\${requestStr ? \` • \${requestStr}\` : ''} • \${usageStr}\`;
|
||||||
updateStatus(statusText, 'ready');
|
updateStatusHtml(statusText, 'ready');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2169,14 +2194,22 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
|
|||||||
// Update status bar with new totals
|
// Update status bar with new totals
|
||||||
updateStatusWithTotals();
|
updateStatusWithTotals();
|
||||||
|
|
||||||
// Show current request info if available
|
// Show current request info if available (only for API users)
|
||||||
if (message.data.currentCost || message.data.currentDuration) {
|
if (!subscriptionType && (message.data.currentCost || message.data.currentDuration)) {
|
||||||
const currentCostStr = message.data.currentCost ? \`$\${message.data.currentCost.toFixed(4)}\` : 'N/A';
|
const currentCostStr = message.data.currentCost ? \`$\${message.data.currentCost.toFixed(4)}\` : 'N/A';
|
||||||
const currentDurationStr = message.data.currentDuration ? \`\${message.data.currentDuration}ms\` : 'N/A';
|
const currentDurationStr = message.data.currentDuration ? \`\${message.data.currentDuration}ms\` : 'N/A';
|
||||||
addMessage(\`Request completed - Cost: \${currentCostStr}, Duration: \${currentDurationStr}\`, 'system');
|
addMessage(\`Request completed - Cost: \${currentCostStr}, Duration: \${currentDurationStr}\`, 'system');
|
||||||
}
|
}
|
||||||
break;
|
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':
|
case 'sessionResumed':
|
||||||
console.log('Session resumed:', message.data);
|
console.log('Session resumed:', message.data);
|
||||||
showSessionInfo(message.data.sessionId);
|
showSessionInfo(message.data.sessionId);
|
||||||
@@ -2919,9 +2952,19 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
|
|||||||
const date = new Date(conv.startTime).toLocaleDateString();
|
const date = new Date(conv.startTime).toLocaleDateString();
|
||||||
const time = new Date(conv.startTime).toLocaleTimeString();
|
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 = \`
|
item.innerHTML = \`
|
||||||
<div class="conversation-title">\${conv.firstUserMessage.substring(0, 60)}\${conv.firstUserMessage.length > 60 ? '...' : ''}</div>
|
<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>
|
<div class="conversation-preview">Last: \${conv.lastUserMessage.substring(0, 80)}\${conv.lastUserMessage.length > 80 ? '...' : ''}</div>
|
||||||
\`;
|
\`;
|
||||||
|
|
||||||
|
|||||||
@@ -2432,6 +2432,34 @@ const styles = `
|
|||||||
flex: 1;
|
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 {
|
pre {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
|||||||
Reference in New Issue
Block a user