diff --git a/build/open-vsix/build.sh b/build/open-vsix/build.sh index fef8675..91690b2 100755 --- a/build/open-vsix/build.sh +++ b/build/open-vsix/build.sh @@ -6,7 +6,7 @@ set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -VERSION="2.0.5" +VERSION="2.0.6" OUTPUT_NAME="vsix-claude-code-chat-${VERSION}.vsix" echo "Building Open VSIX version ${VERSION}..." diff --git a/package-lock.json b/package-lock.json index 82a87fc..25a657f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "claude-code-chat", - "version": "1.0.0", + "version": "2.0.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claude-code-chat", - "version": "1.0.0", + "version": "2.0.6", "license": "SEE LICENSE IN LICENSE", "devDependencies": { "@types/mocha": "^10.0.10", diff --git a/package.json b/package.json index 638cb28..fee9c4a 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": "2.0.5", + "version": "2.0.6", "publisher": "AndrePimenta", "author": "Andre Pimenta", "repository": { @@ -162,8 +162,8 @@ }, "claudeCodeChat.wsl.nodePath": { "type": "string", - "default": "/usr/bin/node", - "description": "Path to Node.js in the WSL distribution" + "default": "", + "description": "Optional path to Node.js in the WSL distribution. Only needed if Claude was installed via npm. Recent Claude installs ship as a native executable and don't require Node." }, "claudeCodeChat.wsl.claudePath": { "type": "string", diff --git a/src/extension.ts b/src/extension.ts index 589c998..efd0f18 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -2,6 +2,7 @@ import * as vscode from 'vscode'; import * as cp from 'child_process'; import * as util from 'util'; import * as path from 'path'; +import * as fs from 'fs'; import getHtml from './ui'; import { startRouter, stopRouter, setModelConfig, setBaseUrl } from './router'; import { fetchAndResolveModels } from './model-updater'; @@ -481,7 +482,7 @@ class ClaudeChatProvider { this._dismissWSLAlert(); return; case 'runInstallCommand': - this._runInstallCommand(); + this._runInstallCommand(message.method || 'installer'); return; case 'openLoginTerminal': this._openLoginTerminal(); @@ -971,7 +972,7 @@ class ClaudeChatProvider { const wslEnabled = config.get('wsl.enabled', false); const wslDistro = config.get('wsl.distro', 'Ubuntu'); - const nodePath = config.get('wsl.nodePath', '/usr/bin/node'); + const nodePath = config.get('wsl.nodePath', ''); const claudePath = config.get('wsl.claudePath', '/usr/local/bin/claude'); const routerExplicitlyEnabled = config.get('router.enabled', false); const customExecutablePath = config.get('executable.path', ''); @@ -1034,8 +1035,7 @@ class ClaudeChatProvider { .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}`; + const wslCommand = envPrefix + this._buildWslClaudeCommand(nodePath, claudePath, args); // Track WSL state for proper process termination this._isWslProcess = true; @@ -1244,11 +1244,19 @@ class ClaudeChatProvider { }); if (code !== 0 && errorOutput.trim()) { - // Error with output - this._sendAndSaveMessage({ - type: 'error', - data: errorOutput.trim() - }); + // Check if claude command is not installed (Windows cmd.exe) + if (errorOutput.includes('not recognized as an internal or external command')) { + this._postMessage({ + type: 'showInstallModal', + installAttempted: !!this._context.globalState.get('installAttempted') + }); + } else { + // Error with output + this._sendAndSaveMessage({ + type: 'error', + data: errorOutput.trim() + }); + } } }); @@ -1278,9 +1286,10 @@ class ClaudeChatProvider { }); // Check if claude command is not installed - if (error.message.includes('ENOENT') || error.message.includes('command not found')) { + if (error.message.includes('ENOENT') || error.message.includes('command not found') || error.message.includes('not recognized as an internal or external command')) { this._postMessage({ - type: 'showInstallModal' + type: 'showInstallModal', + installAttempted: !!this._context.globalState.get('installAttempted') }); } else { this._sendAndSaveMessage({ @@ -3336,7 +3345,7 @@ class ClaudeChatProvider { 'thinking.intensity': config.get('thinking.intensity', 'think'), 'wsl.enabled': config.get('wsl.enabled', false), 'wsl.distro': config.get('wsl.distro', 'Ubuntu'), - 'wsl.nodePath': config.get('wsl.nodePath', '/usr/bin/node'), + 'wsl.nodePath': config.get('wsl.nodePath', ''), 'wsl.claudePath': config.get('wsl.claudePath', '/usr/local/bin/claude'), 'permissions.yoloMode': config.get('permissions.yoloMode', false), 'router.enabled': config.get('router.enabled', false), @@ -3561,13 +3570,20 @@ class ClaudeChatProvider { }); } - private _openModelTerminal(): 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'); + private _quoteBashArg(value: string): string { + return `'${value.replace(/'/g, `'\"'\"'`)}'`; + } + private _buildWslClaudeCommand(nodePath: string, claudePath: string, args: string[] = []): string { + const trimmedNodePath = nodePath.trim(); + const commandParts = trimmedNodePath + ? [this._quoteBashArg(trimmedNodePath), '--no-warnings', '--enable-source-maps', this._quoteBashArg(claudePath)] + : [this._quoteBashArg(claudePath)]; + const quotedArgs = args.map(arg => this._quoteBashArg(arg)); + return [...commandParts, ...quotedArgs].join(' '); + } + + private _openModelTerminal(): void { // Build command arguments const args = ['/model']; @@ -3576,16 +3592,12 @@ class ClaudeChatProvider { args.push('--resume', this._currentSessionId); } - // Create terminal with the claude /model command + // Launch claude as the terminal process directly — no shell quoting const terminal = vscode.window.createTerminal({ name: 'Claude Model Selection', - location: { viewColumn: vscode.ViewColumn.One } + location: { viewColumn: vscode.ViewColumn.One }, + ...this._buildClaudeTerminalOptions(args) }); - if (wslEnabled) { - terminal.sendText(`wsl -d ${wslDistro} ${nodePath} --no-warnings --enable-source-maps ${claudePath} ${args.join(' ')}`); - } else { - terminal.sendText(`claude ${args.join(' ')}`); - } terminal.show(); // Show info message @@ -3601,78 +3613,121 @@ class ClaudeChatProvider { }); } - private _openUsageTerminal(usageType: string): void { - // Get WSL configuration - const config = vscode.workspace.getConfiguration('claudeCodeChat'); - const wslEnabled = config.get('wsl.enabled', false); - const wslDistro = config.get('wsl.distro', 'Ubuntu'); - + private _openUsageTerminal(_usageType: string): void { const terminal = vscode.window.createTerminal({ name: 'Claude Usage', - location: { viewColumn: vscode.ViewColumn.One } + location: { viewColumn: vscode.ViewColumn.One }, + ...this._buildClaudeTerminalOptions(['/usage']) }); - - let command: string; - if (usageType === 'plan') { - // Plan users get live usage view - command = 'npx -y ccusage blocks --live'; - } else { - // API users get recent usage history - command = 'npx -y ccusage blocks --recent --order desc'; - } - - if (wslEnabled) { - terminal.sendText(`wsl -d ${wslDistro} bash -ic "${command}"`); - } else { - terminal.sendText(command); - } - terminal.show(); } - private _runInstallCommand(): void { - // 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) { + private _runInstallCommand(method: string = 'installer'): void { + let command: string; + if (method === 'npm') { + command = 'npm install -g @anthropic-ai/claude-code'; + } else if (process.platform === 'win32') { + command = 'powershell.exe -Command "irm https://claude.ai/install.ps1 | iex"'; + } else { + command = 'curl -fsSL https://claude.ai/install.sh | bash'; + } + + // Track that user has attempted install at least once + this._context.globalState.update('installAttempted', true); + + cp.exec(command, { timeout: 600000 }, async (error) => { + if (error) { 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.' + error: 'Installation failed. Please run in terminal: ' + command, + method: method }); return; } - cp.exec('npm install -g @anthropic-ai/claude-code', { timeout: 120000 }, (error) => { - if (error) { - this._postMessage({ - type: 'installComplete', - success: false, - error: 'Installation failed. Please run in terminal: npm install -g @anthropic-ai/claude-code' - }); - } else { - this._postMessage({ type: 'installComplete', success: true }); + const available = await this._checkClaudeAvailable(); + if (available) { + this._postMessage({ type: 'installComplete', success: true, method }); + return; + } + + const installLocation = this._getKnownInstallLocation(); + if (!fs.existsSync(installLocation)) { + this._postMessage({ + type: 'installComplete', + success: true, + method, + notOnPath: true, + installLocation + }); + return; + } + + const config = vscode.workspace.getConfiguration('claudeCodeChat'); + const existing = config.get('executable.path', ''); + if (!existing) { + try { + await config.update('executable.path', installLocation, vscode.ConfigurationTarget.Global); + } catch { + // fall through; UI will guide user } + } + this._postMessage({ + type: 'installComplete', + success: true, + method, + configuredPath: installLocation, + existingPathRespected: !!existing }); }); } - private _openLoginTerminal(): void { + private _checkClaudeAvailable(): Promise { + const config = vscode.workspace.getConfiguration('claudeCodeChat'); + if (config.get('wsl.enabled', false)) { + return Promise.resolve(true); + } + const probe = process.platform === 'win32' ? 'where claude' : 'command -v claude'; + return new Promise((resolve) => { + cp.exec(probe, { env: process.env }, (err) => resolve(!err)); + }); + } + + private _getKnownInstallLocation(): string { + const homeDir = process.env.HOME || process.env.USERPROFILE || ''; + const binary = process.platform === 'win32' ? 'claude.exe' : 'claude'; + return path.join(homeDir, '.local', 'bin', binary); + } + + private _buildClaudeTerminalOptions(args: string[] = []): { shellPath: string; shellArgs: string[] } { 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'); + const wslDistro = config.get('wsl.distro', 'Ubuntu'); + const nodePath = config.get('wsl.nodePath', ''); + const claudePath = config.get('wsl.claudePath', '/usr/local/bin/claude'); + const wslCommand = this._buildWslClaudeCommand(nodePath, claudePath, args); + return { + shellPath: process.platform === 'win32' ? 'wsl.exe' : 'wsl', + shellArgs: ['-d', wslDistro, 'bash', '-ic', wslCommand] + }; } + + const custom = (config.get('executable.path', '') || '').trim(); + return { + shellPath: custom || 'claude', + shellArgs: args + }; + } + + private _openLoginTerminal(): void { + const terminal = vscode.window.createTerminal({ + name: 'Claude Login', + location: { viewColumn: vscode.ViewColumn.One }, + ...this._buildClaudeTerminalOptions() + }); terminal.show(); } @@ -3703,12 +3758,6 @@ class ClaudeChatProvider { return; } - 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'); - // Build command arguments const args = [`/${command}`]; @@ -3717,16 +3766,12 @@ class ClaudeChatProvider { args.push('--resume', this._currentSessionId); } - // Create terminal with the claude command + // Launch claude as the terminal process directly — no shell quoting const terminal = vscode.window.createTerminal({ name: `Claude /${command}`, - location: { viewColumn: vscode.ViewColumn.One } + location: { viewColumn: vscode.ViewColumn.One }, + ...this._buildClaudeTerminalOptions(args) }); - if (wslEnabled) { - terminal.sendText(`wsl -d ${wslDistro} ${nodePath} --no-warnings --enable-source-maps ${claudePath} ${args.join(' ')}`); - } else { - terminal.sendText(`claude ${args.join(' ')}`); - } terminal.show(); // Show info message @@ -3945,4 +3990,4 @@ class ClaudeChatProvider { } } } -} \ No newline at end of file +} diff --git a/src/script.ts b/src/script.ts index d91425b..8e4d628 100644 --- a/src/script.ts +++ b/src/script.ts @@ -78,6 +78,7 @@ const getScript = (isTelemetryEnabled: boolean, opencreditsApiUrl: string = 'htt let selectedFileIndex = -1; let planModeEnabled = false; let thinkingModeEnabled = false; + let isWindows = false; let lastPendingEditIndex = -1; // Track the last Edit/MultiEdit/Write toolUse without result let lastPendingEditData = null; // Store diff data for the pending edit { filePath, oldContent, newContent } let attachedImages = []; // Array of { filePath, previewUri } @@ -2800,7 +2801,7 @@ const getScript = (isTelemetryEnabled: boolean, opencreditsApiUrl: string = 'htt } // Install modal functions - function showInstallModal() { + function showInstallModal(installAttempted) { const modal = document.getElementById('installModal'); const main = document.getElementById('installMain'); const progress = document.getElementById('installProgress'); @@ -2813,7 +2814,30 @@ const getScript = (isTelemetryEnabled: boolean, opencreditsApiUrl: string = 'htt if (success) success.style.display = 'none'; if (checkout) checkout.style.display = 'none'; - sendStats('Install modal shown'); + // Show "Didn't work? Try with npm" only if user already attempted install once + var retryOptions = document.getElementById('installRetryOptions'); + if (retryOptions) retryOptions.style.display = installAttempted ? 'block' : 'none'; + + // Show sudo checkbox only on macOS/Linux + var sudoLabel = document.getElementById('installSudoLabel'); + if (sudoLabel) sudoLabel.style.display = (installAttempted && !isWindows) ? 'inline-block' : 'none'; + + sendStats('Install modal shown', installAttempted ? { retryShown: true } : undefined); + } + + function startInstallationWithSudo() { + var useSudo = document.getElementById('installUseSudo') && document.getElementById('installUseSudo').checked; + if (useSudo) { + sendStats('Install started', { method: 'npm-sudo' }); + vscode.postMessage({ + type: 'runTerminalCommand', + command: 'sudo npm install -g @anthropic-ai/claude-code' + }); + // Close the modal — user will complete install in terminal + hideInstallModal(); + } else { + startInstallation('npm'); + } } function showLoginOptionsModal() { @@ -2857,8 +2881,8 @@ const getScript = (isTelemetryEnabled: boolean, opencreditsApiUrl: string = 'htt } } - function startInstallation() { - sendStats('Install started'); + function startInstallation(method) { + sendStats('Install started', { method: method || 'installer' }); // Hide main content, show progress document.getElementById('installMain').style.display = 'none'; @@ -2866,11 +2890,12 @@ const getScript = (isTelemetryEnabled: boolean, opencreditsApiUrl: string = 'htt // Extension handles platform detection and command selection vscode.postMessage({ - type: 'runInstallCommand' + type: 'runInstallCommand', + method: method || 'installer' }); } - function handleInstallComplete(success, error) { + function handleInstallComplete(success, error, extra) { document.getElementById('installProgress').style.display = 'none'; const successEl = document.getElementById('installSuccess'); @@ -2881,9 +2906,23 @@ const getScript = (isTelemetryEnabled: boolean, opencreditsApiUrl: string = 'htt if (ocOption) ocOption.style.display = opencreditsEnabled ? '' : 'none'; if (success) { - sendStats('Install success'); - successEl.querySelector('.install-success-text').textContent = 'Installed'; - successEl.querySelector('.install-success-hint').textContent = 'Send a message to get started'; + if (extra && extra.configuredPath) { + sendStats('Install auto configured path', { existingPathRespected: !!extra.existingPathRespected }); + successEl.querySelector('.install-success-text').textContent = 'Installed'; + const hint = extra.existingPathRespected + ? 'Claude was installed but not on your PATH. Your existing executable.path setting was left unchanged.' + : 'Configured automatically. Send a message to get started.'; + successEl.querySelector('.install-success-hint').textContent = hint; + } else if (extra && extra.notOnPath) { + sendStats('Install location not found'); + successEl.querySelector('.install-success-text').textContent = 'Installed'; + successEl.querySelector('.install-success-hint').textContent = + 'Claude was installed but could not be located. Set claudeCodeChat.executable.path manually to your claude binary.'; + } else { + sendStats('Install success'); + successEl.querySelector('.install-success-text').textContent = 'Installed'; + successEl.querySelector('.install-success-hint').textContent = 'Send a message to get started'; + } } else { sendStats('Install failed', { error: (error || 'Unknown error').substring(0, 200) }); // Show error state @@ -3702,12 +3741,17 @@ const getScript = (isTelemetryEnabled: boolean, opencreditsApiUrl: string = 'htt case 'showInstallModal': sendStats('Claude not installed'); - showInstallModal(); + showInstallModal(message.installAttempted); updateStatus('Claude Code not installed', 'error'); break; case 'installComplete': - handleInstallComplete(message.success, message.error); + handleInstallComplete(message.success, message.error, { + configuredPath: message.configuredPath, + notOnPath: message.notOnPath, + installLocation: message.installLocation, + existingPathRespected: message.existingPathRespected + }); if (message.success) { updateStatus('Ready', 'success'); } @@ -4789,7 +4833,7 @@ const getScript = (isTelemetryEnabled: boolean, opencreditsApiUrl: string = 'htt settings: { 'wsl.enabled': wslEnabled, 'wsl.distro': wslDistro || 'Ubuntu', - 'wsl.nodePath': wslNodePath || '/usr/bin/node', + 'wsl.nodePath': wslNodePath, 'wsl.claudePath': wslClaudePath || '/usr/local/bin/claude', 'permissions.yoloMode': yoloMode, 'executable.path': executablePath, @@ -5067,7 +5111,7 @@ const getScript = (isTelemetryEnabled: boolean, opencreditsApiUrl: string = 'htt document.getElementById('wsl-enabled').checked = message.data['wsl.enabled'] || false; document.getElementById('wsl-distro').value = message.data['wsl.distro'] || 'Ubuntu'; - document.getElementById('wsl-node-path').value = message.data['wsl.nodePath'] || '/usr/bin/node'; + document.getElementById('wsl-node-path').value = message.data['wsl.nodePath'] ?? ''; document.getElementById('wsl-claude-path').value = message.data['wsl.claudePath'] || '/usr/local/bin/claude'; document.getElementById('yolo-mode').checked = message.data['permissions.yoloMode'] || false; @@ -5228,6 +5272,7 @@ const getScript = (isTelemetryEnabled: boolean, opencreditsApiUrl: string = 'htt } if (message.type === 'platformInfo') { + isWindows = !!message.data.isWindows; // Check if user is on Windows and show WSL alert if not dismissed and WSL not already enabled if (message.data.isWindows && !message.data.wslAlertDismissed && !message.data.wslEnabled) { // Small delay to ensure UI is ready @@ -5247,4 +5292,4 @@ const getScript = (isTelemetryEnabled: boolean, opencreditsApiUrl: string = 'htt ${getPluginsScript()} ` -export default getScript; \ No newline at end of file +export default getScript; diff --git a/src/ui.ts b/src/ui.ts index 8e9525d..647a874 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -307,14 +307,6 @@ const getHtml = (isTelemetryEnabled: boolean, opencreditsApiUrl: string = 'https -
- - -

- Find your node installation path in WSL by running: which node -

-
-
@@ -322,6 +314,14 @@ const getHtml = (isTelemetryEnabled: boolean, opencreditsApiUrl: string = 'https Find your claude installation path in WSL by running: which claude

+ +
+ + +

+ Optional. Only needed if you previously installed Claude via npm. Recent Claude installs ship as a native executable and don't need Node. Set it by running: which node +

+
@@ -583,6 +583,15 @@ const getHtml = (isTelemetryEnabled: boolean, opencreditsApiUrl: string = 'https Install Now + + View documentation @@ -1080,4 +1089,4 @@ const getHtml = (isTelemetryEnabled: boolean, opencreditsApiUrl: string = 'https `; -export default getHtml; \ No newline at end of file +export default getHtml;