package com.claudecode.repl; import com.claudecode.config.AppConfig.ProviderInfo; import com.claudecode.command.CommandContext; import com.claudecode.command.CommandRegistry; import com.claudecode.console.*; import com.claudecode.core.AgentLoop; import com.claudecode.core.ConversationPersistence; import com.claudecode.tool.ToolRegistry; import com.claudecode.tool.impl.AskUserQuestionTool; 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.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.Scanner; import java.util.function.Function; /** * REPL 会话管理器 —— 对应 claude-code/src/REPL.tsx。 *

* 使用 JLine 3 提供丰富的终端交互体验: *

* 当 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 ProviderInfo providerInfo; private final ConversationPersistence persistence; private final PrintStream out; private final ToolStatusRenderer toolStatusRenderer; private final MarkdownRenderer markdownRenderer; private final SpinnerAnimation spinner; private final ThinkingRenderer thinkingRenderer; private final StatusLine statusLine; /** 对话摘要(取第一次用户输入的前40字) */ private String conversationSummary = ""; private volatile boolean running = true; /** 当前活跃的 LineReader(JLine 模式下用于 AskUser 和权限确认) */ private volatile LineReader activeReader; /** 当前活跃的 Scanner(Scanner 模式下用于 AskUser 和权限确认) */ private volatile Scanner activeScanner; public ReplSession(AgentLoop agentLoop, ToolRegistry toolRegistry, CommandRegistry commandRegistry, ProviderInfo providerInfo) { this.agentLoop = agentLoop; this.toolRegistry = toolRegistry; this.commandRegistry = commandRegistry; this.providerInfo = providerInfo; this.persistence = new ConversationPersistence(); // 强制使用 UTF-8 编码输出,确保 emoji 等 Unicode 字符在 Windows 终端正常显示 this.out = new PrintStream(System.out, true, StandardCharsets.UTF_8); this.toolStatusRenderer = new ToolStatusRenderer(out); this.markdownRenderer = new MarkdownRenderer(out); this.spinner = new SpinnerAnimation(out); this.thinkingRenderer = new ThinkingRenderer(out); this.statusLine = new StatusLine(out); setupAgentCallbacks(); setupToolContextCallbacks(); } /** 注册 ToolContext 回调(AskUser 用户输入) */ private void setupToolContextCallbacks() { // 注册 AskUserQuestionTool 所需的用户输入回调 var toolContext = agentLoop.getToolContext(); if (toolContext != null) { toolContext.set(AskUserQuestionTool.USER_INPUT_CALLBACK, (Function) this::readUserInputDuringAgentLoop); } } /** 注册 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()); } }); // 流式输出第一个 token 到达时停止 spinner agentLoop.setOnStreamStart(() -> spinner.stop()); agentLoop.setOnAssistantMessage(text -> { // 阻塞模式回调:流式模式下由 onToken 实时输出,此回调不触发 }); // 权限确认回调:非只读工具执行前请求用户确认 agentLoop.setOnPermissionRequest(request -> { spinner.stop(); return promptPermission(request); }); // Thinking 内容回调:显示 AI 思考过程 agentLoop.setOnThinkingContent(thinkingText -> { spinner.stop(); thinkingRenderer.render(thinkingText); }); } /** * 启动 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()) { boolean isDumb = "dumb".equals(terminal.getType()); if (isDumb) { log.info("当前为 dumb 终端模式,建议使用 Windows Terminal / PowerShell / cmd 获得完整体验"); } // 配置 Parser:支持反斜杠续行 (\) 和 三引号块 (""") DefaultParser parser = new DefaultParser(); parser.setEscapeChars(new char[]{'\\'}); // 反斜杠续行 LineReader reader = LineReaderBuilder.builder() .terminal(terminal) .parser(parser) .completer(new ClaudeCodeCompleter(commandRegistry, toolRegistry)) .variable(LineReader.HISTORY_FILE, historyDir.resolve("history")) .variable(LineReader.HISTORY_SIZE, 1000) .variable(LineReader.SECONDARY_PROMPT_PATTERN, "%P ... ") .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); printBanner(terminal); // 设置活跃的 reader,供 AskUser 和权限确认使用 this.activeReader = reader; // 非 dumb 终端启用底部状态行 boolean isDumb2 = "dumb".equals(terminal.getType()); if (!isDumb2) { statusLine.enable(providerInfo.model(), agentLoop.getTokenTracker()); } CommandContext cmdContext = new CommandContext(agentLoop, toolRegistry, out, () -> running = false); while (running) { String input; try { input = reader.readLine(prompt).strip(); } catch (UserInterruptException e) { spinner.stop(); out.println(AnsiStyle.dim(" ^C")); continue; } catch (EndOfFileException e) { break; } if (input.isEmpty()) { continue; } handleInput(input, cmdContext); } saveConversation(); out.println(AnsiStyle.dim("\n Goodbye! 👋\n")); } } /** 打印启动 Banner(JLine 模式) */ private void printBanner(Terminal terminal) { BannerPrinter.printCompact(out); // 显示 API 提供者、模型和 URL out.println(AnsiStyle.dim(" Provider: ") + AnsiStyle.cyan(providerInfo.provider().toUpperCase()) + AnsiStyle.dim(" Model: ") + AnsiStyle.cyan(providerInfo.model())); out.println(AnsiStyle.dim(" API URL: ") + AnsiStyle.cyan(providerInfo.baseUrl())); out.println(AnsiStyle.dim(" Work Dir: " + 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(" Provider: ") + AnsiStyle.cyan(providerInfo.provider().toUpperCase()) + AnsiStyle.dim(" Model: ") + AnsiStyle.cyan(providerInfo.model())); out.println(AnsiStyle.dim(" API URL: ") + AnsiStyle.cyan(providerInfo.baseUrl())); out.println(AnsiStyle.dim(" Work Dir: " + 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); this.activeScanner = scanner; 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); } saveConversation(); 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; } // 记录对话摘要(取第一次用户输入前40字) if (conversationSummary.isEmpty()) { conversationSummary = input.length() > 40 ? input.substring(0, 40) : input; } // Agent 循环(流式输出) try { spinner.start("Thinking..."); out.println(); // 换行准备输出区域 // 流式回调:逐 token 输出到终端 String response = agentLoop.runStreaming(input, token -> { out.print(token); out.flush(); }); spinner.stop(); out.println(); // 流式输出结束后换行 // 刷新底部状态行(显示最新 token 用量) if (statusLine.isEnabled()) { out.println(statusLine.renderInline()); } out.println(); } catch (Exception e) { spinner.stop(); out.println(AnsiStyle.red("\n ✗ Error: " + e.getMessage())); log.error("Agent 循环异常", e); out.println(); } } /** 退出时保存对话历史 */ private void saveConversation() { var history = agentLoop.getMessageHistory(); // 只有有实际对话内容时才保存(至少包含系统提示+用户消息+助手回复) if (history.size() > 2) { var file = persistence.save(history, conversationSummary); if (file != null) { out.println(AnsiStyle.dim(" 💾 对话已保存: " + file.getFileName())); } } } /** 获取对话持久化管理器 */ public ConversationPersistence getPersistence() { return persistence; } public void stop() { running = false; } // ==================== 权限确认 UI ==================== /** * 显示权限确认提示并等待用户输入。 * 用于危险操作(文件写入、bash 执行等)前的安全确认。 */ private boolean promptPermission(AgentLoop.PermissionRequest request) { out.println(); out.println(AnsiStyle.yellow(" ⚠ 权限确认")); out.println(" " + "─".repeat(50)); out.println(" " + AnsiStyle.bold("工具: ") + AnsiStyle.cyan(request.toolName())); out.println(" " + AnsiStyle.bold("操作: ") + request.activityDescription()); // 显示参数摘要(截断过长的参数) String argsPreview = request.arguments(); if (argsPreview != null && argsPreview.length() > 200) { argsPreview = argsPreview.substring(0, 200) + "..."; } if (argsPreview != null && !argsPreview.isBlank()) { out.println(" " + AnsiStyle.dim("参数: " + argsPreview)); } out.println(" " + "─".repeat(50)); out.print(" " + AnsiStyle.bold("允许执行?") + AnsiStyle.dim(" [Y/n/always] ") + AnsiStyle.BOLD + AnsiStyle.BRIGHT_CYAN + "→ " + AnsiStyle.RESET); out.flush(); String answer = readLineForPermission(); if (answer == null) return false; answer = answer.strip().toLowerCase(); // "always" → 禁用后续权限确认 if (answer.equals("always") || answer.equals("a")) { agentLoop.setOnPermissionRequest(null); // 移除权限回调 out.println(AnsiStyle.green(" ✓ 已授权所有后续操作")); return true; } // 空字符串或 y/yes → 允许 if (answer.isEmpty() || answer.equals("y") || answer.equals("yes")) { return true; } // 其他输入 → 拒绝 out.println(AnsiStyle.red(" ✗ 操作已拒绝")); return false; } /** 读取权限确认的用户输入(兼容 JLine 和 Scanner 模式) */ private String readLineForPermission() { try { if (activeReader != null) { return activeReader.readLine(); } else if (activeScanner != null && activeScanner.hasNextLine()) { return activeScanner.nextLine(); } } catch (Exception e) { log.debug("读取权限确认输入异常: {}", e.getMessage()); } return null; } // ==================== AskUser 工具回调 ==================== /** * 在 Agent 循环执行过程中读取用户输入。 * 被 AskUserQuestionTool 通过 ToolContext 回调使用。 */ private String readUserInputDuringAgentLoop(String prompt) { spinner.stop(); out.print(prompt); out.print(" " + AnsiStyle.BOLD + AnsiStyle.BRIGHT_CYAN + "→ " + AnsiStyle.RESET); out.flush(); try { if (activeReader != null) { return activeReader.readLine(); } else if (activeScanner != null && activeScanner.hasNextLine()) { return activeScanner.nextLine(); } } catch (UserInterruptException e) { return "(用户取消)"; } catch (Exception e) { log.debug("读取用户输入异常: {}", e.getMessage()); } return null; } }