diff --git a/src/main/java/com/claudecode/console/BannerPrinter.java b/src/main/java/com/claudecode/console/BannerPrinter.java index 2b0e528..7e38d8c 100644 --- a/src/main/java/com/claudecode/console/BannerPrinter.java +++ b/src/main/java/com/claudecode/console/BannerPrinter.java @@ -5,41 +5,92 @@ import java.io.PrintStream; /** * Banner 打印器 —— 对应 claude-code/src/components/Banner.tsx。 *

- * 在启动时打印 ASCII Art Logo 和版本信息。 + * 在启动时打印带边框的 Logo 和版本信息。 + * 参考 Copilot CLI / Claude Code 的边框样式。 */ public class BannerPrinter { private static final String VERSION = "0.1.0-SNAPSHOT"; + // 边框字符 + private static final String TL = "╭", TR = "╮", BL = "╰", BR = "╯"; + private static final String H = "─", V = "│"; + /** - * 打印 claude-code-java 启动 banner。 + * 打印带边框的启动 Banner。 + * + * @param out 输出流 + * @param provider API 提供者名称 + * @param model 模型名称 + * @param baseUrl API URL + * @param workDir 工作目录 + * @param toolCount 工具数量 + * @param cmdCount 命令数量 + * @param termInfo 终端信息 */ - public static void print(PrintStream out) { - String banner = """ - %s - ██████╗██╗ █████╗ ██╗ ██╗██████╗ ███████╗ - ██╔════╝██║ ██╔══██╗██║ ██║██╔══██╗██╔════╝ - ██║ ██║ ███████║██║ ██║██║ ██║█████╗ - ██║ ██║ ██╔══██║██║ ██║██║ ██║██╔══╝ - ╚██████╗███████╗██║ ██║╚██████╔╝██████╔╝███████╗ - ╚═════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝ - %s ██████╗ ██████╗ ██████╗ ███████╗ - ██╔════╝██╔═══██╗██╔══██╗██╔════╝ - ██║ ██║ ██║██║ ██║█████╗ - ██║ ██║ ██║██║ ██║██╔══╝ - ╚██████╗╚██████╔╝██████╔╝███████╗ - ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ - %s - """.formatted(AnsiStyle.BRIGHT_CYAN, AnsiStyle.BRIGHT_MAGENTA, AnsiStyle.RESET); - - out.println(banner); - out.println(AnsiStyle.bold(" Claude Code (Java)") + AnsiStyle.dim(" v" + VERSION)); - out.println(AnsiStyle.dim(" Powered by Spring AI • Type /help for commands")); + public static void printBoxed(PrintStream out, String provider, String model, + String baseUrl, String workDir, + int toolCount, int cmdCount, String termInfo) { + int boxWidth = 90; + String hr = H.repeat(boxWidth - 2); + + // Logo(简洁的 Java 咖啡杯 + 项目名) + String[] logo = { + " ╭───╮ ", + " │ ☕ │ ", + " ╭──╰───╯──╮", + " │ CLAUDE │", + " │ CODE │", + " ╰──────────╯" + }; + + // 右侧信息 + String titleLine = "Claude Code Java v" + VERSION; + String descLine = "Describe a task to get started."; + String tipLine = "Tip: /help for commands, Tab to complete"; + + // 打印顶部边框 out.println(); + out.println(" " + TL + "─── " + AnsiStyle.BOLD + AnsiStyle.BRIGHT_CYAN + + "Claude Code Java" + AnsiStyle.RESET + AnsiStyle.DIM + " v" + VERSION + + AnsiStyle.RESET + " " + H.repeat(boxWidth - 28 - VERSION.length()) + TR); + + // Logo + 右侧信息(双列布局) + int logoWidth = 16; // logo 视觉宽度 + int rightStart = logoWidth + 4; + int contentWidth = boxWidth - 4; // 边框内可用宽度 + + String[] rightInfo = { + "", + AnsiStyle.BOLD + "Welcome!" + AnsiStyle.RESET, + "", + AnsiStyle.DIM + "Provider: " + AnsiStyle.RESET + AnsiStyle.CYAN + provider.toUpperCase() + AnsiStyle.RESET + + AnsiStyle.DIM + " Model: " + AnsiStyle.RESET + AnsiStyle.CYAN + model + AnsiStyle.RESET, + AnsiStyle.DIM + "Work Dir: " + workDir + AnsiStyle.RESET, + AnsiStyle.DIM + "Tools: " + toolCount + " | Commands: " + cmdCount + " | " + termInfo + AnsiStyle.RESET, + }; + + int maxRows = Math.max(logo.length, rightInfo.length); + for (int i = 0; i < maxRows; i++) { + String leftPart = i < logo.length ? logo[i] : ""; + String rightPart = i < rightInfo.length ? rightInfo[i] : ""; + + // 左侧 logo 部分(固定宽度,无 ANSI 所以直接 pad) + String paddedLeft = padRight(leftPart, logoWidth); + + // 输出行 + out.println(" " + V + " " + + AnsiStyle.BRIGHT_CYAN + paddedLeft + AnsiStyle.RESET + + AnsiStyle.DIM + " │ " + AnsiStyle.RESET + + rightPart); + } + + // 底部边框 + out.println(" " + BL + hr + BR); } /** - * 精简版 banner(用于窄终端)。 + * 精简版 banner(用于窄终端或 Scanner 模式)。 */ public static void printCompact(PrintStream out) { out.println(); @@ -48,4 +99,16 @@ public class BannerPrinter { out.println(AnsiStyle.dim(" Type /help for commands • Ctrl+D to exit")); out.println(); } + + /** 右侧补空格到指定视觉宽度 */ + private static String padRight(String s, int width) { + int len = s.length(); + if (len >= width) return s; + return s + " ".repeat(width - len); + } + + /** 获取版本号 */ + public static String getVersion() { + return VERSION; + } } diff --git a/src/main/java/com/claudecode/console/ThinkingRenderer.java b/src/main/java/com/claudecode/console/ThinkingRenderer.java index d04d780..9293cb7 100644 --- a/src/main/java/com/claudecode/console/ThinkingRenderer.java +++ b/src/main/java/com/claudecode/console/ThinkingRenderer.java @@ -5,7 +5,7 @@ import java.io.PrintStream; /** * Thinking 内容渲染器 —— 对应 claude-code/src/components/Thinking.tsx。 *

- * 显示 AI 模型的思考过程(extended thinking)。 + * 使用 ● 圆点 + <thought> 标签样式显示 AI 的思考过程(参考 Copilot CLI)。 */ public class ThinkingRenderer { @@ -15,25 +15,28 @@ public class ThinkingRenderer { this.out = out; } - /** 渲染 thinking 内容块 */ + /** 渲染 thinking 内容块(Copilot CLI 的 <thought> 标签风格) */ public void render(String thinkingContent) { if (thinkingContent == null || thinkingContent.isBlank()) { return; } out.println(); - out.println(AnsiStyle.DIM + AnsiStyle.ITALIC + " 💭 Thinking..." + AnsiStyle.RESET); + out.println(AnsiStyle.BRIGHT_MAGENTA + " ● " + AnsiStyle.DIM + "" + AnsiStyle.RESET); - // 显示 thinking 内容(缩进并用暗色) + // 缩进显示 thinking 内容 for (String line : thinkingContent.lines().toList()) { - out.println(AnsiStyle.DIM + " │ " + line + AnsiStyle.RESET); + out.println(AnsiStyle.DIM + " " + line + AnsiStyle.RESET); } + + out.println(AnsiStyle.DIM + " " + AnsiStyle.RESET); out.println(); } /** 渲染 thinking 开始标记 */ public void renderStart() { - out.print(AnsiStyle.DIM + AnsiStyle.ITALIC + " 💭 Thinking..." + AnsiStyle.RESET); + out.print(AnsiStyle.BRIGHT_MAGENTA + " ● " + AnsiStyle.DIM + AnsiStyle.ITALIC + + "Thinking..." + AnsiStyle.RESET); } /** 渲染 thinking 结束标记 */ diff --git a/src/main/java/com/claudecode/console/ToolStatusRenderer.java b/src/main/java/com/claudecode/console/ToolStatusRenderer.java index 5f940f6..0dce39c 100644 --- a/src/main/java/com/claudecode/console/ToolStatusRenderer.java +++ b/src/main/java/com/claudecode/console/ToolStatusRenderer.java @@ -5,7 +5,7 @@ import java.io.PrintStream; /** * 工具调用状态渲染器 —— 对应 claude-code/src/components/ToolStatus.tsx。 *

- * 在终端中显示工具调用的进度和结果。 + * 使用彩色 ● 圆点标识工具调用状态,配合 ⎿ 显示结果(参考 Claude Code 样式)。 */ public class ToolStatusRenderer { @@ -17,16 +17,17 @@ public class ToolStatusRenderer { /** 渲染工具调用开始 */ public void renderStart(String toolName, String args) { - out.println(AnsiStyle.dim(" ─────────────────────────────────────────")); - out.print(AnsiStyle.YELLOW + " ⚙ " + AnsiStyle.BOLD + toolName + AnsiStyle.RESET); - out.println(AnsiStyle.dim(" running...")); - // 如果有简短参数,显示 + out.println(); + out.print(AnsiStyle.BRIGHT_BLUE + " ● " + AnsiStyle.BOLD + toolName + AnsiStyle.RESET); + + // 提取并显示简短参数 if (args != null && !args.isBlank()) { String summary = extractSummary(toolName, args); if (summary != null) { - out.println(AnsiStyle.dim(" " + summary)); + out.print(AnsiStyle.dim("(" + summary + ")")); } } + out.println(AnsiStyle.dim(" running...")); } /** 渲染工具调用完成 */ @@ -37,36 +38,40 @@ public class ToolStatusRenderer { display = display.substring(0, 497) + "..."; } - out.println(AnsiStyle.GREEN + " ✓ " + AnsiStyle.BOLD + toolName + AnsiStyle.RESET + out.println(AnsiStyle.GREEN + " ● " + AnsiStyle.BOLD + toolName + AnsiStyle.RESET + AnsiStyle.dim(" done")); + if (display != null && !display.isBlank()) { - // 缩进输出每一行 - for (String line : display.lines().toList()) { - out.println(AnsiStyle.dim(" " + line)); + // 使用 ⎿ 前缀显示结果(Claude Code 风格) + String[] lines = display.lines().toArray(String[]::new); + for (int i = 0; i < lines.length; i++) { + if (i == 0) { + out.println(AnsiStyle.DIM + " ⎿ " + lines[i] + AnsiStyle.RESET); + } else { + out.println(AnsiStyle.DIM + " " + lines[i] + AnsiStyle.RESET); + } } } - out.println(AnsiStyle.dim(" ─────────────────────────────────────────")); } /** 渲染工具错误 */ public void renderError(String toolName, String error) { - out.println(AnsiStyle.RED + " ✗ " + AnsiStyle.BOLD + toolName + AnsiStyle.RESET + out.println(AnsiStyle.RED + " ● " + AnsiStyle.BOLD + toolName + AnsiStyle.RESET + AnsiStyle.red(" error")); if (error != null) { - out.println(AnsiStyle.red(" " + error)); + out.println(AnsiStyle.DIM + " ⎿ " + AnsiStyle.RED + error + AnsiStyle.RESET); } } /** 从 JSON 参数中提取人类可读的摘要 */ private String extractSummary(String toolName, String args) { try { - // 简单提取关键字段 if (args.contains("\"command\"")) { int start = args.indexOf("\"command\""); int valStart = args.indexOf("\"", start + 10) + 1; int valEnd = args.indexOf("\"", valStart); if (valStart > 0 && valEnd > valStart) { - String cmd = args.substring(valStart, Math.min(valEnd, valStart + 80)); + String cmd = args.substring(valStart, Math.min(valEnd, valStart + 60)); return "$ " + cmd; } } @@ -86,6 +91,14 @@ public class ToolStatusRenderer { return "pattern: " + args.substring(valStart, valEnd); } } + if (args.contains("\"query\"")) { + int start = args.indexOf("\"query\""); + int valStart = args.indexOf("\"", start + 8) + 1; + int valEnd = args.indexOf("\"", valStart); + if (valStart > 0 && valEnd > valStart) { + return "\"" + args.substring(valStart, Math.min(valEnd, valStart + 60)) + "\""; + } + } } catch (Exception e) { // 忽略解析错误 } diff --git a/src/main/java/com/claudecode/repl/ReplSession.java b/src/main/java/com/claudecode/repl/ReplSession.java index f7219c5..0ae66bb 100644 --- a/src/main/java/com/claudecode/repl/ReplSession.java +++ b/src/main/java/com/claudecode/repl/ReplSession.java @@ -220,16 +220,6 @@ public class ReplSession { /** 打印启动 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() + " | Commands: " + commandRegistry.getCommands().size())); - boolean isDumb = "dumb".equals(terminal.getType()); int w = terminal.getWidth(); int h = terminal.getHeight(); @@ -244,14 +234,27 @@ public class ReplSession { termInfo += " [vim]"; } - out.println(AnsiStyle.dim(" Terminal: " + termInfo)); - - if (isDumb) { - out.println(AnsiStyle.yellow(" ⚠ Dumb 终端模式:Tab补全和行编辑可能受限")); - out.println(AnsiStyle.yellow(" 建议在 Windows Terminal / PowerShell / cmd.exe 中运行")); + if (isDumb || w < 60) { + // 窄终端/dumb 模式用精简 Banner + BannerPrinter.printCompact(out); + out.println(AnsiStyle.dim(" Provider: ") + AnsiStyle.cyan(providerInfo.provider().toUpperCase()) + + AnsiStyle.dim(" Model: ") + AnsiStyle.cyan(providerInfo.model())); + out.println(AnsiStyle.dim(" Work Dir: " + System.getProperty("user.dir"))); + if (isDumb) { + out.println(AnsiStyle.yellow(" ⚠ Dumb 终端模式:建议在 Windows Terminal / PowerShell 中运行")); + } } else { - out.println(AnsiStyle.dim(" Tip: Tab to complete commands, ↑↓ to browse history, Ctrl+D to exit")); + // 标准终端用带边框的 Banner + BannerPrinter.printBoxed(out, + providerInfo.provider(), + providerInfo.model(), + providerInfo.baseUrl(), + System.getProperty("user.dir"), + toolRegistry.size(), + commandRegistry.getCommands().size(), + termInfo); } + out.println(); } @@ -312,6 +315,11 @@ public class ReplSession { spinner.start("Thinking..."); out.println(); // 换行准备输出区域 + long startTime = System.currentTimeMillis(); + + // AI 回复前的 ● 标识 + out.println(AnsiStyle.BRIGHT_CYAN + " ● " + AnsiStyle.RESET); + // 流式回调:逐 token 输出到终端 String response = agentLoop.runStreaming(input, token -> { out.print(token); @@ -321,6 +329,12 @@ public class ReplSession { spinner.stop(); out.println(); // 流式输出结束后换行 + // 显示耗时 + long elapsed = (System.currentTimeMillis() - startTime) / 1000; + if (elapsed > 0) { + out.println(AnsiStyle.DIM + " ✻ Worked for " + elapsed + "s" + AnsiStyle.RESET); + } + // 刷新底部状态行(显示最新 token 用量) if (statusLine.isEnabled()) { out.println(statusLine.renderInline()); @@ -328,7 +342,7 @@ public class ReplSession { out.println(); } catch (Exception e) { spinner.stop(); - out.println(AnsiStyle.red("\n ✗ Error: " + e.getMessage())); + out.println(AnsiStyle.RED + "\n ● Error: " + AnsiStyle.RESET + e.getMessage()); log.error("Agent 循环异常", e); out.println(); }