返回 CodeWhale
guidance.rs
根目录 / crates / tui / src / tools / shell / guidance.rs
1 //! Model-facing command syntax follows the same dispatcher as execution.
2
3 use crate::shell_dispatcher::{ShellKind, global_dispatcher};
4 use std::sync::OnceLock;
5
6 const POWERSHELL_GUIDANCE: &str = "Use PowerShell syntax. Bash is a legacy tool name, not a Bash interpreter. \
7 For JSON use Invoke-RestMethod; for text use Invoke-WebRequest -UseBasicParsing \
8 on Windows PowerShell to avoid dependency on the Internet Explorer engine. \
9 Do not assume head, sed, awk, or other Unix utilities are installed. \
10 Use PowerShell cmdlets or verified available programs; parse JSON and select needed fields \
11 instead of appending head. Bash heredocs are not PowerShell syntax. Use PowerShell \
12 5.1-compatible syntax (no && or ||) unless the detected executable is pwsh. Example: \
13 $text = 'sample'; $text.Substring(0, [Math]::Min(3, $text.Length)).";
14
15 const BASH_GUIDANCE: &str = "Use Bash syntax: pipelines, redirections, $(command), \
16 and && / || are supported. Quote paths and variable expansions, such as \"$path\"; \
17 use single quotes for literal text. For literal multiline input, use a quoted heredoc \
18 delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed \
19 programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\n' 'sample'.";
20
21 const SH_GUIDANCE: &str = "Use POSIX sh syntax: pipelines, redirections, $(command), \
22 and && / || are supported. Quote paths and variable expansions, such as \"$path\"; \
23 use single quotes for literal text. Do not use Bash-only arrays, [[ ... ]], \
24 process substitution, or here-strings. Use only installed programs and portable \
25 utility options. Example: printf '%s\\n' 'sample'.";
26
27 const ZSH_GUIDANCE: &str = "Use zsh syntax. Quote paths, literal wildcard patterns, \
28 and variable expansions; unmatched unquoted globs can fail before a command runs. \
29 A bare word starting with = undergoes =command PATH expansion (e.g. echo === fails); \
30 quote such arguments, e.g. echo '==='. Do not assume Bash array indexing or word \
31 splitting rules. Use only installed programs; do not assume GNU-specific flags on macOS/BSD.";
32
33 const CMD_GUIDANCE: &str = "Use cmd.exe syntax: %NAME% expands environment variables; use double quotes \
34 around paths containing spaces (single quotes are not quoting delimiters). \
35 Use cmd built-ins or installed programs, not Bash or PowerShell syntax. \
36 Do not assume Unix utilities are installed. Example: echo sample";
37
38 const FISH_GUIDANCE: &str = "Use fish syntax: set NAME value for variables, \
39 and begin ... end for blocks. Bash assignment NAME=value and \
40 heredocs are not portable fish syntax. Quote paths and use only \
41 installed programs. Example: printf '%s\\n' 'sample'.";
42
43 const FALLBACK_GUIDANCE: &str = "Use the detected shell's syntax and only installed programs; \
44 do not infer Bash syntax from the legacy tool name.";
45
46 pub(super) fn command_guidance(kind: &ShellKind) -> String {
47 let syntax = match kind {
48 // Match execution's PowerShell-family detection, including custom paths.
49 _ if kind.is_powershell() => POWERSHELL_GUIDANCE,
50 ShellKind::Cmd => CMD_GUIDANCE,
51 ShellKind::Sh => SH_GUIDANCE,
52 ShellKind::Bash => BASH_GUIDANCE,
53 ShellKind::Custom { binary, .. } => {
54 match std::path::Path::new(binary)
55 .file_stem()
56 .and_then(|name| name.to_str())
57 .map(str::to_ascii_lowercase)
58 .as_deref()
59 {
60 Some("bash") => BASH_GUIDANCE,
61 Some("sh" | "dash" | "ash") => SH_GUIDANCE,
62 Some("zsh") => ZSH_GUIDANCE,
63 Some("fish") => FISH_GUIDANCE,
64 _ => FALLBACK_GUIDANCE,
65 }
66 }
67 _ => FALLBACK_GUIDANCE,
68 };
69 format!(
70 "The command to execute. Actual execution shell: `{}`. {syntax}",
71 kind.binary()
72 )
73 }
74
75 pub(super) fn runtime_command_guidance() -> &'static str {
76 static GUIDANCE: OnceLock<String> = OnceLock::new();
77 GUIDANCE.get_or_init(|| command_guidance(global_dispatcher().kind()))
78 }
79
80 pub(super) fn description() -> &'static str {
81 static DESCRIPTION: OnceLock<String> = OnceLock::new();
82 DESCRIPTION.get_or_init(|| {
83 format!(
84 "{} Execute in the workspace. Action \"run\" (default) executes a command; \
85 \"wait\" blocks for a background task until completion or timeout; \"interact\" sends stdin to a background task; \
86 \"cancel\" kills a background task. Pass wait=false for a nonblocking task snapshot. Foreground mode is for bounded commands; \
87 use background=true for work expected to take >5 seconds.",
88 runtime_command_guidance()
89 )
90 })
91 }
92
93 // Interpreter syntax lives on the command parameter. Repeating it in the
94 // tool description adds the same bytes to every active request.
95 pub(super) fn foreground_description() -> &'static str {
96 "Execute a shell command in the workspace and return stdout and stderr. Output keeps the last 2000 lines or 50KB. An optional timeout is expressed in seconds; when omitted the command is killed after 120 seconds, so pass an explicit timeout for work expected to take longer. In Ask, after a sandbox denial, retry the exact command once with sandbox_permissions (the narrowest wider mode that suffices) and a one-sentence justification; the approval prompt asks the user."
97 }
98
99 #[cfg(test)]
100 mod tests {
101 use super::*;
102
103 #[test]
104 fn shell_guidance_preserves_unix_shell_contracts() {
105 for (binary, expected) in [
106 ("/bin/bash", BASH_GUIDANCE),
107 ("bash", BASH_GUIDANCE),
108 ("/usr/local/bin/bash", BASH_GUIDANCE),
109 ("/bin/sh", SH_GUIDANCE),
110 ("/bin/dash", SH_GUIDANCE),
111 ("/bin/ash", SH_GUIDANCE),
112 ("/bin/zsh", ZSH_GUIDANCE),
113 ] {
114 let text = command_guidance(&ShellKind::Custom {
115 binary: binary.into(),
116 flag: "-lc".into(),
117 });
118 assert!(text.contains(expected), "missing guidance for {binary}");
119 assert!(!text.contains("Use PowerShell syntax"));
120 }
121 assert!(command_guidance(&ShellKind::Bash).contains(BASH_GUIDANCE));
122 assert!(command_guidance(&ShellKind::Sh).contains(SH_GUIDANCE));
123 }
124
125 #[test]
126 fn shell_guidance_matches_each_interpreter() {
127 for kind in [
128 ShellKind::Pwsh,
129 ShellKind::WindowsPowerShell,
130 ShellKind::Cmd,
131 ShellKind::Sh,
132 ShellKind::Bash,
133 ShellKind::Custom {
134 binary: "/bin/zsh".into(),
135 flag: "-lc".into(),
136 },
137 ShellKind::Custom {
138 binary: "/opt/pwsh".into(),
139 flag: "-c".into(),
140 },
141 ShellKind::Custom {
142 binary: "/bin/fish".into(),
143 flag: "-c".into(),
144 },
145 ] {
146 let text = command_guidance(&kind);
147 assert!(text.contains(kind.binary()));
148 assert_eq!(text.contains("Use PowerShell syntax"), kind.is_powershell());
149 assert_eq!(
150 text.contains("=command PATH expansion"),
151 kind.binary() == "/bin/zsh"
152 );
153 assert!(!text.contains("user's login shell"));
154 }
155 }
156 }
157
157 lines RUST