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:
+ *
+ * - Windows: PowerShell > cmd.exe
+ * - Unix/Linux/macOS: bash(或 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));
}