diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index ebcd2f882..4f2f948b1 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,11 @@ ## [Unreleased] +### Changed + +- Changed persistent shell from opt-out to opt-in (default: off) for improved reliability; enable via Settings > Bash > Persistent shell or `OMP_SHELL_PERSIST=1` +- Added new "Bash" settings tab grouping shell-related settings (force basic shell, persistent shell, interceptor, intercept ls) + ## [9.5.0] - 2026-02-01 ### Added diff --git a/packages/coding-agent/src/config/settings-manager.ts b/packages/coding-agent/src/config/settings-manager.ts index 3f8e9370c..596766416 100644 --- a/packages/coding-agent/src/config/settings-manager.ts +++ b/packages/coding-agent/src/config/settings-manager.ts @@ -139,6 +139,11 @@ export interface CommitSettings { changelogMaxDiffChars?: number; } +export interface BashSettings { + /** Use persistent shell session instead of fresh shell per command (default: false) */ + persistentShell?: boolean; +} + export interface EditSettings { fuzzyMatch?: boolean; // default: true (accept high-confidence fuzzy matches for whitespace/indentation) fuzzyThreshold?: number; // default: 0.95 (similarity threshold for fuzzy matching) @@ -242,6 +247,7 @@ export interface Settings { lsp?: LspSettings; python?: PythonSettings; commit?: CommitSettings; + bash?: BashSettings; edit?: EditSettings; ttsr?: TtsrSettings; todoCompletion?: TodoCompletionSettings; @@ -331,6 +337,7 @@ const DEFAULT_SETTINGS: Settings = { mcp: { enableProjectConfig: true }, lsp: { formatOnWrite: false, diagnosticsOnWrite: true, diagnosticsOnEdit: false }, python: { toolMode: "both", kernelMode: "session", sharedGateway: true }, + bash: { persistentShell: false }, edit: { fuzzyMatch: true, fuzzyThreshold: 0.95, streamingAbort: false }, ttsr: { enabled: true, contextMode: "discard", repeatMode: "once", repeatGap: 10 }, providers: { webSearch: "auto", image: "auto" }, @@ -1444,6 +1451,19 @@ export class SettingsManager { await this.save(); } + getBashPersistentShell(): boolean { + return this.settings.bash?.persistentShell ?? false; + } + + async setBashPersistentShell(enabled: boolean): Promise { + if (!this.globalSettings.bash) { + this.globalSettings.bash = {}; + } + this.globalSettings.bash.persistentShell = enabled; + this.markModified("bash", "persistentShell"); + await this.save(); + } + getMCPProjectConfigEnabled(): boolean { return this.settings.mcp?.enableProjectConfig ?? true; } diff --git a/packages/coding-agent/src/exec/bash-executor.ts b/packages/coding-agent/src/exec/bash-executor.ts index 471dda5d5..d3feca1f9 100644 --- a/packages/coding-agent/src/exec/bash-executor.ts +++ b/packages/coding-agent/src/exec/bash-executor.ts @@ -34,10 +34,11 @@ export interface BashResult { } export async function executeBash(command: string, options?: BashExecutorOptions): Promise { - const { shell, args, env, prefix } = await SettingsManager.getGlobalShellConfig(); + const settings = await SettingsManager.acquire(); + const { shell, args, env, prefix } = settings.getShellConfig(); const snapshotPath = await getOrCreateSnapshot(shell, env); - if (shouldUsePersistentShell(shell)) { + if (shouldUsePersistentShell(settings.getBashPersistentShell())) { return await executeShellCommand({ shell, env, prefix, snapshotPath }, command, { cwd: options?.cwd, timeout: options?.timeout, @@ -52,19 +53,18 @@ export async function executeBash(command: string, options?: BashExecutorOptions return await executeBashOnce(command, options, { shell, args, env, prefix, snapshotPath }); } -function shouldUsePersistentShell(shell: string): boolean { +/** + * Determine whether to use persistent shell sessions. + * Priority: OMP_SHELL_PERSIST env var > settings > default (false) + */ +function shouldUsePersistentShell(settingValue: boolean): boolean { + // Env var takes precedence (for debugging/override) const flag = parseEnvFlag(process.env.OMP_SHELL_PERSIST); if (flag !== undefined) return flag; + // Windows never uses persistent shell (too unreliable) if (process.platform === "win32") return false; - const normalized = shell.toLowerCase(); - return ( - normalized.includes("bash") || - normalized.includes("zsh") || - normalized.includes("fish") || - normalized.endsWith("/sh") || - normalized.endsWith("\\\\sh") || - normalized.endsWith("sh") - ); + // Use setting value (defaults to false) + return settingValue; } function parseEnvFlag(value: string | undefined): boolean | undefined { diff --git a/packages/coding-agent/src/modes/components/settings-defs.ts b/packages/coding-agent/src/modes/components/settings-defs.ts index a4ccb9f14..3d449d332 100644 --- a/packages/coding-agent/src/modes/components/settings-defs.ts +++ b/packages/coding-agent/src/modes/components/settings-defs.ts @@ -254,33 +254,6 @@ export const SETTINGS_DEFS: SettingDef[] = [ // ═══════════════════════════════════════════════════════════════════════════ // Tools tab - Tool-specific settings // ═══════════════════════════════════════════════════════════════════════════ - { - id: "bashInterceptor", - tab: "tools", - type: "boolean", - label: "Bash interceptor", - description: "Block shell commands that have dedicated tools (grep, cat, etc.)", - get: sm => sm.getBashInterceptorEnabled(), - set: (sm, v) => sm.setBashInterceptorEnabled(v), - }, - { - id: "shellForceBasic", - tab: "tools", - type: "boolean", - label: "Force basic shell", - description: "Use bash/sh even if your default shell is different", - get: sm => sm.getShellForceBasic(), - set: (sm, v) => sm.setShellForceBasic(v), - }, - { - id: "bashInterceptorSimpleLs", - tab: "tools", - type: "boolean", - label: "Intercept simple ls", - description: "Intercept bare ls commands (when bash interceptor is enabled)", - get: sm => sm.getBashInterceptorSimpleLsEnabled(), - set: (sm, v) => sm.setBashInterceptorSimpleLsEnabled(v), - }, { id: "pythonToolMode", tab: "tools", @@ -440,6 +413,46 @@ export const SETTINGS_DEFS: SettingDef[] = [ ], }, + // ═══════════════════════════════════════════════════════════════════════════ + // Bash tab - Shell/Bash settings + // ═══════════════════════════════════════════════════════════════════════════ + { + id: "shellForceBasic", + tab: "bash", + type: "boolean", + label: "Force basic shell", + description: "Use bash/sh even if your default shell is different", + get: sm => sm.getShellForceBasic(), + set: (sm, v) => sm.setShellForceBasic(v), + }, + { + id: "bashPersistentShell", + tab: "bash", + type: "boolean", + label: "Persistent shell", + description: "Reuse shell session across commands (experimental, can get stuck)", + get: sm => sm.getBashPersistentShell(), + set: (sm, v) => sm.setBashPersistentShell(v), + }, + { + id: "bashInterceptor", + tab: "bash", + type: "boolean", + label: "Interceptor", + description: "Block shell commands that have dedicated tools (grep, cat, etc.)", + get: sm => sm.getBashInterceptorEnabled(), + set: (sm, v) => sm.setBashInterceptorEnabled(v), + }, + { + id: "bashInterceptorSimpleLs", + tab: "bash", + type: "boolean", + label: "Intercept ls", + description: "Intercept bare ls commands (when interceptor is enabled)", + get: sm => sm.getBashInterceptorSimpleLsEnabled(), + set: (sm, v) => sm.setBashInterceptorSimpleLsEnabled(v), + }, + // ═══════════════════════════════════════════════════════════════════════════ // Display tab - Visual/UI settings // ═══════════════════════════════════════════════════════════════════════════ diff --git a/packages/coding-agent/src/modes/components/settings-selector.ts b/packages/coding-agent/src/modes/components/settings-selector.ts index 3daf52150..28bc850be 100644 --- a/packages/coding-agent/src/modes/components/settings-selector.ts +++ b/packages/coding-agent/src/modes/components/settings-selector.ts @@ -116,6 +116,7 @@ type TabId = string; const SETTINGS_TABS: Tab[] = [ { id: "behavior", label: "Behavior" }, { id: "tools", label: "Tools" }, + { id: "bash", label: "Bash" }, { id: "display", label: "Display" }, { id: "ttsr", label: "TTSR" }, { id: "status", label: "Status" },