fix: 修复滚动限制 + 自动检测Shell类型
滚动修复: - 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>
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
package com.claudecode.context;
|
package com.claudecode.context;
|
||||||
|
|
||||||
|
import com.claudecode.tool.impl.BashTool;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 系统提示词构建器 —— 对应 claude-code/src/prompts.ts。
|
* 系统提示词构建器 —— 对应 claude-code/src/prompts.ts。
|
||||||
* <p>
|
* <p>
|
||||||
@@ -66,8 +68,8 @@ public class SystemPromptBuilder {
|
|||||||
sb.append("- Working directory: ").append(workDir).append("\n");
|
sb.append("- Working directory: ").append(workDir).append("\n");
|
||||||
sb.append("- OS: ").append(osName).append("\n");
|
sb.append("- OS: ").append(osName).append("\n");
|
||||||
sb.append("- User: ").append(userName).append("\n");
|
sb.append("- User: ").append(userName).append("\n");
|
||||||
sb.append("- Shell: ").append(System.getenv().getOrDefault("SHELL",
|
// 使用 BashTool 检测到的 shell 信息(比 COMSPEC/SHELL 环境变量更准确)
|
||||||
System.getenv().getOrDefault("COMSPEC", "unknown"))).append("\n");
|
sb.append(BashTool.getShellHint());
|
||||||
sb.append("\n");
|
sb.append("\n");
|
||||||
|
|
||||||
// 行为准则
|
// 行为准则
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package com.claudecode.tool.impl;
|
|||||||
|
|
||||||
import com.claudecode.tool.Tool;
|
import com.claudecode.tool.Tool;
|
||||||
import com.claudecode.tool.ToolContext;
|
import com.claudecode.tool.ToolContext;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
@@ -10,15 +12,47 @@ import java.util.Map;
|
|||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bash 工具 —— 对应 claude-code/src/tools/bash/BashTool.ts。
|
* Shell 工具 —— 对应 claude-code/src/tools/bash/BashTool.ts。
|
||||||
* <p>
|
* <p>
|
||||||
* 在指定工作目录中执行 shell 命令,返回 stdout/stderr 输出。
|
* 在指定工作目录中执行 shell 命令,返回 stdout/stderr 输出。
|
||||||
|
* 自动检测最佳可用 shell:
|
||||||
|
* <ul>
|
||||||
|
* <li>Windows: PowerShell > cmd.exe</li>
|
||||||
|
* <li>Unix/Linux/macOS: bash(或 SHELL 环境变量)</li>
|
||||||
|
* </ul>
|
||||||
*/
|
*/
|
||||||
public class BashTool implements Tool {
|
public class BashTool implements Tool {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(BashTool.class);
|
||||||
|
|
||||||
/** 默认超时(秒) */
|
/** 默认超时(秒) */
|
||||||
private static final int DEFAULT_TIMEOUT = 120;
|
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
|
@Override
|
||||||
public String name() {
|
public String name() {
|
||||||
return "Bash";
|
return "Bash";
|
||||||
@@ -26,6 +60,19 @@ public class BashTool implements Tool {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String description() {
|
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 """
|
return """
|
||||||
Execute a bash command in the working directory. \
|
Execute a bash command in the working directory. \
|
||||||
Use this for file operations, running scripts, installing packages, \
|
Use this for file operations, running scripts, installing packages, \
|
||||||
@@ -60,15 +107,7 @@ public class BashTool implements Tool {
|
|||||||
Path workDir = context.getWorkDir();
|
Path workDir = context.getWorkDir();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 根据操作系统选择 shell
|
ProcessBuilder pb = buildProcess(command);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
pb.directory(workDir.toFile());
|
pb.directory(workDir.toFile());
|
||||||
pb.redirectErrorStream(true);
|
pb.redirectErrorStream(true);
|
||||||
|
|
||||||
@@ -104,10 +143,101 @@ public class BashTool implements Tool {
|
|||||||
@Override
|
@Override
|
||||||
public String activityDescription(Map<String, Object> input) {
|
public String activityDescription(Map<String, Object> input) {
|
||||||
String cmd = (String) input.getOrDefault("command", "");
|
String cmd = (String) input.getOrDefault("command", "");
|
||||||
// 截断过长的命令
|
|
||||||
if (cmd.length() > 60) {
|
if (cmd.length() > 60) {
|
||||||
cmd = cmd.substring(0, 57) + "...";
|
cmd = cmd.substring(0, 57) + "...";
|
||||||
}
|
}
|
||||||
return "⚡ " + cmd;
|
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";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.claudecode.console.BannerPrinter;
|
|||||||
import com.claudecode.core.AgentLoop;
|
import com.claudecode.core.AgentLoop;
|
||||||
import com.claudecode.core.TokenTracker;
|
import com.claudecode.core.TokenTracker;
|
||||||
import com.claudecode.tool.ToolRegistry;
|
import com.claudecode.tool.ToolRegistry;
|
||||||
|
import com.claudecode.tool.impl.BashTool;
|
||||||
import com.claudecode.tui.UIMessage.*;
|
import com.claudecode.tui.UIMessage.*;
|
||||||
import io.mybatis.jink.component.*;
|
import io.mybatis.jink.component.*;
|
||||||
import io.mybatis.jink.input.Key;
|
import io.mybatis.jink.input.Key;
|
||||||
@@ -92,6 +93,9 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
private volatile boolean askInputMode = false; // 是否在自由输入模式(选择"其他"后)
|
private volatile boolean askInputMode = false; // 是否在自由输入模式(选择"其他"后)
|
||||||
private volatile String askQuestion; // 当前问题文本
|
private volatile String askQuestion; // 当前问题文本
|
||||||
|
|
||||||
|
/** 最近一次渲染的总行数(用于滚动限制) */
|
||||||
|
private volatile int lastRenderedItemCount = 0;
|
||||||
|
|
||||||
/** 首次用户输入回调(用于 conversation summary) */
|
/** 首次用户输入回调(用于 conversation summary) */
|
||||||
private Consumer<String> onFirstUserInput;
|
private Consumer<String> onFirstUserInput;
|
||||||
|
|
||||||
@@ -226,8 +230,8 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
Text.of(
|
Text.of(
|
||||||
Text.of("Tools: ").dimmed(),
|
Text.of("Tools: ").dimmed(),
|
||||||
Text.of(String.valueOf(toolCount)).color(Color.BRIGHT_CYAN),
|
Text.of(String.valueOf(toolCount)).color(Color.BRIGHT_CYAN),
|
||||||
Text.of(" │ Commands: ").dimmed(),
|
Text.of(" │ Shell: ").dimmed(),
|
||||||
Text.of(String.valueOf(cmdCount)).color(Color.BRIGHT_CYAN)
|
Text.of(BashTool.getDetectedShellName()).color(Color.BRIGHT_CYAN)
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -283,6 +287,9 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 记录总行数(供 scroll() 使用)
|
||||||
|
lastRenderedItemCount = allItems.size();
|
||||||
|
|
||||||
// 虚拟滚动
|
// 虚拟滚动
|
||||||
List<Renderable> visibleItems;
|
List<Renderable> visibleItems;
|
||||||
if (maxLines > 0 && allItems.size() > maxLines) {
|
if (maxLines > 0 && allItems.size() > maxLines) {
|
||||||
@@ -984,8 +991,8 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
// ==================== 滚动 ====================
|
// ==================== 滚动 ====================
|
||||||
|
|
||||||
private void scroll(TuiState s, int delta) {
|
private void scroll(TuiState s, int delta) {
|
||||||
int totalMessages = s.messages.size() + 1; // +1 for initial system msg
|
int totalItems = lastRenderedItemCount;
|
||||||
int maxOffset = Math.max(0, totalMessages - 1);
|
int maxOffset = Math.max(0, totalItems - 1);
|
||||||
int newOffset = Math.max(0, Math.min(s.scrollOffset + delta, maxOffset));
|
int newOffset = Math.max(0, Math.min(s.scrollOffset + delta, maxOffset));
|
||||||
setState(new TuiState(s.inputText, s.messages, newOffset, s.thinking, s.thinkingText));
|
setState(new TuiState(s.inputText, s.messages, newOffset, s.thinking, s.thinkingText));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user