From a30dfa9ad076530fc09b40fb2ada1683d6c503a0 Mon Sep 17 00:00:00 2001 From: abel533 Date: Sat, 4 Apr 2026 18:33:42 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=BB=9A=E5=8A=A8?= =?UTF-8?q?=E9=99=90=E5=88=B6=20+=20=E8=87=AA=E5=8A=A8=E6=A3=80=E6=B5=8BSh?= =?UTF-8?q?ell=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 滚动修复: - scroll() 使用实际渲染行数而非消息数计算 maxOffset - 解决 /help 等多行输出无法滚动到顶部的问题 Shell 检测: - Windows 自动检测 PowerShell 7+ > Windows PowerShell > cmd.exe - 动态调整 BashTool 描述,告知AI使用正确的命令语法 - 系统提示词包含详细的 Shell 使用指南(PowerShell cmdlets) - header 显示检测到的 Shell 类型 - Unix 优先 bash,回退 sh Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../context/SystemPromptBuilder.java | 6 +- .../com/claudecode/tool/impl/BashTool.java | 152 ++++++++++++++++-- .../claudecode/tui/ClaudeCodeComponent.java | 15 +- 3 files changed, 156 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/claudecode/context/SystemPromptBuilder.java b/src/main/java/com/claudecode/context/SystemPromptBuilder.java index ff0b6c8..ed6cf3e 100644 --- a/src/main/java/com/claudecode/context/SystemPromptBuilder.java +++ b/src/main/java/com/claudecode/context/SystemPromptBuilder.java @@ -1,5 +1,7 @@ package com.claudecode.context; +import com.claudecode.tool.impl.BashTool; + /** * 系统提示词构建器 —— 对应 claude-code/src/prompts.ts。 *

@@ -66,8 +68,8 @@ public class SystemPromptBuilder { sb.append("- Working directory: ").append(workDir).append("\n"); sb.append("- OS: ").append(osName).append("\n"); sb.append("- User: ").append(userName).append("\n"); - sb.append("- Shell: ").append(System.getenv().getOrDefault("SHELL", - System.getenv().getOrDefault("COMSPEC", "unknown"))).append("\n"); + // 使用 BashTool 检测到的 shell 信息(比 COMSPEC/SHELL 环境变量更准确) + sb.append(BashTool.getShellHint()); sb.append("\n"); // 行为准则 diff --git a/src/main/java/com/claudecode/tool/impl/BashTool.java b/src/main/java/com/claudecode/tool/impl/BashTool.java index 135dff5..3832b9c 100644 --- a/src/main/java/com/claudecode/tool/impl/BashTool.java +++ b/src/main/java/com/claudecode/tool/impl/BashTool.java @@ -2,6 +2,8 @@ package com.claudecode.tool.impl; import com.claudecode.tool.Tool; import com.claudecode.tool.ToolContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.BufferedReader; import java.io.InputStreamReader; @@ -10,15 +12,47 @@ import java.util.Map; import java.util.concurrent.TimeUnit; /** - * Bash 工具 —— 对应 claude-code/src/tools/bash/BashTool.ts。 + * Shell 工具 —— 对应 claude-code/src/tools/bash/BashTool.ts。 *

* 在指定工作目录中执行 shell 命令,返回 stdout/stderr 输出。 + * 自动检测最佳可用 shell: + *

*/ public class BashTool implements Tool { + private static final Logger log = LoggerFactory.getLogger(BashTool.class); + /** 默认超时(秒) */ private static final int DEFAULT_TIMEOUT = 120; + /** 检测到的 shell 类型 */ + public enum ShellType { + POWERSHELL("PowerShell", "pwsh", "-NoProfile", "-Command"), + POWERSHELL_WINDOWS("PowerShell", "powershell.exe", "-NoProfile", "-Command"), + CMD("cmd.exe", "cmd.exe", "/c", null), + BASH("Bash", "bash", "-c", null), + SH("sh", "sh", "-c", null); + + final String displayName; + final String executable; + final String flag1; + final String flag2; // 可选的额外 flag + + ShellType(String displayName, String executable, String flag1, String flag2) { + this.displayName = displayName; + this.executable = executable; + this.flag1 = flag1; + this.flag2 = flag2; + } + } + + private static final boolean IS_WINDOWS = System.getProperty("os.name", "").toLowerCase().contains("win"); + private static final ShellType DETECTED_SHELL = detectShell(); + private static final String SHELL_HINT = buildShellHint(); + @Override public String name() { return "Bash"; @@ -26,6 +60,19 @@ public class BashTool implements Tool { @Override public String description() { + if (IS_WINDOWS && DETECTED_SHELL.displayName.equals("PowerShell")) { + return """ + Execute a command in the working directory using PowerShell. \ + Use PowerShell syntax (Get-ChildItem, Select-String, Get-Content, etc). \ + Common equivalents: ls→Get-ChildItem, grep→Select-String, cat→Get-Content, \ + rm→Remove-Item, cp→Copy-Item, mv→Move-Item, find→Get-ChildItem -Recurse. \ + Commands run in a subprocess with timeout protection."""; + } else if (IS_WINDOWS) { + return """ + Execute a command in the working directory using cmd.exe. \ + Use Windows cmd syntax (dir, type, find, etc). \ + Commands run in a subprocess with timeout protection."""; + } return """ Execute a bash command in the working directory. \ Use this for file operations, running scripts, installing packages, \ @@ -60,15 +107,7 @@ public class BashTool implements Tool { Path workDir = context.getWorkDir(); try { - // 根据操作系统选择 shell - boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win"); - ProcessBuilder pb; - if (isWindows) { - pb = new ProcessBuilder("cmd", "/c", command); - } else { - pb = new ProcessBuilder("bash", "-c", command); - } - + ProcessBuilder pb = buildProcess(command); pb.directory(workDir.toFile()); pb.redirectErrorStream(true); @@ -104,10 +143,101 @@ public class BashTool implements Tool { @Override public String activityDescription(Map input) { String cmd = (String) input.getOrDefault("command", ""); - // 截断过长的命令 if (cmd.length() > 60) { cmd = cmd.substring(0, 57) + "..."; } return "⚡ " + cmd; } + + /** 构建 ProcessBuilder,根据检测到的 shell 类型 */ + private ProcessBuilder buildProcess(String command) { + return switch (DETECTED_SHELL) { + case POWERSHELL -> new ProcessBuilder("pwsh", "-NoProfile", "-Command", command); + case POWERSHELL_WINDOWS -> new ProcessBuilder("powershell.exe", "-NoProfile", "-Command", command); + case CMD -> new ProcessBuilder("cmd.exe", "/c", command); + case BASH -> new ProcessBuilder("bash", "-c", command); + case SH -> new ProcessBuilder("sh", "-c", command); + }; + } + + /** 检测最佳可用 shell */ + private static ShellType detectShell() { + if (IS_WINDOWS) { + // 优先 pwsh (PowerShell 7+) + if (isCommandAvailable("pwsh", "--version")) { + log.info("Detected shell: PowerShell 7+ (pwsh)"); + return ShellType.POWERSHELL; + } + // 回退到 Windows PowerShell 5.x + if (isCommandAvailable("powershell.exe", "-NoProfile", "-Command", "echo ok")) { + log.info("Detected shell: Windows PowerShell (powershell.exe)"); + return ShellType.POWERSHELL_WINDOWS; + } + // 最终回退到 cmd + log.info("Detected shell: cmd.exe (fallback)"); + return ShellType.CMD; + } + + // Unix: 优先 bash + String shellEnv = System.getenv("SHELL"); + if (shellEnv != null && shellEnv.contains("bash")) { + return ShellType.BASH; + } + if (isCommandAvailable("bash", "--version")) { + return ShellType.BASH; + } + return ShellType.SH; + } + + /** 检查命令是否可用 */ + private static boolean isCommandAvailable(String... cmd) { + try { + Process p = new ProcessBuilder(cmd) + .redirectErrorStream(true) + .start(); + p.getInputStream().readAllBytes(); + return p.waitFor(5, TimeUnit.SECONDS) && p.exitValue() == 0; + } catch (Exception e) { + return false; + } + } + + /** 获取 shell 提示信息(供系统提示词使用) */ + public static String getShellHint() { + return SHELL_HINT; + } + + /** 获取检测到的 shell 显示名 */ + public static String getDetectedShellName() { + return DETECTED_SHELL.displayName; + } + + private static String buildShellHint() { + if (IS_WINDOWS && DETECTED_SHELL.displayName.equals("PowerShell")) { + return """ + - Shell: %s (detected on Windows) + - IMPORTANT: The Bash tool executes commands via PowerShell, NOT bash or cmd.exe. + - Use PowerShell native cmdlets and syntax: + - List files: Get-ChildItem (or ls/dir aliases) + - Search text: Select-String -Pattern "xxx" -Path *.java + - Read file: Get-Content file.txt + - Delete: Remove-Item path + - Copy: Copy-Item src dst + - Move: Move-Item src dst + - Find files: Get-ChildItem -Recurse -Filter "*.java" + - Current dir: Get-Location (or pwd) + - Environment vars: $env:PATH + - Pipe: cmd1 | cmd2 + - String comparison: -eq, -ne, -like, -match + - Standard tools (git, mvn, npm, java, python) work normally. + """.formatted(DETECTED_SHELL.displayName); + } else if (IS_WINDOWS) { + return """ + - Shell: cmd.exe (Windows) + - Use Windows cmd syntax: dir, type, find, etc. + - Standard tools (git, mvn, npm) work normally. + """; + } + return "- Shell: " + DETECTED_SHELL.displayName + "\n"; + } } diff --git a/src/main/java/com/claudecode/tui/ClaudeCodeComponent.java b/src/main/java/com/claudecode/tui/ClaudeCodeComponent.java index 3be6c32..e7c7894 100644 --- a/src/main/java/com/claudecode/tui/ClaudeCodeComponent.java +++ b/src/main/java/com/claudecode/tui/ClaudeCodeComponent.java @@ -6,6 +6,7 @@ import com.claudecode.console.BannerPrinter; import com.claudecode.core.AgentLoop; import com.claudecode.core.TokenTracker; import com.claudecode.tool.ToolRegistry; +import com.claudecode.tool.impl.BashTool; import com.claudecode.tui.UIMessage.*; import io.mybatis.jink.component.*; import io.mybatis.jink.input.Key; @@ -92,6 +93,9 @@ public class ClaudeCodeComponent extends Component private volatile boolean askInputMode = false; // 是否在自由输入模式(选择"其他"后) private volatile String askQuestion; // 当前问题文本 + /** 最近一次渲染的总行数(用于滚动限制) */ + private volatile int lastRenderedItemCount = 0; + /** 首次用户输入回调(用于 conversation summary) */ private Consumer onFirstUserInput; @@ -226,8 +230,8 @@ public class ClaudeCodeComponent extends Component Text.of( Text.of("Tools: ").dimmed(), Text.of(String.valueOf(toolCount)).color(Color.BRIGHT_CYAN), - Text.of(" │ Commands: ").dimmed(), - Text.of(String.valueOf(cmdCount)).color(Color.BRIGHT_CYAN) + Text.of(" │ Shell: ").dimmed(), + Text.of(BashTool.getDetectedShellName()).color(Color.BRIGHT_CYAN) ) }; @@ -283,6 +287,9 @@ public class ClaudeCodeComponent extends Component )); } + // 记录总行数(供 scroll() 使用) + lastRenderedItemCount = allItems.size(); + // 虚拟滚动 List visibleItems; if (maxLines > 0 && allItems.size() > maxLines) { @@ -984,8 +991,8 @@ public class ClaudeCodeComponent extends Component // ==================== 滚动 ==================== private void scroll(TuiState s, int delta) { - int totalMessages = s.messages.size() + 1; // +1 for initial system msg - int maxOffset = Math.max(0, totalMessages - 1); + int totalItems = lastRenderedItemCount; + int maxOffset = Math.max(0, totalItems - 1); int newOffset = Math.max(0, Math.min(s.scrollOffset + delta, maxOffset)); setState(new TuiState(s.inputText, s.messages, newOffset, s.thinking, s.thinkingText)); }