Skip to content

Commit 15bac7e

Browse files
authored
feat: optimize shell prompt (#517)
1 parent 2be53fe commit 15bac7e

File tree

3 files changed

+318
-4
lines changed

3 files changed

+318
-4
lines changed

src/core/environment/getEnvironmentDetails.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { formatResponse } from "../prompts/responses"
2020

2121
import { Task } from "../task/Task"
2222
import { formatReminderSection } from "./reminder"
23-
import { getShell } from "../../utils/shell"
23+
import { getShell, getWindowsTerminalInfo } from "../../utils/shell"
2424
import { getOperatingSystem } from "../../utils/zgsmUtils"
2525
import { defaultLang } from "../../utils/language"
2626

@@ -238,8 +238,28 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo
238238
globalCustomInstructions: promptSuggestion + simpleAskSuggestion + shellSuggestion + globalCustomInstructions,
239239
language: language ?? formatLanguage(await defaultLang()),
240240
})
241+
242+
const formatUnsupport = (data: string[]): string => {
243+
return data.join("\n")
244+
}
245+
241246
details += `\n\n# Operating System\n${getOperatingSystem()}`
242247
details += `\n\n# Default Shell\n${getShell()}`
248+
const winTerminalInfo = await getWindowsTerminalInfo()
249+
250+
if (winTerminalInfo) {
251+
const { version, name, unsupportSyntax, features } = winTerminalInfo
252+
details += `\n\n# Shell Version\n${name} ${version}`
253+
254+
if (unsupportSyntax) {
255+
details += `\n\n## Shell Unsupport Syntax\n${formatUnsupport(unsupportSyntax)}`
256+
}
257+
258+
if (features) {
259+
details += `\n\n## Shell Support Syntax\n${formatUnsupport(features)}`
260+
}
261+
}
262+
243263
details += `\n\n# Current Mode\n`
244264
details += `<slug>${currentMode}</slug>\n`
245265
details += `<name>${modeDetails.name}</name>\n`

src/utils/shell.ts

Lines changed: 243 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import * as vscode from "vscode"
22
import { userInfo } from "os"
3+
import { terminalUnsupportedSyntax } from "./shellConstants"
34
import fs from "fs"
45
import * as path from "path"
6+
import { exec } from "child_process"
7+
import { promisify } from "util"
58

69
// Security: Allowlist of approved shell executables to prevent arbitrary command execution
710
const SHELL_ALLOWLIST = new Set<string>([
@@ -16,6 +19,7 @@ const SHELL_ALLOWLIST = new Set<string>([
1619

1720
// Windows WSL
1821
"C:\\Windows\\System32\\wsl.exe",
22+
"wsl.exe",
1923

2024
// Git Bash on Windows
2125
"C:\\Program Files\\Git\\bin\\bash.exe",
@@ -142,9 +146,15 @@ function getWindowsTerminalConfig() {
142146
const config = vscode.workspace.getConfiguration("terminal.integrated")
143147
const defaultProfileName = config.get<string>("defaultProfile.windows")
144148
const profiles = config.get<WindowsTerminalProfiles>("profiles.windows") || {}
145-
return { defaultProfileName, profiles }
149+
return {
150+
defaultProfileName,
151+
profiles,
152+
}
146153
} catch {
147-
return { defaultProfileName: null, profiles: {} as WindowsTerminalProfiles }
154+
return {
155+
defaultProfileName: null,
156+
profiles: {} as WindowsTerminalProfiles,
157+
}
148158
}
149159
}
150160

@@ -203,7 +213,7 @@ function getWindowsShellFromVSCode(): string | null {
203213
if (normalizedPath) {
204214
// If there's an explicit PowerShell path, return that
205215
return normalizedPath
206-
} else if (profile?.source === "PowerShell") {
216+
} else if (profile?.source === "PowerShell" && profile?.path) {
207217
// If the profile is sourced from PowerShell, assume the newest
208218
return SHELL_PATHS.POWERSHELL_7
209219
}
@@ -587,3 +597,233 @@ function detectLinuxVSCodeDefaultShell(): string | null {
587597
export function getActiveTerminalShellType(): string | null {
588598
return detectSystemAvailableShell()
589599
}
600+
601+
interface TerminalUnsupportedSyntax {
602+
unsupported?: string[]
603+
features?: string[]
604+
}
605+
606+
export interface TerminalInfo {
607+
name: string
608+
version: string
609+
path: string
610+
unsupportSyntax?: string[]
611+
features?: string[]
612+
}
613+
614+
const execAsync = promisify(exec)
615+
616+
/**
617+
* Get PowerShell version information by executing command
618+
* @param shellPath PowerShell path
619+
* @returns Version string or null
620+
*/
621+
async function getPowerShellVersion(shellPath: string): Promise<string | null> {
622+
try {
623+
// Execute PowerShell command to get version information
624+
const { stdout } = await execAsync(`"${shellPath}" -Command "$PSVersionTable"`)
625+
const getPSVersion = (str: string) => str.replace(/\\x1b\[[0-9;]*m/g, "").match(/PSVersion\s+([0-9.]+)/)
626+
const versionMatch = getPSVersion(stdout)
627+
if (versionMatch) {
628+
const major = versionMatch[1]
629+
// Try to get more detailed version information
630+
try {
631+
const { stdout: detailStdout } = await execAsync(
632+
`"${shellPath}" -Command "$PSVersionTable.PSVersion.ToString()"`,
633+
)
634+
return detailStdout.trim()
635+
} catch {
636+
return major
637+
}
638+
}
639+
return null
640+
} catch (error) {
641+
console.debug("[getPowerShellVersion] Failed to get PowerShell version:", error)
642+
return null
643+
}
644+
}
645+
646+
/**
647+
* Get Git Bash version information by executing command
648+
*/
649+
export async function getGitBashVersion(): Promise<{ path: string; version: string } | { version: null }> {
650+
async function getBashPathsFromWhere(): Promise<string[]> {
651+
try {
652+
const { stdout } = await execAsync("where bash")
653+
return stdout
654+
.split(/\r?\n/)
655+
.map((p) => p.trim())
656+
.filter(Boolean)
657+
} catch {
658+
return []
659+
}
660+
}
661+
662+
async function getBashPathFromRegistry(): Promise<string | null> {
663+
try {
664+
const { stdout } = await execAsync('reg query "HKEY_LOCAL_MACHINE\\SOFTWARE\\GitForWindows" /v InstallPath')
665+
const match = stdout.match(/InstallPath\s+REG_SZ\s+(.+)/)
666+
if (match) {
667+
return `${match[1]}\\bin\\bash.exe`
668+
}
669+
} catch {}
670+
return null
671+
}
672+
673+
const fallbackPaths = [
674+
"C:\\Program Files\\Git\\bin\\bash.exe",
675+
"C:\\Program Files (x86)\\Git\\bin\\bash.exe",
676+
"C:\\Program Files\\Git\\usr\\bin\\bash.exe",
677+
]
678+
679+
const paths = new Set<string>()
680+
681+
for (const p of await getBashPathsFromWhere()) paths.add(p)
682+
const regPath = await getBashPathFromRegistry()
683+
if (regPath) paths.add(regPath)
684+
for (const p of fallbackPaths) paths.add(p)
685+
686+
const validCandidates: { path: string; version: string }[] = []
687+
688+
for (const path of paths) {
689+
try {
690+
const { stdout } = await execAsync(`"${path}" --version`)
691+
const versionMatch = stdout.match(/version\s+([\d.]+)/i)
692+
if (versionMatch) {
693+
validCandidates.push({ path, version: versionMatch[1] })
694+
}
695+
} catch {
696+
continue
697+
}
698+
}
699+
700+
if (validCandidates.length === 0) {
701+
console.debug("[getGitBashVersion] No valid Git Bash found.")
702+
return { version: null }
703+
}
704+
705+
// Prioritize the "Git for Windows" version
706+
const preferred = validCandidates.find((c) => c.path.toLowerCase().includes("program files\\git"))
707+
return preferred || validCandidates[0]
708+
}
709+
710+
/**
711+
* Get CMD version information by executing command
712+
* @param shellPath CMD path
713+
* @returns Version string or null
714+
*/
715+
async function getCMDVersion(shellPath: string): Promise<string | null> {
716+
try {
717+
// Execute ver command to get Windows version information
718+
const { stdout } = await execAsync(`cmd /c ver`)
719+
const versionMatch = stdout.match(/([\d.]+)/)
720+
if (versionMatch) {
721+
return versionMatch[1]
722+
}
723+
return null
724+
} catch (error) {
725+
console.debug("[getCMDVersion] Failed to get CMD version:", error)
726+
return null
727+
}
728+
}
729+
730+
/**
731+
* Get terminal name and version information on Windows operating system
732+
* The retrieval logic is consistent with getShell, but returns terminal name and version information
733+
* Get accurate version information by executing commands
734+
*
735+
* @returns Terminal information object containing name, version and path
736+
*/
737+
export async function getWindowsTerminalInfo(): Promise<TerminalInfo | null> {
738+
if (process.platform !== "win32") {
739+
return null
740+
}
741+
742+
const shellPath = getShell()
743+
744+
// Determine terminal name and version based on path
745+
if (shellPath.toLowerCase().includes("pwsh.exe")) {
746+
// PowerShell 7+
747+
const version = await getPowerShellVersion(shellPath)
748+
return {
749+
name: "PowerShell",
750+
version: version || "Unknown",
751+
path: shellPath,
752+
unsupportSyntax: terminalUnsupportedSyntax.powershell7.unsupported,
753+
features: terminalUnsupportedSyntax.powershell7.features,
754+
}
755+
} else if (shellPath.toLowerCase().includes("powershell.exe")) {
756+
// Windows PowerShell (5.1 and earlier versions)
757+
const version = await getPowerShellVersion(shellPath)
758+
return {
759+
name: "Windows PowerShell",
760+
version: version || "5.1",
761+
path: shellPath,
762+
unsupportSyntax: terminalUnsupportedSyntax.powershell5.unsupported,
763+
features: terminalUnsupportedSyntax.powershell5.features,
764+
}
765+
} else if (shellPath.toLowerCase().includes("cmd.exe")) {
766+
// Command Prompt
767+
const version = await getCMDVersion(shellPath)
768+
return {
769+
name: "Command Prompt",
770+
version: version || "Built-in",
771+
path: shellPath,
772+
unsupportSyntax: terminalUnsupportedSyntax.cmd.unsupported,
773+
}
774+
} else if (shellPath.toLowerCase().includes("bash.exe")) {
775+
// Git Bash, MSYS2, MinGW, Cygwin, etc.
776+
if (shellPath.toLowerCase().includes("git")) {
777+
const { version } = await getGitBashVersion()
778+
return {
779+
name: "Git Bash",
780+
version: version || "Unknown",
781+
path: shellPath,
782+
unsupportSyntax: terminalUnsupportedSyntax.gitBash.unsupported,
783+
features: terminalUnsupportedSyntax.gitBash.features,
784+
}
785+
} else if (shellPath.toLowerCase().includes("msys64")) {
786+
const { version } = await getGitBashVersion()
787+
return {
788+
name: "MSYS2",
789+
version: version || "64-bit",
790+
path: shellPath,
791+
}
792+
} else if (shellPath.toLowerCase().includes("msys32")) {
793+
const { version } = await getGitBashVersion()
794+
return {
795+
name: "MSYS2",
796+
version: version || "32-bit",
797+
path: shellPath,
798+
}
799+
} else if (shellPath.toLowerCase().includes("mingw")) {
800+
const { version } = await getGitBashVersion()
801+
return {
802+
name: "MinGW",
803+
version: version || "Unknown",
804+
path: shellPath,
805+
}
806+
} else if (shellPath.toLowerCase().includes("cygwin")) {
807+
const { version } = await getGitBashVersion()
808+
return {
809+
name: "Cygwin",
810+
version: version || "Unknown",
811+
path: shellPath,
812+
}
813+
} else {
814+
const { version } = await getGitBashVersion()
815+
return {
816+
name: "Bash",
817+
version: version || "Unknown",
818+
path: shellPath,
819+
}
820+
}
821+
} else {
822+
// Unknown terminal
823+
return {
824+
name: "Unknown Terminal",
825+
version: "Unknown",
826+
path: shellPath,
827+
}
828+
}
829+
}

src/utils/shellConstants.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* @file terminal-unsupport-syntax
3+
*/
4+
export const terminalUnsupportedSyntax = {
5+
cmd: {
6+
unsupported: [
7+
"Does not support piping (|) with complex expressions",
8+
"No support for && and || logical operators",
9+
"Does not support function definitions like PowerShell or Bash",
10+
"Limited variable usage — no $variable expansion",
11+
"No subshell syntax like $(...) or backticks",
12+
],
13+
},
14+
powershell5: {
15+
unsupported: [
16+
"No support for && and || operators",
17+
"No support for ternary operator (?:)",
18+
"No support for null-coalescing operator (??)",
19+
"Limited pipeline parallelism",
20+
"Not cross-platform (Windows only)",
21+
],
22+
features: [
23+
"Use semicolon (;) to separate multiple commands",
24+
"Use -eq, -ne, -gt, -lt for comparisons",
25+
"Use traditional pipeline syntax with |",
26+
],
27+
},
28+
powershell7: {
29+
unsupported: [
30+
"Still limited compared to Bash for inline command substitution",
31+
"Some Windows-specific modules are deprecated",
32+
],
33+
features: [
34+
"Support for && and || operators (like Bash)",
35+
"Support for ternary operator (?:)",
36+
"Support for null-coalescing operator (??)",
37+
"Improved pipeline performance",
38+
"Cross-platform support",
39+
],
40+
},
41+
gitBash: {
42+
unsupported: [
43+
"Does not support PowerShell cmdlets",
44+
"Limited access to Windows environment variables",
45+
"No native PowerShell object pipeline",
46+
"Limited Unicode and emoji rendering on Windows",
47+
],
48+
features: [
49+
"Support for && and || operators",
50+
"Support for $(...) command substitution",
51+
"Support for standard UNIX-style piping",
52+
],
53+
},
54+
}

0 commit comments

Comments
 (0)