diff --git a/src/core/prompts/sections/__tests__/system-info.spec.ts b/src/core/prompts/sections/__tests__/system-info.spec.ts index 7c3b53c426..421c3a23b6 100644 --- a/src/core/prompts/sections/__tests__/system-info.spec.ts +++ b/src/core/prompts/sections/__tests__/system-info.spec.ts @@ -10,6 +10,7 @@ vi.mock("../../../../utils/shell", () => ({ })) import { getSystemInfoSection } from "../system-info" +import { getShell } from "../../../../utils/shell" import osName from "os-name" const mockOsName = osName as unknown as ReturnType @@ -101,4 +102,27 @@ describe("getSystemInfoSection", () => { // sentinel string) breaks this exact join. expect(result).toContain("default directory for all tool operations. When the user initially gives you a task") }) + + describe("shell syntax", () => { + const cmdPath = "C:\\Windows\\System32\\cmd.exe" + + it("states the dialect for cmd.exe when execute_command is available", () => { + vi.mocked(getShell).mockReturnValueOnce(cmdPath) + + const result = getSystemInfoSection(mockCwd, policyFor()) + + expect(result).toContain(`Default Shell: ${cmdPath} +Shell Syntax: Commands run through Windows cmd.exe`) + }) + + it("omits the dialect without execute_command", () => { + vi.mocked(getShell).mockReturnValueOnce(cmdPath) + + expect(getSystemInfoSection(mockCwd, policyFor(false))).not.toContain("Shell Syntax") + }) + + it("omits the dialect for POSIX shells", () => { + expect(getSystemInfoSection(mockCwd, policyFor())).not.toContain("Shell Syntax") + }) + }) }) diff --git a/src/core/prompts/sections/system-info.ts b/src/core/prompts/sections/system-info.ts index 003c555d75..6e7067bf7c 100644 --- a/src/core/prompts/sections/system-info.ts +++ b/src/core/prompts/sections/system-info.ts @@ -2,6 +2,7 @@ import os from "os" import osName from "os-name" import { getShell } from "../../../utils/shell" +import { describeShellDialect } from "../../../utils/shellDialect" import type { EffectiveToolPolicy } from "../tools/effective-tool-policy" @@ -33,12 +34,19 @@ export function getSystemInfoSection(cwd: string, policy: EffectiveToolPolicy): ? " New terminals will be created in the current workspace directory, however if you change directories in a terminal it will then have a different working directory; changing directories in a terminal does not modify the workspace directory, because you do not have access to change the workspace directory." : "" + const shell = getShell() + const shellDialect = executeCommandAvailable ? describeShellDialect(shell) : undefined + const shellDialectLine = shellDialect + ? ` +Shell Syntax: ${shellDialect}` + : "" + const details = `==== SYSTEM INFORMATION Operating System: ${osInfo} -Default Shell: ${getShell()} +Default Shell: ${shell}${shellDialectLine} Home Directory: ${os.homedir().toPosix()} Current Workspace Directory: ${cwd.toPosix()} diff --git a/src/utils/__tests__/shellDialect.spec.ts b/src/utils/__tests__/shellDialect.spec.ts new file mode 100644 index 0000000000..c5db9ee722 --- /dev/null +++ b/src/utils/__tests__/shellDialect.spec.ts @@ -0,0 +1,23 @@ +import { describeShellDialect } from "../shellDialect" + +describe("describeShellDialect", () => { + it.each([ + { shell: "C:\\Windows\\System32\\cmd.exe", expected: "cmd.exe" }, + { shell: "C:\\Windows\\System32\\CMD.EXE", expected: "cmd.exe" }, + { shell: "C:\\Program Files\\PowerShell\\7\\pwsh.exe", expected: "PowerShell" }, + { shell: "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", expected: "PowerShell" }, + ])("names the dialect for $shell", ({ shell, expected }) => { + expect(describeShellDialect(shell)).toContain(expected) + }) + + it("warns that ';' does not chain commands in cmd.exe", () => { + expect(describeShellDialect("C:\\Windows\\System32\\cmd.exe")).toContain("`;` is not a separator") + }) + + it.each(["/bin/bash", "/bin/zsh", "C:\\Program Files\\Git\\bin\\bash.exe", "C:\\Windows\\System32\\wsl.exe"])( + "adds nothing for POSIX shell %s", + (shell) => { + expect(describeShellDialect(shell)).toBeUndefined() + }, + ) +}) diff --git a/src/utils/shellDialect.ts b/src/utils/shellDialect.ts new file mode 100644 index 0000000000..048f62cd35 --- /dev/null +++ b/src/utils/shellDialect.ts @@ -0,0 +1,30 @@ +/** + * Describes the command syntax of Windows-native shells for the system prompt. + * + * Rules and habits written for bash silently do the wrong thing under these shells + * (e.g. `;` is not a command separator in cmd.exe), so the model is told which + * dialect it is writing for. Returns undefined for POSIX shells, where the shell + * path already names the dialect. + */ +export function describeShellDialect(shellPath: string): string | undefined { + // path.basename follows the host platform; split on both separators so Windows paths work anywhere. + const name = (shellPath.split(/[\\/]/).pop() ?? "").toLowerCase() + + if (name === "cmd.exe" || name === "cmd") { + return ( + "Commands run through Windows cmd.exe, not bash. Use cmd syntax: `&` or `&&` to chain commands " + + "(`;` is not a separator), `%VAR%` for variables, `set X=Y`, `%ERRORLEVEL%` for exit codes, " + + "and `dir`, `type`, `findstr`. POSIX tools may be missing or shadowed by other binaries on PATH." + ) + } + + if (name === "powershell.exe" || name === "pwsh.exe" || name === "powershell" || name === "pwsh") { + return ( + "Commands run through PowerShell, not bash. Use PowerShell syntax: `;` to chain commands, " + + "`$env:VAR` for environment variables, `$LASTEXITCODE` for exit codes, and cmdlets such as " + + "`Get-ChildItem` and `Select-String`." + ) + } + + return undefined +}