Replace PATH injection with post-install executable.path auto-config

Drops the cross-platform PATH-injection workaround in favor of probing
claude availability after install and writing the known install location
to claudeCodeChat.executable.path when it's not on PATH. Terminals for
login/model/usage/slash commands now launch claude directly via
createTerminal's shellPath/shellArgs so they work identically across
PowerShell, cmd, bash, and zsh. WSL nodePath is now optional (recent
Claude ships as a native binary) and its field moved below claudePath.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
andrepimenta
2026-04-23 22:44:00 +01:00
parent 3d8bcf5241
commit cb5943eec5
6 changed files with 216 additions and 117 deletions

View File

@@ -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}..."

4
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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<boolean>('wsl.enabled', false);
const wslDistro = config.get<string>('wsl.distro', 'Ubuntu');
const nodePath = config.get<string>('wsl.nodePath', '/usr/bin/node');
const nodePath = config.get<string>('wsl.nodePath', '');
const claudePath = config.get<string>('wsl.claudePath', '/usr/local/bin/claude');
const routerExplicitlyEnabled = config.get<boolean>('router.enabled', false);
const customExecutablePath = config.get<string>('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<string>('thinking.intensity', 'think'),
'wsl.enabled': config.get<boolean>('wsl.enabled', false),
'wsl.distro': config.get<string>('wsl.distro', 'Ubuntu'),
'wsl.nodePath': config.get<string>('wsl.nodePath', '/usr/bin/node'),
'wsl.nodePath': config.get<string>('wsl.nodePath', ''),
'wsl.claudePath': config.get<string>('wsl.claudePath', '/usr/local/bin/claude'),
'permissions.yoloMode': config.get<boolean>('permissions.yoloMode', false),
'router.enabled': config.get<boolean>('router.enabled', false),
@@ -3561,13 +3570,20 @@ class ClaudeChatProvider {
});
}
private _openModelTerminal(): void {
const config = vscode.workspace.getConfiguration('claudeCodeChat');
const wslEnabled = config.get<boolean>('wsl.enabled', false);
const wslDistro = config.get<string>('wsl.distro', 'Ubuntu');
const nodePath = config.get<string>('wsl.nodePath', '/usr/bin/node');
const claudePath = config.get<string>('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<boolean>('wsl.enabled', false);
const wslDistro = config.get<string>('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<string>('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<boolean> {
const config = vscode.workspace.getConfiguration('claudeCodeChat');
if (config.get<boolean>('wsl.enabled', false)) {
return Promise.resolve(true);
}
const probe = process.platform === 'win32' ? 'where claude' : 'command -v claude';
return new Promise<boolean>((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<boolean>('wsl.enabled', false);
const wslDistro = config.get<string>('wsl.distro', 'Ubuntu');
const nodePath = config.get<string>('wsl.nodePath', '/usr/bin/node');
const claudePath = config.get<string>('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<string>('wsl.distro', 'Ubuntu');
const nodePath = config.get<string>('wsl.nodePath', '');
const claudePath = config.get<string>('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<string>('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<boolean>('wsl.enabled', false);
const wslDistro = config.get<string>('wsl.distro', 'Ubuntu');
const nodePath = config.get<string>('wsl.nodePath', '/usr/bin/node');
const claudePath = config.get<string>('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 {
}
}
}
}
}

View File

@@ -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()}
</script>`
export default getScript;
export default getScript;

View File

@@ -307,14 +307,6 @@ const getHtml = (isTelemetryEnabled: boolean, opencreditsApiUrl: string = 'https
<input type="text" id="wsl-distro" class="file-search-input" style="width: 100%;" placeholder="Ubuntu" onchange="updateSettings()">
</div>
<div style="margin-bottom: 12px;">
<label style="display: block; margin-bottom: 4px; font-size: 12px; color: var(--vscode-descriptionForeground);">Node.js Path in WSL</label>
<input type="text" id="wsl-node-path" class="file-search-input" style="width: 100%;" placeholder="/usr/bin/node" onchange="updateSettings()">
<p style="font-size: 11px; color: var(--vscode-descriptionForeground); margin: 4px 0 0 0;">
Find your node installation path in WSL by running: <code style="background: var(--vscode-textCodeBlock-background); padding: 2px 4px; border-radius: 3px;">which node</code>
</p>
</div>
<div style="margin-bottom: 12px;">
<label style="display: block; margin-bottom: 4px; font-size: 12px; color: var(--vscode-descriptionForeground);">Claude Path in WSL</label>
<input type="text" id="wsl-claude-path" class="file-search-input" style="width: 100%;" placeholder="/usr/local/bin/claude" onchange="updateSettings()">
@@ -322,6 +314,14 @@ const getHtml = (isTelemetryEnabled: boolean, opencreditsApiUrl: string = 'https
Find your claude installation path in WSL by running: <code style="background: var(--vscode-textCodeBlock-background); padding: 2px 4px; border-radius: 3px;">which claude</code>
</p>
</div>
<div style="margin-bottom: 12px;">
<label style="display: block; margin-bottom: 4px; font-size: 12px; color: var(--vscode-descriptionForeground);">Node.js Path in WSL (Optional)</label>
<input type="text" id="wsl-node-path" class="file-search-input" style="width: 100%;" placeholder="/usr/bin/node" onchange="updateSettings()">
<p style="font-size: 11px; color: var(--vscode-descriptionForeground); margin: 4px 0 0 0;">
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: <code style="background: var(--vscode-textCodeBlock-background); padding: 2px 4px; border-radius: 3px;">which node</code>
</p>
</div>
</div>
</div>
@@ -583,6 +583,15 @@ const getHtml = (isTelemetryEnabled: boolean, opencreditsApiUrl: string = 'https
Install Now
</button>
<div id="installRetryOptions" style="display: none; margin-top: 8px;">
<button class="install-link" id="installRetryNpmBtn" onclick="startInstallationWithSudo()" style="background: none; border: none; color: var(--vscode-textLink-foreground); cursor: pointer; text-decoration: underline; padding: 4px;">
Didn't work? Try with npm
</button>
<label id="installSudoLabel" style="display: none; margin-left: 10px; font-size: 11px; color: var(--vscode-descriptionForeground); cursor: pointer;">
<input type="checkbox" id="installUseSudo" style="vertical-align: middle;"> Use sudo
</label>
</div>
<a href="https://docs.anthropic.com/en/docs/claude-code" target="_blank" class="install-link">
View documentation
</a>
@@ -1080,4 +1089,4 @@ const getHtml = (isTelemetryEnabled: boolean, opencreditsApiUrl: string = 'https
</body>
</html>`;
export default getHtml;
export default getHtml;