From 89dcf1f81ed01f274b54a367092fc49702c479b1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 20 Apr 2026 11:11:24 +0000
Subject: [PATCH 1/9] Initial plan
From 1c6a368e83d8e6da1d9fed4eea35af45c2b2d686 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 20 Apr 2026 11:24:18 +0000
Subject: [PATCH 2/9] feat: add configurable shell selection with
powershell-first defaults
Agent-Logs-Url: https://github.com/codomium/FreeCode/sessions/8072f19d-aa04-4bb8-ac06-e402de917903
Co-authored-by: codomium <255525663+codomium@users.noreply.github.com>
---
electron-app/main.js | 236 +++++++++++++++++++++++--------
electron-app/renderer/chat.css | 10 ++
electron-app/renderer/chat.js | 32 ++++-
electron-app/renderer/index.html | 23 +++
vscode-extension/extension.js | 144 +++++++++++++------
vscode-extension/package.json | 21 +++
6 files changed, 368 insertions(+), 98 deletions(-)
diff --git a/electron-app/main.js b/electron-app/main.js
index d4ef330..946c008 100644
--- a/electron-app/main.js
+++ b/electron-app/main.js
@@ -24,7 +24,7 @@ const path = require('path');
const fs = require('fs');
const os = require('os');
const crypto = require('crypto');
-const { execFile } = require('child_process');
+const { execFile, spawnSync } = require('child_process');
const { pathToFileURL } = require('url');
// ── Constants ─────────────────────────────────────────────────────────────────
@@ -99,6 +99,7 @@ const DEFAULT_SETTINGS = {
autoAttachActiveFile: false,
pinnedFiles: [],
workspacePath: os.homedir(),
+ defaultShell: 'auto', // 'auto' | 'powershell' | 'wsl' | 'ubuntu' | 'bash' | 'cmd'
customProviders: [], // [{ id, name, baseUrl, apiKey, models:[{id,name}], headers:[{name,value}] }]
};
@@ -668,6 +669,7 @@ ipcMain.on('renderer-message', async (event, msg) => {
activeSession: activeMessages,
activeSessionId,
workspacePath: s.workspacePath || os.homedir(),
+ defaultShell: s.defaultShell || 'auto',
// full settings for settings panel
maxTurns: s.maxTurns || 20,
showToolOutput: s.showToolOutput !== false,
@@ -1084,7 +1086,7 @@ ipcMain.on('renderer-message', async (event, msg) => {
// ── Save a single setting ─────────────────────────────────────────────
case 'saveSettings': {
- const allowed = ['model','permissionMode','maxTurns','showToolOutput','nvidiaApiKey','workspacePath'];
+ const allowed = ['model','permissionMode','maxTurns','showToolOutput','nvidiaApiKey','workspacePath','defaultShell'];
if (msg.key && allowed.includes(msg.key)) {
saveSetting(msg.key, msg.value);
applyEnvFromSettings();
@@ -1101,7 +1103,68 @@ ipcMain.on('renderer-message', async (event, msg) => {
path: msg.value,
});
}
+ if (msg.key === 'defaultShell') {
+ _preferredShell = null;
+ const sh = detectPreferredShell();
+ send({ type: 'shellInfo', shell: sh.type });
+ }
+ }
+ break;
+ }
+
+ case 'detectShells': {
+ const results = {};
+
+ try {
+ const r = spawnSync('powershell.exe', ['-NoProfile', '-Command', 'echo ok'], {
+ encoding: 'utf-8', timeout: 2000, windowsHide: true,
+ });
+ results.powershell = r.status === 0 && String(r.stdout || '').trim() === 'ok';
+ } catch {
+ results.powershell = false;
+ }
+
+ try {
+ const r = spawnSync('wsl.exe', ['--status'], {
+ encoding: 'utf-8', timeout: 3000, windowsHide: true,
+ });
+ results.wsl = r.status === 0;
+ } catch {
+ results.wsl = false;
+ }
+
+ try {
+ const r = spawnSync('wsl.exe', ['-d', 'Ubuntu', '--', 'echo', 'ok'], {
+ encoding: 'utf-8', timeout: 3000, windowsHide: true,
+ });
+ results.ubuntu = r.status === 0 && String(r.stdout || '').trim() === 'ok';
+ } catch {
+ results.ubuntu = false;
+ }
+
+ if (process.platform !== 'win32') {
+ try {
+ const r = spawnSync('bash', ['-c', 'echo ok'], {
+ encoding: 'utf-8', timeout: 1000,
+ });
+ results.bash = r.status === 0;
+ } catch {
+ results.bash = false;
+ }
+ } else {
+ results.bash = false;
+ }
+
+ try {
+ const r = spawnSync('cmd.exe', ['/c', 'echo ok'], {
+ encoding: 'utf-8', timeout: 2000, windowsHide: true,
+ });
+ results.cmd = r.status === 0;
+ } catch {
+ results.cmd = false;
}
+
+ send({ type: 'shellsDetected', shells: results });
break;
}
@@ -1262,44 +1325,114 @@ ipcMain.on('renderer-message', async (event, msg) => {
// ── Handle run-in-terminal ────────────────────────────────────────────────────
/**
- * Detect the preferred shell on Windows.
- * Priority: Ubuntu WSL distro → generic WSL → PowerShell → bash (non-Windows).
+ * Detect the preferred shell.
+ * Uses explicit user preference when set; otherwise auto-detects
+ * with PowerShell-first behavior on Windows.
* Result is cached after first call.
*/
let _preferredShell = null;
+
+const POWERSHELL_SHIMS_MODULE = `# freecode-shims.psm1 — FreeCode PowerShell POSIX compatibility shims
+function which { param([string]$cmd) $r = Get-Command $cmd -EA SilentlyContinue; if ($r) { $r.Source } else { Write-Error "which: $cmd: not found"; exit 1 } }
+function grep { param([string]$pat, [Parameter(ValueFromRemainingArguments)][string[]]$f) if ($f) { Select-String -Pattern $pat -Path $f | ForEach-Object { "$($_.Path):$($_.LineNumber):$($_.Line)" } } else { $input | Select-String -Pattern $pat | ForEach-Object { $_.Line } } }
+function cat { param([Parameter(ValueFromRemainingArguments)][string[]]$paths) if ($paths) { Get-Content $paths } else { $input } }
+function touch { param([string]$p) if (Test-Path $p) { (Get-Item $p).LastWriteTime = Get-Date } else { New-Item -ItemType File -Path $p -Force | Out-Null } }
+function wc { param([string]$flag="-l") $lines = @($input); switch ($flag) { "-l" { $lines.Count } "-c" { ($lines -join "\`n").Length } "-w" { ($lines -join " ").Split() | Where-Object { $_ } | Measure-Object | Select-Object -Expand Count } default { $lines.Count } } }
+function head { param([int]$n=10, [string]$file="") if ($file) { Get-Content $file -TotalCount $n } else { $input | Select-Object -First $n } }
+function tail { param([int]$n=10, [string]$file="") if ($file) { Get-Content $file -Tail $n } else { $input | Select-Object -Last $n } }
+function find { param([string]$path=".", [string]$name="*") Get-ChildItem -Path $path -Recurse -Filter $name -ErrorAction SilentlyContinue | Select-Object -Expand FullName }
+function pwd { (Get-Location).Path }
+function ls { param([Parameter(ValueFromRemainingArguments)][string[]]$a) Get-ChildItem @a }
+function mkdir { param([Parameter(ValueFromRemainingArguments)][string[]]$a) New-Item -ItemType Directory @a -Force }
+function rm { param([switch]$r, [switch]$f, [Parameter(ValueFromRemainingArguments)][string[]]$paths) Remove-Item $paths -Recurse:$r -Force:$f -ErrorAction SilentlyContinue }
+function cp { param([string]$src, [string]$dst) Copy-Item -Path $src -Destination $dst -Recurse -Force }
+function mv { param([string]$src, [string]$dst) Move-Item -Path $src -Destination $dst -Force }
+function echo { param([Parameter(ValueFromRemainingArguments)][string[]]$a) Write-Output ($a -join " ") }
+function sed { param([string]$expr, [string]$file="") if ($expr -match "^s/(.+)/(.+)/") { $pat=$Matches[1]; $rep=$Matches[2]; if ($file) { (Get-Content $file) -replace $pat,$rep | Set-Content $file } else { $input | ForEach-Object { $_ -replace $pat,$rep } } } }
+function awk { param([string]$prog, [Parameter(ValueFromRemainingArguments)][string[]]$files) Write-Warning "awk: limited PowerShell shim — consider installing gawk via winget" }
+Export-ModuleMember -Function *
+`;
+
+function resolveShellByName(name) {
+ const map = {
+ powershell: {
+ type: 'powershell',
+ exe: 'powershell.exe',
+ args: ['-NoProfile', '-NonInteractive', '-Command'],
+ syntax: 'powershell',
+ },
+ wsl: {
+ type: 'wsl',
+ exe: 'wsl.exe',
+ args: ['bash', '-c'],
+ syntax: 'bash',
+ },
+ ubuntu: {
+ type: 'ubuntu',
+ exe: 'wsl.exe',
+ args: ['-d', 'Ubuntu', '--', 'bash', '-c'],
+ syntax: 'bash',
+ },
+ bash: {
+ type: 'bash',
+ exe: 'bash',
+ args: ['-c'],
+ syntax: 'bash',
+ },
+ cmd: {
+ type: 'cmd',
+ exe: 'cmd.exe',
+ args: ['/c'],
+ syntax: 'cmd',
+ },
+ };
+ return map[name] || map.powershell;
+}
+
+function isPowerShellAvailable() {
+ try {
+ const r = spawnSync('powershell.exe', ['-NoProfile', '-Command', 'exit 0'], {
+ encoding: 'utf-8', timeout: 2000, windowsHide: true,
+ });
+ return r.status === 0;
+ } catch {
+ return false;
+ }
+}
+
+function ensurePowerShellShimsModule() {
+ const modPath = path.join(getUserData(), 'freecode-shims.psm1');
+ if (!fs.existsSync(modPath)) {
+ fs.writeFileSync(modPath, POWERSHELL_SHIMS_MODULE, 'utf8');
+ }
+ return modPath;
+}
+
+function quotePowerShellPath(p) {
+ return String(p || '').replace(/'/g, "''");
+}
+
function detectPreferredShell() {
if (_preferredShell) return _preferredShell;
- if (process.platform !== 'win32') {
- _preferredShell = { type: 'bash', exe: 'bash', args: [] };
+
+ const s = getSettings();
+ const pref = s.defaultShell || 'auto';
+ if (pref !== 'auto') {
+ _preferredShell = resolveShellByName(pref);
return _preferredShell;
}
- const { spawnSync } = require('child_process');
-
- // Try Ubuntu WSL distro first
- try {
- const r = spawnSync('wsl.exe', ['-d', 'Ubuntu', '--', 'echo', 'ok'], {
- encoding: 'utf-8', timeout: 4000, windowsHide: true,
- });
- if (r.status === 0 && (r.stdout || '').trim() === 'ok') {
- _preferredShell = { type: 'ubuntu', exe: 'wsl.exe', args: ['-d', 'Ubuntu', '--', 'bash', '-c'] };
- return _preferredShell;
- }
- } catch { /* ignore */ }
+ if (process.platform !== 'win32') {
+ _preferredShell = resolveShellByName('bash');
+ return _preferredShell;
+ }
- // Try generic WSL
- try {
- const r = spawnSync('wsl.exe', ['--status'], {
- encoding: 'utf-8', timeout: 4000, windowsHide: true,
- });
- if (r.status === 0) {
- _preferredShell = { type: 'wsl', exe: 'wsl.exe', args: ['bash', '-c'] };
- return _preferredShell;
- }
- } catch { /* ignore */ }
+ if (isPowerShellAvailable()) {
+ _preferredShell = resolveShellByName('powershell');
+ return _preferredShell;
+ }
- // Fall back to PowerShell
- _preferredShell = { type: 'powershell', exe: 'powershell.exe', args: ['-NoProfile', '-NonInteractive', '-Command'] };
+ _preferredShell = resolveShellByName('cmd');
return _preferredShell;
}
@@ -1319,6 +1452,10 @@ function handleRunInTerminal(code) {
spawn('wsl.exe', [], {
cwd, detached: true, stdio: 'ignore', windowsHide: false,
});
+ } else if (shell.type === 'cmd') {
+ spawn('cmd.exe', ['/k', code], {
+ cwd, detached: true, stdio: 'ignore', windowsHide: false,
+ });
} else {
spawn('powershell.exe', [
'-NoProfile', '-NoExit', '-Command', code,
@@ -1342,30 +1479,15 @@ function handleTerminalRun(command, reqId, send) {
const cwd = getSettings().workspacePath || os.homedir();
const shell = detectPreferredShell();
- let shellExe, shellArgs;
- if (shell.type === 'ubuntu') {
- shellExe = 'wsl.exe';
- shellArgs = ['-d', 'Ubuntu', '--', 'bash', '-c', command];
- } else if (shell.type === 'wsl') {
- shellExe = 'wsl.exe';
- shellArgs = ['bash', '-c', command];
- } else if (shell.type === 'powershell') {
- // Inject POSIX shims so common Unix tools work in PS
- const TERMINAL_PS_SHIMS = [
- 'function which { param([string]$cmd) $r = Get-Command $cmd -ErrorAction SilentlyContinue; if ($r) { $r.Source } else { Write-Error "which: $cmd not found" } }',
- 'function grep { param([string]$pattern, [Parameter(ValueFromRemainingArguments)][string[]]$paths) if ($paths) { Select-String -Pattern $pattern -Path $paths | ForEach-Object { "$($_.Path):$($_.LineNumber):$($_.Line)" } } else { $input | Select-String -Pattern $pattern | ForEach-Object { $_.Line } } }',
- 'function cat { param([Parameter(ValueFromRemainingArguments)][string[]]$paths) Get-Content $paths }',
- 'function touch { param([string]$path) if (Test-Path $path) { (Get-Item $path).LastWriteTime = Get-Date } else { New-Item -ItemType File -Path $path -Force | Out-Null } }',
- 'function head { param([string]$flag="", [string]$file="") $n=10; if ($flag -match "^-(\\d+)$") { $n=[int]$Matches[1]; $file="" } elseif ($flag -eq "-n") { $n=[int]$file; $file="" } elseif ($flag -ne "") { $file=$flag }; if ($file) { Get-Content $file | Select-Object -First $n } else { $input | Select-Object -First $n } }',
- 'function tail { param([string]$flag="", [string]$file="") $n=10; if ($flag -match "^-(\\d+)$") { $n=[int]$Matches[1]; $file="" } elseif ($flag -eq "-n") { $n=[int]$file; $file="" } elseif ($flag -ne "") { $file=$flag }; if ($file) { Get-Content $file | Select-Object -Last $n } else { $input | Select-Object -Last $n } }',
- 'function find { param([string]$basePath=".", [string]$nameFlag="", [string]$namePattern="*") if ($nameFlag -ne "-name") { $namePattern=$nameFlag }; Get-ChildItem -Path $basePath -Recurse -Filter $namePattern -Force | ForEach-Object { $_.FullName } }',
- 'function pwd { (Get-Location).Path }',
- ].join('; ');
- shellExe = 'powershell.exe';
- shellArgs = ['-NoProfile', '-NonInteractive', '-Command', `${TERMINAL_PS_SHIMS}; ${command}`];
- } else {
- shellExe = 'bash';
- shellArgs = ['-c', command];
+ let shellExe = shell.exe;
+ let shellArgs = [...shell.args, command];
+ if (shell.type === 'powershell') {
+ const modulePath = ensurePowerShellShimsModule();
+ const prelude = `Import-Module '${quotePowerShellPath(modulePath)}' -Force; `;
+ shellArgs = [...shell.args, `${prelude}${command}`];
+ } else if (shell.type === 'cmd') {
+ shellExe = 'cmd.exe';
+ shellArgs = ['/c', command];
}
const { spawn } = require('child_process');
@@ -1411,7 +1533,9 @@ function handleTerminalRun(command, reqId, send) {
async function handleRunPrompt(message, contextFilePaths, fileRefs, send) {
isCancelled = false;
- let fullPrompt = message;
+ const shellHint = detectPreferredShell();
+ const basePrompt = `[Execution shell: ${shellHint.type} (${shellHint.syntax}). Generate shell commands that match this shell syntax.]\n\n${message}`;
+ let fullPrompt = basePrompt;
// Inject context file contents
const allPaths = new Set(contextFilePaths || []);
@@ -1434,7 +1558,7 @@ async function handleRunPrompt(message, contextFilePaths, fileRefs, send) {
} catch { /* skip unreadable */ }
}
if (fileContents.length > 0) {
- fullPrompt = message + '\n\n[Context files:]' + fileContents.join('');
+ fullPrompt = basePrompt + '\n\n[Context files:]' + fileContents.join('');
}
}
diff --git a/electron-app/renderer/chat.css b/electron-app/renderer/chat.css
index df75246..38fb701 100644
--- a/electron-app/renderer/chat.css
+++ b/electron-app/renderer/chat.css
@@ -2032,6 +2032,11 @@ select:focus { border-color: var(--accent); }
padding: 0 12px 8px;
}
+#setting-shell-availability {
+ flex: 1;
+ padding: 0;
+}
+
.settings-link {
font-size: 11px;
color: var(--accent);
@@ -3205,6 +3210,11 @@ select:focus { border-color: var(--accent); }
border-color: rgba(78,201,176,0.35);
color: #4ec9b0;
}
+.terminal-shell-badge[data-shell="cmd"] {
+ background: rgba(255,176,64,0.12);
+ border-color: rgba(255,176,64,0.35);
+ color: #ffb040;
+}
.terminal-btn {
background: none;
diff --git a/electron-app/renderer/chat.js b/electron-app/renderer/chat.js
index 6b5de08..5a9a6ee 100644
--- a/electron-app/renderer/chat.js
+++ b/electron-app/renderer/chat.js
@@ -196,6 +196,9 @@
const settingMode = document.getElementById('setting-mode');
const settingMaxTurns = document.getElementById('setting-max-turns');
const settingShowToolOutput = document.getElementById('setting-show-tool-output');
+ const settingDefaultShell = document.getElementById('setting-default-shell');
+ const settingShellAvailability = document.getElementById('setting-shell-availability');
+ const settingsDetectShellsBtn = document.getElementById('settings-detect-shells-btn');
const settingsSetKeyBtn = document.getElementById('settings-set-key-btn');
const settingNvidiaKey = document.getElementById('setting-nvidia-key');
const settingsSaveNvidiaBtn = document.getElementById('settings-save-nvidia-btn');
@@ -1953,7 +1956,7 @@
// Update the terminal shell badge to show the active shell type
const badge = document.getElementById('terminal-shell-badge');
if (badge) {
- const labels = { ubuntu: '🐧 Ubuntu', wsl: '⚙ WSL', powershell: '💙 PS', bash: '$ bash' };
+ const labels = { ubuntu: '🐧 Ubuntu', wsl: '⚙ WSL', powershell: '💙 PS', bash: '$ bash', cmd: '⊞ cmd' };
badge.textContent = labels[msg.shell] || msg.shell;
badge.dataset.shell = msg.shell || '';
badge.title = `Active shell: ${msg.shell}`;
@@ -1961,6 +1964,13 @@
break;
}
+ case 'shellsDetected': {
+ if (settingShellAvailability) {
+ settingShellAvailability.textContent = formatShellAvailability(msg.shells || {});
+ }
+ break;
+ }
+
case 'workspaceChanged':
currentWorkspacePath = msg.path || '';
if (settingWorkspace) settingWorkspace.value = currentWorkspacePath;
@@ -3347,6 +3357,7 @@
if (settingMode) settingMode.value = msg.mode || 'default';
if (settingMaxTurns) settingMaxTurns.value = msg.maxTurns || 20;
if (settingShowToolOutput) settingShowToolOutput.checked = msg.showToolOutput !== false;
+ if (settingDefaultShell) settingDefaultShell.value = msg.defaultShell || 'auto';
if (settingNvidiaKey) settingNvidiaKey.placeholder = msg.hasNvidiaKey ? '••••••• (set — enter to change)' : 'nvapi-… (leave blank to clear)';
// Custom providers
if (Array.isArray(msg.customProviders)) {
@@ -3356,6 +3367,11 @@
}
}
+ function formatShellAvailability(shells) {
+ const icon = (ok) => (ok ? '✅' : '❌');
+ return `PowerShell ${icon(!!shells.powershell)} WSL ${icon(!!shells.wsl)} Ubuntu ${icon(!!shells.ubuntu)} Bash ${icon(!!shells.bash)} cmd ${icon(!!shells.cmd)}`;
+ }
+
function openSettingsPanel() {
if (!settingsPanel) return;
settingsPanel.classList.add('visible');
@@ -3402,6 +3418,19 @@
});
}
+ if (settingDefaultShell) {
+ settingDefaultShell.addEventListener('change', () => {
+ vscode.postMessage({ type: 'saveSettings', key: 'defaultShell', value: settingDefaultShell.value || 'auto' });
+ });
+ }
+
+ if (settingsDetectShellsBtn) {
+ settingsDetectShellsBtn.addEventListener('click', () => {
+ if (settingShellAvailability) settingShellAvailability.textContent = 'Detecting…';
+ vscode.postMessage({ type: 'detectShells' });
+ });
+ }
+
if (settingsPickWorkspace) {
settingsPickWorkspace.addEventListener('click', () => {
// Use existing pickFile flow indirectly; here we fire workspace open via main menu equivalent
@@ -4641,4 +4670,3 @@
vscode.postMessage({ type: 'ready' });
}());
-
diff --git a/electron-app/renderer/index.html b/electron-app/renderer/index.html
index 63cba41..4c9c92e 100644
--- a/electron-app/renderer/index.html
+++ b/electron-app/renderer/index.html
@@ -563,6 +563,29 @@
FreeCode
+
+
Terminal & Shell
+
+
+
+
+
Used by the AI agent for all terminal commands.
+
+
Available shells
+
+ Not detected yet.
+
+
+
+
+
API Keys
diff --git a/vscode-extension/extension.js b/vscode-extension/extension.js
index 7df0284..16eada4 100644
--- a/vscode-extension/extension.js
+++ b/vscode-extension/extension.js
@@ -762,7 +762,9 @@ class ClaudeCodeViewProvider {
async _runPrompt(message, contextFilePaths, fileRefs) {
this._isCancelled = false;
- let fullPrompt = message;
+ const shellHint = detectPreferredTerminalShell();
+ const basePrompt = `[Execution shell: ${shellHint.type} (${shellHint.syntax}). Generate shell commands that match this shell syntax.]\n\n${message}`;
+ let fullPrompt = basePrompt;
// Inject context file contents
const allPaths = new Set(contextFilePaths || []);
@@ -796,7 +798,7 @@ class ClaudeCodeViewProvider {
}
}
if (fileContents.length > 0) {
- fullPrompt = message + '\n\n[Context files:]' + fileContents.join('');
+ fullPrompt = basePrompt + '\n\n[Context files:]' + fileContents.join('');
}
}
@@ -1189,32 +1191,105 @@ function deactivate() {
const MAX_TERMINAL_BYTES = 512 * 1024; // 512 KB
-/**
- * Minimal PowerShell POSIX shims — same as bash.mjs but used here for the
- * integrated terminal panel in the extension webview.
- */
-const TERMINAL_PS_SHIMS = [
- 'function which { param([string]$cmd) $r = Get-Command $cmd -ErrorAction SilentlyContinue; if ($r) { $r.Source } else { Write-Error "which: $cmd not found" } }',
- 'function grep { param([string]$pattern, [Parameter(ValueFromRemainingArguments)][string[]]$paths) if ($paths) { Select-String -Pattern $pattern -Path $paths | ForEach-Object { "$($_.Path):$($_.LineNumber):$($_.Line)" } } else { $input | Select-String -Pattern $pattern | ForEach-Object { $_.Line } } }',
- 'function cat { param([Parameter(ValueFromRemainingArguments)][string[]]$paths) Get-Content $paths }',
- 'function touch { param([string]$path) if (Test-Path $path) { (Get-Item $path).LastWriteTime = Get-Date } else { New-Item -ItemType File -Path $path -Force | Out-Null } }',
- 'function wc { param([string]$flag) $lines = @($input); if ($flag -eq "-l") { $lines.Count } elseif ($flag -eq "-c") { ($lines -join "`n").Length } else { $lines.Count } }',
- 'function head { param([string]$flag="", [string]$file="") $n=10; if ($flag -match "^-(\\d+)$") { $n=[int]$Matches[1]; $file="" } elseif ($flag -eq "-n") { $n=[int]$file; $file="" } elseif ($flag -ne "") { $file=$flag }; if ($file) { Get-Content $file | Select-Object -First $n } else { $input | Select-Object -First $n } }',
- 'function tail { param([string]$flag="", [string]$file="") $n=10; if ($flag -match "^-(\\d+)$") { $n=[int]$Matches[1]; $file="" } elseif ($flag -eq "-n") { $n=[int]$file; $file="" } elseif ($flag -ne "") { $file=$flag }; if ($file) { Get-Content $file | Select-Object -Last $n } else { $input | Select-Object -Last $n } }',
- 'function find { param([string]$basePath=".", [string]$nameFlag="", [string]$namePattern="*") if ($nameFlag -ne "-name") { $namePattern=$nameFlag }; Get-ChildItem -Path $basePath -Recurse -Filter $namePattern -Force | ForEach-Object { $_.FullName } }',
- 'function pwd { (Get-Location).Path }',
-].join('; ');
+const TERMINAL_POWERSHELL_SHIMS_MODULE = `# freecode-shims.psm1 — FreeCode PowerShell POSIX compatibility shims
+function which { param([string]$cmd) $r = Get-Command $cmd -EA SilentlyContinue; if ($r) { $r.Source } else { Write-Error "which: $cmd: not found"; exit 1 } }
+function grep { param([string]$pat, [Parameter(ValueFromRemainingArguments)][string[]]$f) if ($f) { Select-String -Pattern $pat -Path $f | ForEach-Object { "$($_.Path):$($_.LineNumber):$($_.Line)" } } else { $input | Select-String -Pattern $pat | ForEach-Object { $_.Line } } }
+function cat { param([Parameter(ValueFromRemainingArguments)][string[]]$paths) if ($paths) { Get-Content $paths } else { $input } }
+function touch { param([string]$p) if (Test-Path $p) { (Get-Item $p).LastWriteTime = Get-Date } else { New-Item -ItemType File -Path $p -Force | Out-Null } }
+function wc { param([string]$flag="-l") $lines = @($input); switch ($flag) { "-l" { $lines.Count } "-c" { ($lines -join "\`n").Length } "-w" { ($lines -join " ").Split() | Where-Object { $_ } | Measure-Object | Select-Object -Expand Count } default { $lines.Count } } }
+function head { param([int]$n=10, [string]$file="") if ($file) { Get-Content $file -TotalCount $n } else { $input | Select-Object -First $n } }
+function tail { param([int]$n=10, [string]$file="") if ($file) { Get-Content $file -Tail $n } else { $input | Select-Object -Last $n } }
+function find { param([string]$path=".", [string]$name="*") Get-ChildItem -Path $path -Recurse -Filter $name -ErrorAction SilentlyContinue | Select-Object -Expand FullName }
+function pwd { (Get-Location).Path }
+function ls { param([Parameter(ValueFromRemainingArguments)][string[]]$a) Get-ChildItem @a }
+function mkdir { param([Parameter(ValueFromRemainingArguments)][string[]]$a) New-Item -ItemType Directory @a -Force }
+function rm { param([switch]$r, [switch]$f, [Parameter(ValueFromRemainingArguments)][string[]]$paths) Remove-Item $paths -Recurse:$r -Force:$f -ErrorAction SilentlyContinue }
+function cp { param([string]$src, [string]$dst) Copy-Item -Path $src -Destination $dst -Recurse -Force }
+function mv { param([string]$src, [string]$dst) Move-Item -Path $src -Destination $dst -Force }
+function echo { param([Parameter(ValueFromRemainingArguments)][string[]]$a) Write-Output ($a -join " ") }
+function sed { param([string]$expr, [string]$file="") if ($expr -match "^s/(.+)/(.+)/") { $pat=$Matches[1]; $rep=$Matches[2]; if ($file) { (Get-Content $file) -replace $pat,$rep | Set-Content $file } else { $input | ForEach-Object { $_ -replace $pat,$rep } } } }
+function awk { param([string]$prog, [Parameter(ValueFromRemainingArguments)][string[]]$files) Write-Warning "awk: limited PowerShell shim — consider installing gawk via winget" }
+Export-ModuleMember -Function *
+`;
+
+let _preferredTerminalShell = null;
+let _preferredTerminalShellName = null;
+
+function isPowerShellAvailable() {
+ const { spawnSync } = require('child_process');
+ try {
+ const r = spawnSync('powershell.exe', ['-NoProfile', '-Command', 'exit 0'], {
+ encoding: 'utf-8', timeout: 2000, windowsHide: true,
+ });
+ return r.status === 0;
+ } catch {
+ return false;
+ }
+}
+
+function resolveShellByName(name) {
+ const map = {
+ powershell: { type: 'powershell', exe: 'powershell.exe', args: ['-NoProfile', '-NonInteractive', '-Command'], syntax: 'powershell' },
+ wsl: { type: 'wsl', exe: 'wsl.exe', args: ['bash', '-c'], syntax: 'bash' },
+ ubuntu: { type: 'ubuntu', exe: 'wsl.exe', args: ['-d', 'Ubuntu', '--', 'bash', '-c'], syntax: 'bash' },
+ bash: { type: 'bash', exe: 'bash', args: ['-c'], syntax: 'bash' },
+ cmd: { type: 'cmd', exe: 'cmd.exe', args: ['/c'], syntax: 'cmd' },
+ };
+ return map[name] || map.powershell;
+}
+
+function detectPreferredTerminalShell() {
+ const config = vscode.workspace.getConfiguration('openClaudeCode');
+ const pref = config.get('defaultShell', 'auto');
+
+ if (_preferredTerminalShell && _preferredTerminalShellName === pref) {
+ return _preferredTerminalShell;
+ }
+
+ if (pref !== 'auto') {
+ _preferredTerminalShellName = pref;
+ _preferredTerminalShell = resolveShellByName(pref);
+ return _preferredTerminalShell;
+ }
+
+ if (process.platform !== 'win32') {
+ _preferredTerminalShellName = 'bash';
+ _preferredTerminalShell = resolveShellByName('bash');
+ return _preferredTerminalShell;
+ }
+
+ _preferredTerminalShellName = 'auto';
+ _preferredTerminalShell = isPowerShellAvailable()
+ ? resolveShellByName('powershell')
+ : resolveShellByName('cmd');
+ return _preferredTerminalShell;
+}
+
+function ensurePowerShellShimsModule() {
+ const fs = require('fs');
+ const baseDir = extensionContext?.globalStorageUri?.fsPath || __dirname;
+ try { fs.mkdirSync(baseDir, { recursive: true }); } catch { /* ignore */ }
+ const shimPath = path.join(baseDir, 'freecode-shims.psm1');
+ if (!fs.existsSync(shimPath)) {
+ fs.writeFileSync(shimPath, TERMINAL_POWERSHELL_SHIMS_MODULE, 'utf8');
+ }
+ return shimPath;
+}
+
+function quotePowerShellPath(p) {
+ return String(p || '').replace(/'/g, "''");
+}
/**
* Execute `command` in the workspace shell and stream output to the webview.
- * On Windows: tries WSL first, then falls back to PowerShell with POSIX shims.
- * On Unix: uses bash.
+ * Uses openClaudeCode.defaultShell when explicitly set.
+ * In auto mode: PowerShell is preferred on Windows; bash on Unix.
* @param {string} command
* @param {string|null} reqId
* @param {Function} send — webview.postMessage wrapper
*/
function runTerminalCommand(command, reqId, send) {
- const { spawn, spawnSync } = require('child_process');
+ const { spawn } = require('child_process');
const os = require('os');
const cwd = (() => {
@@ -1225,27 +1300,16 @@ function runTerminalCommand(command, reqId, send) {
}
})();
- let shellExe, shellArgs;
+ const shell = detectPreferredTerminalShell();
+ let shellExe = shell.exe;
+ let shellArgs = [...shell.args, command];
- if (process.platform === 'win32') {
- // Try WSL first for a proper POSIX bash environment
- let wslOk = false;
- try {
- const r = spawnSync('wsl.exe', ['--status'], { encoding: 'utf-8', timeout: 3000, windowsHide: true });
- wslOk = r.status === 0;
- } catch { /* ignore */ }
-
- if (wslOk) {
- shellExe = 'wsl.exe';
- shellArgs = ['bash', '-c', command];
- } else {
- // PowerShell with POSIX shims prepended
- shellExe = 'powershell.exe';
- shellArgs = ['-NoProfile', '-NonInteractive', '-Command', `${TERMINAL_PS_SHIMS}; ${command}`];
- }
- } else {
- shellExe = 'bash';
- shellArgs = ['-c', command];
+ if (shell.type === 'powershell') {
+ const modulePath = ensurePowerShellShimsModule();
+ shellArgs = [...shell.args, `Import-Module '${quotePowerShellPath(modulePath)}' -Force; ${command}`];
+ } else if (shell.type === 'cmd') {
+ shellExe = 'cmd.exe';
+ shellArgs = ['/c', command];
}
const proc = spawn(shellExe, shellArgs, {
diff --git a/vscode-extension/package.json b/vscode-extension/package.json
index c3274dc..ac22b70 100644
--- a/vscode-extension/package.json
+++ b/vscode-extension/package.json
@@ -167,6 +167,27 @@
],
"description": "Permission mode for file and shell operations"
},
+ "openClaudeCode.defaultShell": {
+ "type": "string",
+ "enum": [
+ "auto",
+ "powershell",
+ "wsl",
+ "ubuntu",
+ "bash",
+ "cmd"
+ ],
+ "enumDescriptions": [
+ "Auto-detect best available shell (PowerShell preferred on Windows)",
+ "Windows PowerShell (default on Windows)",
+ "WSL — Windows Subsystem for Linux (generic)",
+ "WSL Ubuntu distro specifically",
+ "Bash (macOS/Linux default)",
+ "Windows Command Prompt (cmd.exe)"
+ ],
+ "default": "auto",
+ "description": "Default shell used by the AI agent for running commands. PowerShell is the default on Windows."
+ },
"openClaudeCode.maxTurns": {
"type": "number",
"default": 20,
From c6d3e88065ee87f77ddb1aecd05f2f8d0d2ce0fe Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 20 Apr 2026 11:29:46 +0000
Subject: [PATCH 3/9] chore: address validation feedback and finalize shell
updates
Agent-Logs-Url: https://github.com/codomium/FreeCode/sessions/8072f19d-aa04-4bb8-ac06-e402de917903
Co-authored-by: codomium <255525663+codomium@users.noreply.github.com>
---
electron-app/main.js | 2 +-
electron-app/renderer/chat.js | 4 ++--
vscode-extension/extension.js | 41 +----------------------------------
3 files changed, 4 insertions(+), 43 deletions(-)
diff --git a/electron-app/main.js b/electron-app/main.js
index 946c008..61dd71d 100644
--- a/electron-app/main.js
+++ b/electron-app/main.js
@@ -1333,7 +1333,7 @@ ipcMain.on('renderer-message', async (event, msg) => {
let _preferredShell = null;
const POWERSHELL_SHIMS_MODULE = `# freecode-shims.psm1 — FreeCode PowerShell POSIX compatibility shims
-function which { param([string]$cmd) $r = Get-Command $cmd -EA SilentlyContinue; if ($r) { $r.Source } else { Write-Error "which: $cmd: not found"; exit 1 } }
+function which { param([string]$cmd) $r = Get-Command $cmd -ErrorAction SilentlyContinue; if ($r) { $r.Source } else { Write-Error "which: $cmd: not found"; exit 1 } }
function grep { param([string]$pat, [Parameter(ValueFromRemainingArguments)][string[]]$f) if ($f) { Select-String -Pattern $pat -Path $f | ForEach-Object { "$($_.Path):$($_.LineNumber):$($_.Line)" } } else { $input | Select-String -Pattern $pat | ForEach-Object { $_.Line } } }
function cat { param([Parameter(ValueFromRemainingArguments)][string[]]$paths) if ($paths) { Get-Content $paths } else { $input } }
function touch { param([string]$p) if (Test-Path $p) { (Get-Item $p).LastWriteTime = Get-Date } else { New-Item -ItemType File -Path $p -Force | Out-Null } }
diff --git a/electron-app/renderer/chat.js b/electron-app/renderer/chat.js
index 5a9a6ee..533ab0b 100644
--- a/electron-app/renderer/chat.js
+++ b/electron-app/renderer/chat.js
@@ -3368,8 +3368,8 @@
}
function formatShellAvailability(shells) {
- const icon = (ok) => (ok ? '✅' : '❌');
- return `PowerShell ${icon(!!shells.powershell)} WSL ${icon(!!shells.wsl)} Ubuntu ${icon(!!shells.ubuntu)} Bash ${icon(!!shells.bash)} cmd ${icon(!!shells.cmd)}`;
+ const status = (ok) => (ok ? 'available' : 'unavailable');
+ return `PowerShell: ${status(!!shells.powershell)} · WSL: ${status(!!shells.wsl)} · Ubuntu: ${status(!!shells.ubuntu)} · Bash: ${status(!!shells.bash)} · cmd: ${status(!!shells.cmd)}`;
}
function openSettingsPanel() {
diff --git a/vscode-extension/extension.js b/vscode-extension/extension.js
index 16eada4..a9f472d 100644
--- a/vscode-extension/extension.js
+++ b/vscode-extension/extension.js
@@ -1191,27 +1191,6 @@ function deactivate() {
const MAX_TERMINAL_BYTES = 512 * 1024; // 512 KB
-const TERMINAL_POWERSHELL_SHIMS_MODULE = `# freecode-shims.psm1 — FreeCode PowerShell POSIX compatibility shims
-function which { param([string]$cmd) $r = Get-Command $cmd -EA SilentlyContinue; if ($r) { $r.Source } else { Write-Error "which: $cmd: not found"; exit 1 } }
-function grep { param([string]$pat, [Parameter(ValueFromRemainingArguments)][string[]]$f) if ($f) { Select-String -Pattern $pat -Path $f | ForEach-Object { "$($_.Path):$($_.LineNumber):$($_.Line)" } } else { $input | Select-String -Pattern $pat | ForEach-Object { $_.Line } } }
-function cat { param([Parameter(ValueFromRemainingArguments)][string[]]$paths) if ($paths) { Get-Content $paths } else { $input } }
-function touch { param([string]$p) if (Test-Path $p) { (Get-Item $p).LastWriteTime = Get-Date } else { New-Item -ItemType File -Path $p -Force | Out-Null } }
-function wc { param([string]$flag="-l") $lines = @($input); switch ($flag) { "-l" { $lines.Count } "-c" { ($lines -join "\`n").Length } "-w" { ($lines -join " ").Split() | Where-Object { $_ } | Measure-Object | Select-Object -Expand Count } default { $lines.Count } } }
-function head { param([int]$n=10, [string]$file="") if ($file) { Get-Content $file -TotalCount $n } else { $input | Select-Object -First $n } }
-function tail { param([int]$n=10, [string]$file="") if ($file) { Get-Content $file -Tail $n } else { $input | Select-Object -Last $n } }
-function find { param([string]$path=".", [string]$name="*") Get-ChildItem -Path $path -Recurse -Filter $name -ErrorAction SilentlyContinue | Select-Object -Expand FullName }
-function pwd { (Get-Location).Path }
-function ls { param([Parameter(ValueFromRemainingArguments)][string[]]$a) Get-ChildItem @a }
-function mkdir { param([Parameter(ValueFromRemainingArguments)][string[]]$a) New-Item -ItemType Directory @a -Force }
-function rm { param([switch]$r, [switch]$f, [Parameter(ValueFromRemainingArguments)][string[]]$paths) Remove-Item $paths -Recurse:$r -Force:$f -ErrorAction SilentlyContinue }
-function cp { param([string]$src, [string]$dst) Copy-Item -Path $src -Destination $dst -Recurse -Force }
-function mv { param([string]$src, [string]$dst) Move-Item -Path $src -Destination $dst -Force }
-function echo { param([Parameter(ValueFromRemainingArguments)][string[]]$a) Write-Output ($a -join " ") }
-function sed { param([string]$expr, [string]$file="") if ($expr -match "^s/(.+)/(.+)/") { $pat=$Matches[1]; $rep=$Matches[2]; if ($file) { (Get-Content $file) -replace $pat,$rep | Set-Content $file } else { $input | ForEach-Object { $_ -replace $pat,$rep } } } }
-function awk { param([string]$prog, [Parameter(ValueFromRemainingArguments)][string[]]$files) Write-Warning "awk: limited PowerShell shim — consider installing gawk via winget" }
-Export-ModuleMember -Function *
-`;
-
let _preferredTerminalShell = null;
let _preferredTerminalShellName = null;
@@ -1265,21 +1244,6 @@ function detectPreferredTerminalShell() {
return _preferredTerminalShell;
}
-function ensurePowerShellShimsModule() {
- const fs = require('fs');
- const baseDir = extensionContext?.globalStorageUri?.fsPath || __dirname;
- try { fs.mkdirSync(baseDir, { recursive: true }); } catch { /* ignore */ }
- const shimPath = path.join(baseDir, 'freecode-shims.psm1');
- if (!fs.existsSync(shimPath)) {
- fs.writeFileSync(shimPath, TERMINAL_POWERSHELL_SHIMS_MODULE, 'utf8');
- }
- return shimPath;
-}
-
-function quotePowerShellPath(p) {
- return String(p || '').replace(/'/g, "''");
-}
-
/**
* Execute `command` in the workspace shell and stream output to the webview.
* Uses openClaudeCode.defaultShell when explicitly set.
@@ -1304,10 +1268,7 @@ function runTerminalCommand(command, reqId, send) {
let shellExe = shell.exe;
let shellArgs = [...shell.args, command];
- if (shell.type === 'powershell') {
- const modulePath = ensurePowerShellShimsModule();
- shellArgs = [...shell.args, `Import-Module '${quotePowerShellPath(modulePath)}' -Force; ${command}`];
- } else if (shell.type === 'cmd') {
+ if (shell.type === 'cmd') {
shellExe = 'cmd.exe';
shellArgs = ['/c', command];
}
From bedb4bf38ca6f976f79d827661f11a5885d24cfd Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 20 Apr 2026 13:19:46 +0000
Subject: [PATCH 4/9] feat: implement prompt history, @folder/@url/@symbols,
diff auto-show, MCP marketplace
Agent-Logs-Url: https://github.com/codomium/FreeCode/sessions/8aa8656d-2df8-4564-80f3-4db5a1ffed9f
Co-authored-by: codomium <255525663+codomium@users.noreply.github.com>
---
electron-app/main.js | 166 ++++++++++++
electron-app/renderer/chat.css | 86 +++++++
electron-app/renderer/chat.js | 423 +++++++++++++++++++++++++++++--
electron-app/renderer/index.html | 21 +-
4 files changed, 675 insertions(+), 21 deletions(-)
diff --git a/electron-app/main.js b/electron-app/main.js
index 61dd71d..144773b 100644
--- a/electron-app/main.js
+++ b/electron-app/main.js
@@ -101,6 +101,7 @@ const DEFAULT_SETTINGS = {
workspacePath: os.homedir(),
defaultShell: 'auto', // 'auto' | 'powershell' | 'wsl' | 'ubuntu' | 'bash' | 'cmd'
customProviders: [], // [{ id, name, baseUrl, apiKey, models:[{id,name}], headers:[{name,value}] }]
+ mcpServers: {}, // { serverId: { command, args } }
};
function getSettings() {
@@ -224,9 +225,33 @@ class InProcessAgentBridge {
const model = this._model || appSettings.model || 'claude-sonnet-4-6';
+ // Connect MCP servers saved in app settings
+ const mcpClients = [];
+ const appMcpServers = appSettings.mcpServers || {};
+ // Merge with any in ~/.claude/settings.json (already loaded in `settings`)
+ const allMcpServers = Object.assign({}, settings.mcpServers || {}, appMcpServers);
+ if (Object.keys(allMcpServers).length > 0) {
+ const { McpClient } = await import(v2url('mcp/client.mjs'));
+ for (const [name, config] of Object.entries(allMcpServers)) {
+ try {
+ const client = new McpClient(config);
+ await client.connect();
+ const mcpTools = await client.listTools();
+ tools.registerMcpTools(mcpTools, (toolName, toolArgs) => client.callTool(toolName, toolArgs));
+ mcpClients.push(client);
+ } catch (err) {
+ console.warn(`MCP server "${name}" failed to connect: ${err.message}`);
+ }
+ }
+ }
+
+ const mcpResourceTool = tools.get('ReadMcpResource');
+ if (mcpResourceTool) mcpResourceTool._mcpClients = mcpClients;
+
this._loop = createAgentLoop({ model, tools, permissions, settings, hooks });
this._loop.state._agentLoader = agentLoader;
this._loop.state._skillsLoader = skillsLoader;
+ this._loop.state._mcpClients = mcpClients;
this._loop.state._hooks = settings.hooks;
this._loop.state._permissionMode = permMode;
@@ -675,6 +700,7 @@ ipcMain.on('renderer-message', async (event, msg) => {
showToolOutput: s.showToolOutput !== false,
hasNvidiaKey: !!(s.nvidiaApiKey || process.env.NVIDIA_API_KEY),
customProviders: Array.isArray(s.customProviders) ? s.customProviders : [],
+ mcpServers: (s.mcpServers && typeof s.mcpServers === 'object') ? s.mcpServers : {},
});
// Report which shell the integrated terminal will use
@@ -870,6 +896,123 @@ ipcMain.on('renderer-message', async (event, msg) => {
break;
}
+ // ── Folder tree for @folder context chip ─────────────────────────────
+ case 'getFolderTree': {
+ const rootDir = getSettings().workspacePath || os.homedir();
+ const SKIP_DIRS = new Set(['node_modules','.git','dist','.next','__pycache__','.cache','.idea','.vscode','build','out','.DS_Store']);
+ function buildTextTree(dir, prefix, depth) {
+ if (depth > 4) return '';
+ let entries;
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return ''; }
+ entries.sort((a, b) => {
+ if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
+ return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
+ });
+ let out = '';
+ for (let i = 0; i < entries.length; i++) {
+ const e = entries[i];
+ if (e.isDirectory() && SKIP_DIRS.has(e.name)) continue;
+ const isLast = i === entries.length - 1;
+ const connector = isLast ? '└── ' : '├── ';
+ const childPrefix = prefix + (isLast ? ' ' : '│ ');
+ out += prefix + connector + e.name + (e.isDirectory() ? '/' : '') + '\n';
+ if (e.isDirectory()) {
+ out += buildTextTree(path.join(dir, e.name), childPrefix, depth + 1);
+ }
+ }
+ return out;
+ }
+ const tree = path.basename(rootDir) + '/\n' + buildTextTree(rootDir, '', 0);
+ send({ type: 'folderTree', content: tree });
+ break;
+ }
+
+ // ── Fetch URL for @url context chip ───────────────────────────────────
+ case 'fetchUrl': {
+ const targetUrl = String(msg.url || '');
+ if (!targetUrl || !/^https?:\/\//.test(targetUrl)) {
+ send({ type: 'urlContent', url: targetUrl, content: '(invalid URL)', error: 'Invalid URL' });
+ break;
+ }
+ try {
+ const content = await new Promise((resolve, reject) => {
+ const httpMod = targetUrl.startsWith('https://') ? require('https') : require('http');
+ const reqOpts = new URL(targetUrl);
+ const req = httpMod.get({ hostname: reqOpts.hostname, path: reqOpts.pathname + (reqOpts.search || ''),
+ port: reqOpts.port, headers: { 'User-Agent': 'FreeCode/1.0 (context-fetch)', 'Accept': 'text/html,text/plain' } },
+ (res) => {
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
+ resolve(`(redirect to ${res.headers.location} — please use the final URL directly)`);
+ return;
+ }
+ const chunks = [];
+ res.on('data', (c) => chunks.push(c));
+ res.on('end', () => {
+ let text = Buffer.concat(chunks).toString('utf8');
+ // Strip HTML tags for readability
+ text = text.replace(/