style: CLI视觉样式升级 - 边框Banner+●圆点+⎿结果+耗时显示

- BannerPrinter: 带边框的启动Banner(╭╮╰╯框线+咖啡杯Logo+双列布局)
- ToolStatusRenderer: 彩色●圆点标识+⎿结果前缀(Claude Code风格)
- ThinkingRenderer: <thought>标签块显示(Copilot CLI风格)
- ReplSession: AI回复●前缀+✻耗时显示+智能Banner选择

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
liuzh
2026-04-01 23:33:35 +08:00
co-authored by Copilot
parent 1fc7a6a495
commit c52d5313a2
4 changed files with 156 additions and 63 deletions
@@ -5,41 +5,92 @@ import java.io.PrintStream;
/**
* Banner 打印器 —— 对应 claude-code/src/components/Banner.tsx。
* <p>
* 在启动时打印 ASCII Art Logo 和版本信息。
* 在启动时打印带边框的 Logo 和版本信息。
* 参考 Copilot CLI / Claude Code 的边框样式。
*/
public class BannerPrinter {
private static final String VERSION = "0.1.0-SNAPSHOT";
/**
* 打印 claude-code-java 启动 banner。
*/
public static void print(PrintStream out) {
String banner = """
%s
██████╗██╗ █████╗ ██╗ ██╗██████╗ ███████╗
██╔════╝██║ ██╔══██╗██║ ██║██╔══██╗██╔════╝
██║ ██║ ███████║██║ ██║██║ ██║█████╗
██║ ██║ ██╔══██║██║ ██║██║ ██║██╔══╝
╚██████╗███████╗██║ ██║╚██████╔╝██████╔╝███████╗
╚═════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝
%s ██████╗ ██████╗ ██████╗ ███████╗
██╔════╝██╔═══██╗██╔══██╗██╔════╝
██║ ██║ ██║██║ ██║█████╗
██║ ██║ ██║██║ ██║██╔══╝
╚██████╗╚██████╔╝██████╔╝███████╗
╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝
%s
""".formatted(AnsiStyle.BRIGHT_CYAN, AnsiStyle.BRIGHT_MAGENTA, AnsiStyle.RESET);
// 边框字符
private static final String TL = "", TR = "", BL = "", BR = "";
private static final String H = "", V = "";
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"));
/**
* 打印带边框的启动 Banner。
*
* @param out 输出流
* @param provider API 提供者名称
* @param model 模型名称
* @param baseUrl API URL
* @param workDir 工作目录
* @param toolCount 工具数量
* @param cmdCount 命令数量
* @param termInfo 终端信息
*/
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;
}
}
@@ -5,7 +5,7 @@ import java.io.PrintStream;
/**
* Thinking 内容渲染器 —— 对应 claude-code/src/components/Thinking.tsx。
* <p>
* 显示 AI 模型的思考过程(extended thinking)。
* 使用 ● 圆点 + &lt;thought&gt; 标签样式显示 AI 的思考过程(参考 Copilot CLI)。
*/
public class ThinkingRenderer {
@@ -15,25 +15,28 @@ public class ThinkingRenderer {
this.out = out;
}
/** 渲染 thinking 内容块 */
/** 渲染 thinking 内容块Copilot CLI 的 &lt;thought&gt; 标签风格) */
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 + "<thought>" + 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 + " </thought>" + 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 结束标记 */
@@ -5,7 +5,7 @@ import java.io.PrintStream;
/**
* 工具调用状态渲染器 —— 对应 claude-code/src/components/ToolStatus.tsx。
* <p>
* 在终端中显示工具调用的进度和结果
* 使用彩色 ● 圆点标识工具调用状态,配合 ⎿ 显示结果(参考 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) {
// 忽略解析错误
}