From deca7de8d542eea511d97d92fb4c49aa639f6bbd Mon Sep 17 00:00:00 2001 From: andrepimenta Date: Sat, 11 Apr 2026 23:54:13 +0100 Subject: [PATCH] Bring in mcp-skills-plugins branch: marketplace, plugins, skills, OpenCredits, and image previews Major release adding: - MCP marketplace with curated registry and search across multiple registries - Plugins and skills marketplace integration - OpenCredits payment integration with model selection and checkout flow - Image preview before sending (paste, file picker) - Self-hosted Umami analytics with custom events - Support feedback modal with Discord webhook - Local OpenAI to Anthropic router for model routing - Inline stop button replacing send during processing - Improved install flow requiring Node.js 18+ - WSL env var passthrough fixes - Better error handling and Windows compatibility Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/settings.json | 5 + .claude/settings.local.json | 8 +- .gitignore | 3 +- .vscodeignore | 3 +- backup.sh | 21 + package.json | 22 +- src/extension.ts | 1547 +++++++++++++++++++----- src/model-updater.ts | 148 +++ src/plugins-script.ts | 150 +++ src/plugins-ui.ts | 26 + src/recommended-models.json | 66 ++ src/router/formatRequest.ts | 265 +++++ src/router/formatResponse.ts | 37 + src/router/index.ts | 2 + src/router/server.ts | 220 ++++ src/router/streamResponse.ts | 219 ++++ src/script.ts | 2176 +++++++++++++++++++++++++++++++--- src/skills-script.ts | 286 +++++ src/skills-ui.ts | 51 + src/top-mcp-servers.json | 479 ++++++++ src/top-plugins.json | 240 ++++ src/top-skills.json | 289 +++++ src/ui-styles.ts | 1823 +++++++++++++++++++++++++++- src/ui.ts | 544 +++++++-- tsconfig.json | 5 +- 25 files changed, 7994 insertions(+), 641 deletions(-) create mode 100644 .claude/settings.json create mode 100755 backup.sh create mode 100644 src/model-updater.ts create mode 100644 src/plugins-script.ts create mode 100644 src/plugins-ui.ts create mode 100644 src/recommended-models.json create mode 100644 src/router/formatRequest.ts create mode 100644 src/router/formatResponse.ts create mode 100644 src/router/index.ts create mode 100644 src/router/server.ts create mode 100644 src/router/streamResponse.ts create mode 100644 src/skills-script.ts create mode 100644 src/skills-ui.ts create mode 100644 src/top-mcp-servers.json create mode 100644 src/top-plugins.json create mode 100644 src/top-skills.json diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..9030888 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,5 @@ +{ + "enabledPlugins": { + "frontend-design@claude-plugins-official": true + } +} diff --git a/.claude/settings.local.json b/.claude/settings.local.json index ba69db5..c5e70b7 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -5,9 +5,13 @@ "Bash(grep:*)", "Bash(sed:*)", "Bash(rg:*)", - "Bash(npx tsc:*)" + "Bash(npx tsc:*)", + "mcp__ide__getDiagnostics", + "Bash(curl -s 'https://skills.sh/' -H 'user-agent: Mozilla/5.0')", + "Bash(curl -s 'https://skills.sh/' -H 'user-agent: Mozilla/5.0' -o /tmp/skills_page.html)", + "Bash(python3 /tmp/skills_parse.py)" ], "deny": [] }, "enableAllProjectMcpServers": false -} \ No newline at end of file +} diff --git a/.gitignore b/.gitignore index a20aa9e..bedfeb8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ dist node_modules .vscode-test/ *.vsix -backup \ No newline at end of file +backup +backup-files diff --git a/.vscodeignore b/.vscodeignore index 295df75..ab1e862 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -14,4 +14,5 @@ backup claude-code-chat-permissions-mcp/** node_modules mcp-permissions.js -build \ No newline at end of file +backup-files +build diff --git a/backup.sh b/backup.sh new file mode 100755 index 0000000..adac868 --- /dev/null +++ b/backup.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# Backup script for src folder + +# Get the directory where the script is located +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Create backup directory if it doesn't exist +BACKUP_DIR="$SCRIPT_DIR/backup-files" +mkdir -p "$BACKUP_DIR" + +# Generate timestamp +TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S") + +# Create backup filename +BACKUP_NAME="src-backup-$TIMESTAMP" + +# Copy src folder to backup +cp -r "$SCRIPT_DIR/src" "$BACKUP_DIR/$BACKUP_NAME" + +echo "Backup created: $BACKUP_DIR/$BACKUP_NAME" diff --git a/package.json b/package.json index 05b3db5..798bf0f 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "claude-code-chat", "displayName": "Chat for Claude Code", "description": "Beautiful Claude Code Chat Interface for VS Code", - "version": "1.1.0", + "version": "2.0.0", "publisher": "AndrePimenta", "author": "Andre Pimenta", "repository": { @@ -185,6 +185,26 @@ "type": "boolean", "default": false, "description": "Enable Yolo Mode to skip all permission checks. Use with caution as Claude can execute any command without asking." + }, + "claudeCodeChat.executable.path": { + "type": "string", + "default": "", + "description": "Custom path to the Claude Code executable. Leave empty to use the default 'claude' command." + }, + "claudeCodeChat.environment.variables": { + "type": "object", + "default": {}, + "description": "Custom environment variables to pass to Claude Code. Example: {\"ANTHROPIC_API_KEY\": \"sk-...\"}" + }, + "claudeCodeChat.environment.disabled": { + "type": "boolean", + "default": false, + "description": "When enabled, custom environment variables are not passed to Claude Code." + }, + "claudeCodeChat.router.enabled": { + "type": "boolean", + "default": false, + "description": "Enable the local router to convert OpenAI format to Anthropic format. Required for providers that use OpenAI-compatible APIs." } } } diff --git a/src/extension.ts b/src/extension.ts index 6d62328..106cd56 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,6 +3,14 @@ import * as cp from 'child_process'; import * as util from 'util'; import * as path from 'path'; import getHtml from './ui'; +import { startRouter, stopRouter, setModelConfig, setBaseUrl } from './router'; +import { fetchAndResolveModels } from './model-updater'; +import recommendedModels from './recommended-models.json'; + +// OpenCredits environment configuration +let OPENCREDITS_API_URL = 'https://ccc.api.opencredits.ai'; +let OPENCREDITS_WEB_URL = 'https://ccc.opencredits.ai'; +let OPENCREDITS_PUBLISHABLE_KEY = 'oc_pk_c43da4f9a9484ae484ad29bc97cc354f'; const exec = util.promisify(cp.exec); @@ -18,11 +26,15 @@ class DiffContentProvider implements vscode.TextDocumentContentProvider { } export function activate(context: vscode.ExtensionContext) { - console.log('Claude Code Chat extension is being activated!'); + + if (context.extensionMode === vscode.ExtensionMode.Development) { + OPENCREDITS_API_URL = 'http://localhost:8787'; + OPENCREDITS_WEB_URL = 'http://localhost:3000'; + OPENCREDITS_PUBLISHABLE_KEY = 'oc_pk_c78315e9ff3a425ebca398bb69282429'; + } const provider = new ClaudeChatProvider(context.extensionUri, context); const disposable = vscode.commands.registerCommand('claude-code-chat.openChat', (column?: vscode.ViewColumn) => { - console.log('Claude Code Chat command executed!'); provider.show(column); }); @@ -31,7 +43,7 @@ export function activate(context: vscode.ExtensionContext) { }); // Register webview view provider for sidebar chat (using shared provider instance) - const webviewProvider = new ClaudeChatWebviewProvider(context.extensionUri, context, provider); + const webviewProvider = new ClaudeChatWebviewProvider(context.extensionUri, provider); vscode.window.registerWebviewViewProvider('claude-code-chat.chat', webviewProvider); // Register custom content provider for read-only diff views @@ -41,7 +53,6 @@ export function activate(context: vscode.ExtensionContext) { // Listen for configuration changes const configChangeDisposable = vscode.workspace.onDidChangeConfiguration(event => { if (event.affectsConfiguration('claudeCodeChat.wsl')) { - console.log('WSL configuration changed, starting new session'); provider.newSessionOnConfigChange(); } }); @@ -53,11 +64,45 @@ export function activate(context: vscode.ExtensionContext) { statusBarItem.command = 'claude-code-chat.openChat'; statusBarItem.show(); - context.subscriptions.push(disposable, loadConversationDisposable, configChangeDisposable, statusBarItem); - console.log('Claude Code Chat extension activation completed successfully!'); + // Register URI handler for deep links (e.g., OpenCredits key callback) + const uriHandler = vscode.window.registerUriHandler({ + async handleUri(uri: vscode.Uri) { + + // Handle OpenCredits key callback: vscode://AndrePimenta.claude-code-chat/opencredits-key?key=xxx + if (uri.path === '/opencredits-key') { + const params = new URLSearchParams(uri.query); + const key = params.get('key'); + + if (key) { + // Save the key and OpenCredits env vars + const config = vscode.workspace.getConfiguration('claudeCodeChat'); + const envVars = config.get>('environment.variables', {}); + envVars['ANTHROPIC_AUTH_TOKEN'] = key; + envVars['ANTHROPIC_BASE_URL'] = OPENCREDITS_API_URL; + + await config.update('environment.variables', envVars, vscode.ConfigurationTarget.Global); + + // Handle pending model activation after payment + await provider.handleOpenCreditsKeyReceived(key); + + // Show success message + vscode.window.showInformationMessage('OpenCredits account connected! You can now use Claude Code Chat.', 'Open Chat').then(selection => { + if (selection === 'Open Chat') { + provider.show(); + } + }); + } + } + } + }); + + context.subscriptions.push(disposable, loadConversationDisposable, configChangeDisposable, statusBarItem, uriHandler); } -export function deactivate() { } +export function deactivate() { + // Stop the local router when the extension is deactivated + stopRouter().catch(err => console.error('Failed to stop router:', err)); +} interface ConversationData { sessionId: string; @@ -76,7 +121,6 @@ interface ConversationData { class ClaudeChatWebviewProvider implements vscode.WebviewViewProvider { constructor( private readonly _extensionUri: vscode.Uri, - private readonly _context: vscode.ExtensionContext, private readonly _chatProvider: ClaudeChatProvider ) { } @@ -99,7 +143,6 @@ class ClaudeChatWebviewProvider implements vscode.WebviewViewProvider { if (webviewView.visible) { // Close main panel when sidebar becomes visible if (this._chatProvider._panel) { - console.log('Closing main panel because sidebar became visible'); this._chatProvider._panel.dispose(); this._chatProvider._panel = undefined; } @@ -122,6 +165,7 @@ class ClaudeChatProvider { 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 _pendingModelAfterPayment: string | null = null; private _currentSessionId: string | undefined; private _backupRepoPath: string | undefined; private _commits: Array<{ id: string, sha: string, message: string, timestamp: string }> = []; @@ -162,7 +206,6 @@ class ClaudeChatProvider { // Initialize backup repository and conversations this._initializeBackupRepo(); this._initializeConversations(); - this._initializeMCPConfig(); // Load conversation index from workspace state this._conversationIndex = this._context.workspaceState.get('claude.conversationIndex', []); @@ -230,6 +273,83 @@ class ClaudeChatProvider { }, 100); } + // Get the OpenCredits API key from environment variables + private _getOpenCreditsKey(): string { + const config = vscode.workspace.getConfiguration('claudeCodeChat'); + const envVars = config.get>('environment.variables', {}); + return envVars['ANTHROPIC_AUTH_TOKEN'] || ''; + } + + // Check if the configured base URL points to OpenCredits + private _isOpenCredits(): boolean { + const config = vscode.workspace.getConfiguration('claudeCodeChat'); + if (config.get('environment.disabled', false)) { + return false; + } + const envVars = config.get>('environment.variables', {}); + const baseUrl = envVars['ANTHROPIC_BASE_URL'] || ''; + return baseUrl.includes('opencredits.ai') || baseUrl.includes('localhost:8787'); + } + + private async _setEnvsDisabled(disabled: boolean): Promise { + const config = vscode.workspace.getConfiguration('claudeCodeChat'); + await config.update('environment.disabled', disabled, vscode.ConfigurationTarget.Global); + this._sendCurrentSettings(); + + if (!disabled && (this._isOpenCredits() || this._getOpenCreditsKey())) { + this._sendOpenCreditsBalance(); + } else if (disabled) { + this._postMessage({ type: 'opencreditsBalance', balance: null }); + } + } + + private static readonly OC_KEY_SECRET = 'opencredits.userKey'; + + private async _saveOpenCreditsKey(key: string) { + await this._context.secrets.store(ClaudeChatProvider.OC_KEY_SECRET, key); + } + + private async _getSavedOpenCreditsKey(): Promise { + return await this._context.secrets.get(ClaudeChatProvider.OC_KEY_SECRET) || null; + } + + public async handleOpenCreditsKeyReceived(key: string) { + // Persist key in encrypted storage + await this._saveOpenCreditsKey(key); + + this._postMessage({ + type: 'opencreditsKeyReceived', + key: key + }); + + if (this._pendingModelAfterPayment) { + const pendingModel = this._pendingModelAfterPayment; + this._pendingModelAfterPayment = null; + + try { + this._updateLocalRouterModel(pendingModel); + this._selectedModel = pendingModel; + this._context.workspaceState.update('claude.selectedModel', pendingModel); + await this._setModelEnvVars(pendingModel); + + const balance = await this._fetchOpenCreditsBalance(); + + this._postMessage({ + type: 'opencreditsActivated', + model: pendingModel, + balance: balance + }); + + } catch (error) { + console.error('Failed to activate model after payment:', error); + } + } else { + await this._sendOpenCreditsBalance(); + } + + this._sendCurrentSettings(); + } + private _postMessage(message: any) { if (this._panel && this._panel.webview) { this._panel.webview.postMessage(message); @@ -238,6 +358,17 @@ class ClaudeChatProvider { } } + private async _getImageDataUri(filePath: string): Promise { + try { + const imageData = await vscode.workspace.fs.readFile(vscode.Uri.file(filePath)); + const base64 = Buffer.from(imageData).toString('base64'); + const ext = path.extname(filePath).toLowerCase(); + return `data:${ClaudeChatProvider.IMAGE_MEDIA_TYPES[ext] || 'image/png'};base64,${base64}`; + } catch { + return undefined; + } + } + private _sendReadyMessage() { // Send current session info if available /*if (this._currentSessionId) { @@ -276,6 +407,11 @@ class ClaudeChatProvider { // Send current settings to webview this._sendCurrentSettings(); + // Fetch and send OpenCredits balance if using OpenCredits + if (this._isOpenCredits() || this._getOpenCreditsKey()) { + this._sendOpenCreditsBalance(); + } + // Send saved draft message if any if (this._draftMessage) { this._postMessage({ @@ -285,10 +421,10 @@ class ClaudeChatProvider { } } - private _handleWebviewMessage(message: any) { + private async _handleWebviewMessage(message: any) { switch (message.type) { case 'sendMessage': - this._sendMessageToClaude(message.text, message.planMode, message.thinkingMode); + this._sendMessageToClaude(message.text, message.planMode, message.thinkingMode, message.images); return; case 'newSession': this._newSession(); @@ -314,6 +450,15 @@ class ClaudeChatProvider { case 'getSettings': this._sendCurrentSettings(); return; + case 'getEnvVars': { + const evConfig = vscode.workspace.getConfiguration('claudeCodeChat'); + const evVars = evConfig.get>('environment.variables', {}); + this._postMessage({ type: 'envVarsData', data: evVars }); + return; + } + case 'setEnvsDisabled': + await this._setEnvsDisabled(!!message.disabled); + return; case 'updateSettings': this._updateSettings(message.settings); return; @@ -321,7 +466,7 @@ class ClaudeChatProvider { this._getClipboardText(); return; case 'selectModel': - this._setSelectedModel(message.model); + this._setSelectedModel(message.model, message.tierModels); return; case 'openModelTerminal': this._openModelTerminal(); @@ -338,6 +483,113 @@ class ClaudeChatProvider { case 'runInstallCommand': this._runInstallCommand(); return; + case 'openLoginTerminal': + this._openLoginTerminal(); + return; + case 'openFundsPage': + if (message.pendingModel) { + this._pendingModelAfterPayment = message.pendingModel; + } + vscode.env.openExternal(vscode.Uri.parse(`${OPENCREDITS_WEB_URL}/embed/checkout`)); + return; + case 'setPendingModel': + if (message.pendingModel) { + this._pendingModelAfterPayment = message.pendingModel; + } + return; + case 'opencreditsKeyFromCheckout': + if (message.key) { + // Save the key and OpenCredits env vars (same as URI handler) + const checkoutConfig = vscode.workspace.getConfiguration('claudeCodeChat'); + const checkoutEnvVars = checkoutConfig.get>('environment.variables', {}); + checkoutEnvVars['ANTHROPIC_AUTH_TOKEN'] = message.key; + checkoutEnvVars['ANTHROPIC_BASE_URL'] = OPENCREDITS_API_URL; + checkoutConfig.update('environment.variables', checkoutEnvVars, vscode.ConfigurationTarget.Global).then( + () => { + this.handleOpenCreditsKeyReceived(message.key); + }, + (err: Error) => { + console.error('Failed to save OpenCredits env vars from checkout:', err); + this._postMessage({ + type: 'checkoutSaveError', + message: 'Failed to save your account credentials. Please try again.' + }); + } + ); + // Bring VS Code window to foreground + if (this._panel) { + this._panel.reveal(vscode.ViewColumn.One); + } + const focusCmd = process.platform === 'darwin' ? 'open' + : process.platform === 'win32' ? 'start' + : 'xdg-open'; + cp.spawn(focusCmd, [`${vscode.env.uriScheme}://AndrePimenta.claude-code-chat/focus`], { detached: true, stdio: 'ignore' }).unref(); + } + return; + case 'openOpenCreditsAccount': + this._openOpenCreditsAccount(); + return; + case 'restoreOpenCredits': { + const savedKey = await this._getSavedOpenCreditsKey(); + if (savedKey) { + const restoreConfig = vscode.workspace.getConfiguration('claudeCodeChat'); + const restoreEnvVars = restoreConfig.get>('environment.variables', {}); + restoreEnvVars['ANTHROPIC_AUTH_TOKEN'] = savedKey; + restoreEnvVars['ANTHROPIC_BASE_URL'] = OPENCREDITS_API_URL; + await restoreConfig.update('environment.variables', restoreEnvVars, vscode.ConfigurationTarget.Global); + await this.handleOpenCreditsKeyReceived(savedKey); + } + return; + } + case 'checkSavedOpenCredits': { + const hasSaved = !!(await this._getSavedOpenCreditsKey()); + this._postMessage({ type: 'savedOpenCreditsStatus', hasSavedKey: hasSaved }); + return; + } + case 'saveCustomProvider': + if (message.envVars) { + const cpConfig = vscode.workspace.getConfiguration('claudeCodeChat'); + const cpEnvVars = cpConfig.get>('environment.variables', {}); + Object.assign(cpEnvVars, message.envVars); + cpConfig.update('environment.variables', cpEnvVars, vscode.ConfigurationTarget.Global).then( + () => { + this._postMessage({ type: 'customProviderSaved' }); + }, + (err: Error) => { + console.error('Failed to save custom provider:', err); + } + ); + } + return; + case 'copyToClipboard': + if (message.text) { + vscode.env.clipboard.writeText(message.text); + } + return; + case 'saveOpenCreditsKeyEarly': + // Save key to secrets early (before payment) so it can be recovered if VS Code closes + if (message.key) { + this._saveOpenCreditsKey(message.key); + } + return; + case 'openExternalUrl': { + const extUrl = message.url; + try { + if (process.platform === 'win32') { + cp.exec(`start "" "${extUrl}"`, { windowsHide: true }); + } else { + const openCmd = process.platform === 'darwin' ? 'open' : 'xdg-open'; + const proc = cp.spawn(openCmd, [extUrl], { detached: true, stdio: 'ignore' }); + proc.on('error', () => { + vscode.env.openExternal(vscode.Uri.parse(extUrl)); + }); + proc.unref(); + } + } catch { + vscode.env.openExternal(vscode.Uri.parse(extUrl)); + } + return; + } case 'openFile': this._openFileInEditor(message.filePath); return; @@ -353,6 +605,15 @@ class ClaudeChatProvider { case 'permissionResponse': this._handlePermissionResponse(message.id, message.approved, message.alwaysAllow); return; + case 'askUserQuestionResponse': + this._handleAskUserQuestionResponse(message.id, message.answers); + return; + case 'showInfoMessage': + vscode.window.showInformationMessage(message.message); + return; + case 'marketplaceFetch': + this._fetchMarketplace(message.url, message.append, message.isSearch); + return; case 'getPermissions': this._sendPermissions(); return; @@ -362,14 +623,38 @@ class ClaudeChatProvider { case 'addPermission': this._addPermission(message.toolName, message.command); return; + case 'loadSkills': + this._loadSkills(); + return; + case 'saveSkill': + this._saveSkill(message.name, message.scope, message.content); + return; + case 'deleteSkill': + this._deleteSkill(message.name, message.scope); + return; + case 'searchSkills': + this._searchSkills(message.query); + return; + case 'runTerminalCommand': + this._runTerminalCommand(message.command); + return; + case 'loadPlugins': + this._loadPlugins(); + return; + case 'installPlugin': + this._installPlugin(message.installId); + return; + case 'removePlugin': + this._removePlugin(message.installId); + return; case 'loadMCPServers': this._loadMCPServers(); return; case 'saveMCPServer': - this._saveMCPServer(message.name, message.config); + this._saveMCPServer(message.name, message.config, message.scope || 'project'); return; case 'deleteMCPServer': - this._deleteMCPServer(message.name); + this._deleteMCPServer(message.name, message.scope || 'project'); return; case 'getCustomSnippets': this._sendCustomSnippets(); @@ -413,7 +698,6 @@ class ClaudeChatProvider { public showInWebview(webview: vscode.Webview, webviewView?: vscode.WebviewView) { // Close main panel if it's open if (this._panel) { - console.log('Closing main panel because sidebar is opening'); this._panel.dispose(); this._panel = undefined; } @@ -443,6 +727,124 @@ class ClaudeChatProvider { this._sendReadyMessage(); }, 100); } + + // Check feature flags and auto-update models (non-blocking) + this._checkFeatureFlags().then(enabled => { + if (enabled) { + this._autoUpdateRecommendedModels().catch(() => {}); + } + }).catch(() => {}); + } + + private static readonly FEATURES_CACHE_KEY = 'claude.featureFlags'; + private static readonly FEATURES_CACHE_TTL = 0; // Always re-fetch for now + private static readonly MODEL_CACHE_KEY = 'claude.recommendedModelsCache.v1'; + private static readonly MODEL_CACHE_TTL = 24 * 60 * 60 * 1000; // 1 day + + private async _checkFeatureFlags(): Promise { + + // Check cache first + const cached = this._context.globalState.get<{ timestamp: number; opencredits_enabled: boolean }>(ClaudeChatProvider.FEATURES_CACHE_KEY); + if (cached && Date.now() - cached.timestamp < ClaudeChatProvider.FEATURES_CACHE_TTL) { + this._postMessage({ type: 'featureFlags', opencredits_enabled: cached.opencredits_enabled }); + return cached.opencredits_enabled; + } + + try { + const res = await fetch(OPENCREDITS_API_URL + '/v1/features'); + if (res.ok) { + const data = await res.json() as { opencredits_enabled: boolean; country: string }; + this._context.globalState.update(ClaudeChatProvider.FEATURES_CACHE_KEY, { + timestamp: Date.now(), + opencredits_enabled: data.opencredits_enabled + }); + this._postMessage({ type: 'featureFlags', opencredits_enabled: data.opencredits_enabled }); + return data.opencredits_enabled; + } + } catch (e) { + console.error('[OpenCredits] Feature flags fetch failed:', e); + } + + // Default to disabled if fetch fails + this._postMessage({ type: 'featureFlags', opencredits_enabled: false }); + return false; + } + + private static readonly REFERENCE_MODEL = 'anthropic/claude-opus-4.6'; + private static get PUBLISHABLE_KEY() { return OPENCREDITS_PUBLISHABLE_KEY; } + private static readonly IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg']; + private static readonly IMAGE_MEDIA_TYPES: Record = { + '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', + '.gif': 'image/gif', '.webp': 'image/webp', '.bmp': 'image/bmp', '.svg': 'image/svg+xml' + }; + + private async _autoUpdateRecommendedModels() { + // Check cache first + const cached = this._context.globalState.get<{ timestamp: number; models: any[]; creditsPricing?: any }>(ClaudeChatProvider.MODEL_CACHE_KEY); + if (cached && Date.now() - cached.timestamp < ClaudeChatProvider.MODEL_CACHE_TTL) { + this._postMessage({ + type: 'updateRecommendedModels', + models: cached.models, + creditsPricing: cached.creditsPricing + }); + return; + } + + const updated = await fetchAndResolveModels(recommendedModels as any[], OPENCREDITS_API_URL); + if (updated && updated.length > 0) { + // Fetch credit pricing for recommended models + reference model + const modelIds = updated.map((m: any) => m.id); + // Also include tier model IDs + for (const m of updated) { + if ((m as any).tierModels) { + const tiers = (m as any).tierModels; + for (const key of Object.keys(tiers)) { + if (tiers[key] && !modelIds.includes(tiers[key])) { + modelIds.push(tiers[key]); + } + } + } + } + if (!modelIds.includes(ClaudeChatProvider.REFERENCE_MODEL)) { + modelIds.push(ClaudeChatProvider.REFERENCE_MODEL); + } + if (!modelIds.includes('anthropic/claude-sonnet-4.6')) { + modelIds.push('anthropic/claude-sonnet-4.6'); + } + + let creditsPricing: any = null; + try { + const pricingRes = await fetch(OPENCREDITS_API_URL + '/v1/credits/pricing', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + publishable_key: ClaudeChatProvider.PUBLISHABLE_KEY, + models: modelIds + }) + }); + if (pricingRes.ok) { + const pricingData = await pricingRes.json() as any; + creditsPricing = { + referenceModel: ClaudeChatProvider.REFERENCE_MODEL, + models: pricingData.models || [], + tokenAssumption: pricingData.token_assumption + }; + } + } catch (e) { + console.error('[OpenCredits] Credit pricing fetch failed:', e); + } + + this._context.globalState.update(ClaudeChatProvider.MODEL_CACHE_KEY, { + timestamp: Date.now(), + models: updated, + creditsPricing + }); + this._postMessage({ + type: 'updateRecommendedModels', + models: updated, + creditsPricing + }); + } } public reinitializeWebview() { @@ -455,7 +857,7 @@ class ClaudeChatProvider { } } - private async _sendMessageToClaude(message: string, planMode?: boolean, thinkingMode?: boolean) { + private async _sendMessageToClaude(message: string, planMode?: boolean, thinkingMode?: boolean, images?: string[]) { const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; const cwd = workspaceFolder ? workspaceFolder.uri.fsPath : process.cwd(); @@ -509,7 +911,7 @@ class ClaudeChatProvider { await this._createBackupCommit(message); } catch (e) { - console.log("error", e); + console.error("error", e); } // Show loading indicator @@ -539,10 +941,15 @@ class ClaudeChatProvider { args.push('--permission-prompt-tool', 'stdio'); } - // Add MCP config if user has custom servers configured - const mcpConfigPath = this.getMCPConfigPath(); + // Pass extension's MCP config to Claude CLI (only if file exists) + const mcpConfigPath = this._getExtensionMCPConfigPath(); if (mcpConfigPath) { - args.push('--mcp-config', this.convertToWSLPath(mcpConfigPath)); + try { + await vscode.workspace.fs.stat(vscode.Uri.file(mcpConfigPath)); + args.push('--mcp-config', this.convertToWSLPath(mcpConfigPath)); + } catch { + // File doesn't exist, skip --mcp-config + } } // Add plan mode if enabled @@ -550,34 +957,85 @@ class ClaudeChatProvider { args.push('--permission-mode', 'plan'); } - // Add model selection if not using default - if (this._selectedModel && this._selectedModel !== 'default') { + // Add model selection for Claude models only (opus, sonnet) + // OpenCredits models are handled via env vars or router mapping + const claudeModels = ['opus', 'sonnet']; + if (this._selectedModel && claudeModels.includes(this._selectedModel)) { args.push('--model', this._selectedModel); } // Add session resume if we have a current session if (this._currentSessionId) { args.push('--resume', this._currentSessionId); - console.log('Resuming session:', this._currentSessionId); - } else { - console.log('Starting new session'); } - console.log('Claude command args:', args); const wslEnabled = config.get('wsl.enabled', false); const wslDistro = config.get('wsl.distro', 'Ubuntu'); const nodePath = config.get('wsl.nodePath', '/usr/bin/node'); const claudePath = config.get('wsl.claudePath', '/usr/local/bin/claude'); + const routerExplicitlyEnabled = config.get('router.enabled', false); + const customExecutablePath = config.get('executable.path', ''); + const envsDisabled = config.get('environment.disabled', false); + const customEnvVars = envsDisabled ? {} : config.get>('environment.variables', {}); + + // Check if using OpenCredits (base URL contains opencredits.ai) + const isOpenCredits = this._isOpenCredits(); + + // Router is only used when explicitly enabled (fallback for older OpenCredits support) + // OpenCredits now supports Anthropic API format directly, so env vars pass through + const useRouter = routerExplicitlyEnabled && !wslEnabled; + let claudeProcess: cp.ChildProcess; // Create new AbortController for this request this._abortController = new AbortController(); + // Build environment variables - apply custom env vars from settings + let spawnEnv: NodeJS.ProcessEnv = { + ...process.env, + FORCE_COLOR: '0', + NO_COLOR: '1', + ...customEnvVars // Apply custom environment variables (ANTHROPIC_AUTH_TOKEN, ANTHROPIC_BASE_URL, etc.) + }; + + // OpenCredits: clear Anthropic-specific vars so Claude CLI uses env vars directly + if (isOpenCredits) { + spawnEnv.ANTHROPIC_API_KEY = ''; + spawnEnv.DISABLE_TELEMETRY = 'true'; + spawnEnv.DISABLE_COST_WARNINGS = 'true'; + delete spawnEnv.CLAUDE_CODE_USE_BEDROCK; + } + + // If router explicitly enabled, start local router and override ANTHROPIC_BASE_URL + if (useRouter) { + // Pass the real ANTHROPIC_BASE_URL to the router before starting + const realBaseUrl = customEnvVars['ANTHROPIC_BASE_URL'] || ''; + if (realBaseUrl) { + setBaseUrl(realBaseUrl); + } + + const routerPort = await this._ensureLocalRouter(); + spawnEnv.ANTHROPIC_BASE_URL = `http://127.0.0.1:${routerPort}`; + spawnEnv.NO_PROXY = '127.0.0.1'; + } + if (wslEnabled) { - // Use WSL with bash -ic for proper environment loading - console.log('Using WSL configuration:', { wslDistro, nodePath, claudePath }); - const wslCommand = `"${nodePath}" --no-warnings --enable-source-maps "${claudePath}" ${args.join(' ')}`; + // Build env var exports to prepend to the WSL command + // WSL doesn't reliably inherit Windows env vars via spawn + const wslEnvOverrides: Record = { ...customEnvVars }; + if (isOpenCredits) { + wslEnvOverrides['ANTHROPIC_API_KEY'] = ''; + wslEnvOverrides['DISABLE_TELEMETRY'] = 'true'; + wslEnvOverrides['DISABLE_COST_WARNINGS'] = 'true'; + } + const envExports = Object.entries(wslEnvOverrides) + .map(([k, v]) => `export ${k}="${v.replace(/"/g, '\\"')}"`) + .join(' && '); + const envPrefix = envExports ? envExports + ' && ' : ''; + + const quotedArgs = args.map(a => a.includes(' ') ? `"${a}"` : a).join(' '); + const wslCommand = `${envPrefix}"${nodePath}" --no-warnings --enable-source-maps "${claudePath}" ${quotedArgs}`; // Track WSL state for proper process termination this._isWslProcess = true; @@ -588,29 +1046,21 @@ class ClaudeChatProvider { detached: process.platform !== 'win32', cwd: cwd, stdio: ['pipe', 'pipe', 'pipe'], - env: { - ...process.env, - FORCE_COLOR: '0', - NO_COLOR: '1' - } + env: spawnEnv }); } else { // Not using WSL this._isWslProcess = false; - // Use native claude command - console.log('Using native Claude command'); - claudeProcess = cp.spawn('claude', args, { + // Use native claude command (or custom executable if configured) + const executable = customExecutablePath || 'claude'; + claudeProcess = cp.spawn(executable, args, { signal: this._abortController.signal, shell: process.platform === 'win32', detached: process.platform !== 'win32', cwd: cwd, stdio: ['pipe', 'pipe', 'pipe'], - env: { - ...process.env, - FORCE_COLOR: '0', - NO_COLOR: '1' - } + env: spawnEnv }); } @@ -633,16 +1083,86 @@ class ClaudeChatProvider { claudeProcess.stdin.write(JSON.stringify(initRequest) + '\n'); } + // Build message content — detect image file paths and inline them as base64 + const content: Array<{type: string; text?: string; source?: {type: string; media_type: string; data: string}}> = []; + const imageExtensions = ClaudeChatProvider.IMAGE_EXTENSIONS; + const imageMediaTypes = ClaudeChatProvider.IMAGE_MEDIA_TYPES; + + // Scan message for image file paths and inline them as base64 + const imagePathRegex = /(\/[^\s]+\.(?:png|jpg|jpeg|gif|webp|bmp))\b/gi; + let lastIndex = 0; + let match; + while ((match = imagePathRegex.exec(actualMessage)) !== null) { + const imagePath = match[1]; + const ext = path.extname(imagePath).toLowerCase(); + if (imageExtensions.includes(ext)) { + try { + const imageData = await vscode.workspace.fs.readFile(vscode.Uri.file(imagePath)); + const base64 = Buffer.from(imageData).toString('base64'); + // Flush text before this match + const textBefore = actualMessage.substring(lastIndex, match.index); + if (textBefore.trim()) { + content.push({ type: 'text', text: textBefore.trim() }); + } + content.push({ + type: 'image', + source: { + type: 'base64', + media_type: imageMediaTypes[ext] || 'image/png', + data: base64 + } + }); + lastIndex = match.index + match[0].length; + } catch (e) { + console.error('Could not read image file:', imagePath, e); + } + } + } + // Add remaining text + const remaining = actualMessage.substring(lastIndex); + if (remaining.trim()) { + content.push({ type: 'text', text: remaining.trim() }); + } + + // Add explicitly attached images + if (images && images.length > 0) { + for (const imagePath of images) { + const ext = imageExtensions.find(e => imagePath.toLowerCase().endsWith(e)); + if (ext) { + try { + const imageData = await vscode.workspace.fs.readFile(vscode.Uri.file(imagePath)); + const base64 = Buffer.from(imageData).toString('base64'); + content.push({ + type: 'image', + source: { + type: 'base64', + media_type: imageMediaTypes[ext] || 'image/png', + data: base64 + } + }); + } catch (e) { + console.error('Could not read attached image:', imagePath, e); + } + } + } + } + + // Ensure at least one text block + if (content.length === 0) { + content.push({ type: 'text', text: actualMessage }); + } + const userMessage = { type: 'user', session_id: this._currentSessionId || '', message: { role: 'user', - content: [{ type: 'text', text: actualMessage }] + content: content }, parent_tool_use_id: null }; - claudeProcess.stdin.write(JSON.stringify(userMessage) + '\n'); + const userMessageJson = JSON.stringify(userMessage); + claudeProcess.stdin.write(userMessageJson + '\n'); } let rawOutput = ''; @@ -684,7 +1204,7 @@ class ClaudeChatProvider { this._processJsonStreamData(jsonData); } catch (error) { - console.log('Failed to parse JSON line:', line, error); + console.error('Failed to parse JSON line:', line, error); } } } @@ -698,8 +1218,6 @@ class ClaudeChatProvider { } claudeProcess.on('close', (code) => { - console.log('Claude process closed with code:', code); - console.log('Claude stderr output:', errorOutput); if (!this._currentClaudeProcess) { return; @@ -735,7 +1253,7 @@ class ClaudeChatProvider { }); claudeProcess.on('error', (error) => { - console.log('Claude process error:', error.message); + console.error('Claude process error:', error.message); if (!this._currentClaudeProcess) { return; @@ -778,7 +1296,6 @@ class ClaudeChatProvider { case 'system': if (jsonData.subtype === 'init') { // System initialization message - session ID will be captured from final result - console.log('System initialized'); this._currentSessionId = jsonData.session_id; //this._sendAndSaveMessage({ type: 'init', data: { sessionId: jsonData.session_id; } }) @@ -794,13 +1311,11 @@ class ClaudeChatProvider { } else if (jsonData.subtype === 'status') { // Handle status changes (e.g., compacting) if (jsonData.status === 'compacting') { - console.log('Conversation compacting started'); this._sendAndSaveMessage({ type: 'compacting', data: { isCompacting: true } }); } else if (jsonData.status === null) { - console.log('Status cleared'); this._sendAndSaveMessage({ type: 'compacting', data: { isCompacting: false } @@ -808,7 +1323,6 @@ class ClaudeChatProvider { } } else if (jsonData.subtype === 'compact_boundary') { // Compact boundary - conversation was compacted, reset token counts - console.log('Compact boundary received', jsonData.compact_metadata); // Reset tokens since the conversation is now summarized this._totalTokensInput = 0; @@ -1008,7 +1522,12 @@ class ClaudeChatProvider { case 'result': if (jsonData.subtype === 'success') { // Check for login errors - if (jsonData.is_error && jsonData.result && jsonData.result.includes('Invalid API key')) { + if (jsonData.is_error && jsonData.result && ( + jsonData.result.includes('Invalid API key') || + jsonData.result.includes('Not logged in') || + jsonData.result.includes('/login') || + jsonData.result.includes('not authenticated') + )) { this._handleLoginRequired(); return; } @@ -1017,15 +1536,6 @@ class ClaudeChatProvider { // Capture session ID from final result if (jsonData.session_id) { - const isNewSession = !this._currentSessionId; - const sessionChanged = this._currentSessionId && this._currentSessionId !== jsonData.session_id; - - console.log('Session ID found in result:', { - sessionId: jsonData.session_id, - isNewSession, - sessionChanged, - currentSessionId: this._currentSessionId - }); this._currentSessionId = jsonData.session_id; @@ -1052,11 +1562,6 @@ class ClaudeChatProvider { this._totalCost += jsonData.total_cost_usd; } - console.log('Result received:', { - cost: jsonData.total_cost_usd, - duration: jsonData.duration_ms, - turns: jsonData.num_turns - }); // Send updated totals to webview this._postMessage({ @@ -1071,6 +1576,11 @@ class ClaudeChatProvider { currentTurns: jsonData.num_turns } }); + + // Refresh OpenCredits balance after each request if using OpenCredits + if (this._isOpenCredits() || this._getOpenCreditsKey()) { + this._sendOpenCreditsBalance(); + } } break; } @@ -1110,9 +1620,6 @@ class ClaudeChatProvider { } public newSessionOnConfigChange() { - // Reinitialize MCP config with new WSL paths - this._initializeMCPConfig(); - // Start a new session due to configuration change this._newSession(); @@ -1129,7 +1636,7 @@ class ClaudeChatProvider { }); } - private _handleLoginRequired() { + private async _handleLoginRequired() { this._isProcessing = false; @@ -1139,41 +1646,19 @@ class ClaudeChatProvider { data: { isProcessing: false } }); - // Show login required message - this._postMessage({ - type: 'loginRequired' - }); - - // Get configuration to check if WSL is enabled - const config = vscode.workspace.getConfiguration('claudeCodeChat'); - const wslEnabled = config.get('wsl.enabled', false); - const wslDistro = config.get('wsl.distro', 'Ubuntu'); - const nodePath = config.get('wsl.nodePath', '/usr/bin/node'); - const claudePath = config.get('wsl.claudePath', '/usr/local/bin/claude'); - - // Open terminal and run claude login - const terminal = vscode.window.createTerminal({ - name: 'Claude Login', - location: { viewColumn: vscode.ViewColumn.One } - }); - if (wslEnabled) { - terminal.sendText(`wsl -d ${wslDistro} ${nodePath} --no-warnings --enable-source-maps ${claudePath}`); + // Check if OpenCredits is enabled - if so, show options modal + const opencreditsEnabled = await this._checkFeatureFlags(); + if (opencreditsEnabled) { + this._postMessage({ + type: 'showLoginOptions' + }); } else { - terminal.sendText('claude'); + // Just open login terminal directly + this._openLoginTerminal(); + this._postMessage({ + type: 'loginRequired' + }); } - terminal.show(); - - // Show info message - vscode.window.showInformationMessage( - 'Please login with your Claude plan or API key in the terminal, then come back to this chat.', - 'OK' - ); - - // Send message to UI about terminal - this._postMessage({ - type: 'terminalOpened', - data: `Please login with your Claude plan or API key in the terminal, then come back to this chat.`, - }); } private async _initializeBackupRepo(): Promise { @@ -1186,7 +1671,6 @@ class ClaudeChatProvider { console.error('No workspace storage available'); return; } - console.log('Workspace storage path:', storagePath); this._backupRepoPath = path.join(storagePath, 'backups', '.git'); // Create backup git directory if it doesn't exist @@ -1202,7 +1686,6 @@ class ClaudeChatProvider { await exec(`git --git-dir="${this._backupRepoPath}" config user.name "Claude Code Chat"`); await exec(`git --git-dir="${this._backupRepoPath}" config user.email "claude@anthropic.com"`); - console.log(`Initialized backup repository at: ${this._backupRepoPath}`); } } catch (error: any) { console.error('Failed to initialize backup repository:', error.message); @@ -1264,7 +1747,6 @@ class ClaudeChatProvider { data: commitInfo }); - console.log(`Created backup commit: ${commitInfo.sha.substring(0, 8)} - ${actualMessage}`); } catch (error: any) { console.error('Failed to create backup commit:', error.message); } @@ -1333,62 +1815,12 @@ class ClaudeChatProvider { await vscode.workspace.fs.stat(vscode.Uri.file(this._conversationsPath)); } catch { await vscode.workspace.fs.createDirectory(vscode.Uri.file(this._conversationsPath)); - console.log(`Created conversations directory at: ${this._conversationsPath}`); } } catch (error: any) { console.error('Failed to initialize conversations directory:', error.message); } } - private async _initializeMCPConfig(): Promise { - try { - const storagePath = this._context.storageUri?.fsPath; - if (!storagePath) { return; } - - // Create MCP config directory - const mcpConfigDir = path.join(storagePath, 'mcp'); - try { - await vscode.workspace.fs.stat(vscode.Uri.file(mcpConfigDir)); - } catch { - await vscode.workspace.fs.createDirectory(vscode.Uri.file(mcpConfigDir)); - console.log(`Created MCP config directory at: ${mcpConfigDir}`); - } - - // Create or update mcp-servers.json, preserving user's custom servers - // Note: Permissions are now handled via stdio, not MCP - const mcpConfigPath = path.join(mcpConfigDir, 'mcp-servers.json'); - - // Load existing config or create new one - let mcpConfig: any = { mcpServers: {} }; - const mcpConfigUri = vscode.Uri.file(mcpConfigPath); - - try { - const existingContent = await vscode.workspace.fs.readFile(mcpConfigUri); - mcpConfig = JSON.parse(new TextDecoder().decode(existingContent)); - console.log('Loaded existing MCP config, preserving user servers'); - } catch { - console.log('No existing MCP config found, creating new one'); - } - - // Ensure mcpServers exists - if (!mcpConfig.mcpServers) { - mcpConfig.mcpServers = {}; - } - - // Remove old permissions server if it exists (migrating from file-based to stdio) - if (mcpConfig.mcpServers['claude-code-chat-permissions']) { - delete mcpConfig.mcpServers['claude-code-chat-permissions']; - console.log('Removed legacy permissions MCP server (now using stdio)'); - } - - const configContent = new TextEncoder().encode(JSON.stringify(mcpConfig, null, 2)); - await vscode.workspace.fs.writeFile(mcpConfigUri, configContent); - - console.log(`Updated MCP config at: ${mcpConfigPath}`); - } catch (error: any) { - console.error('Failed to initialize MCP config:', error.message); - } - } /** * Check if a tool is pre-approved in local permissions @@ -1462,10 +1894,6 @@ class ClaudeChatProvider { 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); @@ -1484,13 +1912,12 @@ class ClaudeChatProvider { * Handle control_request messages from Claude CLI via stdio * This is the new permission flow that replaces the MCP file-based approach */ - private async _handleControlRequest(controlRequest: any, claudeProcess: cp.ChildProcess): Promise { + private async _handleControlRequest(controlRequest: any, _claudeProcess: cp.ChildProcess): Promise { const request = controlRequest.request; const requestId = controlRequest.request_id; // Only handle can_use_tool requests (permission requests) if (request?.subtype !== 'can_use_tool') { - console.log('Ignoring non-permission control request:', request?.subtype); return; } @@ -1499,13 +1926,17 @@ class ClaudeChatProvider { const suggestions = request.permission_suggestions; const toolUseId = request.tool_use_id; - console.log(`Permission request for tool: ${toolName}, requestId: ${requestId}`); + + // Handle AskUserQuestion tool separately + if (toolName === 'AskUserQuestion') { + this._handleAskUserQuestion(requestId, input, toolUseId); + return; + } // Check if this tool is pre-approved const isPreApproved = await this._isToolPreApproved(toolName, input); if (isPreApproved) { - console.log(`Tool ${toolName} is pre-approved, auto-allowing`); // Auto-approve without showing UI this._sendPermissionResponse(requestId, true, { requestId, @@ -1601,8 +2032,6 @@ class ClaudeChatProvider { } const responseJson = JSON.stringify(response) + '\n'; - console.log('Sending permission response:', responseJson); - console.log('Always allow:', alwaysAllow, 'Suggestions included:', !!pendingRequest.suggestions); this._currentClaudeProcess.stdin.write(responseJson); } @@ -1643,18 +2072,111 @@ class ClaudeChatProvider { } } + /** + * Handle AskUserQuestion tool - show questions UI and collect answers + */ + private _handleAskUserQuestion(requestId: string, input: Record, toolUseId: string): void { + const questions = (input.questions as any[]) || []; + + // Store the pending request + this._pendingPermissionRequests.set(requestId, { + requestId, + toolName: 'AskUserQuestion', + input, + suggestions: undefined, + toolUseId + }); + + // Send to UI for rendering + this._sendAndSaveMessage({ + type: 'askUserQuestion', + data: { + id: requestId, + questions: questions, + status: 'pending' + } + }); + } + + /** + * Handle user's answers to AskUserQuestion + */ + private _handleAskUserQuestionResponse(requestId: string, answers: Record): void { + const pendingRequest = this._pendingPermissionRequests.get(requestId); + if (!pendingRequest) { + console.error('No pending AskUserQuestion request found for id:', requestId); + return; + } + + this._pendingPermissionRequests.delete(requestId); + + if (!this._currentClaudeProcess?.stdin || this._currentClaudeProcess.stdin.destroyed) { + console.error('Cannot send AskUserQuestion response: stdin not available'); + return; + } + + const response = { + type: 'control_response', + response: { + subtype: 'success', + request_id: requestId, + response: { + behavior: 'allow', + updatedInput: { + questions: (pendingRequest.input as any).questions, + answers: answers + }, + toolUseID: pendingRequest.toolUseId + } + } + }; + + const responseJson = JSON.stringify(response) + '\n'; + this._currentClaudeProcess.stdin.write(responseJson); + + // Update the saved conversation message to reflect answered status + const savedMsg = this._currentConversation.find( + m => m.messageType === 'askUserQuestion' && m.data?.id === requestId + ); + if (savedMsg) { + savedMsg.data = { ...savedMsg.data, status: 'answered', answers: answers }; + void this._saveCurrentConversation(); + } + + // Update UI status + this._postMessage({ + type: 'updateAskUserQuestionStatus', + data: { + id: requestId, + status: 'answered', + answers: answers + } + }); + } + /** * Cancel all pending permission requests (called when process ends) */ private _cancelPendingPermissionRequests(): void { - for (const [id, _request] of this._pendingPermissionRequests) { - this._postMessage({ - type: 'updatePermissionStatus', - data: { - id: id, - status: 'cancelled' - } - }); + for (const [id, request] of this._pendingPermissionRequests) { + if (request.toolName === 'AskUserQuestion') { + this._postMessage({ + type: 'updateAskUserQuestionStatus', + data: { + id: id, + status: 'cancelled', + answers: null + } + }); + } else { + this._postMessage({ + type: 'updatePermissionStatus', + data: { + id: id, + status: 'cancelled' + } + }); + } } this._pendingPermissionRequests.clear(); } @@ -1706,7 +2228,6 @@ class ClaudeChatProvider { const permissionsContent = new TextEncoder().encode(JSON.stringify(permissions, null, 2)); await vscode.workspace.fs.writeFile(permissionsUri, permissionsContent); - console.log(`Saved local permission for ${toolName}`); } catch (error) { console.error('Error saving local permission:', error); } @@ -1880,7 +2401,6 @@ class ClaudeChatProvider { // Send updated permissions to UI this._sendPermissions(); - console.log(`Removed permission for ${toolName}${command ? ` command: ${command}` : ''}`); } catch (error) { console.error('Error removing permission:', error); } @@ -1945,120 +2465,349 @@ class ClaudeChatProvider { // Send updated permissions to UI this._sendPermissions(); - console.log(`Added permission for ${toolName}${command ? ` command: ${command}` : ' (all commands)'}`); } catch (error) { console.error('Error adding permission:', error); } } + // ─── Skills ─── + + private async _loadSkills(): Promise { + const skills: { name: string; scope: string; description: string; content: string }[] = []; + const homeDir = process.env.HOME || process.env.USERPROFILE || ''; + + // Scan personal skills + const personalDir = path.join(homeDir, '.claude', 'skills'); + try { + const entries = await vscode.workspace.fs.readDirectory(vscode.Uri.file(personalDir)); + for (const [name, type] of entries) { + if (type === vscode.FileType.Directory) { + const skillPath = path.join(personalDir, name, 'SKILL.md'); + try { + const content = await vscode.workspace.fs.readFile(vscode.Uri.file(skillPath)); + const text = new TextDecoder().decode(content); + const descMatch = text.match(/description:\s*(.+)/); + const bodyMatch = text.match(/^---[\s\S]*?---\s*([\s\S]*)$/); + skills.push({ name, scope: 'personal', description: descMatch ? descMatch[1].trim() : '', content: bodyMatch ? bodyMatch[1].trim() : text }); + } catch { /* no SKILL.md */ } + } + } + } catch { /* dir doesn't exist */ } + + // Scan project skills + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + if (workspaceFolder) { + const projectDir = path.join(workspaceFolder, '.claude', 'skills'); + try { + const entries = await vscode.workspace.fs.readDirectory(vscode.Uri.file(projectDir)); + for (const [name, type] of entries) { + if (type === vscode.FileType.Directory) { + const skillPath = path.join(projectDir, name, 'SKILL.md'); + try { + const content = await vscode.workspace.fs.readFile(vscode.Uri.file(skillPath)); + const text = new TextDecoder().decode(content); + const descMatch = text.match(/description:\s*(.+)/); + const bodyMatch = text.match(/^---[\s\S]*?---\s*([\s\S]*)$/); + skills.push({ name, scope: 'project', description: descMatch ? descMatch[1].trim() : '', content: bodyMatch ? bodyMatch[1].trim() : text }); + } catch { /* no SKILL.md */ } + } + } + } catch { /* dir doesn't exist */ } + } + + this._postMessage({ type: 'skillsList', data: skills }); + } + + private async _saveSkill(name: string, scope: string, content: string): Promise { + try { + let baseDir: string; + if (scope === 'project') { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + if (!workspaceFolder) { throw new Error('No workspace folder'); } + baseDir = path.join(workspaceFolder, '.claude', 'skills'); + } else { + const homeDir = process.env.HOME || process.env.USERPROFILE || ''; + baseDir = path.join(homeDir, '.claude', 'skills'); + } + + const skillDir = path.join(baseDir, name); + await vscode.workspace.fs.createDirectory(vscode.Uri.file(skillDir)); + const skillPath = path.join(skillDir, 'SKILL.md'); + await vscode.workspace.fs.writeFile(vscode.Uri.file(skillPath), new TextEncoder().encode(content)); + + this._postMessage({ type: 'skillSaved', data: { name } }); + vscode.window.showInformationMessage(`Skill "${name}" created successfully.`); + } catch (err: any) { + vscode.window.showErrorMessage(`Failed to create skill: ${err.message}`); + } + } + + private async _deleteSkill(name: string, scope: string): Promise { + try { + let baseDir: string; + if (scope === 'project') { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + if (!workspaceFolder) { throw new Error('No workspace folder'); } + baseDir = path.join(workspaceFolder, '.claude', 'skills'); + } else { + const homeDir = process.env.HOME || process.env.USERPROFILE || ''; + baseDir = path.join(homeDir, '.claude', 'skills'); + } + + const skillDir = path.join(baseDir, name); + await vscode.workspace.fs.delete(vscode.Uri.file(skillDir), { recursive: true }); + + this._postMessage({ type: 'skillDeleted', data: { name } }); + vscode.window.showInformationMessage(`Skill "${name}" deleted.`); + } catch (err: any) { + vscode.window.showErrorMessage(`Failed to delete skill: ${err.message}`); + } + } + + private async _searchSkills(query: string): Promise { + try { + const res = await fetch(`https://skills.sh/api/search?q=${encodeURIComponent(query)}&limit=20`); + if (!res.ok) { throw new Error('HTTP ' + res.status); } + const data = await res.json() as any; + this._postMessage({ type: 'skillsSearchResponse', data }); + } catch (err: any) { + this._postMessage({ type: 'skillsSearchResponse', data: { skills: [] } }); + } + } + + // ─── Plugins ─── + + private async _getClaudeSettingsPath(): Promise { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + if (!workspaceFolder) { return undefined; } + return path.join(workspaceFolder, '.claude', 'settings.json'); + } + + private async _readClaudeSettings(): Promise { + const settingsPath = await this._getClaudeSettingsPath(); + if (!settingsPath) { return {}; } + try { + const content = await vscode.workspace.fs.readFile(vscode.Uri.file(settingsPath)); + return JSON.parse(new TextDecoder().decode(content)); + } catch { + return {}; + } + } + + private async _writeClaudeSettings(settings: any): Promise { + const settingsPath = await this._getClaudeSettingsPath(); + if (!settingsPath) { return; } + const dirPath = path.dirname(settingsPath); + await vscode.workspace.fs.createDirectory(vscode.Uri.file(dirPath)); + await vscode.workspace.fs.writeFile( + vscode.Uri.file(settingsPath), + new TextEncoder().encode(JSON.stringify(settings, null, 2) + '\n') + ); + } + + private async _loadPlugins(): Promise { + const settings = await this._readClaudeSettings(); + const enabled = settings.enabledPlugins || {}; + this._postMessage({ type: 'pluginsList', data: { enabled } }); + } + + private async _installPlugin(installId: string): Promise { + try { + const settings = await this._readClaudeSettings(); + if (!settings.enabledPlugins) { settings.enabledPlugins = {}; } + settings.enabledPlugins[installId] = true; + await this._writeClaudeSettings(settings); + this._postMessage({ type: 'pluginInstalled', data: { installId } }); + vscode.window.showInformationMessage(`Plugin "${installId.replace(/@.*$/, '')}" enabled.`); + } catch (err: any) { + vscode.window.showErrorMessage(`Failed to enable plugin: ${err.message}`); + } + } + + private async _removePlugin(installId: string): Promise { + try { + const settings = await this._readClaudeSettings(); + if (settings.enabledPlugins) { + delete settings.enabledPlugins[installId]; + if (Object.keys(settings.enabledPlugins).length === 0) { + delete settings.enabledPlugins; + } + } + await this._writeClaudeSettings(settings); + this._postMessage({ type: 'pluginRemoved', data: { installId } }); + vscode.window.showInformationMessage(`Plugin "${installId.replace(/@.*$/, '')}" removed.`); + } catch (err: any) { + vscode.window.showErrorMessage(`Failed to remove plugin: ${err.message}`); + } + } + + private _runTerminalCommand(command: string): void { + const terminal = vscode.window.createTerminal({ + name: 'Claude Code', + location: vscode.TerminalLocation.Editor + }); + terminal.show(); + terminal.sendText(command); + } + + private async _fetchMarketplace(url: string, append?: boolean, isSearch?: boolean): Promise { + try { + const res = await fetch(url, { + headers: { 'accept': 'application/json' } + }); + if (!res.ok) { throw new Error('HTTP ' + res.status); } + const data = await res.json() as any; + data._append = !!append; + data._isSearch = !!isSearch; + this._postMessage({ type: 'marketplaceResponse', data }); + } catch (err: any) { + console.error('Marketplace fetch error:', err); + this._postMessage({ type: 'marketplaceError', data: { error: err.message } }); + } + } + + private _getExtensionMCPConfigPath(): string | undefined { + const storagePath = this._context.storageUri?.fsPath; + if (!storagePath) { return undefined; } + return path.join(storagePath, 'mcp', 'mcp-servers.json'); + } + + private _getMCPConfigPathForScope(scope: string): string | undefined { + if (scope === 'global') { + const homeDir = process.env.HOME || process.env.USERPROFILE || ''; + return homeDir ? path.join(homeDir, '.claude.json') : undefined; + } + if (scope === 'project') { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + return workspaceFolder ? path.join(workspaceFolder, '.mcp.json') : undefined; + } + // 'extension' scope — the private config + return this._getExtensionMCPConfigPath(); + } + + private async _readMCPConfigFile(filePath: string): Promise> { + try { + const content = await vscode.workspace.fs.readFile(vscode.Uri.file(filePath)); + const config = JSON.parse(new TextDecoder().decode(content)); + return config.mcpServers || {}; + } catch { + return {}; + } + } + private async _loadMCPServers(): Promise { try { - const mcpConfigPath = this.getMCPConfigPath(); - if (!mcpConfigPath) { - this._postMessage({ type: 'mcpServers', data: {} }); - return; + const servers: Record = {}; + + // Read extension's private config + const extPath = this._getExtensionMCPConfigPath(); + if (extPath) { + const extServers = await this._readMCPConfigFile(extPath); + for (const [name, config] of Object.entries(extServers)) { + if (name === 'claude-code-chat-permissions') continue; + servers[name] = { ...config as any, _scope: 'extension' }; + } } - const mcpConfigUri = vscode.Uri.file(mcpConfigPath); - let mcpConfig: any = { mcpServers: {} }; - - try { - const content = await vscode.workspace.fs.readFile(mcpConfigUri); - mcpConfig = JSON.parse(new TextDecoder().decode(content)); - } catch (error) { - console.log('MCP config file not found or error reading:', error); - // File doesn't exist, return empty servers + // Read project .mcp.json + const projectPath = this._getMCPConfigPathForScope('project'); + if (projectPath) { + const projectServers = await this._readMCPConfigFile(projectPath); + for (const [name, config] of Object.entries(projectServers)) { + if (!servers[name]) { + servers[name] = { ...config as any, _scope: 'project' }; + } + } } - // Filter out internal servers before sending to UI - const filteredServers = Object.fromEntries( - Object.entries(mcpConfig.mcpServers || {}).filter(([name]) => name !== 'claude-code-chat-permissions') - ); - this._postMessage({ type: 'mcpServers', data: filteredServers }); + // Read global ~/.claude.json + const globalPath = this._getMCPConfigPathForScope('global'); + if (globalPath) { + const globalServers = await this._readMCPConfigFile(globalPath); + for (const [name, config] of Object.entries(globalServers)) { + if (!servers[name]) { + servers[name] = { ...config as any, _scope: 'global' }; + } + } + } + + this._postMessage({ type: 'mcpServers', data: servers }); } catch (error) { console.error('Error loading MCP servers:', error); this._postMessage({ type: 'mcpServerError', data: { error: 'Failed to load MCP servers' } }); } } - private async _saveMCPServer(name: string, config: any): Promise { + private async _saveMCPServer(name: string, config: any, scope: string): Promise { try { - const mcpConfigPath = this.getMCPConfigPath(); - if (!mcpConfigPath) { - this._postMessage({ type: 'mcpServerError', data: { error: 'Storage path not available' } }); + // Remove internal _scope field before saving + const cleanConfig = { ...config }; + delete cleanConfig._scope; + + const configPath = this._getMCPConfigPathForScope(scope); + if (!configPath) { + this._postMessage({ type: 'mcpServerError', data: { error: scope === 'project' ? 'No workspace folder open' : 'Config path not available' } }); return; } - const mcpConfigUri = vscode.Uri.file(mcpConfigPath); - let mcpConfig: any = { mcpServers: {} }; + // Ensure directory exists for extension scope + if (scope === 'extension') { + const dir = vscode.Uri.file(path.dirname(configPath)); + try { await vscode.workspace.fs.stat(dir); } catch { + await vscode.workspace.fs.createDirectory(dir); + } + } + + const configUri = vscode.Uri.file(configPath); + let fileConfig: any = {}; - // Load existing config try { - const content = await vscode.workspace.fs.readFile(mcpConfigUri); - mcpConfig = JSON.parse(new TextDecoder().decode(content)); + const content = await vscode.workspace.fs.readFile(configUri); + fileConfig = JSON.parse(new TextDecoder().decode(content)); } catch { - // File doesn't exist, use default structure + // File doesn't exist } - // Ensure mcpServers exists - if (!mcpConfig.mcpServers) { - mcpConfig.mcpServers = {}; + if (!fileConfig.mcpServers) { + fileConfig.mcpServers = {}; } - // Add/update the server - mcpConfig.mcpServers[name] = config; + fileConfig.mcpServers[name] = cleanConfig; - // Ensure directory exists - const mcpDir = vscode.Uri.file(path.dirname(mcpConfigPath)); - try { - await vscode.workspace.fs.stat(mcpDir); - } catch { - await vscode.workspace.fs.createDirectory(mcpDir); - } - - // Save the config - const configContent = new TextEncoder().encode(JSON.stringify(mcpConfig, null, 2)); - await vscode.workspace.fs.writeFile(mcpConfigUri, configContent); + const configContent = new TextEncoder().encode(JSON.stringify(fileConfig, null, 2)); + await vscode.workspace.fs.writeFile(configUri, configContent); this._postMessage({ type: 'mcpServerSaved', data: { name } }); - console.log(`Saved MCP server: ${name}`); } catch (error) { console.error('Error saving MCP server:', error); this._postMessage({ type: 'mcpServerError', data: { error: 'Failed to save MCP server' } }); } } - private async _deleteMCPServer(name: string): Promise { + private async _deleteMCPServer(name: string, scope: string): Promise { try { - const mcpConfigPath = this.getMCPConfigPath(); - if (!mcpConfigPath) { - this._postMessage({ type: 'mcpServerError', data: { error: 'Storage path not available' } }); + const configPath = this._getMCPConfigPathForScope(scope); + if (!configPath) { + this._postMessage({ type: 'mcpServerError', data: { error: 'Config path not available' } }); return; } - const mcpConfigUri = vscode.Uri.file(mcpConfigPath); - let mcpConfig: any = { mcpServers: {} }; + const configUri = vscode.Uri.file(configPath); + let fileConfig: any = {}; - // Load existing config try { - const content = await vscode.workspace.fs.readFile(mcpConfigUri); - mcpConfig = JSON.parse(new TextDecoder().decode(content)); + const content = await vscode.workspace.fs.readFile(configUri); + fileConfig = JSON.parse(new TextDecoder().decode(content)); } catch { - // File doesn't exist, nothing to delete - this._postMessage({ type: 'mcpServerError', data: { error: 'MCP config file not found' } }); + this._postMessage({ type: 'mcpServerError', data: { error: 'Config file not found' } }); return; } - // Delete the server - if (mcpConfig.mcpServers && mcpConfig.mcpServers[name]) { - delete mcpConfig.mcpServers[name]; - - // Save the updated config - const configContent = new TextEncoder().encode(JSON.stringify(mcpConfig, null, 2)); - await vscode.workspace.fs.writeFile(mcpConfigUri, configContent); - + if (fileConfig.mcpServers && fileConfig.mcpServers[name]) { + delete fileConfig.mcpServers[name]; + const configContent = new TextEncoder().encode(JSON.stringify(fileConfig, null, 2)); + await vscode.workspace.fs.writeFile(configUri, configContent); this._postMessage({ type: 'mcpServerDeleted', data: { name } }); - console.log(`Deleted MCP server: ${name}`); } else { this._postMessage({ type: 'mcpServerError', data: { error: `Server '${name}' not found` } }); } @@ -2096,7 +2845,6 @@ class ClaudeChatProvider { data: { snippet } }); - console.log('Saved custom snippet:', snippet.name); } catch (error) { console.error('Error saving custom snippet:', error); this._postMessage({ @@ -2119,7 +2867,6 @@ class ClaudeChatProvider { data: { snippetId } }); - console.log('Deleted custom snippet:', snippetId); } else { this._postMessage({ type: 'error', @@ -2147,13 +2894,6 @@ class ClaudeChatProvider { return windowsPath; } - public getMCPConfigPath(): string | undefined { - const storagePath = this._context.storageUri?.fsPath; - if (!storagePath) { return undefined; } - - const configPath = path.join(storagePath, 'mcp', 'mcp-servers.json'); - return path.join(configPath); - } private _sendAndSaveMessage(message: { type: string, data: any }): void { @@ -2234,7 +2974,6 @@ class ClaudeChatProvider { // Update conversation index this._updateConversationIndex(filename, conversationData); - console.log(`Saved conversation: ${filename}`, this._conversationsPath); } catch (error: any) { console.error('Failed to save conversation:', error.message); } @@ -2317,13 +3056,16 @@ class ClaudeChatProvider { }); if (result && result.length > 0) { - // Send the selected file paths back to webview - result.forEach(uri => { - this._postMessage({ - type: 'imagePath', - path: uri.fsPath - }); - }); + for (const uri of result) { + const dataUri = await this._getImageDataUri(uri.fsPath); + if (dataUri) { + this._postMessage({ + type: 'imageAttached', + filePath: uri.fsPath, + previewUri: dataUri + }); + } + } } } catch (error) { @@ -2380,7 +3122,6 @@ class ClaudeChatProvider { return; } - console.log(`Terminating Claude process group (PID: ${pid})...`); // 3. Kill process group (handles children) await this._killProcessGroup(pid, 'SIGTERM'); @@ -2402,15 +3143,12 @@ class ClaudeChatProvider { // 5. Force kill if still running if (processToKill && !processToKill.killed) { - console.log(`Force killing Claude process group (PID: ${pid})...`); await this._killProcessGroup(pid, 'SIGKILL'); } - console.log('Claude process group terminated'); } private async _stopClaudeProcess(): Promise { - console.log('Stop request received'); this._isProcessing = false; @@ -2431,6 +3169,9 @@ class ClaudeChatProvider { type: 'error', data: '⏹️ Claude code was stopped.' }); + + // Refresh OpenCredits balance (request may have consumed credits) + this._sendOpenCreditsBalance(); } private _updateConversationIndex(filename: string, conversationData: ConversationData): void { @@ -2471,12 +3212,10 @@ class ClaudeChatProvider { } private async _loadConversationHistory(filename: string): Promise { - console.log("_loadConversationHistory"); if (!this._conversationsPath) { return; } try { const filePath = path.join(this._conversationsPath, filename); - console.log("filePath", filePath); let conversationData: ConversationData; try { @@ -2530,6 +3269,13 @@ class ClaudeChatProvider { messageData = { ...message.data, status: 'expired' }; } + // For askUserQuestion loaded from history, expire pending ones if no active process + if (message.messageType === 'askUserQuestion' && + message.data?.status === 'pending' && + !this._currentClaudeProcess) { + messageData = { ...message.data, status: 'expired' }; + } + this._postMessage({ type: message.messageType, data: messageData @@ -2538,7 +3284,7 @@ class ClaudeChatProvider { try { requestStartTime = new Date(message.timestamp).getTime() } catch (e) { - console.log(e) + console.error('Failed to parse message timestamp:', e); } } } @@ -2575,14 +3321,13 @@ class ClaudeChatProvider { }, 50); }, 100); // Small delay to ensure webview is ready - console.log(`Loaded conversation history: ${filename}`); } catch (error: any) { console.error('Failed to load conversation history:', error.message); } } private _getHtmlForWebview(): string { - return getHtml(vscode.env?.isTelemetryEnabled); + return getHtml(vscode.env?.isTelemetryEnabled, OPENCREDITS_API_URL, OPENCREDITS_WEB_URL, OPENCREDITS_PUBLISHABLE_KEY); } private _sendCurrentSettings(): void { @@ -2593,7 +3338,12 @@ class ClaudeChatProvider { 'wsl.distro': config.get('wsl.distro', 'Ubuntu'), 'wsl.nodePath': config.get('wsl.nodePath', '/usr/bin/node'), 'wsl.claudePath': config.get('wsl.claudePath', '/usr/local/bin/claude'), - 'permissions.yoloMode': config.get('permissions.yoloMode', false) + 'permissions.yoloMode': config.get('permissions.yoloMode', false), + 'router.enabled': config.get('router.enabled', false), + 'executable.path': config.get('executable.path', ''), + 'environment.variables': config.get>('environment.variables', {}), + 'environment.disabled': config.get('environment.disabled', false), + 'isOpenCredits': this._isOpenCredits() }; this._postMessage({ @@ -2610,7 +3360,6 @@ class ClaudeChatProvider { // Clear any global setting and set workspace setting await config.update('permissions.yoloMode', true, vscode.ConfigurationTarget.Workspace); - console.log('YOLO Mode enabled - all future permissions will be skipped'); // Send updated settings to UI this._sendCurrentSettings(); @@ -2630,18 +3379,34 @@ class ClaudeChatProvider { try { for (const [key, value] of Object.entries(settings)) { if (key === 'permissions.yoloMode') { - // YOLO mode is workspace-specific - await config.update(key, value, vscode.ConfigurationTarget.Workspace); + // YOLO mode: try workspace first, fall back to global + try { + await config.update(key, value, vscode.ConfigurationTarget.Workspace); + } catch { + await config.update(key, value, vscode.ConfigurationTarget.Global); + } } else { // Other settings are global (user-wide) await config.update(key, value, vscode.ConfigurationTarget.Global); } } - console.log('Settings updated:', settings); - } catch (error) { - console.error('Failed to update settings:', error); - vscode.window.showErrorMessage('Failed to update settings'); + // Re-send settings so webview gets updated isOpenCredits flag, etc. + this._sendCurrentSettings(); + + // Update balance display based on new env vars + if (this._isOpenCredits() || this._getOpenCreditsKey()) { + this._sendOpenCreditsBalance(); + } else { + // Clear balance if no longer OpenCredits + this._postMessage({ + type: 'opencreditsBalance', + balance: null + }); + } + } catch (error: any) { + console.error('Failed to update settings:', error?.message || error); + vscode.window.showErrorMessage(`Failed to update settings: ${error?.message || 'Unknown error'}`); } } @@ -2657,24 +3422,145 @@ class ClaudeChatProvider { } } - private _setSelectedModel(model: string): void { - // Validate model name to prevent issues mentioned in the GitHub issue - const validModels = ['opus', 'sonnet', 'default']; - if (validModels.includes(model)) { + private async _setSelectedModel(model: string, tierModels?: { sonnet: string; opus: string; haiku: string }): Promise { + // Valid Claude models + const validClaudeModels = ['opus', 'sonnet', 'default']; + + if (validClaudeModels.includes(model)) { this._selectedModel = model; - console.log('Model selected:', model); // Store the model preference in workspace state this._context.workspaceState.update('claude.selectedModel', model); + // Remove model env vars so Claude CLI uses defaults + await this._removeModelEnvVars(); + + // Refresh settings UI to reflect removed env vars + this._sendCurrentSettings(); + // Show confirmation - vscode.window.showInformationMessage(`Claude model switched to: ${model.charAt(0).toUpperCase() + model.slice(1)}`); + vscode.window.showInformationMessage(`Model switched to: ${model.charAt(0).toUpperCase() + model.slice(1)}`); } else { - console.error('Invalid model selected:', model); - vscode.window.showErrorMessage(`Invalid model: ${model}. Please select Opus, Sonnet, or Default.`); + // Any other model is treated as a OpenCredits model + this._selectedModel = model; + + // Store the model preference in workspace state + this._context.workspaceState.update('claude.selectedModel', model); + + // Set model env vars so Claude CLI routes to this model + await this._setModelEnvVars(model, tierModels); + + // Notify webview that model is switching + this._postMessage({ + type: 'modelSwitching', + model: model + }); + + // Update the local router config to use this model + this._updateLocalRouterModel(model, tierModels); + + // Fetch and send balance + await this._sendOpenCreditsBalance(); + + // Notify webview that model switch is complete + this._postMessage({ + type: 'modelSwitched', + model: model + }); + + // Show confirmation + vscode.window.showInformationMessage(`Model switched to: ${model}`); } } + // Set model env vars for non-Claude models + private async _setModelEnvVars(model: string, tierModels?: { sonnet: string; opus: string; haiku: string }): Promise { + const config = vscode.workspace.getConfiguration('claudeCodeChat'); + const envVars = config.get>('environment.variables', {}); + envVars['ANTHROPIC_DEFAULT_SONNET_MODEL'] = tierModels?.sonnet || model; + envVars['ANTHROPIC_DEFAULT_OPUS_MODEL'] = tierModels?.opus || model; + envVars['ANTHROPIC_DEFAULT_HAIKU_MODEL'] = tierModels?.haiku || model; + await config.update('environment.variables', envVars, vscode.ConfigurationTarget.Global); + } + + // Remove model env vars so Claude CLI uses defaults + private async _removeModelEnvVars(): Promise { + const config = vscode.workspace.getConfiguration('claudeCodeChat'); + const envVars = config.get>('environment.variables', {}); + const filtered: Record = {}; + for (const [key, value] of Object.entries(envVars)) { + if (key !== 'ANTHROPIC_DEFAULT_SONNET_MODEL' && + key !== 'ANTHROPIC_DEFAULT_OPUS_MODEL' && + key !== 'ANTHROPIC_DEFAULT_HAIKU_MODEL') { + filtered[key] = value; + } + } + await config.update('environment.variables', filtered, vscode.ConfigurationTarget.Global); + } + + // Fetch OpenCredits account balance + private async _fetchOpenCreditsBalance(): Promise { + const userKey = this._getOpenCreditsKey(); + + if (!userKey) { + return null; + } + + try { + const response = await fetch(OPENCREDITS_API_URL + '/v1/credits/balance', { + method: 'GET', + headers: { + 'X-User-Key': userKey, + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + console.error('Failed to fetch OpenCredits balance:', response.status); + return null; + } + + const data = await response.json() as { balance?: number }; + return data.balance != null ? data.balance : null; + } catch (error) { + console.error('Error fetching OpenCredits balance:', error); + return null; + } + } + + private async _sendOpenCreditsBalance(): Promise { + const balance = await this._fetchOpenCreditsBalance(); + this._postMessage({ + type: 'opencreditsBalance', + balance: balance + }); + } + + private async _openOpenCreditsAccount(): Promise { + const url = OPENCREDITS_WEB_URL + '/dashboard'; + + // Open via native OS command + const openCmd = process.platform === 'darwin' ? 'open' + : process.platform === 'win32' ? 'start' + : 'xdg-open'; + cp.spawn(openCmd, [url], { detached: true, stdio: 'ignore' }).unref(); + + // Show fallback modal in webview in case native open didn't work + this._postMessage({ + type: 'openedExternalUrl', + url: url + }); + } + + // Update the model configuration for the local router + private _updateLocalRouterModel(model: string, tierModels?: { sonnet: string; opus: string; haiku: string }): void { + setModelConfig({ + haikuModel: tierModels?.haiku || model, + sonnetModel: tierModels?.sonnet || model, + opusModel: tierModels?.opus || model + }); + } + private _openModelTerminal(): void { const config = vscode.workspace.getConfiguration('claudeCodeChat'); const wslEnabled = config.get('wsl.enabled', false); @@ -2745,47 +3631,71 @@ class ClaudeChatProvider { } 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; - } + // Check if npm is available with Node >= 18 + cp.exec('node -e "process.exit(parseInt(process.version.slice(1)) >= 18 ? 0 : 1)"', (checkErr) => { + if (checkErr) { + this._postMessage({ + type: 'installComplete', + success: false, + error: 'Node.js 18+ is required. Please install Node.js from https://nodejs.org/en/download and try again.' + }); + return; } - 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) => { + cp.exec('npm install -g @anthropic-ai/claude-code', { timeout: 120000 }, (error) => { if (error) { this._postMessage({ type: 'installComplete', success: false, - error: stderr || error.message + error: 'Installation failed. Please run in terminal: npm install -g @anthropic-ai/claude-code' }); } else { - this._postMessage({ - type: 'installComplete', - success: true - }); + this._postMessage({ type: 'installComplete', success: true }); } }); }); } + private _openLoginTerminal(): void { + const config = vscode.workspace.getConfiguration('claudeCodeChat'); + const wslEnabled = config.get('wsl.enabled', false); + const wslDistro = config.get('wsl.distro', 'Ubuntu'); + const nodePath = config.get('wsl.nodePath', '/usr/bin/node'); + const claudePath = config.get('wsl.claudePath', '/usr/local/bin/claude'); + + const terminal = vscode.window.createTerminal({ + name: 'Claude Login', + location: { viewColumn: vscode.ViewColumn.One } + }); + + if (wslEnabled) { + terminal.sendText(`wsl -d ${wslDistro} ${nodePath} --no-warnings --enable-source-maps ${claudePath}`); + } else { + terminal.sendText('claude'); + } + terminal.show(); + } + + // Start the local router and return its port + private async _ensureLocalRouter(): Promise { + // Update model config with the selected model, restoring tier models from persisted env vars + if (this._selectedModel && this._selectedModel !== 'default') { + const config = vscode.workspace.getConfiguration('claudeCodeChat'); + const envVars = config.get>('environment.variables', {}); + const sonnet = envVars['ANTHROPIC_DEFAULT_SONNET_MODEL']; + const opus = envVars['ANTHROPIC_DEFAULT_OPUS_MODEL']; + const haiku = envVars['ANTHROPIC_DEFAULT_HAIKU_MODEL']; + const tierModels = (sonnet || opus || haiku) + ? { sonnet: sonnet || this._selectedModel, opus: opus || this._selectedModel, haiku: haiku || this._selectedModel } + : undefined; + this._updateLocalRouterModel(this._selectedModel, tierModels); + } + + // Start the router if not already running + const port = await startRouter(); + return port; + } + private _executeSlashCommand(command: string): void { // Handle /compact in chat instead of spawning a terminal if (command === 'compact') { @@ -3003,12 +3913,11 @@ class ClaudeChatProvider { const imagePath = vscode.Uri.joinPath(imagesDir, imageFileName); await vscode.workspace.fs.writeFile(imagePath, buffer); - // Send the file path back to webview + // Send the file path back to webview — use the original data URL for preview this._postMessage({ - type: 'imagePath', - data: { - filePath: imagePath.fsPath - } + type: 'imageAttached', + filePath: imagePath.fsPath, + previewUri: imageData }); } catch (error) { diff --git a/src/model-updater.ts b/src/model-updater.ts new file mode 100644 index 0000000..bbcddef --- /dev/null +++ b/src/model-updater.ts @@ -0,0 +1,148 @@ +interface ApiModel { + id: string; + name?: string; + description?: string; + pricing?: { prompt: number; completion: number; currency?: string; unit?: string }; + context_length?: number; + max_output_tokens?: number; + [key: string]: any; +} + +interface BundledModel { + id: string; + name: string; + description?: string; + provider: string; + quickLabel?: string; + context_length?: number; + max_output_tokens?: number; + tierModels?: { sonnet: string; opus: string; haiku: string }; + [key: string]: any; +} + +interface ProviderResolver { + main: RegExp; + opus?: RegExp; + haiku?: RegExp; +} + +function parseVersion(ver: string): number[] { + return ver.split('.').map(Number); +} + +function compareVersions(a: string, b: string): number { + const va = parseVersion(a); + const vb = parseVersion(b); + for (let i = 0; i < Math.max(va.length, vb.length); i++) { + const na = va[i] || 0; + const nb = vb[i] || 0; + if (na !== nb) { return na - nb; } + } + return 0; +} + +function findHighestMatch(apiModels: ApiModel[], regex: RegExp): ApiModel | null { + let best: ApiModel | null = null; + let bestVer: string | null = null; + for (const m of apiModels) { + const match = regex.exec(m.id); + if (match) { + const ver = match[1] || '0'; + if (!bestVer || compareVersions(ver, bestVer) > 0) { + bestVer = ver; + best = m; + } + } + } + return best; +} + +const providerResolvers: Record = { + 'zai/glm-': { + main: /^zai\/glm-(\d+(?:\.\d+)?)$/, + haiku: /^zai\/GLM-([\d.]+)-(?:Air|Flash)$/i + }, + 'openai/gpt-': { + main: /^openai\/gpt-([\d.]+)-codex$/, + haiku: /^openai\/gpt-([\d.]+)-codex-mini$/ + }, + 'gemini-': { + main: /^(?:google\/)?gemini-([\d.]+)-pro-preview$/, + opus: /^(?:google\/)?gemini-([\d.]+)-pro-preview-thinking$/, + haiku: /^(?:google\/)?gemini-([\d.]+)-flash(?:-preview)?$/ + }, + 'deepseek/deepseek-': { + main: /^deepseek\/deepseek-v([\d.]+)[-:]thinking$/ + }, + 'minimax/minimax-': { + main: /^minimax\/minimax-m([\d.]+)$/ + }, + 'moonshotai/kimi-': { + main: /^moonshotai\/kimi-k([\d.]+)$/, + haiku: /^moonshotai\/kimi-k([\d.]+)-turbo$/ + } +}; + +export function resolveLatestModels(apiModels: ApiModel[], bundledModels: BundledModel[]): BundledModel[] { + return bundledModels.map(bundled => { + const b: BundledModel = JSON.parse(JSON.stringify(bundled)); + + let resolver: ProviderResolver | null = null; + for (const prefix of Object.keys(providerResolvers)) { + if (b.id.toLowerCase().startsWith(prefix)) { + resolver = providerResolvers[prefix]; + break; + } + } + if (!resolver) { return b; } + + // Resolve main (sonnet-tier) model + const mainMatch = findHighestMatch(apiModels, resolver.main); + if (mainMatch) { + b.id = mainMatch.id; + b.name = mainMatch.name || b.name; + b.description = mainMatch.description || b.description; + b.context_length = mainMatch.context_length || b.context_length; + b.max_output_tokens = mainMatch.max_output_tokens || b.max_output_tokens; + if (b.tierModels) { + b.tierModels.sonnet = mainMatch.id; + if (!resolver.opus) { + b.tierModels.opus = mainMatch.id; + } + } + } + + // Resolve opus-tier model (e.g. Gemini thinking variant) + if (resolver.opus && b.tierModels) { + const opusMatch = findHighestMatch(apiModels, resolver.opus); + if (opusMatch) { + b.tierModels.opus = opusMatch.id; + } + } + + // Resolve haiku-tier model + if (resolver.haiku && b.tierModels) { + const haikuMatch = findHighestMatch(apiModels, resolver.haiku); + if (haikuMatch) { + b.tierModels.haiku = haikuMatch.id; + } + } + + return b; + }); +} + +export async function fetchAndResolveModels(bundledModels: BundledModel[], apiBaseUrl: string = 'https://ccc.api.opencredits.ai'): Promise { + try { + const response = await fetch(apiBaseUrl + '/v1/models'); + const data: any = await response.json(); + const apiModels: ApiModel[] = data.data || data; + if (!Array.isArray(apiModels) || apiModels.length === 0) { + return null; + } + return resolveLatestModels(apiModels, bundledModels); + } catch (e) { + console.log('Auto-update models failed:', e); + return null; + } +} diff --git a/src/plugins-script.ts b/src/plugins-script.ts new file mode 100644 index 0000000..888f8b9 --- /dev/null +++ b/src/plugins-script.ts @@ -0,0 +1,150 @@ +const getPluginsScript = () => ` + // ─── Plugins ─── + var topPlugins = (window.__topPlugins || []); + var pluginsDisplayedList = null; + + function formatPluginName(name) { + return name.replace(/-/g, ' ').replace(/\\b\\w/g, function(c) { return c.toUpperCase(); }); + } + + function showPluginsModal() { + document.getElementById('pluginsModal').style.display = 'flex'; + loadInstalledPlugins(); + renderAvailablePlugins(topPlugins); + } + + function hidePluginsModal() { + document.getElementById('pluginsModal').style.display = 'none'; + } + + function loadInstalledPlugins() { + vscode.postMessage({ type: 'loadPlugins' }); + } + + function displayPlugins(data) { + var pluginsList = document.getElementById('pluginsList'); + pluginsList.innerHTML = ''; + var enabled = data.enabled || {}; + + var keys = Object.keys(enabled); + if (keys.length === 0) { + pluginsList.innerHTML = '
' + + '
' + + '
No plugins enabled
' + + '
'; + return; + } + + keys.forEach(function(installId) { + var isEnabled = enabled[installId]; + var name = installId.replace(/@.*$/, ''); + var displayName = formatPluginName(name); + var plugin = topPlugins.find(function(p) { return p.installId === installId; }); + var desc = plugin ? plugin.description : ''; + var verified = plugin ? plugin.verified : false; + + var item = document.createElement('div'); + item.className = 'mcp-server-item'; + var verifiedHtml = verified ? '' : ''; + var statusHtml = isEnabled ? 'enabled' : 'disabled'; + item.innerHTML = '
' + + '
' + escapeHtml(displayName) + verifiedHtml + ' ' + statusHtml + '
' + + (desc ? '
' + escapeHtml(desc) + '
' : '') + + '
' + + '
' + + '' + + '
'; + pluginsList.appendChild(item); + }); + } + + function renderAvailablePlugins(plugins) { + var grid = document.getElementById('pluginsGrid'); + if (!grid) return; + if (!plugins || plugins.length === 0) { + grid.innerHTML = '
No plugins found.
'; + return; + } + var html = ''; + plugins.forEach(function(plugin) { + var name = plugin.name || 'Unknown'; + var displayName = formatPluginName(name); + var desc = escapeHtml(plugin.description || 'No description'); + var verified = plugin.verified; + var safeId = escapeHtml(plugin.installId || name).replace(/'/g, '''); + + html += '
' + + '
' + + '
' + escapeHtml(displayName.charAt(0).toUpperCase()) + '
' + + '
' + + '
' + escapeHtml(displayName) + '
' + + '
' + + '
' + + '
' + desc + '
' + + '
'; + }); + grid.innerHTML = html; + } + + function searchPlugins(query) { + if (!query) { + renderAvailablePlugins(topPlugins); + return; + } + var q = query.toLowerCase(); + var filtered = topPlugins.filter(function(p) { + return (p.name && p.name.toLowerCase().indexOf(q) >= 0) || + (p.description && p.description.toLowerCase().indexOf(q) >= 0); + }); + renderAvailablePlugins(filtered); + } + + function showPluginDetail(installId) { + var plugin = topPlugins.find(function(p) { return p.installId === installId; }); + if (!plugin) return; + + var name = plugin.name || 'Unknown'; + var displayName = formatPluginName(name); + var desc = plugin.description || 'No description available.'; + var verified = plugin.verified; + var verifiedHtml = verified ? '✓ Anthropic verified' : ''; + + var grid = document.getElementById('pluginsGrid'); + pluginsDisplayedList = grid.innerHTML; + + grid.innerHTML = '
' + + '' + + '
' + + '
' + escapeHtml(displayName.charAt(0).toUpperCase()) + '
' + + '
' + + '
' + escapeHtml(displayName) + '
' + + '
' + verifiedHtml + '
' + + '
' + + '' + + '
' + + '
' + escapeHtml(desc) + '
' + + '' + + '
Adds ' + escapeHtml(installId) + ' to .claude/settings.json
' + + '
'; + } + + function backToPluginsList() { + var grid = document.getElementById('pluginsGrid'); + if (pluginsDisplayedList) { + grid.innerHTML = pluginsDisplayedList; + } else { + renderAvailablePlugins(topPlugins); + } + } + + function installPlugin(installId) { + vscode.postMessage({ type: 'installPlugin', installId: installId }); + hidePluginsModal(); + } + + function removePlugin(installId) { + vscode.postMessage({ type: 'removePlugin', installId: installId }); + } +`; + +export default getPluginsScript; diff --git a/src/plugins-ui.ts b/src/plugins-ui.ts new file mode 100644 index 0000000..c80c0b6 --- /dev/null +++ b/src/plugins-ui.ts @@ -0,0 +1,26 @@ +const getPluginsHtml = () => ` + + +`; + +export default getPluginsHtml; diff --git a/src/recommended-models.json b/src/recommended-models.json new file mode 100644 index 0000000..716280a --- /dev/null +++ b/src/recommended-models.json @@ -0,0 +1,66 @@ +[ + { + "id": "openai/gpt-5.3-codex", + "name": "GPT 5.3 Codex", + "description": "Coding-focused GPT-5.3 variant with optimized routing.", + "context_length": 400000, + "max_output_tokens": 128000, + "credits_per_request": 4.921875, + "provider": "OpenAI", + "quickLabel": "GPT", + "tierModels": { "sonnet": "openai/gpt-5.3-codex", "opus": "openai/gpt-5.3-codex", "haiku": "openai/gpt-5.1-codex-mini" } + }, + { + "id": "google/gemini-3.1-pro-preview", + "name": "Gemini 3.1 Pro Preview", + "description": "Google's Gemini 3.1 Pro with enhanced reasoning and multimodal support.", + "context_length": 1000000, + "max_output_tokens": 64000, + "credits_per_request": 4.375, + "provider": "Google", + "quickLabel": "Gemini", + "tierModels": { "sonnet": "google/gemini-3.1-pro-preview", "opus": "google/gemini-3.1-pro-preview", "haiku": "google/gemini-3-flash" } + }, + { + "id": "minimax/minimax-m2.7", + "name": "Minimax M2.7", + "description": "MiniMax M2.7 with enhanced context understanding and improved complex tool use. Optimized for agentic workflows and long-horizon tasks.", + "context_length": 204800, + "max_output_tokens": 131000, + "credits_per_request": 0.46875, + "provider": "MiniMax", + "quickLabel": "MiniMax" + }, + { + "id": "moonshotai/kimi-k2.5", + "name": "Kimi K2.5", + "description": "Kimi K2.5 is Moonshot AI's native multimodal model with strong general reasoning, visual coding, and agentic tool-calling.", + "context_length": 262114, + "max_output_tokens": 262114, + "credits_per_request": 1.125, + "provider": "Moonshot AI", + "quickLabel": "Kimi", + "tierModels": { "sonnet": "moonshotai/kimi-k2.5", "opus": "moonshotai/kimi-k2.5", "haiku": "moonshotai/kimi-k2-turbo" } + }, + { + "id": "zai/glm-5", + "name": "GLM 5", + "description": "GLM-5 is the latest GLM series text model with stronger reasoning, long-context chat, and reliable tool use.", + "context_length": 202800, + "max_output_tokens": 131100, + "credits_per_request": 1.3125, + "provider": "Zhipu AI", + "quickLabel": "GLM", + "tierModels": { "sonnet": "zai/glm-5", "opus": "zai/glm-5", "haiku": "zai/glm-4.7-flash" } + }, + { + "id": "deepseek/deepseek-v3.2-thinking", + "name": "DeepSeek V3.2 Thinking", + "description": "DeepSeek V3.2 thinking/reasoner mode. Reasoning-first model built for agents. First DeepSeek model with thinking-in-tool-use capability.", + "context_length": 128000, + "max_output_tokens": 64000, + "credits_per_request": 0.21875, + "provider": "DeepSeek", + "quickLabel": "DeepSeek" + } +] diff --git a/src/router/formatRequest.ts b/src/router/formatRequest.ts new file mode 100644 index 0000000..f584edd --- /dev/null +++ b/src/router/formatRequest.ts @@ -0,0 +1,265 @@ +interface MessageCreateParamsBase { + model: string; + messages: any[]; + system?: any; + temperature?: number; + tools?: any[]; + stream?: boolean; +} + + +/** + * Validates OpenAI format messages to ensure complete tool_calls/tool message pairing. + * Requires tool messages to immediately follow assistant messages with tool_calls. + * Enforces strict immediate following sequence between tool_calls and tool messages. + */ +function validateOpenAIToolCalls(messages: any[]): any[] { + const validatedMessages: any[] = []; + + for (let i = 0; i < messages.length; i++) { + const currentMessage = { ...messages[i] }; + + // Process assistant messages with tool_calls + if (currentMessage.role === "assistant" && currentMessage.tool_calls) { + const validToolCalls: any[] = []; + const removedToolCallIds: string[] = []; + + // Collect all immediately following tool messages + const immediateToolMessages: any[] = []; + let j = i + 1; + while (j < messages.length && messages[j].role === "tool") { + immediateToolMessages.push(messages[j]); + j++; + } + + // For each tool_call, check if there's an immediately following tool message + currentMessage.tool_calls.forEach((toolCall: any) => { + const hasImmediateToolMessage = immediateToolMessages.some(toolMsg => + toolMsg.tool_call_id === toolCall.id + ); + + if (hasImmediateToolMessage) { + validToolCalls.push(toolCall); + } else { + removedToolCallIds.push(toolCall.id); + } + }); + + // Update the assistant message + if (validToolCalls.length > 0) { + currentMessage.tool_calls = validToolCalls; + } else { + delete currentMessage.tool_calls; + } + + + // Only include message if it has content or valid tool_calls + if (currentMessage.content || currentMessage.tool_calls) { + validatedMessages.push(currentMessage); + } + } + + // Process tool messages + else if (currentMessage.role === "tool") { + let hasImmediateToolCall = false; + + // Check if the immediately preceding assistant message has matching tool_call + if (i > 0) { + const prevMessage = messages[i - 1]; + if (prevMessage.role === "assistant" && prevMessage.tool_calls) { + hasImmediateToolCall = prevMessage.tool_calls.some((toolCall: any) => + toolCall.id === currentMessage.tool_call_id + ); + } else if (prevMessage.role === "tool") { + // Check for assistant message before the sequence of tool messages + for (let k = i - 1; k >= 0; k--) { + if (messages[k].role === "tool") continue; + if (messages[k].role === "assistant" && messages[k].tool_calls) { + hasImmediateToolCall = messages[k].tool_calls.some((toolCall: any) => + toolCall.id === currentMessage.tool_call_id + ); + } + break; + } + } + } + + if (hasImmediateToolCall) { + validatedMessages.push(currentMessage); + } + } + + // For all other message types, include as-is + else { + validatedMessages.push(currentMessage); + } + } + + return validatedMessages; +} + +// Model configuration - set from extension +interface ModelConfig { + haikuModel: string; + sonnetModel: string; + opusModel: string; +} + +let modelConfig: ModelConfig | null = null; + +export function setModelConfig(config: ModelConfig): void { + modelConfig = config; + console.log('[Router] Model config updated:', config); +} + +export function mapModel(anthropicModel: string): string { + console.log('[Router] Mapping model:', anthropicModel); + + // If model already contains '/', it's already a provider model ID - return as-is + if (anthropicModel.includes('/')) { + console.log(`[Router] Model already has provider prefix, passing through: ${anthropicModel}`); + return anthropicModel; + } + + if (!modelConfig) { + console.log('[Router] No model config set, returning as-is'); + return anthropicModel; + } + + if (anthropicModel.includes('haiku') && modelConfig.haikuModel) { + console.log(`[Router] Mapping haiku -> ${modelConfig.haikuModel}`); + return modelConfig.haikuModel; + } else if (anthropicModel.includes('sonnet') && modelConfig.sonnetModel) { + console.log(`[Router] Mapping sonnet -> ${modelConfig.sonnetModel}`); + return modelConfig.sonnetModel; + } else if (anthropicModel.includes('opus') && modelConfig.opusModel) { + console.log(`[Router] Mapping opus -> ${modelConfig.opusModel}`); + return modelConfig.opusModel; + } + + console.log(`[Router] No mapping found for model: ${anthropicModel}, passing through`); + return anthropicModel; +} + +export function formatAnthropicToOpenAI(body: MessageCreateParamsBase): any { + const { model, messages, system = [], temperature, tools, stream } = body; + + const openAIMessages = Array.isArray(messages) + ? messages.flatMap((anthropicMessage) => { + const openAiMessagesFromThisAnthropicMessage: any[] = []; + + if (!Array.isArray(anthropicMessage.content)) { + if (typeof anthropicMessage.content === "string") { + openAiMessagesFromThisAnthropicMessage.push({ + role: anthropicMessage.role, + content: anthropicMessage.content, + }); + } + return openAiMessagesFromThisAnthropicMessage; + } + + if (anthropicMessage.role === "assistant") { + const assistantMessage: any = { + role: "assistant", + content: null, + }; + let textContent = ""; + const toolCalls: any[] = []; + + anthropicMessage.content.forEach((contentPart: any) => { + if (contentPart.type === "text") { + textContent += (typeof contentPart.text === "string" + ? contentPart.text + : JSON.stringify(contentPart.text)) + "\n"; + } else if (contentPart.type === "tool_use") { + toolCalls.push({ + id: contentPart.id, + type: "function", + function: { + name: contentPart.name, + arguments: JSON.stringify(contentPart.input), + }, + }); + } + }); + + const trimmedTextContent = textContent.trim(); + if (trimmedTextContent.length > 0) { + assistantMessage.content = trimmedTextContent; + } + if (toolCalls.length > 0) { + assistantMessage.tool_calls = toolCalls; + } + if (assistantMessage.content || (assistantMessage.tool_calls && assistantMessage.tool_calls.length > 0)) { + openAiMessagesFromThisAnthropicMessage.push(assistantMessage); + } + } else if (anthropicMessage.role === "user") { + let userTextMessageContent = ""; + const subsequentToolMessages: any[] = []; + + anthropicMessage.content.forEach((contentPart: any) => { + if (contentPart.type === "text") { + userTextMessageContent += (typeof contentPart.text === "string" + ? contentPart.text + : JSON.stringify(contentPart.text)) + "\n"; + } else if (contentPart.type === "tool_result") { + subsequentToolMessages.push({ + role: "tool", + tool_call_id: contentPart.tool_use_id, + content: typeof contentPart.content === "string" + ? contentPart.content + : JSON.stringify(contentPart.content), + }); + } + }); + + const trimmedUserText = userTextMessageContent.trim(); + if (trimmedUserText.length > 0) { + openAiMessagesFromThisAnthropicMessage.push({ + role: "user", + content: trimmedUserText, + }); + } + openAiMessagesFromThisAnthropicMessage.push(...subsequentToolMessages); + } + return openAiMessagesFromThisAnthropicMessage; + }) + : []; + + const systemMessages = Array.isArray(system) + ? system.map((item) => ({ + role: "system", + content: typeof item === "string" ? item : item.text + })) + : typeof system === "string" && system.length > 0 + ? [{ role: "system", content: system }] + : []; + + const data: any = { + model: mapModel(model), + messages: [...systemMessages, ...openAIMessages], + temperature, + stream, + }; + + // Request usage stats in streaming responses + if (stream) { + data.stream_options = { include_usage: true }; + } + + if (tools) { + data.tools = tools.map((item: any) => ({ + type: "function", + function: { + name: item.name, + description: item.description, + parameters: item.input_schema, + }, + })); + } + + // Validate OpenAI messages to ensure complete tool_calls/tool message pairing + data.messages = [...systemMessages, ...validateOpenAIToolCalls(openAIMessages)]; + + return data; +} \ No newline at end of file diff --git a/src/router/formatResponse.ts b/src/router/formatResponse.ts new file mode 100644 index 0000000..4dde0ba --- /dev/null +++ b/src/router/formatResponse.ts @@ -0,0 +1,37 @@ +export function formatOpenAIToAnthropic(completion: any, model: string): any { + const messageId = "msg_" + Date.now(); + const message = completion.choices[0].message; + + const content: any[] = []; + if (message.content) { + content.push({ text: message.content, type: "text" }); + } + if (message.tool_calls) { + for (const item of message.tool_calls) { + content.push({ + type: 'tool_use', + id: item.id, + name: item.function?.name, + input: item.function?.arguments ? JSON.parse(item.function.arguments) : {}, + }); + } + } + + const hasToolUse = message.tool_calls && message.tool_calls.length > 0; + const usage = completion.usage || {}; + + const result = { + id: messageId, + type: "message", + role: "assistant", + content: content, + stop_reason: hasToolUse ? "tool_use" : "end_turn", + stop_sequence: null, + model, + usage: { + input_tokens: usage.prompt_tokens || 0, + output_tokens: usage.completion_tokens || 0, + }, + }; + return result; +} \ No newline at end of file diff --git a/src/router/index.ts b/src/router/index.ts new file mode 100644 index 0000000..d735d67 --- /dev/null +++ b/src/router/index.ts @@ -0,0 +1,2 @@ +export { startRouter, stopRouter, isRouterRunning, getRouterPort, setBaseUrl } from './server'; +export { setModelConfig } from './formatRequest'; diff --git a/src/router/server.ts b/src/router/server.ts new file mode 100644 index 0000000..f02e5b7 --- /dev/null +++ b/src/router/server.ts @@ -0,0 +1,220 @@ +import * as http from 'http'; +import { formatAnthropicToOpenAI } from './formatRequest'; +import { streamOpenAIToAnthropic } from './streamResponse'; +import { formatOpenAIToAnthropic } from './formatResponse'; + +const DEFAULT_PORT = 31548; +const DEFAULT_BASE_URL = "http://localhost:8787/v1"; + +let server: http.Server | null = null; +let currentPort: number = DEFAULT_PORT; +let baseUrl: string = DEFAULT_BASE_URL; + +export function setBaseUrl(url: string): void { + baseUrl = url || DEFAULT_BASE_URL; + console.log('[Router] Base URL set to:', baseUrl); +} + +// Helper to parse JSON body +async function parseBody(req: http.IncomingMessage): Promise { + return new Promise((resolve, reject) => { + let body = ''; + req.on('data', chunk => { + body += chunk.toString(); + // Prevent payload too large (50MB limit) + if (body.length > 50 * 1024 * 1024) { + req.destroy(); + reject(new Error('Payload too large')); + } + }); + req.on('end', () => { + try { + resolve(body ? JSON.parse(body) : {}); + } catch (e) { + reject(new Error('Invalid JSON')); + } + }); + req.on('error', reject); + }); +} + +function createServer(): http.Server { + return http.createServer(async (req, res) => { + const url = new URL(req.url || '/', `http://${req.headers.host}`); + const method = req.method || 'GET'; + + try { + // POST /v1/messages + if (url.pathname === '/v1/messages' && method === 'POST') { + console.log('[Router] 📥 Received request to /v1/messages'); + + const anthropicRequest = await parseBody(req); + const openaiRequest = formatAnthropicToOpenAI(anthropicRequest); + + console.log('[Router] 🔄 Converted to OpenAI format:', { + model: openaiRequest.model, + stream: openaiRequest.stream, + messageCount: openaiRequest.messages?.length + }); + + const bearerToken = (req.headers['x-api-key'] as string) || + (req.headers.authorization as string)?.replace("Bearer ", "").replace("bearer ", ""); + + if (!bearerToken || bearerToken.trim() === '') { + console.log('[Router] ❌ No bearer token found'); + res.writeHead(401, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + type: 'error', + error: { + type: 'authentication_error', + message: 'No API key provided. Please configure your OpenCredits user key in environment variables.' + } + })); + return; + } + + const fetchHeaders = { + "Content-Type": "application/json", + "Authorization": `Bearer ${bearerToken}`, + "HTTP-Referer": "https://claude-code-chat.local", + "X-Title": "Claude-Code-Chat-Router" + }; + + const openaiResponse = await fetch(`${baseUrl}/chat/completions`, { + method: "POST", + headers: fetchHeaders, + body: JSON.stringify(openaiRequest), + }); + + console.log('[Router] 📥 Response status:', openaiResponse.status); + + if (!openaiResponse.ok) { + const errorText = await openaiResponse.text(); + console.log('[Router] ❌ Error:', errorText); + + // Try to parse as JSON, otherwise use raw text + let errorMessage = errorText; + try { + const parsed = JSON.parse(errorText); + errorMessage = parsed.error?.message || parsed.message || errorText; + } catch {} + + res.writeHead(openaiResponse.status, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + type: 'error', + error: { + type: openaiResponse.status === 401 ? 'authentication_error' : 'api_error', + message: `[Router] ${errorMessage}` + } + })); + return; + } + + if (openaiRequest.stream) { + console.log('[Router] 🌊 Starting stream response'); + const anthropicStream = streamOpenAIToAnthropic( + openaiResponse.body as ReadableStream, + openaiRequest.model + ); + + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + }); + + const reader = anthropicStream.getReader(); + + const pump = async () => { + try { + while (true) { + const { done, value } = await reader.read(); + if (done) { + res.end(); + break; + } + res.write(value); + } + } catch (error) { + console.error('[Router] Stream error:', error); + res.end(); + } + }; + + pump(); + } else { + const openaiData = await openaiResponse.json(); + const anthropicResponse = formatOpenAIToAnthropic(openaiData, openaiRequest.model); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(anthropicResponse)); + } + return; + } + + // 404 Not Found + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('Not Found'); + } catch (error) { + console.error('[Router] Error processing request:', error); + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + type: 'error', + error: { + type: 'api_error', + message: `[Router] Internal error: ${(error as Error).message}` + } + })); + } + }); +} + +export function startRouter(port: number = DEFAULT_PORT): Promise { + return new Promise((resolve, reject) => { + if (server) { + console.log('[Router] Already running on port', currentPort); + resolve(currentPort); + return; + } + + server = createServer(); + + server.on('error', (err: NodeJS.ErrnoException) => { + if (err.code === 'EADDRINUSE') { + console.log(`[Router] Port ${port} in use, trying ${port + 1}`); + server = null; + startRouter(port + 1).then(resolve).catch(reject); + } else { + reject(err); + } + }); + + server.listen(port, () => { + currentPort = port; + console.log(`[Router] 🚀 Running on http://localhost:${port}`); + resolve(port); + }); + }); +} + +export function stopRouter(): Promise { + return new Promise((resolve) => { + if (!server) { + resolve(); + return; + } + + server.close(() => { + console.log('[Router] Stopped'); + server = null; + resolve(); + }); + }); +} + +export function isRouterRunning(): boolean { + return server !== null; +} + +export function getRouterPort(): number { + return currentPort; +} diff --git a/src/router/streamResponse.ts b/src/router/streamResponse.ts new file mode 100644 index 0000000..afe1bc3 --- /dev/null +++ b/src/router/streamResponse.ts @@ -0,0 +1,219 @@ +export function streamOpenAIToAnthropic(openaiStream: ReadableStream, model: string): ReadableStream { + const messageId = "msg_" + Date.now(); + + const enqueueSSE = (controller: ReadableStreamDefaultController, eventType: string, data: any) => { + const sseMessage = `event: ${eventType}\ndata: ${JSON.stringify(data)}\n\n`; + controller.enqueue(new TextEncoder().encode(sseMessage)); + }; + + return new ReadableStream({ + async start(controller) { + // Send message_start event + const messageStart = { + type: "message_start", + message: { + id: messageId, + type: "message", + role: "assistant", + content: [], + model, + stop_reason: null, + stop_sequence: null, + usage: { input_tokens: 0, output_tokens: 0 }, + }, + }; + enqueueSSE(controller, "message_start", messageStart); + + let contentBlockIndex = 0; + let hasAnyBlock = false; + let hasStartedTextBlock = false; + let isToolUse = false; + let currentToolCallId: string | null = null; + let toolCallJsonMap = new Map(); + let streamUsage: { input_tokens: number; output_tokens: number } | null = null; + + const reader = openaiStream.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) { + // Process any remaining data in buffer + if (buffer.trim()) { + const lines = buffer.split('\n'); + for (const line of lines) { + if (line.trim() && line.startsWith('data: ')) { + const data = line.slice(6).trim(); + if (data === '[DONE]') continue; + + try { + const parsed = JSON.parse(data); + processStreamChunk(parsed); + } catch (e) { + // Parse error + } + } + } + } + break; + } + + // Decode chunk and add to buffer + const chunk = decoder.decode(value, { stream: true }); + buffer += chunk; + + // Process complete lines from buffer + const lines = buffer.split('\n'); + // Keep the last potentially incomplete line in buffer + buffer = lines.pop() || ''; + + // Process complete lines in order + for (const line of lines) { + if (line.trim() && line.startsWith('data: ')) { + const data = line.slice(6).trim(); + if (data === '[DONE]') continue; + + try { + const parsed = JSON.parse(data); + processStreamChunk(parsed); + } catch (e) { + // Parse error + continue; + } + } + } + } + } finally { + reader.releaseLock(); + } + + function processStreamChunk(parsed: any) { + // Capture usage from the chunk if available + if (parsed.usage) { + streamUsage = { + input_tokens: parsed.usage.prompt_tokens || 0, + output_tokens: parsed.usage.completion_tokens || 0, + }; + } + + const delta = parsed.choices?.[0]?.delta; + if (delta) { + processStreamDelta(delta); + } + } + + function closeCurrentBlock() { + if (hasAnyBlock) { + enqueueSSE(controller, "content_block_stop", { + type: "content_block_stop", + index: contentBlockIndex, + }); + contentBlockIndex++; + } + hasAnyBlock = true; + } + + function processStreamDelta(delta: any) { + + // Handle tool calls + if (delta.tool_calls?.length > 0) { + for (const toolCall of delta.tool_calls) { + const toolCallId = toolCall.id; + + if (toolCallId && toolCallId !== currentToolCallId) { + closeCurrentBlock(); + + isToolUse = true; + hasStartedTextBlock = false; + currentToolCallId = toolCallId; + toolCallJsonMap.set(toolCallId, ""); + + const toolBlock = { + type: "tool_use", + id: toolCallId, + name: toolCall.function?.name, + input: {}, + }; + + enqueueSSE(controller, "content_block_start", { + type: "content_block_start", + index: contentBlockIndex, + content_block: toolBlock, + }); + } + + if (toolCall.function?.arguments && currentToolCallId) { + const currentJson = toolCallJsonMap.get(currentToolCallId) || ""; + toolCallJsonMap.set(currentToolCallId, currentJson + toolCall.function.arguments); + + enqueueSSE(controller, "content_block_delta", { + type: "content_block_delta", + index: contentBlockIndex, + delta: { + type: "input_json_delta", + partial_json: toolCall.function.arguments, + }, + }); + } + } + } else if (delta.content) { + if (isToolUse) { + closeCurrentBlock(); + isToolUse = false; + currentToolCallId = null; + } + + if (!hasStartedTextBlock) { + if (!hasAnyBlock) { + hasAnyBlock = true; + } + enqueueSSE(controller, "content_block_start", { + type: "content_block_start", + index: contentBlockIndex, + content_block: { + type: "text", + text: "", + }, + }); + hasStartedTextBlock = true; + } + + enqueueSSE(controller, "content_block_delta", { + type: "content_block_delta", + index: contentBlockIndex, + delta: { + type: "text_delta", + text: delta.content, + }, + }); + } + } + + // Close last content block + if (hasAnyBlock) { + enqueueSSE(controller, "content_block_stop", { + type: "content_block_stop", + index: contentBlockIndex, + }); + } + + // Send message_delta and message_stop + enqueueSSE(controller, "message_delta", { + type: "message_delta", + delta: { + stop_reason: isToolUse ? "tool_use" : "end_turn", + stop_sequence: null, + }, + usage: streamUsage || { input_tokens: 0, output_tokens: 0 }, + }); + + enqueueSSE(controller, "message_stop", { + type: "message_stop", + }); + + controller.close(); + }, + }); +} diff --git a/src/script.ts b/src/script.ts index 7029415..80202bf 100644 --- a/src/script.ts +++ b/src/script.ts @@ -1,4 +1,66 @@ -const getScript = (isTelemetryEnabled: boolean) => `` export default getScript; \ No newline at end of file diff --git a/src/skills-script.ts b/src/skills-script.ts new file mode 100644 index 0000000..35e110c --- /dev/null +++ b/src/skills-script.ts @@ -0,0 +1,286 @@ +const getSkillsScript = () => ` + // ─── Skills ─── + var skillsSearchTimeout = null; + var skillsCache = null; + var topSkills = (window.__topSkills || []); + + function showSkillsModal() { + document.getElementById('skillsModal').style.display = 'flex'; + loadInstalledSkills(); + if (topSkills.length > 0) { + renderFeaturedSkills(topSkills); + } + } + + function renderFeaturedSkills(skills) { + var grid = document.getElementById('skillsGrid'); + if (!grid) return; + var html = ''; + skills.forEach(function(skill) { + var name = skill.name || 'Unknown'; + var installs = skill.installs || 0; + var source = skill.source || ''; + var installsHtml = installs > 0 ? '' + (installs >= 1000 ? (Math.round(installs / 100) / 10) + 'k' : installs) + ' installs' : ''; + var safeId = escapeHtml(skill.id || name).replace(/'/g, '''); + + var rawUrl = skill.rawUrl || ''; + var installsText = installs >= 1000 ? (Math.round(installs / 100) / 10) + 'k installs' : (installs > 0 ? installs + ' installs' : ''); + html += '
' + + '
' + + '
' + escapeHtml(name.charAt(0).toUpperCase()) + '
' + + '
' + + '
' + escapeHtml(name) + '
' + + '
' + installsHtml + '
' + + '
' + + '
' + + '
' + escapeHtml(source) + '
' + + '
'; + }); + grid.innerHTML = html; + } + + function hideSkillsModal() { + document.getElementById('skillsModal').style.display = 'none'; + } + + function loadInstalledSkills() { + vscode.postMessage({ type: 'loadSkills' }); + } + + function displaySkills(skills) { + var skillsList = document.getElementById('skillsList'); + skillsList.innerHTML = ''; + + if (!skills || skills.length === 0) { + skillsList.innerHTML = '
' + + '
' + + '
No skills installed
' + + '' + + '
'; + return; + } + + skills.forEach(function(skill, idx) { + var item = document.createElement('div'); + item.className = 'mcp-server-item'; + item.style.flexDirection = 'column'; + item.style.alignItems = 'stretch'; + var desc = skill.description || 'No description'; + var content = skill.content || ''; + var detailId = 'skill-detail-' + idx; + item.innerHTML = '
' + + '
' + + '
' + escapeHtml(skill.name) + ' ' + escapeHtml(skill.scope) + '
' + + '
' + escapeHtml(desc) + '
' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + ''; + skillsList.appendChild(item); + }); + + // Add create button at bottom + var addDiv = document.createElement('div'); + addDiv.className = 'mcp-add-server'; + addDiv.innerHTML = ''; + skillsList.appendChild(addDiv); + } + + function showSkillAddForm() { + document.getElementById('skillsList').style.display = 'none'; + document.getElementById('skillsMarketplace').style.display = 'none'; + document.getElementById('skillAddForm').style.display = 'block'; + // Clear form + document.getElementById('skillName').value = ''; + document.getElementById('skillDescription').value = ''; + document.getElementById('skillContent').value = ''; + document.getElementById('skillName').disabled = false; + } + + function hideSkillAddForm() { + document.getElementById('skillsList').style.display = ''; + document.getElementById('skillsMarketplace').style.display = 'block'; + document.getElementById('skillAddForm').style.display = 'none'; + loadInstalledSkills(); + } + + function saveSkill() { + var name = document.getElementById('skillName').value.trim(); + var description = document.getElementById('skillDescription').value.trim(); + var scope = document.getElementById('skillScope').value; + var content = document.getElementById('skillContent').value; + + if (!name) return; + + // Build SKILL.md content + var skillMd = '---\\n'; + skillMd += 'name: ' + name + '\\n'; + if (description) { + skillMd += 'description: ' + description + '\\n'; + } + skillMd += '---\\n\\n'; + skillMd += content || ''; + + vscode.postMessage({ + type: 'saveSkill', + name: name, + scope: scope, + content: skillMd + }); + + hideSkillAddForm(); + } + + function deleteSkill(name, scope) { + vscode.postMessage({ + type: 'deleteSkill', + name: name, + scope: scope + }); + } + + function searchSkills(query) { + clearTimeout(skillsSearchTimeout); + skillsSearchTimeout = setTimeout(function() { + if (!query || query.length < 2) { + renderFeaturedSkills(topSkills); + return; + } + // Filter featured locally first + var q = query.toLowerCase(); + var local = topSkills.filter(function(s) { + return (s.name && s.name.toLowerCase().indexOf(q) >= 0) || + (s.source && s.source.toLowerCase().indexOf(q) >= 0); + }); + if (local.length > 0) { + renderFeaturedSkills(local); + } else { + var grid = document.getElementById('skillsGrid'); + grid.innerHTML = '
Searching...
'; + } + // Also search API + vscode.postMessage({ type: 'searchSkills', query: query }); + }, 300); + } + + function handleSkillsSearchResponse(data) { + var grid = document.getElementById('skillsGrid'); + if (!grid) return; + + var skills = data.skills || []; + if (skills.length === 0) { + grid.innerHTML = '
No skills found.
'; + return; + } + + var html = ''; + skills.forEach(function(skill) { + var name = skill.name || skill.skillId || 'Unknown'; + var installs = skill.installs || 0; + var source = skill.source || ''; + var safeId = escapeHtml(skill.id || name).replace(/'/g, '''); + + var installsHtml = installs > 0 ? '' + (installs >= 1000 ? (Math.round(installs / 100) / 10) + 'k' : installs) + ' installs' : ''; + + var rawUrl = skill.rawUrl || ''; + var installsText = installs >= 1000 ? (Math.round(installs / 100) / 10) + 'k installs' : (installs > 0 ? installs + ' installs' : ''); + html += '
' + + '
' + + '
' + escapeHtml(name.charAt(0).toUpperCase()) + '
' + + '
' + + '
' + escapeHtml(name) + '
' + + '
' + installsHtml + '
' + + '
' + + '
' + + '
' + escapeHtml(source) + '
' + + '
'; + }); + grid.innerHTML = html; + } + + var skillsDisplayedList = null; + + function installSkillFromMarketplace(el) { + var source = el.dataset.skillSource; + var name = el.dataset.skillName; + var installs = el.dataset.skillInstalls || ''; + + if (!source || !name) return; + + var repoUrl = 'https://github.com/' + source.replace(/^github\\//, ''); + var installsHtml = installs ? '' + installs + '' : ''; + + var grid = document.getElementById('skillsGrid'); + // Save current grid content to restore on back + skillsDisplayedList = grid.innerHTML; + + grid.innerHTML = '
' + + '' + + '
' + + '
' + escapeHtml(name.charAt(0).toUpperCase()) + '
' + + '
' + + '
' + escapeHtml(name) + '
' + + '
' + + installsHtml + + 'GitHub' + + '
' + + '
' + + '
' + + '
' + escapeHtml('Source: ' + source) + '
' + + '
' + + '
Install to
' + + '
' + + '' + + '
' + + '
' + + '
' + + '' + + '
Opens a terminal running npx skills add via skills.sh
' + + '
' + + '
'; + } + + function backToSkillsList() { + var grid = document.getElementById('skillsGrid'); + if (skillsDisplayedList) { + grid.innerHTML = skillsDisplayedList; + } else { + renderFeaturedSkills(topSkills); + } + } + + function toggleSkillDetail(id) { + var el = document.getElementById(id); + if (!el) return; + el.style.display = el.style.display === 'none' ? 'block' : 'none'; + } + + function confirmSkillInstall(btn) { + var source = btn.dataset.source; + var name = btn.dataset.name; + var scope = document.getElementById('skillInstallScope').value; + + var repoUrl = 'https://github.com/' + source.replace(/^github\\//, ''); + var command = 'npx -y skills add ' + repoUrl + ' --skill ' + name + ' --agent claude-code -y'; + if (scope === 'global') { + command += ' --global'; + } + + vscode.postMessage({ + type: 'runTerminalCommand', + command: command + }); + + hideSkillsModal(); + } +`; + +export default getSkillsScript; diff --git a/src/skills-ui.ts b/src/skills-ui.ts new file mode 100644 index 0000000..be530d0 --- /dev/null +++ b/src/skills-ui.ts @@ -0,0 +1,51 @@ +const getSkillsHtml = () => ` + + +`; + +export default getSkillsHtml; diff --git a/src/top-mcp-servers.json b/src/top-mcp-servers.json new file mode 100644 index 0000000..2e8928d --- /dev/null +++ b/src/top-mcp-servers.json @@ -0,0 +1,479 @@ +[ + { + "id": "sequential-thinking", + "name": "Sequential Thinking", + "description": "Step-by-step reasoning capabilities", + "icon": "", + "stars": 0, + "url": "", + "installType": "npm", + "installConfig": { + "type": "stdio", + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-sequential-thinking" + ] + }, + "featured": true + }, + { + "id": "memory", + "name": "Memory", + "description": "Knowledge graph storage", + "icon": "", + "stars": 0, + "url": "", + "installType": "npm", + "installConfig": { + "type": "stdio", + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-memory" + ] + }, + "featured": true + }, + { + "id": "puppeteer", + "name": "Puppeteer", + "description": "Browser automation", + "icon": "", + "stars": 0, + "url": "", + "installType": "npm", + "installConfig": { + "type": "stdio", + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-puppeteer" + ] + }, + "featured": true + }, + { + "id": "fetch", + "name": "Fetch", + "description": "HTTP requests & web scraping", + "icon": "", + "stars": 0, + "url": "", + "installType": "npm", + "installConfig": { + "type": "stdio", + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-fetch" + ] + }, + "featured": true + }, + { + "id": "filesystem", + "name": "Filesystem", + "description": "File operations & management", + "icon": "", + "stars": 0, + "url": "", + "installType": "npm", + "installConfig": { + "type": "stdio", + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-filesystem" + ] + }, + "featured": true + }, + { + "id": "io.github.upstash/context7", + "name": "Context7", + "description": "Up-to-date code docs for any prompt", + "icon": "", + "stars": 0, + "url": "https://github.com/upstash/context7", + "installType": "npm", + "installConfig": { + "type": "stdio", + "command": "npx", + "args": [ + "-y", + "@upstash/context7-mcp" + ], + "env": { + "CONTEXT7_API_KEY": "" + } + }, + "featured": true + }, + { + "id": "com.airtable/mcp", + "name": "Airtable", + "description": "Official Airtable MCP server for managing bases, tables, and records.", + "icon": "", + "stars": 0, + "url": "", + "installType": "http", + "installConfig": { + "type": "http", + "url": "https://mcp.airtable.com/mcp", + "headers": { + "Authorization": "" + } + } + }, + { + "id": "com.apify/mcp", + "name": "Apify", + "description": "Extract data from social media, search engines, maps, e-commerce sites, and any website using thousands of ready-made tools from Apify Store.", + "icon": "", + "stars": 0, + "url": "https://github.com/apify/apify-mcp-server", + "installType": "http", + "installConfig": { + "type": "http", + "url": "https://mcp.apify.com" + } + }, + { + "id": "io.github.browserbase/mcp-server-browserbase", + "name": "Browserbase", + "description": "MCP server for AI web browser automation using Browserbase and Stagehand", + "icon": "", + "stars": 0, + "url": "https://github.com/browserbase/mcp-server-browserbase", + "installType": "npm", + "installConfig": { + "type": "stdio", + "command": "npx", + "args": [ + "-y", + "@browserbasehq/mcp-server-browserbase" + ], + "env": { + "BROWSERBASE_API_KEY": "", + "BROWSERBASE_PROJECT_ID": "", + "GEMINI_API_KEY": "" + } + } + }, + { + "id": "io.github.clerk/mcp-server", + "name": "Clerk", + "description": "Access Clerk authentication docs, SDK snippets, and quickstart guides", + "icon": "", + "stars": 0, + "url": "https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server", + "installType": "http", + "installConfig": { + "type": "http", + "url": "https://mcp.clerk.com/mcp" + } + }, + { + "id": "com.cloudflare.mcp/mcp", + "name": "Cloudflare", + "description": "Cloudflare MCP servers", + "icon": "", + "stars": 0, + "url": "https://github.com/cloudflare/mcp-server-cloudflare", + "installType": "http", + "installConfig": { + "type": "http", + "url": "https://docs.mcp.cloudflare.com/mcp" + } + }, + { + "id": "ai.exa/mcp", + "name": "Exa", + "description": "Web search and code search MCP server powered by Exa", + "icon": "", + "stars": 0, + "url": "https://github.com/exa-labs/exa-mcp-server", + "installType": "http", + "installConfig": { + "type": "http", + "url": "https://mcp.exa.ai/mcp" + } + }, + { + "id": "com.figma/mcp", + "name": "Figma", + "description": "Official Figma MCP server for accessing design files, components, and design context", + "icon": "", + "stars": 0, + "url": "https://help.figma.com/hc/en-us/articles/35281350665623-Figma-MCP-collection-How-to-set-up-the-Figma-remote-MCP-server-preferred", + "installType": "http", + "installConfig": { + "type": "http", + "url": "https://mcp.figma.com/mcp" + } + }, + { + "id": "dev.firecrawl/mcp", + "name": "Firecrawl", + "description": "Web scraping, crawling, search, and structured data extraction powered by Firecrawl.", + "icon": "", + "stars": 0, + "url": "https://github.com/firecrawl/firecrawl-mcp-server", + "installType": "http", + "installConfig": { + "type": "http", + "url": "https://mcp.firecrawl.dev/v2/mcp", + "headers": { + "Authorization": "" + } + } + }, + { + "id": "io.github.github/github-mcp-server", + "name": "GitHub", + "description": "Official GitHub MCP server for repos, issues, PRs, and workflows", + "icon": "", + "stars": 0, + "url": "https://github.com/github/github-mcp-server", + "installType": "http", + "installConfig": { + "type": "http", + "url": "https://api.githubcopilot.com/mcp/" + } + }, + { + "id": "app.linear/linear", + "name": "Linear", + "description": "MCP server for Linear project management and issue tracking", + "icon": "", + "stars": 0, + "url": "", + "installType": "sse", + "installConfig": { + "type": "sse", + "url": "https://mcp.linear.app/sse" + } + }, + { + "id": "com.mux/mcp", + "name": "Mux", + "description": "The official MCP Server for the Mux API", + "icon": "", + "stars": 0, + "url": "https://github.com/muxinc/mux-node-sdk", + "installType": "http", + "installConfig": { + "type": "http", + "url": "https://mcp.mux.com", + "headers": { + "Authorization": "" + } + } + }, + { + "id": "com.neon/mcp", + "name": "Neon", + "description": "Official Neon MCP server for managing Neon projects and Postgres databases.", + "icon": "", + "stars": 0, + "url": "https://github.com/neondatabase/mcp-server-neon", + "installType": "http", + "installConfig": { + "type": "http", + "url": "https://mcp.neon.tech/mcp", + "headers": { + "Authorization": "", + "x-read-only": "" + } + } + }, + { + "id": "com.netlify/mcp", + "name": "Netlify", + "description": "Netlify's official MCP server for builds, deploys, and project management.", + "icon": "", + "stars": 0, + "url": "https://github.com/netlify/netlify-mcp", + "installType": "npm", + "installConfig": { + "type": "stdio", + "command": "npx", + "args": [ + "-y", + "@netlify/mcp" + ], + "env": { + "NETLIFY_PERSONAL_ACCESS_TOKEN": "" + } + } + }, + { + "id": "io.github.vercel/next-devtools-mcp", + "name": "Next.js Devtools", + "description": "Next.js development tools MCP server with stdio transport", + "icon": "", + "stars": 0, + "url": "https://github.com/vercel/next-devtools-mcp", + "installType": "npm", + "installConfig": { + "type": "stdio", + "command": "npx", + "args": [ + "-y", + "next-devtools-mcp" + ] + } + }, + { + "id": "com.notion/mcp", + "name": "Notion", + "description": "Official Notion MCP server", + "icon": "", + "stars": 0, + "url": "", + "installType": "http", + "installConfig": { + "type": "http", + "url": "https://mcp.notion.com/mcp" + } + }, + { + "id": "io.github.railwayapp/mcp-server", + "name": "Railway", + "description": "Official Railway MCP server", + "icon": "", + "stars": 0, + "url": "https://github.com/railwayapp/railway-mcp-server", + "installType": "npm", + "installConfig": { + "type": "stdio", + "command": "npx", + "args": [ + "-y", + "@railway/mcp-server" + ] + } + }, + { + "id": "com.render/mcp", + "name": "Render", + "description": "Official Render MCP server for managing Render resources.", + "icon": "", + "stars": 0, + "url": "https://github.com/render-oss/render-mcp-server", + "installType": "http", + "installConfig": { + "type": "http", + "url": "https://mcp.render.com/mcp", + "headers": { + "Authorization": "" + } + } + }, + { + "id": "com.resend/mcp", + "name": "Resend", + "description": "Official Resend MCP server for email operations and audience management.", + "icon": "", + "stars": 0, + "url": "https://github.com/resend/mcp-send-email", + "installType": "npm", + "installConfig": { + "type": "stdio", + "command": "npx", + "args": [ + "-y", + "resend-mcp" + ], + "env": { + "RESEND_API_KEY": "" + } + } + }, + { + "id": "io.sanity.www/mcp", + "name": "Sanity", + "description": "Direct access to your Sanity projects (content, datasets, releases, schemas) and agent rules", + "icon": "", + "stars": 0, + "url": "https://github.com/sanity-io/agent-toolkit", + "installType": "http", + "installConfig": { + "type": "http", + "url": "https://mcp.sanity.io" + } + }, + { + "id": "io.github.getsentry/sentry-mcp", + "name": "Sentry", + "description": "MCP server for Sentry issue tracking and debugging", + "icon": "", + "stars": 0, + "url": "https://github.com/getsentry/sentry-mcp", + "installType": "npm", + "installConfig": { + "type": "stdio", + "command": "npx", + "args": [ + "-y", + "@sentry/mcp-server" + ], + "env": { + "SENTRY_ACCESS_TOKEN": "" + } + } + }, + { + "id": "com.slack/mcp", + "name": "Slack", + "description": "Official Slack MCP server for search, messaging, canvases, and users.", + "icon": "", + "stars": 0, + "url": "https://github.com/slackapi/slack-mcp-plugin", + "installType": "http", + "installConfig": { + "type": "http", + "url": "https://mcp.slack.com/mcp" + } + }, + { + "id": "com.stripe/mcp", + "name": "Stripe", + "description": "Official Stripe MCP server for Stripe API tools.", + "icon": "", + "stars": 0, + "url": "https://github.com/stripe/agent-toolkit", + "installType": "http", + "installConfig": { + "type": "http", + "url": "https://mcp.stripe.com" + } + }, + { + "id": "com.supabase/mcp", + "name": "Supabase", + "description": "MCP server for interacting with the Supabase platform", + "icon": "", + "stars": 0, + "url": "https://github.com/supabase-community/supabase-mcp", + "installType": "http", + "installConfig": { + "type": "http", + "url": "https://mcp.supabase.com/mcp" + } + }, + { + "id": "com.vercel/vercel-mcp", + "name": "Vercel", + "description": "An MCP server for Vercel", + "icon": "", + "stars": 0, + "url": "https://github.com/vercel/vercel-mcp-overview", + "installType": "http", + "installConfig": { + "type": "http", + "url": "https://mcp.vercel.com" + } + } +] \ No newline at end of file diff --git a/src/top-plugins.json b/src/top-plugins.json new file mode 100644 index 0000000..92ff4a0 --- /dev/null +++ b/src/top-plugins.json @@ -0,0 +1,240 @@ +[ + { + "name": "agent-sdk-dev", + "description": "Claude Agent SDK Development Plugin", + "verified": true, + "type": "official", + "installId": "agent-sdk-dev@claude-plugins-official" + }, + { + "name": "claude-code-setup", + "description": "Analyze codebases and recommend tailored Claude Code automations such as hooks, skills, MCP servers, and subagents.", + "verified": true, + "type": "official", + "installId": "claude-code-setup@claude-plugins-official" + }, + { + "name": "claude-md-management", + "description": "Tools to maintain and improve CLAUDE.md files - audit quality, capture session learnings, and keep project memory current.", + "verified": true, + "type": "official", + "installId": "claude-md-management@claude-plugins-official" + }, + { + "name": "code-review", + "description": "Automated code review for pull requests using multiple specialized agents with confidence-based scoring", + "verified": true, + "type": "official", + "installId": "code-review@claude-plugins-official" + }, + { + "name": "code-simplifier", + "description": "Agent that simplifies and refines code for clarity, consistency, and maintainability while preserving functionality", + "verified": true, + "type": "official", + "installId": "code-simplifier@claude-plugins-official" + }, + { + "name": "commit-commands", + "description": "Streamline your git workflow with simple commands for committing, pushing, and creating pull requests", + "verified": true, + "type": "official", + "installId": "commit-commands@claude-plugins-official" + }, + { + "name": "explanatory-output-style", + "description": "Adds educational insights about implementation choices and codebase patterns (mimics the deprecated Explanatory output style)", + "verified": true, + "type": "official", + "installId": "explanatory-output-style@claude-plugins-official" + }, + { + "name": "feature-dev", + "description": "Comprehensive feature development workflow with specialized agents for codebase exploration, architecture design, and quality review", + "verified": true, + "type": "official", + "installId": "feature-dev@claude-plugins-official" + }, + { + "name": "frontend-design", + "description": "Frontend design skill for UI/UX implementation", + "verified": true, + "type": "official", + "installId": "frontend-design@claude-plugins-official" + }, + { + "name": "hookify", + "description": "Easily create hooks to prevent unwanted behaviors by analyzing conversation patterns", + "verified": true, + "type": "official", + "installId": "hookify@claude-plugins-official" + }, + { + "name": "learning-output-style", + "description": "Interactive learning mode that requests meaningful code contributions at decision points (mimics the unshipped Learning output style)", + "verified": true, + "type": "official", + "installId": "learning-output-style@claude-plugins-official" + }, + { + "name": "math-olympiad", + "description": "Solve competition math (IMO, Putnam, USAMO) with adversarial verification that catches what self-verification misses. Fresh-context verifiers attack proofs with specific failure patterns. Calibrated abstention over bluffing.", + "verified": true, + "type": "official", + "installId": "math-olympiad@claude-plugins-official" + }, + { + "name": "mcp-server-dev", + "description": "Skills for designing and building MCP servers that work seamlessly with Claude \u2014 guides you through deployment models (remote HTTP, MCPB, local), tool design patterns, auth, and interactive MCP apps.", + "verified": true, + "type": "official", + "installId": "mcp-server-dev@claude-plugins-official" + }, + { + "name": "playground", + "description": "Creates interactive HTML playgrounds \u2014 self-contained single-file explorers with visual controls, live preview, and prompt output with copy button", + "verified": true, + "type": "official", + "installId": "playground@claude-plugins-official" + }, + { + "name": "plugin-dev", + "description": "Plugin development toolkit with skills for creating agents, commands, hooks, MCP integrations, and comprehensive plugin structure guidance", + "verified": true, + "type": "official", + "installId": "plugin-dev@claude-plugins-official" + }, + { + "name": "pr-review-toolkit", + "description": "Comprehensive PR review agents specializing in comments, tests, error handling, type design, code quality, and code simplification", + "verified": true, + "type": "official", + "installId": "pr-review-toolkit@claude-plugins-official" + }, + { + "name": "ralph-loop", + "description": "Continuous self-referential AI loops for interactive iterative development, implementing the Ralph Wiggum technique. Run Claude in a while-true loop with the same prompt until task completion.", + "verified": true, + "type": "official", + "installId": "ralph-loop@claude-plugins-official" + }, + { + "name": "security-guidance", + "description": "Security reminder hook that warns about potential security issues when editing files, including command injection, XSS, and unsafe code patterns", + "verified": true, + "type": "official", + "installId": "security-guidance@claude-plugins-official" + }, + { + "name": "skill-creator", + "description": "Create new skills, improve existing skills, and measure skill performance. Use when users want to create a skill from scratch, update or optimize an existing skill, run evals to test a skill, or benchmark skill performance with variance analysis.", + "verified": true, + "type": "official", + "installId": "skill-creator@claude-plugins-official" + }, + { + "name": "asana", + "description": "Asana project management integration. Create and manage tasks, search projects, update assignments, track progress, and integrate your development workflow with Asana's work management platform.", + "verified": false, + "type": "external", + "installId": "asana@claude-plugins-official" + }, + { + "name": "context7", + "description": "Upstash Context7 MCP server for up-to-date documentation lookup. Pull version-specific documentation and code examples directly from source repositories into your LLM context.", + "verified": false, + "type": "external", + "installId": "context7@claude-plugins-official" + }, + { + "name": "discord", + "description": "Discord channel for Claude Code \u2014 messaging bridge with built-in access control. Manage pairing, allowlists, and policy via /discord:access.", + "verified": false, + "type": "external", + "installId": "discord@claude-plugins-official" + }, + { + "name": "fakechat", + "description": "Localhost iMessage-style web chat for Claude Code \u2014 test surface with file upload and edits. No tokens, no access control.", + "verified": false, + "type": "external", + "installId": "fakechat@claude-plugins-official" + }, + { + "name": "firebase", + "description": "Google Firebase MCP integration. Manage Firestore databases, authentication, cloud functions, hosting, and storage. Build and manage your Firebase backend directly from your development workflow.", + "verified": false, + "type": "external", + "installId": "firebase@claude-plugins-official" + }, + { + "name": "github", + "description": "Official GitHub MCP server for repository management. Create issues, manage pull requests, review code, search repositories, and interact with GitHub's full API directly from Claude Code.", + "verified": false, + "type": "external", + "installId": "github@claude-plugins-official" + }, + { + "name": "gitlab", + "description": "GitLab DevOps platform integration. Manage repositories, merge requests, CI/CD pipelines, issues, and wikis. Full access to GitLab's comprehensive DevOps lifecycle tools.", + "verified": false, + "type": "external", + "installId": "gitlab@claude-plugins-official" + }, + { + "name": "greptile", + "description": "AI code review agent for GitHub and GitLab. View and resolve Greptile's PR review comments directly from Claude Code.", + "verified": false, + "type": "external", + "installId": "greptile@claude-plugins-official" + }, + { + "name": "laravel-boost", + "description": "Laravel development toolkit MCP server. Provides intelligent assistance for Laravel applications including Artisan commands, Eloquent queries, routing, migrations, and framework-specific code generation.", + "verified": false, + "type": "external", + "installId": "laravel-boost@claude-plugins-official" + }, + { + "name": "linear", + "description": "Linear issue tracking integration. Create issues, manage projects, update statuses, search across workspaces, and streamline your software development workflow with Linear's modern issue tracker.", + "verified": false, + "type": "external", + "installId": "linear@claude-plugins-official" + }, + { + "name": "playwright", + "description": "Browser automation and end-to-end testing MCP server by Microsoft. Enables Claude to interact with web pages, take screenshots, fill forms, click elements, and perform automated browser testing workflows.", + "verified": false, + "type": "external", + "installId": "playwright@claude-plugins-official" + }, + { + "name": "serena", + "description": "Semantic code analysis MCP server providing intelligent code understanding, refactoring suggestions, and codebase navigation through language server protocol integration.", + "verified": false, + "type": "external", + "installId": "serena@claude-plugins-official" + }, + { + "name": "slack", + "description": "Slack workspace integration. Search messages, access channels, read threads, and stay connected with your team's communications while coding. Find relevant discussions and context quickly.", + "verified": false, + "type": "external", + "installId": "slack@claude-plugins-official" + }, + { + "name": "supabase", + "description": "Supabase MCP integration for database operations, authentication, storage, and real-time subscriptions. Manage your Supabase projects, run SQL queries, and interact with your backend directly.", + "verified": false, + "type": "external", + "installId": "supabase@claude-plugins-official" + }, + { + "name": "telegram", + "description": "Telegram channel for Claude Code \u2014 messaging bridge with built-in access control. Manage pairing, allowlists, and policy via /telegram:access.", + "verified": false, + "type": "external", + "installId": "telegram@claude-plugins-official" + } +] \ No newline at end of file diff --git a/src/top-skills.json b/src/top-skills.json new file mode 100644 index 0000000..37bd948 --- /dev/null +++ b/src/top-skills.json @@ -0,0 +1,289 @@ +[ + { + "id": "vercel-labs/skills/find-skills", + "name": "find-skills", + "installs": 654260, + "source": "vercel-labs/skills", + "rawUrl": "https://raw.githubusercontent.com/vercel-labs/skills/main/skills/find-skills/SKILL.md" + }, + { + "id": "vercel-labs/agent-skills/vercel-react-best-practices", + "name": "vercel-react-best-practices", + "installs": 234225, + "source": "vercel-labs/agent-skills", + "rawUrl": "https://raw.githubusercontent.com/vercel-labs/agent-skills/main/skills/react-best-practices/SKILL.md" + }, + { + "id": "vercel-labs/agent-skills/web-design-guidelines", + "name": "web-design-guidelines", + "installs": 187122, + "source": "vercel-labs/agent-skills", + "rawUrl": "https://raw.githubusercontent.com/vercel-labs/agent-skills/main/skills/web-design-guidelines/SKILL.md" + }, + { + "id": "anthropics/skills/frontend-design", + "name": "frontend-design", + "installs": 184608, + "source": "anthropics/skills", + "rawUrl": "https://raw.githubusercontent.com/anthropics/skills/main/skills/frontend-design/SKILL.md" + }, + { + "id": "vercel-labs/agent-browser/agent-browser", + "name": "agent-browser", + "installs": 119125, + "source": "vercel-labs/agent-browser", + "rawUrl": "https://raw.githubusercontent.com/vercel-labs/agent-browser/main/skills/agent-browser/SKILL.md" + }, + { + "id": "anthropics/skills/skill-creator", + "name": "skill-creator", + "installs": 97605, + "source": "anthropics/skills", + "rawUrl": "https://raw.githubusercontent.com/anthropics/skills/main/skills/skill-creator/SKILL.md" + }, + { + "id": "nextlevelbuilder/ui-ux-pro-max-skill/ui-ux-pro-max", + "name": "ui-ux-pro-max", + "installs": 74564, + "source": "nextlevelbuilder/ui-ux-pro-max-skill", + "rawUrl": "https://raw.githubusercontent.com/nextlevelbuilder/ui-ux-pro-max-skill/main/.claude/skills/ui-ux-pro-max/SKILL.md" + }, + { + "id": "microsoft/azure-skills/microsoft-foundry", + "name": "microsoft-foundry", + "installs": 74376, + "source": "microsoft/azure-skills", + "rawUrl": "https://raw.githubusercontent.com/microsoft/azure-skills/main/skills/microsoft-foundry/SKILL.md" + }, + { + "id": "obra/superpowers/brainstorming", + "name": "brainstorming", + "installs": 66697, + "source": "obra/superpowers", + "rawUrl": "https://raw.githubusercontent.com/obra/superpowers/main/skills/brainstorming/SKILL.md" + }, + { + "id": "browser-use/browser-use/browser-use", + "name": "browser-use", + "installs": 52773, + "source": "browser-use/browser-use", + "rawUrl": "https://raw.githubusercontent.com/browser-use/browser-use/main/skills/browser-use/SKILL.md" + }, + { + "id": "coreyhaines31/marketingskills/seo-audit", + "name": "seo-audit", + "installs": 50157, + "source": "coreyhaines31/marketingskills", + "rawUrl": "https://raw.githubusercontent.com/coreyhaines31/marketingskills/main/skills/seo-audit/SKILL.md" + }, + { + "id": "anthropics/skills/pdf", + "name": "pdf", + "installs": 45709, + "source": "anthropics/skills", + "rawUrl": "https://raw.githubusercontent.com/anthropics/skills/main/skills/pdf/SKILL.md" + }, + { + "id": "supabase/agent-skills/supabase-postgres-best-practices", + "name": "supabase-postgres-best-practices", + "installs": 43862, + "source": "supabase/agent-skills", + "rawUrl": "https://raw.githubusercontent.com/supabase/agent-skills/main/skills/supabase-postgres-best-practices/SKILL.md" + }, + { + "id": "coreyhaines31/marketingskills/copywriting", + "name": "copywriting", + "installs": 42743, + "source": "coreyhaines31/marketingskills", + "rawUrl": "https://raw.githubusercontent.com/coreyhaines31/marketingskills/main/skills/copywriting/SKILL.md" + }, + { + "id": "anthropics/skills/pptx", + "name": "pptx", + "installs": 41526, + "source": "anthropics/skills", + "rawUrl": "https://raw.githubusercontent.com/anthropics/skills/main/skills/pptx/SKILL.md" + }, + { + "id": "vercel-labs/next-skills/next-best-practices", + "name": "next-best-practices", + "installs": 40732, + "source": "vercel-labs/next-skills", + "rawUrl": "https://raw.githubusercontent.com/vercel-labs/next-skills/main/skills/next-best-practices/SKILL.md" + }, + { + "id": "squirrelscan/skills/audit-website", + "name": "audit-website", + "installs": 37654, + "source": "squirrelscan/skills", + "rawUrl": "https://raw.githubusercontent.com/squirrelscan/skills/main/audit-website/SKILL.md" + }, + { + "id": "obra/superpowers/systematic-debugging", + "name": "systematic-debugging", + "installs": 36470, + "source": "obra/superpowers", + "rawUrl": "https://raw.githubusercontent.com/obra/superpowers/main/skills/systematic-debugging/SKILL.md" + }, + { + "id": "anthropics/skills/docx", + "name": "docx", + "installs": 35928, + "source": "anthropics/skills", + "rawUrl": "https://raw.githubusercontent.com/anthropics/skills/main/skills/docx/SKILL.md" + }, + { + "id": "obra/superpowers/writing-plans", + "name": "writing-plans", + "installs": 35010, + "source": "obra/superpowers", + "rawUrl": "https://raw.githubusercontent.com/obra/superpowers/main/skills/writing-plans/SKILL.md" + }, + { + "id": "shadcn/ui/shadcn", + "name": "shadcn", + "installs": 33897, + "source": "shadcn/ui", + "rawUrl": "https://raw.githubusercontent.com/shadcn/ui/main/skills/shadcn/SKILL.md" + }, + { + "id": "anthropics/skills/xlsx", + "name": "xlsx", + "installs": 32936, + "source": "anthropics/skills", + "rawUrl": "https://raw.githubusercontent.com/anthropics/skills/main/skills/xlsx/SKILL.md" + }, + { + "id": "obra/superpowers/using-superpowers", + "name": "using-superpowers", + "installs": 30937, + "source": "obra/superpowers", + "rawUrl": "https://raw.githubusercontent.com/obra/superpowers/main/skills/using-superpowers/SKILL.md" + }, + { + "id": "coreyhaines31/marketingskills/marketing-psychology", + "name": "marketing-psychology", + "installs": 30917, + "source": "coreyhaines31/marketingskills", + "rawUrl": "https://raw.githubusercontent.com/coreyhaines31/marketingskills/main/skills/marketing-psychology/SKILL.md" + }, + { + "id": "obra/superpowers/test-driven-development", + "name": "test-driven-development", + "installs": 30410, + "source": "obra/superpowers", + "rawUrl": "https://raw.githubusercontent.com/obra/superpowers/main/skills/test-driven-development/SKILL.md" + }, + { + "id": "anthropics/skills/webapp-testing", + "name": "webapp-testing", + "installs": 29748, + "source": "anthropics/skills", + "rawUrl": "https://raw.githubusercontent.com/anthropics/skills/main/skills/webapp-testing/SKILL.md" + }, + { + "id": "obra/superpowers/executing-plans", + "name": "executing-plans", + "installs": 28743, + "source": "obra/superpowers", + "rawUrl": "https://raw.githubusercontent.com/obra/superpowers/main/skills/executing-plans/SKILL.md" + }, + { + "id": "obra/superpowers/requesting-code-review", + "name": "requesting-code-review", + "installs": 28421, + "source": "obra/superpowers", + "rawUrl": "https://raw.githubusercontent.com/obra/superpowers/main/skills/requesting-code-review/SKILL.md" + }, + { + "id": "coreyhaines31/marketingskills/content-strategy", + "name": "content-strategy", + "installs": 27875, + "source": "coreyhaines31/marketingskills", + "rawUrl": "https://raw.githubusercontent.com/coreyhaines31/marketingskills/main/skills/content-strategy/SKILL.md" + }, + { + "id": "coreyhaines31/marketingskills/programmatic-seo", + "name": "programmatic-seo", + "installs": 27820, + "source": "coreyhaines31/marketingskills", + "rawUrl": "https://raw.githubusercontent.com/coreyhaines31/marketingskills/main/skills/programmatic-seo/SKILL.md" + }, + { + "id": "coreyhaines31/marketingskills/social-content", + "name": "social-content", + "installs": 26700, + "source": "coreyhaines31/marketingskills", + "rawUrl": "https://raw.githubusercontent.com/coreyhaines31/marketingskills/main/skills/social-content/SKILL.md" + }, + { + "id": "coreyhaines31/marketingskills/product-marketing-context", + "name": "product-marketing-context", + "installs": 25930, + "source": "coreyhaines31/marketingskills", + "rawUrl": "https://raw.githubusercontent.com/coreyhaines31/marketingskills/main/skills/product-marketing-context/SKILL.md" + }, + { + "id": "coreyhaines31/marketingskills/marketing-ideas", + "name": "marketing-ideas", + "installs": 25516, + "source": "coreyhaines31/marketingskills", + "rawUrl": "https://raw.githubusercontent.com/coreyhaines31/marketingskills/main/skills/marketing-ideas/SKILL.md" + }, + { + "id": "roin-orca/skills/simple", + "name": "simple", + "installs": 25467, + "source": "roin-orca/skills", + "rawUrl": "https://raw.githubusercontent.com/roin-orca/skills/main/skills/simple/SKILL.md" + }, + { + "id": "coreyhaines31/marketingskills/pricing-strategy", + "name": "pricing-strategy", + "installs": 25142, + "source": "coreyhaines31/marketingskills", + "rawUrl": "https://raw.githubusercontent.com/coreyhaines31/marketingskills/main/skills/pricing-strategy/SKILL.md" + }, + { + "id": "anthropics/skills/mcp-builder", + "name": "mcp-builder", + "installs": 24764, + "source": "anthropics/skills", + "rawUrl": "https://raw.githubusercontent.com/anthropics/skills/main/skills/mcp-builder/SKILL.md" + }, + { + "id": "obra/superpowers/subagent-driven-development", + "name": "subagent-driven-development", + "installs": 24432, + "source": "obra/superpowers", + "rawUrl": "https://raw.githubusercontent.com/obra/superpowers/main/skills/subagent-driven-development/SKILL.md" + }, + { + "id": "coreyhaines31/marketingskills/copy-editing", + "name": "copy-editing", + "installs": 24073, + "source": "coreyhaines31/marketingskills", + "rawUrl": "https://raw.githubusercontent.com/coreyhaines31/marketingskills/main/skills/copy-editing/SKILL.md" + }, + { + "id": "pbakaus/impeccable/frontend-design", + "name": "frontend-design", + "installs": 23984, + "source": "pbakaus/impeccable", + "rawUrl": "https://raw.githubusercontent.com/pbakaus/impeccable/main/.claude/skills/frontend-design/SKILL.md" + }, + { + "id": "pbakaus/impeccable/polish", + "name": "polish", + "installs": 23360, + "source": "pbakaus/impeccable", + "rawUrl": "https://raw.githubusercontent.com/pbakaus/impeccable/main/.claude/skills/polish/SKILL.md" + }, + { + "id": "google-labs-code/stitch-skills/design-md", + "name": "design-md", + "installs": 19272, + "source": "google-labs-code/stitch-skills", + "rawUrl": "https://raw.githubusercontent.com/google-labs-code/stitch-skills/main/skills/design-md/SKILL.md" + } +] \ No newline at end of file diff --git a/src/ui-styles.ts b/src/ui-styles.ts index 0c33137..deb4cae 100644 --- a/src/ui-styles.ts +++ b/src/ui-styles.ts @@ -338,6 +338,206 @@ const styles = ` background-color: rgba(128, 128, 128, 0.05); } + /* AskUserQuestion */ + .ask-user-question { + margin: 4px 12px 20px 12px; + background-color: rgba(0, 122, 204, 0.08); + border: 1px solid rgba(0, 122, 204, 0.3); + border-radius: 8px; + padding: 16px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + animation: slideUp 0.3s ease; + } + + .ask-question-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 12px; + font-weight: 600; + color: var(--vscode-foreground); + } + + .ask-question-header .icon { + font-size: 16px; + } + + .ask-question-content { + font-size: 13px; + line-height: 1.4; + color: var(--vscode-descriptionForeground); + } + + .question-block { + margin-bottom: 16px; + } + + .question-block:last-of-type { + margin-bottom: 12px; + } + + .question-header { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--vscode-descriptionForeground); + margin-bottom: 4px; + } + + .question-text { + font-size: 13px; + font-weight: 500; + color: var(--vscode-foreground); + margin-bottom: 8px; + } + + .question-options { + display: flex; + flex-direction: column; + gap: 4px; + margin-bottom: 6px; + } + + .question-option { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 10px 12px; + border: 1px solid var(--vscode-panel-border); + border-radius: 6px; + cursor: pointer; + transition: all 0.15s ease; + background-color: transparent; + } + + .question-option:hover { + background-color: var(--vscode-list-hoverBackground); + border-color: var(--vscode-focusBorder); + } + + /* Hide native radio/checkbox, use custom styling */ + .question-option input[type="radio"], + .question-option input[type="checkbox"] { + -webkit-appearance: none; + appearance: none; + width: 16px; + height: 16px; + border: 2px solid var(--vscode-descriptionForeground); + background: transparent; + cursor: pointer; + flex-shrink: 0; + margin: 1px 0 0 0; + padding: 0; + transition: all 0.15s ease; + } + + .question-option input[type="radio"] { + border-radius: 50%; + } + + .question-option input[type="checkbox"] { + border-radius: 3px; + } + + .question-option input[type="radio"]:checked { + border-color: var(--vscode-button-background); + background: radial-gradient(circle, var(--vscode-button-background) 40%, transparent 44%); + } + + .question-option input[type="checkbox"]:checked { + border-color: var(--vscode-button-background); + background-color: var(--vscode-button-background); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='white'%3E%3Cpath d='M12.207 4.793a1 1 0 0 1 0 1.414l-5 5a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L6.5 9.086l4.293-4.293a1 1 0 0 1 1.414 0z'/%3E%3C/svg%3E"); + background-size: 12px; + background-position: center; + background-repeat: no-repeat; + } + + /* Selected option card highlight */ + .question-option:has(input:checked) { + border-color: var(--vscode-button-background); + background-color: rgba(0, 122, 204, 0.08); + } + + .option-content { + display: flex; + flex-direction: column; + gap: 2px; + } + + .option-label { + font-size: 13px; + font-weight: 500; + color: var(--vscode-foreground); + } + + .option-description { + font-size: 12px; + color: var(--vscode-descriptionForeground); + } + + .question-freetext { + margin-top: 6px; + } + + .question-freetext-input { + width: 100%; + padding: 6px 10px; + font-size: 13px; + font-family: var(--vscode-font-family); + background-color: var(--vscode-input-background); + color: var(--vscode-input-foreground); + border: 1px solid var(--vscode-input-border, var(--vscode-panel-border)); + border-radius: 4px; + outline: none; + box-sizing: border-box; + } + + .question-freetext-input:focus { + border-color: var(--vscode-focusBorder); + } + + .question-freetext-input:disabled { + opacity: 0.6; + cursor: not-allowed; + } + + .ask-question-buttons { + margin-top: 8px; + display: flex; + gap: 8px; + justify-content: flex-end; + } + + .ask-question-decision { + font-size: 12px; + padding: 8px 12px; + border-radius: 4px; + margin-top: 8px; + } + + .ask-question-decision.allowed { + background-color: rgba(0, 122, 204, 0.1); + color: var(--vscode-foreground); + border: 1px solid rgba(0, 122, 204, 0.2); + } + + .ask-question-decision.expired { + background-color: rgba(128, 128, 128, 0.15); + color: var(--vscode-descriptionForeground); + border: 1px solid rgba(128, 128, 128, 0.3); + } + + .ask-question-decided { + opacity: 0.7; + pointer-events: none; + } + + .ask-question-decided .ask-question-buttons { + display: none; + } + /* Permissions Management */ .permissions-list { max-height: 300px; @@ -585,6 +785,62 @@ const styles = ` line-height: 1.3; } + .env-variables-list { + display: flex; + flex-direction: column; + gap: 8px; + } + + .env-variable-row { + display: flex; + gap: 8px; + align-items: center; + } + + .env-variable-row input { + flex: 1; + padding: 6px 8px; + border: 1px solid var(--vscode-input-border, var(--vscode-panel-border)); + border-radius: 4px; + background-color: var(--vscode-input-background); + color: var(--vscode-input-foreground); + font-size: 12px; + font-family: monospace; + } + + .env-variable-row input:focus { + outline: none; + border-color: var(--vscode-focusBorder); + } + + .env-variable-row input::placeholder { + color: var(--vscode-input-placeholderForeground); + } + + .env-variable-row .env-key { + flex: 0.4; + } + + .env-variable-row .env-value { + flex: 0.6; + } + + .env-variable-remove { + background: transparent; + border: none; + color: var(--vscode-descriptionForeground); + cursor: pointer; + padding: 4px 8px; + font-size: 14px; + opacity: 0.6; + transition: opacity 0.15s ease; + } + + .env-variable-remove:hover { + opacity: 1; + color: var(--vscode-errorForeground); + } + .yolo-mode-section { display: flex; align-items: center; @@ -1331,7 +1587,8 @@ const styles = ` } .input-container { - padding: 10px; + padding: 1px 10px 10px 10px; + margin: 0; border-top: 1px solid var(--vscode-panel-border); background-color: var(--vscode-panel-background); display: flex; @@ -1339,6 +1596,160 @@ const styles = ` position: relative; } + .model-selector-row { + display: flex; + align-items: center; + gap: 6px; + margin-top: 6px; + margin-bottom: 6px; + overflow: hidden; + } + + .model-selector-new { + font-size: 9px; + font-weight: 700; + color: #fff; + background: linear-gradient(135deg, #f97316, #ea580c); + padding: 2px 6px; + border-radius: 4px; + letter-spacing: 0.5px; + } + + .model-selector-main { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + background: rgba(139, 92, 246, 0.1); + border: 1px solid rgba(139, 92, 246, 0.3); + border-radius: 20px; + color: var(--vscode-foreground); + font-size: 10px; + font-weight: 500; + cursor: pointer; + transition: all 0.15s ease; + white-space: nowrap; + } + + .model-selector-main:hover { + background: rgba(139, 92, 246, 0.18); + border-color: rgba(139, 92, 246, 0.4); + } + + #modelDropdownBtn { + background: none; + border-color: var(--vscode-panel-border); + } + + #modelDropdownBtn:hover { + background: rgba(128, 128, 128, 0.15); + border-color: var(--vscode-focusBorder); + } + + #modelDropdownBtn svg { + color: var(--vscode-descriptionForeground); + width: 8px; + height: 8px; + } + + .model-selector-main svg { + color: #8b5cf6; + width: 12px; + height: 12px; + } + + .model-quick-select { + display: flex; + align-items: center; + gap: 4px; + overflow-x: auto; + overflow-y: hidden; + flex: 1; + min-width: 0; + scrollbar-width: none; + -ms-overflow-style: none; + } + + .model-quick-select::-webkit-scrollbar { + display: none; + } + + .model-quick-btn { + display: flex; + align-items: center; + gap: 3px; + padding: 4px 8px; + background: transparent; + border: 1px solid rgba(139, 92, 246, 0.2); + border-radius: 20px; + color: var(--vscode-foreground); + font-size: 10px; + cursor: pointer; + transition: all 0.15s ease; + opacity: 0.8; + white-space: nowrap; + } + + .model-quick-btn:hover { + background: rgba(139, 92, 246, 0.1); + border-color: rgba(139, 92, 246, 0.3); + opacity: 1; + } + + .model-quick-btn.selected { + background: rgba(139, 92, 246, 0.18); + border-color: rgba(139, 92, 246, 0.4); + opacity: 1; + } + + .model-quick-icon { + font-size: 10px; + } + + .model-quick-select { + mask-image: linear-gradient(to right, black calc(100% - 20px), transparent 100%); + -webkit-mask-image: linear-gradient(to right, black calc(100% - 20px), transparent 100%); + } + + .model-more-btn { + display: flex; + align-items: center; + gap: 2px; + padding: 4px 8px; + background: transparent; + border: 1px solid rgba(139, 92, 246, 0.2); + border-radius: 20px; + color: var(--vscode-foreground); + font-size: 10px; + cursor: pointer; + transition: all 0.15s ease; + white-space: nowrap; + opacity: 0.7; + flex-shrink: 0; + } + + .model-more-btn:hover { + background: rgba(139, 92, 246, 0.1); + border-color: rgba(139, 92, 246, 0.3); + opacity: 1; + } + + .model-more-btn.model-dropdown-btn { + padding: 4px 10px; + font-size: 11px; + border-color: var(--vscode-panel-border); + } + + .model-more-btn.model-dropdown-btn:hover { + background: var(--vscode-list-hoverBackground); + border-color: var(--vscode-focusBorder); + } + + .model-more-btn svg { + width: 10px; + height: 10px; + } + .input-modes { display: flex; gap: 16px; @@ -1350,10 +1761,19 @@ const styles = ` .mode-toggle { display: flex; align-items: center; - gap: 6px; + gap: 5px; color: var(--vscode-foreground); - opacity: 0.8; + opacity: 0.7; transition: opacity 0.2s ease; + font-size: 10px; + } + + .left-controls .mode-toggle { + padding: 3px 0; + } + + .left-controls .mode-toggle span { + cursor: pointer; } .mode-toggle span { @@ -1411,9 +1831,54 @@ const styles = ` background-color: var(--vscode-input-background); border: 1px solid var(--vscode-input-border); border-radius: 6px; - overflow: hidden; + overflow: visible; } + .image-preview-container { + display: flex; + flex-wrap: wrap; + gap: 6px; + padding: 8px 8px 0; + } + + .image-preview-item { + position: relative; + width: 56px; + height: 56px; + border-radius: 6px; + overflow: hidden; + border: 1px solid var(--vscode-input-border); + } + + .image-preview-item img { + width: 100%; + height: 100%; + object-fit: cover; + } + + .image-preview-remove { + position: absolute; + top: 2px; + right: 2px; + width: 16px; + height: 16px; + border-radius: 50%; + border: none; + background: rgba(0, 0, 0, 0.6); + color: #fff; + font-size: 10px; + line-height: 16px; + text-align: center; + cursor: pointer; + padding: 0; + display: none; + } + + .image-preview-item:hover .image-preview-remove { + display: block; + } + + .textarea-wrapper:focus-within { border-color: var(--vscode-focusBorder); } @@ -1431,6 +1896,7 @@ const styles = ` line-height: 1.4; overflow-y: hidden; resize: none; + border-radius: 6px 6px 0 0; } .input-field:focus { @@ -1452,6 +1918,7 @@ const styles = ` padding: 2px 4px; border-top: 1px solid var(--vscode-panel-border); background-color: var(--vscode-input-background); + border-radius: 0 0 6px 6px; } .left-controls { @@ -1461,24 +1928,51 @@ const styles = ` } .model-selector { - background-color: rgba(128, 128, 128, 0.15); + background: linear-gradient(135deg, rgba(139, 92, 246, 0.15), rgba(16, 185, 129, 0.15)); color: var(--vscode-foreground); - border: none; - padding: 3px 7px; - border-radius: 4px; + border: 1px solid rgba(139, 92, 246, 0.3); + padding: 4px 10px; + border-radius: 6px; cursor: pointer; font-size: 11px; font-weight: 500; transition: all 0.2s ease; - opacity: 0.9; display: flex; align-items: center; - gap: 4px; + gap: 6px; } .model-selector:hover { - background-color: rgba(128, 128, 128, 0.25); - opacity: 1; + background: linear-gradient(135deg, rgba(139, 92, 246, 0.25), rgba(16, 185, 129, 0.25)); + border-color: rgba(139, 92, 246, 0.5); + } + + .model-selector-label { + display: flex; + align-items: center; + gap: 6px; + } + + .model-selector-label #selectedModel { + font-weight: 600; + color: #a78bfa; + } + + .model-selector-examples { + font-size: 10px; + opacity: 0.6; + font-weight: 400; + } + + .model-selector-badge { + font-size: 8px; + font-weight: 700; + padding: 2px 5px; + border-radius: 3px; + background: linear-gradient(135deg, #f59e0b, #ea580c); + color: white; + text-transform: uppercase; + letter-spacing: 0.3px; } .tools-btn { @@ -1502,6 +1996,131 @@ const styles = ` opacity: 1; } + .plus-btn { + background: none; + border: none; + color: var(--vscode-foreground); + font-size: 16px; + line-height: 1; + cursor: pointer; + padding: 2px 6px; + border-radius: 4px; + opacity: 0.6; + transition: all 0.2s ease; + } + + .plus-btn:hover { + opacity: 1; + background-color: rgba(128, 128, 128, 0.2); + } + + .input-dropdown-btn { + display: flex; + align-items: center; + gap: 3px; + background: none; + border: none; + color: var(--vscode-descriptionForeground); + font-size: 12px; + cursor: pointer; + padding: 2px 6px; + border-radius: 4px; + transition: all 0.15s ease; + } + + .input-dropdown-btn:hover { + color: var(--vscode-foreground); + background-color: rgba(128, 128, 128, 0.15); + } + + #connectBtn { + color: var(--vscode-foreground); + background-color: rgba(128, 128, 128, 0.12); + padding: 3px 8px; + } + + #connectBtn:hover { + background-color: rgba(128, 128, 128, 0.25); + } + + .input-dropdown-btn svg { + opacity: 0.6; + } + + + .input-toggle-btn { + display: flex; + align-items: center; + background: none; + border: 1px solid transparent; + color: var(--vscode-descriptionForeground); + font-size: 12px; + cursor: pointer; + padding: 1px 5px; + border-radius: 4px; + transition: all 0.15s ease; + } + + .input-toggle-btn:hover { + color: var(--vscode-foreground); + background-color: rgba(128, 128, 128, 0.15); + } + + .input-toggle-btn.active { + color: var(--vscode-button-background); + background-color: rgba(0, 122, 204, 0.12); + border-color: rgba(0, 122, 204, 0.3); + } + + .connect-dropdown-wrapper { + position: relative; + } + + .connect-menu { + position: absolute; + bottom: 100%; + left: 0; + margin-bottom: 6px; + background-color: var(--vscode-menu-background); + border: 1px solid var(--vscode-menu-border); + border-radius: 8px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); + min-width: 180px; + padding: 6px 0; + z-index: 1000; + } + + .connect-menu-header { + padding: 8px 14px 6px; + font-size: 11px; + font-weight: 600; + color: var(--vscode-descriptionForeground); + } + + .connect-menu-item { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: 8px 14px; + background: none; + border: none; + color: var(--vscode-foreground); + font-size: 13px; + cursor: pointer; + text-align: left; + transition: background-color 0.1s ease; + } + + .connect-menu-item:hover { + background-color: var(--vscode-list-hoverBackground); + } + + .connect-menu-item svg { + color: var(--vscode-descriptionForeground); + flex-shrink: 0; + } + .slash-btn, .at-btn { background-color: transparent; @@ -1572,6 +2191,28 @@ const styles = ` cursor: not-allowed; } + .stop-inline-btn { + background-color: #ef4444; + color: #fff; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 11px; + font-weight: 500; + display: none; + align-items: center; + justify-content: center; + gap: 2px; + min-width: 39px; + min-height: 11px; + padding: 3px 7px; + box-sizing: content-box; + } + + .stop-inline-btn:hover { + background-color: #dc2626; + } + .secondary-button { background-color: var(--vscode-button-secondaryBackground, rgba(128, 128, 128, 0.2)); color: var(--vscode-button-secondaryForeground, var(--vscode-foreground)); @@ -1765,7 +2406,7 @@ const styles = ` left: 0; width: 100%; height: 100%; - background-color: rgba(0, 0, 0, 0.5); + background-color: rgba(0, 0, 0, 0.75); z-index: 1000; display: flex; align-items: center; @@ -1778,7 +2419,7 @@ const styles = ` border-radius: 8px; width: 700px; max-width: 90vw; - max-height: 80vh; + max-height: 90vh; display: flex; flex-direction: column; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); @@ -1792,6 +2433,7 @@ const styles = ` justify-content: space-between; align-items: center; flex-shrink: 0; + background: linear-gradient(135deg, rgba(139, 92, 246, 0.08), rgba(59, 130, 246, 0.08)); } .tools-modal-body { @@ -1830,13 +2472,17 @@ const styles = ` } /* MCP Modal content area improvements */ - #mcpModal * { + #mcpModal *, + #skillsModal *, + #pluginsModal * { box-sizing: border-box; } - #mcpModal .tools-list { + #mcpModal .tools-list, + #skillsModal .tools-list, + #pluginsModal .tools-list { padding: 24px; - max-height: calc(80vh - 120px); + max-height: calc(90vh - 120px); overflow-y: auto; width: 100%; } @@ -1930,6 +2576,509 @@ const styles = ` align-self: flex-start; } + /* Model modal styles */ + .model-modal-content { + width: 520px; + max-width: 90vw; + max-height: 80vh; + overflow-y: auto; + display: flex; + flex-direction: column; + } + + .model-section { + padding: 16px; + } + + .model-section.opencredits-section { + border-top: 1px solid var(--vscode-panel-border); + } + + .model-section-header { + display: flex; + flex-direction: column; + gap: 4px; + margin-bottom: 14px; + } + + .model-section-title { + font-size: 11px; + font-weight: 700; + letter-spacing: 1px; + display: flex; + align-items: center; + gap: 8px; + color: white; + } + + .new-badge { + font-size: 9px; + font-weight: 700; + padding: 3px 8px; + border-radius: 4px; + background: linear-gradient(135deg, #f59e0b, #ea580c); + color: white; + text-transform: uppercase; + letter-spacing: 0.5px; + box-shadow: 0 2px 8px rgba(245, 158, 11, 0.4); + } + + .beta-badge { + font-size: 9px; + font-weight: 700; + padding: 3px 8px; + border-radius: 4px; + background: rgba(127, 127, 127, 0.25); + color: var(--vscode-descriptionForeground); + text-transform: uppercase; + letter-spacing: 0.5px; + margin-left: auto; + cursor: default; + position: relative; + } + + .beta-badge:hover::after { + content: attr(data-tooltip); + position: absolute; + top: calc(100% + 6px); + right: 0; + background: var(--vscode-editorHoverWidget-background, #1e1e1e); + color: var(--vscode-editorHoverWidget-foreground, #ccc); + border: 1px solid var(--vscode-editorHoverWidget-border, #454545); + padding: 4px 8px; + border-radius: 4px; + font-size: 11px; + font-weight: 400; + letter-spacing: 0; + text-transform: none; + white-space: nowrap; + z-index: 100; + } + + .model-section-subtitle { + font-size: 12px; + color: var(--vscode-descriptionForeground); + } + + .model-section-divider { + height: 1px; + background: var(--vscode-panel-border); + margin: 0 16px; + } + + /* Flexible grid for model cards */ + .model-cards-container { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px; + } + + @media (min-width: 600px) { + .model-cards-container { + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + } + } + + .model-card { + position: relative; + padding: 12px; + background: var(--vscode-input-background); + border: 1px solid var(--vscode-panel-border); + border-left: 3px solid #10b981; + border-radius: 8px; + cursor: pointer; + transition: all 0.15s ease; + display: flex; + flex-direction: column; + gap: 4px; + } + + .model-card:hover { + border-color: #10b981; + border-left: 3px solid #10b981; + background: rgba(16, 185, 129, 0.1); + } + + .model-card.selected { + border-color: #10b981; + border-left: 3px solid #10b981; + background: rgba(16, 185, 129, 0.15); + } + + .model-card-name { + font-size: 13px; + font-weight: 600; + color: var(--vscode-foreground); + line-height: 1.3; + } + + .model-card-provider { + font-size: 10px; + color: var(--vscode-descriptionForeground); + } + + .model-card-price { + font-size: 10px; + color: var(--vscode-descriptionForeground); + margin-top: 4px; + } + + .model-card-requests { + font-size: 10px; + color: var(--vscode-descriptionForeground); + margin-top: 4px; + } + + .claude-card-requests { + font-size: 10px; + color: var(--vscode-descriptionForeground); + margin-top: 4px; + } + + .model-section-links { + display: flex; + justify-content: space-between; + width: 100%; + grid-column: 1 / -1; + } + + .model-section-links a { + font-size: 11px; + color: var(--vscode-foreground); + text-decoration: none; + } + + .model-section-links a:hover { + text-decoration: underline; + } + + .custom-provider-field { + margin-bottom: 12px; + overflow: visible; + } + + .custom-provider-field label { + display: block; + font-size: 11px; + font-weight: 600; + color: var(--vscode-foreground); + margin-bottom: 4px; + } + + .custom-provider-field input { + width: 100%; + padding: 8px 10px; + font-size: 12px; + font-family: inherit; + color: var(--vscode-input-foreground); + background: var(--vscode-input-background); + border: 1px solid var(--vscode-input-border, rgba(255,255,255,0.1)); + border-radius: 4px; + outline: none; + } + + .custom-provider-field input:focus { + border-color: var(--vscode-focusBorder); + } + + .model-combo { + position: relative; + max-width: 100%; + overflow: visible; + } + + .model-combo-input { + width: 100%; + padding: 8px 28px 8px 10px; + font-size: 12px; + font-family: inherit; + color: var(--vscode-input-foreground); + background: var(--vscode-input-background); + border: 1px solid var(--vscode-input-border, rgba(255,255,255,0.2)); + border-radius: 4px; + outline: none; + box-sizing: border-box; + } + + .model-combo::after { + content: ''; + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + width: 0; + height: 0; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 5px solid var(--vscode-descriptionForeground, #888); + pointer-events: none; + } + + .model-combo-input:focus { + border-color: var(--vscode-focusBorder); + } + + .model-combo-dropdown { + display: none; + position: absolute; + top: 100%; + left: 0; + right: 0; + max-height: 120px; + overflow-y: auto; + background: var(--vscode-dropdown-background, #1e1e1e); + border: 1px solid var(--vscode-dropdown-border, rgba(255,255,255,0.2)); + border-radius: 4px; + margin-top: 2px; + z-index: 100; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); + } + + .model-combo.open .model-combo-dropdown { + display: block; + } + + .model-combo-option { + padding: 6px 10px; + font-size: 12px; + cursor: pointer; + color: var(--vscode-dropdown-foreground); + } + + .model-combo-option:hover { + background: var(--vscode-list-hoverBackground, rgba(255,255,255,0.05)); + } + + .model-combo-option .model-combo-option-name { + font-weight: 500; + } + + .model-combo-option .model-combo-option-id { + font-size: 10px; + opacity: 0.6; + } + + .model-combo-custom { + padding: 6px 10px; + font-size: 12px; + cursor: pointer; + color: var(--vscode-textLink-foreground, #3794ff); + border-top: 1px solid var(--vscode-dropdown-border, rgba(255,255,255,0.1)); + } + + .model-combo-custom:hover { + background: var(--vscode-list-hoverBackground, rgba(255,255,255,0.05)); + } + + .model-comparison-header { + font-size: 11px; + color: var(--vscode-descriptionForeground); + margin-bottom: 10px; + line-height: 1.5; + } + + .model-card-unlock { + font-size: 9px; + color: #10b981; + margin-top: 6px; + font-weight: 500; + } + + .model-card.pending { + border-color: rgba(249, 115, 22, 0.5); + background: rgba(249, 115, 22, 0.1); + } + + .model-card-price-label { + display: none; + } + + .price-current { + font-weight: 500; + } + + .price-comparison { + margin-left: 4px; + opacity: 0.7; + } + + .price-comparison s { + text-decoration: line-through; + } + + /* Savings badge */ + .savings-badge { + position: absolute; + top: 8px; + right: 8px; + font-size: 9px; + font-weight: 600; + padding: 2px 6px; + border-radius: 3px; + background: rgba(16, 185, 129, 0.15); + color: #10b981; + border: 1px solid rgba(16, 185, 129, 0.3); + } + + /* More models card */ + .more-models-card { + background: var(--vscode-button-secondaryBackground) !important; + border-style: dashed !important; + } + + .more-models-card:hover { + background: var(--vscode-button-secondaryHoverBackground) !important; + } + + .more-models-card .savings-badge { + display: none; + } + + /* All models browser */ + .all-models-search { + padding: 12px 16px; + border-bottom: 1px solid var(--vscode-panel-border); + } + + .all-models-search input { + width: 100%; + padding: 8px 12px; + border: 1px solid var(--vscode-input-border); + background: var(--vscode-input-background); + color: var(--vscode-input-foreground); + border-radius: 4px; + font-size: 13px; + box-sizing: border-box; + } + + .all-models-search input:focus { + outline: none; + border-color: var(--vscode-focusBorder); + } + + .all-models-list { + max-height: 400px; + overflow-y: auto; + padding: 4px 8px; + } + + .all-models-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 16px; + border-radius: 6px; + cursor: pointer; + margin-bottom: 2px; + background: var(--vscode-list-hoverBackground); + } + + .all-models-item:hover { + background: var(--vscode-list-activeSelectionBackground); + } + + .all-models-item.selected { + background: var(--vscode-list-activeSelectionBackground); + border: 1px solid var(--vscode-focusBorder); + } + + .all-models-item-main { + flex: 1; + min-width: 0; + } + + .all-models-item-name { + font-size: 13px; + font-weight: 500; + color: var(--vscode-foreground); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .all-models-item-provider { + font-size: 11px; + color: var(--vscode-descriptionForeground); + margin-top: 2px; + } + + .all-models-item-details { + display: flex; + gap: 12px; + align-items: center; + flex-shrink: 0; + } + + .all-models-item-context { + font-size: 11px; + color: var(--vscode-descriptionForeground); + background: var(--vscode-badge-background); + padding: 2px 6px; + border-radius: 3px; + } + + .all-models-item-price { + font-size: 11px; + color: var(--vscode-descriptionForeground); + } + + .all-models-loading, + .all-models-error, + .all-models-empty { + text-align: center; + padding: 40px 20px; + color: var(--vscode-descriptionForeground); + } + + .all-models-error { + color: var(--vscode-errorForeground); + } + + + /* Claude Code model cards */ + .claude-cards-container { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 10px; + } + + .claude-card { + padding: 12px; + background: var(--vscode-input-background); + border: 1px solid var(--vscode-panel-border); + border-left: 3px solid #8b5cf6; + border-radius: 8px; + cursor: pointer; + transition: all 0.15s ease; + display: flex; + flex-direction: column; + gap: 4px; + } + + .claude-card:hover { + border-color: #8b5cf6; + border-left: 3px solid #8b5cf6; + background: rgba(139, 92, 246, 0.1); + } + + .claude-card.selected { + border-color: #8b5cf6; + border-left: 3px solid #8b5cf6; + background: rgba(139, 92, 246, 0.15); + } + + .claude-card-name { + font-size: 13px; + font-weight: 600; + color: var(--vscode-foreground); + } + + .claude-card-desc { + font-size: 10px; + color: var(--vscode-descriptionForeground); + line-height: 1.3; + } + /* Thinking intensity slider */ .thinking-slider-container { position: relative; @@ -2438,6 +3587,23 @@ const styles = ` flex: 1; } + .support-btn { + background: none; + border: none; + color: var(--vscode-descriptionForeground); + cursor: pointer; + padding: 2px 4px; + opacity: 0.6; + font-size: 11px; + display: flex; + align-items: center; + gap: 4px; + } + + .support-btn:hover { + opacity: 1; + } + .status-text .usage-badge { display: inline-flex; align-items: center; @@ -2757,12 +3923,14 @@ const styles = ` display: flex; align-items: center; justify-content: space-between; - padding: 20px 24px; + gap: 12px; + padding: 12px 16px; border: 1px solid var(--vscode-panel-border); border-radius: 8px; - margin-bottom: 16px; + margin-bottom: 8px; background-color: var(--vscode-editor-background); transition: all 0.2s ease; + flex-wrap: wrap; } .mcp-server-item:hover { @@ -2772,13 +3940,14 @@ const styles = ` .server-info { flex: 1; + min-width: 0; } .server-name { font-weight: 600; - font-size: 16px; + font-size: 14px; color: var(--vscode-foreground); - margin-bottom: 8px; + margin-bottom: 4px; } .server-type { @@ -2793,18 +3962,18 @@ const styles = ` } .server-config { - font-size: 13px; + font-size: 12px; color: var(--vscode-descriptionForeground); opacity: 0.9; line-height: 1.4; + word-break: break-all; } .server-delete-btn { - padding: 8px 16px; - font-size: 13px; + padding: 4px 10px; + font-size: 12px; color: var(--vscode-errorForeground); border-color: var(--vscode-errorForeground); - min-width: 80px; justify-content: center; } @@ -2821,11 +3990,10 @@ const styles = ` } .server-edit-btn { - padding: 8px 16px; - font-size: 13px; + padding: 4px 10px; + font-size: 12px; color: var(--vscode-foreground); border-color: var(--vscode-panel-border); - min-width: 80px; transition: all 0.2s ease; justify-content: center; } @@ -2901,17 +4069,63 @@ const styles = ` margin-top: 20px; } + .mcp-add-server { + margin-bottom: 0; + padding: 0 4px; + } + + .mcp-auth-btn { + color: var(--vscode-textLink-foreground); + font-size: 12px; + cursor: pointer; + position: relative; + } + + .mcp-auth-btn:hover { + text-decoration: underline; + } + + .mcp-auth-btn:hover::after { + content: attr(data-tooltip); + position: absolute; + bottom: calc(100% + 6px); + right: 0; + background: var(--vscode-editorHoverWidget-background, #1e1e1e); + color: var(--vscode-editorHoverWidget-foreground, #ccc); + border: 1px solid var(--vscode-editorHoverWidget-border, #454545); + padding: 4px 8px; + border-radius: 4px; + font-size: 11px; + white-space: nowrap; + z-index: 100; + } + .no-servers { - text-align: center; + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + padding: 20px 12px; color: var(--vscode-descriptionForeground); - font-style: italic; - padding: 40px 20px; + } + + .no-servers-icon { + opacity: 0.4; + } + + .no-servers-text { + font-size: 13px; + } + + .no-servers-btn { + margin-top: 4px; + font-size: 12px; } /* Popular MCP Servers */ .mcp-popular-servers { - margin-top: 32px; - padding-top: 24px; + margin-top: 12px; + padding-top: 12px; border-top: 1px solid var(--vscode-panel-border); } @@ -2923,6 +4137,27 @@ const styles = ` opacity: 0.9; } + .skill-item-row { + display: flex; + align-items: center; + gap: 12px; + width: 100%; + } + + .skill-item-info { + flex: 1; + min-width: 0; + overflow: hidden; + } + + .skill-item-desc { + font-size: 12px; + color: var(--vscode-descriptionForeground); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .popular-servers-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); @@ -2973,6 +4208,311 @@ const styles = ` text-overflow: ellipsis; } + /* MCP Tabs */ + .mcp-tabs { + display: flex; + gap: 0; + } + + .mcp-tab { + background: none; + border: none; + color: var(--vscode-descriptionForeground); + font-size: 14px; + font-weight: 500; + cursor: pointer; + padding: 4px 12px; + border-bottom: 2px solid transparent; + transition: all 0.15s ease; + } + + .mcp-tab:hover { + color: var(--vscode-foreground); + } + + .mcp-tab.active { + color: var(--vscode-foreground); + border-bottom-color: var(--vscode-button-background); + } + + /* MCP Marketplace */ + .marketplace-search { + padding: 0 0 12px 0; + } + + .marketplace-search input { + width: 100%; + padding: 8px 12px; + font-size: 13px; + font-family: var(--vscode-font-family); + background-color: var(--vscode-input-background); + color: var(--vscode-input-foreground); + border: 1px solid var(--vscode-input-border, var(--vscode-panel-border)); + border-radius: 6px; + outline: none; + box-sizing: border-box; + } + + .marketplace-search input:focus { + border-color: var(--vscode-focusBorder); + } + + .marketplace-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 10px; + } + + .marketplace-item { + padding: 12px; + background-color: var(--vscode-editor-background); + border: 1px solid var(--vscode-panel-border); + border-radius: 6px; + cursor: pointer; + transition: all 0.15s ease; + } + + .marketplace-item:hover { + border-color: var(--vscode-focusBorder); + background-color: var(--vscode-list-hoverBackground); + transform: translateY(-1px); + } + + .marketplace-item-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 6px; + } + + .marketplace-item-icon { + width: 28px; + height: 28px; + border-radius: 6px; + flex-shrink: 0; + object-fit: cover; + } + + .marketplace-item-icon-placeholder { + width: 28px; + height: 28px; + border-radius: 6px; + flex-shrink: 0; + background-color: rgba(128, 128, 128, 0.15); + display: flex; + align-items: center; + justify-content: center; + font-size: 13px; + font-weight: 600; + color: var(--vscode-descriptionForeground); + } + + .marketplace-item-info { + flex: 1; + min-width: 0; + display: flex; + align-items: center; + gap: 6px; + } + + .marketplace-item-name { + font-weight: 600; + font-size: 13px; + color: var(--vscode-foreground); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .marketplace-item-type { + font-size: 9px; + padding: 1px 5px; + border-radius: 3px; + background-color: rgba(128, 128, 128, 0.15); + color: var(--vscode-descriptionForeground); + flex-shrink: 0; + } + + .marketplace-item-desc { + font-size: 11px; + color: var(--vscode-descriptionForeground); + line-height: 1.4; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + } + + .marketplace-item-meta { + display: flex; + align-items: center; + gap: 8px; + margin-top: 2px; + } + + .marketplace-item-stars { + font-size: 11px; + color: var(--vscode-descriptionForeground); + } + + .marketplace-item-lang { + display: flex; + align-items: center; + gap: 4px; + font-size: 11px; + color: var(--vscode-descriptionForeground); + } + + .lang-dot { + width: 8px; + height: 8px; + border-radius: 50%; + display: inline-block; + } + + .marketplace-item-license { + font-size: 10px; + color: var(--vscode-descriptionForeground); + } + + .marketplace-detail-meta { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 12px; + } + + .marketplace-detail-link { + color: var(--vscode-textLink-foreground); + font-size: 12px; + text-decoration: none; + } + + .marketplace-detail-link:hover { + text-decoration: underline; + } + + .marketplace-detail-install { + margin: 12px 0; + } + + .marketplace-loading { + text-align: center; + padding: 24px; + color: var(--vscode-descriptionForeground); + font-size: 13px; + } + + .marketplace-load-more { + text-align: center; + padding: 12px 0; + } + + .marketplace-detail { + padding: 4px 0; + } + + .marketplace-back-btn { + background: none; + border: none; + color: var(--vscode-textLink-foreground); + font-size: 12px; + cursor: pointer; + padding: 0 0 12px 0; + display: block; + } + + .marketplace-back-btn:hover { + text-decoration: underline; + } + + .marketplace-detail-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 14px; + } + + .marketplace-detail-icon { + width: 36px; + height: 36px; + border-radius: 8px; + object-fit: cover; + } + + .marketplace-detail-header-info { + flex: 1; + min-width: 0; + } + + .marketplace-detail-header-meta { + display: flex; + align-items: center; + gap: 8px; + margin-top: 2px; + } + + .marketplace-detail-name { + font-size: 15px; + font-weight: 600; + color: var(--vscode-foreground); + } + + .marketplace-install-btn { + flex-shrink: 0; + align-self: center; + } + + .marketplace-detail-desc { + font-size: 13px; + color: var(--vscode-descriptionForeground); + line-height: 1.5; + margin-bottom: 14px; + } + + .marketplace-detail-config { + background-color: var(--vscode-editor-background); + border: 1px solid var(--vscode-panel-border); + border-radius: 6px; + padding: 10px 12px; + } + + .marketplace-detail-section-title { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--vscode-descriptionForeground); + margin-bottom: 8px; + } + + .marketplace-detail-row { + font-size: 12px; + color: var(--vscode-descriptionForeground); + margin-bottom: 4px; + } + + .marketplace-detail-row code, + .marketplace-detail-env code { + background-color: var(--vscode-textCodeBlock-background); + padding: 2px 6px; + border-radius: 3px; + font-family: var(--vscode-editor-font-family); + font-size: 11px; + } + + .detail-label { + color: var(--vscode-foreground); + font-weight: 500; + } + + .marketplace-detail-env { + font-size: 12px; + color: var(--vscode-descriptionForeground); + margin-left: 12px; + margin-bottom: 2px; + } + /* Processing indicator - morphing orange dot */ .processing-indicator { display: flex; @@ -3054,7 +4594,6 @@ const styles = ` 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; @@ -3090,6 +4629,7 @@ const styles = ` .install-body { text-align: center; + margin-top: 20px; } .install-main { @@ -3238,6 +4778,221 @@ const styles = ` color: var(--vscode-descriptionForeground); } + .install-options { + display: flex; + flex-direction: column; + gap: 10px; + width: 100%; + margin-top: 8px; + } + + .install-option { + width: 100%; + padding: 14px 16px; + background: var(--vscode-button-background); + color: var(--vscode-button-foreground); + border: none; + border-radius: 8px; + cursor: pointer; + transition: all 0.15s; + text-align: left; + display: flex; + flex-direction: column; + gap: 2px; + } + + .install-option:hover { + background: var(--vscode-button-hoverBackground); + transform: translateY(-1px); + } + + .install-option-secondary { + background: transparent; + border: 1px solid var(--vscode-widget-border, var(--vscode-panel-border)); + color: var(--vscode-foreground); + } + + .install-option-secondary:hover { + background: var(--vscode-list-hoverBackground); + border-color: var(--vscode-focusBorder); + } + + .install-option-title { + font-size: 14px; + font-weight: 500; + } + + .install-option-desc { + font-size: 12px; + opacity: 0.8; + } + + .install-funds { + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; + padding: 12px 0; + } + + .install-funds-title { + margin: 0; + font-size: 16px; + font-weight: 600; + color: var(--vscode-foreground); + margin-bottom: 10px; + } + + .install-funds-hint { + margin: 0; + font-size: 13px; + color: var(--vscode-descriptionForeground); + margin-bottom: 10px; + } + + .install-amounts { + display: flex; + flex-wrap: wrap; + gap: 8px; + width: 100%; + } + + .install-amount { + flex: 1 1 calc(33.333% - 6px); + min-width: 60px; + padding: 12px 8px; + font-size: 14px; + font-weight: 600; + background: var(--vscode-input-background); + color: var(--vscode-foreground); + border: 1px solid var(--vscode-widget-border, var(--vscode-panel-border)); + border-radius: 8px; + cursor: pointer; + transition: all 0.15s; + } + + .install-amount:hover { + border-color: var(--vscode-focusBorder); + background: var(--vscode-list-hoverBackground); + } + + .install-custom-amount { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + margin-top: 4px; + } + + .install-custom-currency { + font-size: 14px; + font-weight: 600; + color: var(--vscode-descriptionForeground); + } + + .install-custom-input { + flex: 1; + padding: 10px 12px; + font-size: 14px; + background: var(--vscode-input-background); + color: var(--vscode-foreground); + border: 1px solid var(--vscode-widget-border, var(--vscode-panel-border)); + border-radius: 6px; + outline: none; + } + + .install-custom-input:focus { + border-color: var(--vscode-focusBorder); + } + + .install-custom-input::placeholder { + color: var(--vscode-descriptionForeground); + } + + .install-custom-btn { + padding: 10px 16px; + font-size: 13px; + font-weight: 500; + background: var(--vscode-button-background); + color: var(--vscode-button-foreground); + border: none; + border-radius: 6px; + cursor: pointer; + transition: background 0.15s; + } + + .install-custom-btn:hover { + background: var(--vscode-button-hoverBackground); + } + + .install-powered-by { + font-size: 11px; + color: var(--vscode-descriptionForeground); + margin-top: 8px; + } + + .install-powered-by a { + color: var(--vscode-textLink-foreground); + text-decoration: none; + } + + .install-powered-by a:hover { + text-decoration: underline; + } + + .install-back-btn { + background: none; + border: none; + color: var(--vscode-textLink-foreground); + font-size: 13px; + cursor: pointer; + padding: 8px; + } + + .install-back-btn:hover { + text-decoration: underline; + } + + /* Toast notifications */ + .toast-notification { + position: fixed; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + background: linear-gradient(135deg, #10b981, #059669); + color: white; + padding: 10px 20px; + border-radius: 8px; + font-size: 12px; + font-weight: 500; + z-index: 10000; + animation: toastSlideUp 0.3s ease; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + } + + .toast-notification.fade-out { + opacity: 0; + transform: translateX(-50%) translateY(10px); + transition: all 0.3s ease; + } + + @keyframes toastSlideUp { + from { + transform: translateX(-50%) translateY(20px); + opacity: 0; + } + to { + transform: translateX(-50%) translateY(0); + opacity: 1; + } + } + + /* OpenCredits balance badge style */ + .opencredits-balance { + background: linear-gradient(135deg, rgba(16, 185, 129, 0.15), rgba(5, 150, 105, 0.15)) !important; + color: #10b981 !important; + } + ` export default styles \ No newline at end of file diff --git a/src/ui.ts b/src/ui.ts index a4883e3..bd447ce 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -1,12 +1,19 @@ import getScript from './script'; import styles from './ui-styles' +import recommendedModels from './recommended-models.json' +import topMcpServers from './top-mcp-servers.json' +import topSkills from './top-skills.json' +import topPlugins from './top-plugins.json' +import getSkillsHtml from './skills-ui' +import getPluginsHtml from './plugins-ui' -const getHtml = (isTelemetryEnabled: boolean) => ` +const getHtml = (isTelemetryEnabled: boolean, opencreditsApiUrl: string = 'https://ccc.api.opencredits.ai', opencreditsWebUrl: string = 'https://ccc.opencredits.ai', opencreditsPublishableKey: string = 'oc_pk_c43da4f9a9484ae484ad29bc97cc354f') => ` + Claude Code Chat ${styles} @@ -57,33 +64,48 @@ const getHtml = (isTelemetryEnabled: boolean) => `
-
-
- Plan First -
-
-
- Thinking Mode -
+
+ + +
+
+
- - +
+ + +
+ +
@@ -102,21 +124,19 @@ const getHtml = (isTelemetryEnabled: boolean) => ` +
@@ -127,11 +147,9 @@ const getHtml = (isTelemetryEnabled: boolean) => `
Initializing...
-
@@ -163,54 +181,15 @@ const getHtml = (isTelemetryEnabled: boolean) => `
-
- -
+ + + + ${getSkillsHtml()} + ${getPluginsHtml()} + - ${getScript(isTelemetryEnabled)} + + ${getScript(isTelemetryEnabled, opencreditsApiUrl, opencreditsWebUrl, opencreditsPublishableKey)} - - ${isTelemetryEnabled ? '' : ''} + ${isTelemetryEnabled ? '' : ''} `; diff --git a/tsconfig.json b/tsconfig.json index 78af3c7..1f7ad51 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,8 +6,10 @@ "lib": [ "ES2022" ], + "types": ["node", "mocha"], "sourceMap": true, "rootDir": "src", + "resolveJsonModule": true, "strict": true, /* enable all strict type-checking options */ /* Additional Checks */ // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ @@ -16,6 +18,7 @@ }, "exclude": [ "mcp-permissions.js", - "claude-code-chat-permissions-mcp" + "claude-code-chat-permissions-mcp", + "backup-files" ] }