Files
claude-code/src/main/java/com/claudecode/repl/ReplSession.java
T
liuzh cc0d3f8b00 Revert "fix: 将所有emoji/Unicode特殊字符替换为ASCII安全字符"
This reverts commit 3cd67ffe451be2cc7a66b5c554966f1d5aa58c7c.
2026-04-01 20:43:31 +08:00

250 lines
8.8 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.claudecode.repl;
import com.claudecode.command.CommandContext;
import com.claudecode.command.CommandRegistry;
import com.claudecode.console.*;
import com.claudecode.core.AgentLoop;
import com.claudecode.tool.ToolRegistry;
import org.jline.reader.*;
import org.jline.reader.impl.DefaultParser;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;
import org.jline.utils.AttributedStringBuilder;
import org.jline.utils.AttributedStyle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Scanner;
/**
* REPL 会话管理器 —— 对应 claude-code/src/REPL.tsx。
* <p>
* 使用 JLine 3 提供丰富的终端交互体验:
* <ul>
* <li>行编辑(光标移动、删除、粘贴)</li>
* <li>历史记录(上下箭头浏览、持久化到文件)</li>
* <li>Tab 补全(斜杠命令、工具名称)</li>
* <li>信号处理(Ctrl+C 取消当前输入、Ctrl+D 退出)</li>
* </ul>
* 当 JLine 初始化失败时自动降级到 Scanner 模式。
*/
public class ReplSession {
private static final Logger log = LoggerFactory.getLogger(ReplSession.class);
private final AgentLoop agentLoop;
private final ToolRegistry toolRegistry;
private final CommandRegistry commandRegistry;
private final PrintStream out;
private final ToolStatusRenderer toolStatusRenderer;
private final MarkdownRenderer markdownRenderer;
private final SpinnerAnimation spinner;
private volatile boolean running = true;
public ReplSession(AgentLoop agentLoop,
ToolRegistry toolRegistry,
CommandRegistry commandRegistry) {
this.agentLoop = agentLoop;
this.toolRegistry = toolRegistry;
this.commandRegistry = commandRegistry;
this.out = System.out;
this.toolStatusRenderer = new ToolStatusRenderer(out);
this.markdownRenderer = new MarkdownRenderer(out);
this.spinner = new SpinnerAnimation(out);
setupAgentCallbacks();
}
/** 注册 AgentLoop 事件回调,驱动控制台 UI 渲染 */
private void setupAgentCallbacks() {
agentLoop.setOnToolEvent(event -> {
switch (event.phase()) {
case START -> {
spinner.stop();
toolStatusRenderer.renderStart(event.toolName(), event.arguments());
}
case END -> toolStatusRenderer.renderEnd(event.toolName(), event.result());
}
});
agentLoop.setOnAssistantMessage(text -> {
// 助手文本在 agent 循环结束后由 REPL 统一渲染
});
}
/**
* 启动 REPL —— 优先使用 JLine,失败时降级到 Scanner。
*/
public void start() {
try {
startWithJLine();
} catch (Exception e) {
log.warn("JLine 初始化失败,降级到 Scanner 模式: {}", e.getMessage());
startWithScanner();
}
}
// ==================== JLine 模式 ====================
private void startWithJLine() throws IOException {
Path historyDir = Path.of(System.getProperty("user.home"), ".claude-code-java");
Files.createDirectories(historyDir);
try (Terminal terminal = TerminalBuilder.builder()
.system(true)
.streams(System.in, System.out)
.build()) {
// 检测是否为 dumb 终端并提示
boolean isDumb = "dumb".equals(terminal.getType());
if (isDumb) {
log.info("当前为 dumb 终端模式,建议使用 Windows Terminal / PowerShell / cmd 获得完整体验");
}
LineReader reader = LineReaderBuilder.builder()
.terminal(terminal)
.parser(new DefaultParser())
.completer(new ClaudeCodeCompleter(commandRegistry, toolRegistry))
.variable(LineReader.HISTORY_FILE, historyDir.resolve("history"))
.variable(LineReader.HISTORY_SIZE, 1000)
.option(LineReader.Option.CASE_INSENSITIVE, true)
.option(LineReader.Option.AUTO_LIST, true)
.build();
// 构建彩色提示符
String prompt = new AttributedStringBuilder()
.style(AttributedStyle.BOLD.foreground(AttributedStyle.CYAN))
.append(" ")
.style(AttributedStyle.DEFAULT)
.toAnsi(terminal);
// 续行提示符(多行输入时显示)
String rightPrompt = new AttributedStringBuilder()
.style(AttributedStyle.DEFAULT.foreground(AttributedStyle.BRIGHT))
.append("")
.toAnsi(terminal);
printBanner(terminal);
CommandContext cmdContext = new CommandContext(agentLoop, toolRegistry, out, () -> running = false);
while (running) {
String input;
try {
input = reader.readLine(prompt).strip();
} catch (UserInterruptException e) {
// Ctrl+C —— 取消当前输入,继续等待
spinner.stop();
out.println(AnsiStyle.dim(" ^C"));
continue;
} catch (EndOfFileException e) {
// Ctrl+D —— 退出
break;
}
if (input.isEmpty()) {
continue;
}
handleInput(input, cmdContext);
}
out.println(AnsiStyle.dim("\n Goodbye! 👋\n"));
}
}
/** 打印启动 BannerJLine 模式) */
private void printBanner(Terminal terminal) {
BannerPrinter.printCompact(out);
out.println(AnsiStyle.dim(" Working directory: " + System.getProperty("user.dir")));
out.println(AnsiStyle.dim(" Tools: " + toolRegistry.size() + " registered"));
boolean isDumb = "dumb".equals(terminal.getType());
int w = terminal.getWidth();
int h = terminal.getHeight();
String termInfo = terminal.getType();
if (w > 0 && h > 0) {
termInfo += " (" + w + "×" + h + ")";
}
out.println(AnsiStyle.dim(" Terminal: " + termInfo));
if (isDumb) {
out.println(AnsiStyle.yellow(" ⚠ Dumb 终端模式:Tab补全和行编辑可能受限"));
out.println(AnsiStyle.yellow(" 建议在 Windows Terminal / PowerShell / cmd.exe 中运行"));
} else {
out.println(AnsiStyle.dim(" Tip: Tab to complete commands, ↑↓ to browse history, Ctrl+D to exit"));
}
out.println();
}
// ==================== Scanner 降级模式 ====================
private void startWithScanner() {
BannerPrinter.printCompact(out);
out.println(AnsiStyle.dim(" Working directory: " + System.getProperty("user.dir")));
out.println(AnsiStyle.dim(" Tools: " + toolRegistry.size() + " registered"));
out.println(AnsiStyle.dim(" Mode: Scanner (basic input)"));
out.println();
Scanner scanner = new Scanner(System.in);
CommandContext cmdContext = new CommandContext(agentLoop, toolRegistry, out, () -> running = false);
while (running) {
out.print(AnsiStyle.BOLD + AnsiStyle.BRIGHT_CYAN + " " + AnsiStyle.RESET);
out.flush();
String input;
try {
if (!scanner.hasNextLine()) break;
input = scanner.nextLine().strip();
} catch (Exception e) {
break;
}
if (input.isEmpty()) continue;
handleInput(input, cmdContext);
}
out.println(AnsiStyle.dim("\n Goodbye! 👋\n"));
}
// ==================== 公共输入处理 ====================
/** 处理用户输入(命令分发或 Agent 调用) */
private void handleInput(String input, CommandContext cmdContext) {
// 斜杠命令
if (commandRegistry.isCommand(input)) {
var result = commandRegistry.dispatch(input, cmdContext);
result.ifPresent(out::println);
out.println();
return;
}
// Agent 循环
try {
spinner.start("Thinking...");
String response = agentLoop.run(input);
spinner.stop();
out.println();
markdownRenderer.render(response);
out.println();
} catch (Exception e) {
spinner.stop();
out.println(AnsiStyle.red("\n ✗ Error: " + e.getMessage()));
log.error("Agent 循环异常", e);
out.println();
}
}
public void stop() {
running = false;
}
}