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:
@@ -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)。
|
||||
* 使用 ● 圆点 + <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 + "<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) {
|
||||
// 忽略解析错误
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user