4 Commits

Author SHA1 Message Date
andrepimenta
683148c4cf Add install modal for users without Claude Code CLI
Shows a clean modal when Claude Code is not installed, with one-click
installation that auto-detects platform (npm if node>=18, otherwise
curl/PowerShell). Runs silently in background with progress indicator.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 10:58:44 +00:00
andrepimenta
e18fa5e261 Improve terminal and UI experience
- Open all terminals in editor area (first column) instead of panel
- Update login message to mention Claude plan (Pro/Max) and API key
- Hide "Claude Code Chat" title when window is very small (<350px)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 00:41:53 +00:00
andrepimenta
63299008d0 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>
2025-12-03 00:29:08 +00:00
andrepimenta
14ac46018f Run /compact command in chat instead of spawning terminal
The /compact command now executes through the chat interface using
the existing Claude process, providing a seamless user experience
instead of opening a separate terminal window.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-02 18:12:46 +00:00
4 changed files with 573 additions and 26 deletions

View File

@@ -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,12 +326,18 @@ 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;
case 'dismissWSLAlert': case 'dismissWSLAlert':
this._dismissWSLAlert(); this._dismissWSLAlert();
return; return;
case 'runInstallCommand':
this._runInstallCommand();
return;
case 'openFile': case 'openFile':
this._openFileInEditor(message.filePath); this._openFileInEditor(message.filePath);
return; return;
@@ -597,6 +618,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 +667,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) {
@@ -719,9 +759,8 @@ class ClaudeChatProvider {
// Check if claude command is not installed // Check if claude command is not installed
if (error.message.includes('ENOENT') || error.message.includes('command not found')) { if (error.message.includes('ENOENT') || error.message.includes('command not found')) {
this._sendAndSaveMessage({ this._postMessage({
type: 'error', type: 'showInstallModal'
data: 'Install claude code first: https://www.anthropic.com/claude-code'
}); });
} else { } else {
this._sendAndSaveMessage({ this._sendAndSaveMessage({
@@ -1111,7 +1150,10 @@ class ClaudeChatProvider {
const claudePath = config.get<string>('wsl.claudePath', '/usr/local/bin/claude'); const claudePath = config.get<string>('wsl.claudePath', '/usr/local/bin/claude');
// Open terminal and run claude login // Open terminal and run claude login
const terminal = vscode.window.createTerminal('Claude Login'); const terminal = vscode.window.createTerminal({
name: 'Claude Login',
location: { viewColumn: vscode.ViewColumn.One }
});
if (wslEnabled) { if (wslEnabled) {
terminal.sendText(`wsl -d ${wslDistro} ${nodePath} --no-warnings --enable-source-maps ${claudePath}`); terminal.sendText(`wsl -d ${wslDistro} ${nodePath} --no-warnings --enable-source-maps ${claudePath}`);
} else { } else {
@@ -1121,14 +1163,14 @@ class ClaudeChatProvider {
// Show info message // Show info message
vscode.window.showInformationMessage( vscode.window.showInformationMessage(
'Please login to Claude in the terminal, then come back to this chat to continue.', 'Please login with your Claude plan or API key in the terminal, then come back to this chat.',
'OK' 'OK'
); );
// Send message to UI about terminal // Send message to UI about terminal
this._postMessage({ this._postMessage({
type: 'terminalOpened', type: 'terminalOpened',
data: `Please login to Claude in the terminal, then come back to this chat to continue.`, data: `Please login with your Claude plan or API key in the terminal, then come back to this chat.`,
}); });
} }
@@ -1404,6 +1446,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
@@ -2615,7 +2689,10 @@ class ClaudeChatProvider {
} }
// Create terminal with the claude /model command // Create terminal with the claude /model command
const terminal = vscode.window.createTerminal('Claude Model Selection'); const terminal = vscode.window.createTerminal({
name: 'Claude Model Selection',
location: { viewColumn: vscode.ViewColumn.One }
});
if (wslEnabled) { if (wslEnabled) {
terminal.sendText(`wsl -d ${wslDistro} ${nodePath} --no-warnings --enable-source-maps ${claudePath} ${args.join(' ')}`); terminal.sendText(`wsl -d ${wslDistro} ${nodePath} --no-warnings --enable-source-maps ${claudePath} ${args.join(' ')}`);
} else { } else {
@@ -2636,7 +2713,72 @@ class ClaudeChatProvider {
}); });
} }
private _openUsageTerminal(usageType: string): void {
const terminal = vscode.window.createTerminal({
name: 'Claude Usage',
location: { viewColumn: vscode.ViewColumn.One }
});
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 _runInstallCommand(): void {
const { exec } = require('child_process');
// Check if npm exists and node >= 18
exec('node --version', { shell: true }, (nodeErr: Error | null, nodeStdout: string) => {
let useNpm = false;
if (!nodeErr && nodeStdout) {
// Parse version (e.g., "v18.17.0" -> 18)
const match = nodeStdout.trim().match(/^v(\d+)/);
if (match && parseInt(match[1], 10) >= 18) {
useNpm = true;
}
}
let command: string;
if (useNpm) {
command = 'npm install -g @anthropic-ai/claude-code';
} else if (process.platform === 'win32') {
command = 'irm https://claude.ai/install.ps1 | iex';
} else {
command = 'curl -fsSL https://claude.ai/install.sh | sh';
}
// Run installation silently in the background
exec(command, { shell: true }, (error: Error | null, stdout: string, stderr: string) => {
if (error) {
this._postMessage({
type: 'installComplete',
success: false,
error: stderr || error.message
});
} else {
this._postMessage({
type: 'installComplete',
success: true
});
}
});
});
}
private _executeSlashCommand(command: string): void { private _executeSlashCommand(command: string): void {
// Handle /compact in chat instead of spawning a terminal
if (command === 'compact') {
this._sendMessageToClaude(`/${command}`);
return;
}
const config = vscode.workspace.getConfiguration('claudeCodeChat'); const config = vscode.workspace.getConfiguration('claudeCodeChat');
const wslEnabled = config.get<boolean>('wsl.enabled', false); const wslEnabled = config.get<boolean>('wsl.enabled', false);
const wslDistro = config.get<string>('wsl.distro', 'Ubuntu'); const wslDistro = config.get<string>('wsl.distro', 'Ubuntu');
@@ -2652,7 +2794,10 @@ class ClaudeChatProvider {
} }
// Create terminal with the claude command // Create terminal with the claude command
const terminal = vscode.window.createTerminal(`Claude /${command}`); const terminal = vscode.window.createTerminal({
name: `Claude /${command}`,
location: { viewColumn: vscode.ViewColumn.One }
});
if (wslEnabled) { if (wslEnabled) {
terminal.sendText(`wsl -d ${wslDistro} ${nodePath} --no-warnings --enable-source-maps ${claudePath} ${args.join(' ')}`); terminal.sendText(`wsl -d ${wslDistro} ${nodePath} --no-warnings --enable-source-maps ${claudePath} ${args.join(' ')}`);
} else { } else {

View File

@@ -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');
} }
} }
@@ -1577,6 +1602,51 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
document.getElementById('slashCommandsModal').style.display = 'none'; document.getElementById('slashCommandsModal').style.display = 'none';
} }
// Install modal functions
function showInstallModal() {
const modal = document.getElementById('installModal');
const main = document.getElementById('installMain');
const progress = document.getElementById('installProgress');
const success = document.getElementById('installSuccess');
if (modal) modal.style.display = 'flex';
if (main) main.style.display = 'flex';
if (progress) progress.style.display = 'none';
if (success) success.style.display = 'none';
}
function hideInstallModal() {
document.getElementById('installModal').style.display = 'none';
}
function startInstallation() {
// Hide main content, show progress
document.getElementById('installMain').style.display = 'none';
document.getElementById('installProgress').style.display = 'flex';
// Extension handles platform detection and command selection
vscode.postMessage({
type: 'runInstallCommand'
});
}
function handleInstallComplete(success, error) {
document.getElementById('installProgress').style.display = 'none';
const successEl = document.getElementById('installSuccess');
successEl.style.display = 'flex';
if (success) {
successEl.querySelector('.install-success-text').textContent = 'Installed';
successEl.querySelector('.install-success-hint').textContent = 'Send a message to get started';
} else {
// Show error state
successEl.querySelector('.install-check').style.display = 'none';
successEl.querySelector('.install-success-text').textContent = 'Installation failed';
successEl.querySelector('.install-success-hint').textContent = error || 'Try installing manually from claude.ai/download';
}
}
// Thinking intensity modal functions // Thinking intensity modal functions
function showThinkingIntensityModal() { function showThinkingIntensityModal() {
// Request current settings from VS Code first // Request current settings from VS Code first
@@ -1683,15 +1753,19 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
// Clear the input since user selected a command // Clear the input since user selected a command
messageInput.value = ''; messageInput.value = '';
// Send command to VS Code to execute in terminal // Send command to VS Code to execute
vscode.postMessage({ vscode.postMessage({
type: 'executeSlashCommand', type: 'executeSlashCommand',
command: command command: command
}); });
// Show user feedback // Show user feedback - /compact runs in chat, others in terminal
if (command === 'compact') {
// No message needed - compact runs in chat and shows its own status
} else {
addMessage('user', \`Executing /\${command} command in terminal. Check the terminal output and return when ready.\`, 'assistant'); addMessage('user', \`Executing /\${command} command in terminal. Check the terminal output and return when ready.\`, 'assistant');
} }
}
function handleCustomCommandKeydown(event) { function handleCustomCommandKeydown(event) {
if (event.key === 'Enter') { if (event.key === 'Enter') {
@@ -2165,14 +2239,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);
@@ -2211,10 +2293,23 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
case 'loginRequired': case 'loginRequired':
sendStats('Login required'); sendStats('Login required');
addMessage('🔐 Login Required\\n\\nYour Claude API key is invalid or expired.\\nA terminal has been opened - please run the login process there.\\n\\nAfter logging in, come back to this chat to continue.', 'error'); addMessage('🔐 Login Required\\n\\nPlease login with your Claude plan (Pro/Max) or API key.\\nA terminal has been opened - follow the login process there.\\n\\nAfter logging in, come back to this chat to continue.', 'error');
updateStatus('Login Required', 'error'); updateStatus('Login Required', 'error');
break; break;
case 'showInstallModal':
sendStats('Claude not installed');
showInstallModal();
updateStatus('Claude Code not installed', 'error');
break;
case 'installComplete':
handleInstallComplete(message.success, message.error);
if (message.success) {
updateStatus('Ready', 'success');
}
break;
case 'showRestoreOption': case 'showRestoreOption':
showRestoreContainer(message.data); showRestoreContainer(message.data);
break; break;
@@ -2915,9 +3010,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>
\`; \`;

View File

@@ -28,6 +28,12 @@ const styles = `
letter-spacing: -0.3px; letter-spacing: -0.3px;
} }
@media (max-width: 385px) {
.header h2 {
display: none;
}
}
.controls { .controls {
display: flex; display: flex;
gap: 6px; gap: 6px;
@@ -2432,6 +2438,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;
@@ -2992,6 +3026,218 @@ const styles = `
} }
} }
/* Install Modal Styles */
.install-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
}
.install-modal-backdrop {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(2px);
}
.install-modal-content {
position: relative;
background: var(--vscode-editor-background);
border: 1px solid var(--vscode-widget-border, var(--vscode-panel-border));
border-radius: 12px;
width: 320px;
padding: 32px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
animation: installFadeIn 0.2s ease-out;
}
@keyframes installFadeIn {
from { opacity: 0; transform: scale(0.95) translateY(-8px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.install-close-btn {
position: absolute;
top: 16px;
right: 16px;
width: 28px;
height: 28px;
background: none;
border: none;
color: var(--vscode-descriptionForeground);
cursor: pointer;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.6;
transition: all 0.15s;
}
.install-close-btn:hover {
background: var(--vscode-toolbar-hoverBackground);
opacity: 1;
}
.install-body {
text-align: center;
}
.install-main {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
.install-icon-wrapper {
width: 64px;
height: 64px;
border-radius: 16px;
background: var(--vscode-button-background);
display: flex;
align-items: center;
justify-content: center;
}
.install-icon {
color: var(--vscode-button-foreground);
}
.install-text {
display: flex;
flex-direction: column;
gap: 6px;
}
.install-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--vscode-foreground);
}
.install-desc {
margin: 0;
font-size: 13px;
color: var(--vscode-descriptionForeground);
line-height: 1.4;
}
.install-btn {
width: 100%;
padding: 12px 24px;
font-size: 14px;
font-weight: 500;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.15s;
}
.install-btn:hover {
background: var(--vscode-button-hoverBackground);
transform: translateY(-1px);
}
.install-btn:active {
transform: translateY(0);
}
.install-link {
font-size: 13px;
color: var(--vscode-textLink-foreground);
text-decoration: none;
opacity: 0.9;
}
.install-link:hover {
text-decoration: underline;
opacity: 1;
}
.install-progress {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
padding: 20px 0;
}
.install-spinner {
width: 32px;
height: 32px;
border: 2.5px solid var(--vscode-widget-border, var(--vscode-panel-border));
border-top-color: var(--vscode-button-background);
border-radius: 50%;
animation: installSpin 0.8s linear infinite;
}
@keyframes installSpin {
to { transform: rotate(360deg); }
}
.install-progress-text {
margin: 0;
font-size: 14px;
font-weight: 500;
color: var(--vscode-foreground);
}
.install-progress-hint {
margin: 0;
font-size: 12px;
color: var(--vscode-descriptionForeground);
}
.install-success {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 20px 0;
}
.install-success-icon {
width: 56px;
height: 56px;
border-radius: 50%;
background: rgba(78, 201, 176, 0.15);
display: flex;
align-items: center;
justify-content: center;
}
.install-check {
width: 28px;
height: 28px;
color: var(--vscode-testing-iconPassed, #4ec9b0);
}
.install-success-text {
margin: 0;
font-size: 16px;
font-weight: 600;
color: var(--vscode-foreground);
}
.install-success-hint {
margin: 0;
font-size: 13px;
color: var(--vscode-descriptionForeground);
}
</style>` </style>`
export default styles export default styles

View File

@@ -399,6 +399,57 @@ const getHtml = (isTelemetryEnabled: boolean) => `<!DOCTYPE html>
</div> </div>
</div> </div>
<!-- Install Claude Code modal -->
<div id="installModal" class="install-modal" style="display: none;">
<div class="install-modal-backdrop" onclick="hideInstallModal()"></div>
<div class="install-modal-content">
<button class="install-close-btn" onclick="hideInstallModal()">
<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor">
<path d="M1.5 1.5L10.5 10.5M1.5 10.5L10.5 1.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</button>
<div class="install-body" id="installBody">
<div class="install-main" id="installMain">
<div class="install-icon-wrapper">
<svg class="install-icon" width="40" height="40" viewBox="0 0 24 24" fill="none">
<path d="M21 15V19C21 20.1 20.1 21 19 21H5C3.9 21 3 20.1 3 19V15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 3V15M12 15L7 10M12 15L17 10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<div class="install-text">
<h2 class="install-title">Install Claude Code</h2>
<p class="install-desc">The CLI is required to use this extension</p>
</div>
<button class="install-btn" id="installMainBtn" onclick="startInstallation()">
Install Now
</button>
<a href="https://docs.anthropic.com/en/docs/claude-code" target="_blank" class="install-link">
View documentation
</a>
</div>
<div class="install-progress" id="installProgress" style="display: none;">
<div class="install-spinner"></div>
<p class="install-progress-text">Installing Claude Code...</p>
<p class="install-progress-hint">This may take a minute</p>
</div>
<div class="install-success" id="installSuccess" style="display: none;">
<div class="install-success-icon">
<svg class="install-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
</div>
<p class="install-success-text">Installation Complete</p>
<p class="install-success-hint">Send a message to get started</p>
</div>
</div>
</div>
</div>
<!-- Thinking intensity modal --> <!-- Thinking intensity modal -->
<div id="thinkingIntensityModal" class="tools-modal" style="display: none;"> <div id="thinkingIntensityModal" class="tools-modal" style="display: none;">
<div class="tools-modal-content" style="width: 450px;"> <div class="tools-modal-content" style="width: 450px;">