feat: console rendering enhancements (Phase 1B.4)
- MarkdownRenderer: table parsing and box-drawing character rendering - ToolStatusRenderer: permission dialog beautification, progress bar, duration tracking - SpinnerAnimation: multiple animation styles, elapsed time display, stopAndGetElapsed() Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -87,10 +87,18 @@ public class MarkdownRenderer {
|
|||||||
|
|
||||||
boolean inCodeBlock = false;
|
boolean inCodeBlock = false;
|
||||||
String codeBlockLang = "";
|
String codeBlockLang = "";
|
||||||
|
boolean inTable = false;
|
||||||
|
java.util.List<String> tableLines = new java.util.ArrayList<>();
|
||||||
|
|
||||||
for (String line : markdown.lines().toList()) {
|
for (String line : markdown.lines().toList()) {
|
||||||
// 代码块
|
// 代码块
|
||||||
if (line.stripLeading().startsWith("```")) {
|
if (line.stripLeading().startsWith("```")) {
|
||||||
|
// Flush any pending table
|
||||||
|
if (inTable) {
|
||||||
|
renderTable(tableLines);
|
||||||
|
tableLines.clear();
|
||||||
|
inTable = false;
|
||||||
|
}
|
||||||
if (!inCodeBlock) {
|
if (!inCodeBlock) {
|
||||||
codeBlockLang = line.stripLeading().substring(3).strip().toLowerCase();
|
codeBlockLang = line.stripLeading().substring(3).strip().toLowerCase();
|
||||||
inCodeBlock = true;
|
inCodeBlock = true;
|
||||||
@@ -110,6 +118,20 @@ public class MarkdownRenderer {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Table detection: lines containing | separators
|
||||||
|
String stripped = line.stripLeading();
|
||||||
|
if (stripped.contains("|") && (stripped.startsWith("|") || isTableLine(stripped))) {
|
||||||
|
if (!inTable) {
|
||||||
|
inTable = true;
|
||||||
|
}
|
||||||
|
tableLines.add(stripped);
|
||||||
|
continue;
|
||||||
|
} else if (inTable) {
|
||||||
|
renderTable(tableLines);
|
||||||
|
tableLines.clear();
|
||||||
|
inTable = false;
|
||||||
|
}
|
||||||
|
|
||||||
// 标题
|
// 标题
|
||||||
if (line.startsWith("### ")) {
|
if (line.startsWith("### ")) {
|
||||||
out.println(AnsiStyle.bold(AnsiStyle.CYAN + " " + line.substring(4)) + AnsiStyle.RESET);
|
out.println(AnsiStyle.bold(AnsiStyle.CYAN + " " + line.substring(4)) + AnsiStyle.RESET);
|
||||||
@@ -119,12 +141,12 @@ public class MarkdownRenderer {
|
|||||||
out.println(AnsiStyle.bold(AnsiStyle.MAGENTA + " " + line.substring(2)) + AnsiStyle.RESET);
|
out.println(AnsiStyle.bold(AnsiStyle.MAGENTA + " " + line.substring(2)) + AnsiStyle.RESET);
|
||||||
}
|
}
|
||||||
// 引用块
|
// 引用块
|
||||||
else if (line.stripLeading().startsWith("> ")) {
|
else if (stripped.startsWith("> ")) {
|
||||||
String quoteText = line.stripLeading().substring(2);
|
String quoteText = stripped.substring(2);
|
||||||
out.println(" " + AnsiStyle.DIM + "┃" + AnsiStyle.RESET + " " + AnsiStyle.ITALIC + renderInline(quoteText) + AnsiStyle.RESET);
|
out.println(" " + AnsiStyle.DIM + "┃" + AnsiStyle.RESET + " " + AnsiStyle.ITALIC + renderInline(quoteText) + AnsiStyle.RESET);
|
||||||
}
|
}
|
||||||
// 有序列表
|
// 有序列表
|
||||||
else if (line.stripLeading().matches("^\\d+\\.\\s+.*")) {
|
else if (stripped.matches("^\\d+\\.\\s+.*")) {
|
||||||
Matcher m = Pattern.compile("^(\\s*)(\\d+)\\.\\s+(.*)").matcher(line);
|
Matcher m = Pattern.compile("^(\\s*)(\\d+)\\.\\s+(.*)").matcher(line);
|
||||||
if (m.matches()) {
|
if (m.matches()) {
|
||||||
String indent = m.group(1);
|
String indent = m.group(1);
|
||||||
@@ -136,19 +158,19 @@ public class MarkdownRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 无序列表
|
// 无序列表
|
||||||
else if (line.stripLeading().startsWith("- ") || line.stripLeading().startsWith("* ")) {
|
else if (stripped.startsWith("- ") || stripped.startsWith("* ")) {
|
||||||
int indent = line.length() - line.stripLeading().length();
|
int indent = line.length() - stripped.length();
|
||||||
String prefix = " ".repeat(indent);
|
String prefix = " ".repeat(indent);
|
||||||
out.println(" " + prefix + AnsiStyle.CYAN + "•" + AnsiStyle.RESET + " " + renderInline(line.stripLeading().substring(2)));
|
out.println(" " + prefix + AnsiStyle.CYAN + "•" + AnsiStyle.RESET + " " + renderInline(stripped.substring(2)));
|
||||||
}
|
}
|
||||||
// 复选框列表
|
// 复选框列表
|
||||||
else if (line.stripLeading().startsWith("- [ ] ")) {
|
else if (stripped.startsWith("- [ ] ")) {
|
||||||
out.println(" " + AnsiStyle.DIM + "☐" + AnsiStyle.RESET + " " + renderInline(line.stripLeading().substring(6)));
|
out.println(" " + AnsiStyle.DIM + "☐" + AnsiStyle.RESET + " " + renderInline(stripped.substring(6)));
|
||||||
} else if (line.stripLeading().startsWith("- [x] ") || line.stripLeading().startsWith("- [X] ")) {
|
} else if (stripped.startsWith("- [x] ") || stripped.startsWith("- [X] ")) {
|
||||||
out.println(" " + AnsiStyle.GREEN + "☑" + AnsiStyle.RESET + " " + renderInline(line.stripLeading().substring(6)));
|
out.println(" " + AnsiStyle.GREEN + "☑" + AnsiStyle.RESET + " " + renderInline(stripped.substring(6)));
|
||||||
}
|
}
|
||||||
// 分隔线
|
// 分隔线
|
||||||
else if (line.strip().matches("^[-*]{3,}$")) {
|
else if (stripped.matches("^[-*]{3,}$")) {
|
||||||
out.println(AnsiStyle.dim(" " + "─".repeat(42)));
|
out.println(AnsiStyle.dim(" " + "─".repeat(42)));
|
||||||
}
|
}
|
||||||
// 普通文本
|
// 普通文本
|
||||||
@@ -156,6 +178,120 @@ public class MarkdownRenderer {
|
|||||||
out.println(" " + renderInline(line));
|
out.println(" " + renderInline(line));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flush any remaining table
|
||||||
|
if (inTable) {
|
||||||
|
renderTable(tableLines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 表格渲染 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测行是否是表格行(包含至少一个 | 且有文本内容)。
|
||||||
|
*/
|
||||||
|
private boolean isTableLine(String line) {
|
||||||
|
if (!line.contains("|")) return false;
|
||||||
|
// Separator lines like |---|---|
|
||||||
|
if (line.matches("^\\|?[\\s:-]+\\|[\\s|:-]*$")) return true;
|
||||||
|
// Content lines
|
||||||
|
String[] parts = line.split("\\|");
|
||||||
|
return parts.length >= 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染 Markdown 表格为对齐的终端输出。
|
||||||
|
* 支持标准 Markdown 表格格式(带/不带首尾 |)。
|
||||||
|
*/
|
||||||
|
private void renderTable(java.util.List<String> tableLines) {
|
||||||
|
if (tableLines.isEmpty()) return;
|
||||||
|
|
||||||
|
// Parse all rows
|
||||||
|
var rows = new java.util.ArrayList<String[]>();
|
||||||
|
int separatorIdx = -1;
|
||||||
|
|
||||||
|
for (int i = 0; i < tableLines.size(); i++) {
|
||||||
|
String line = tableLines.get(i).strip();
|
||||||
|
// Remove leading/trailing |
|
||||||
|
if (line.startsWith("|")) line = line.substring(1);
|
||||||
|
if (line.endsWith("|")) line = line.substring(0, line.length() - 1);
|
||||||
|
|
||||||
|
// Check if separator line
|
||||||
|
if (line.matches("[\\s:-]+\\|[\\s|:-]*") || line.matches("[\\s:-]+")) {
|
||||||
|
separatorIdx = i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] cells = line.split("\\|");
|
||||||
|
for (int j = 0; j < cells.length; j++) {
|
||||||
|
cells[j] = cells[j].strip();
|
||||||
|
}
|
||||||
|
rows.add(cells);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rows.isEmpty()) return;
|
||||||
|
|
||||||
|
// Calculate column widths
|
||||||
|
int maxCols = rows.stream().mapToInt(r -> r.length).max().orElse(0);
|
||||||
|
int[] widths = new int[maxCols];
|
||||||
|
for (String[] row : rows) {
|
||||||
|
for (int j = 0; j < row.length && j < maxCols; j++) {
|
||||||
|
widths[j] = Math.max(widths[j], stripAnsi(row[j]).length());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Ensure minimum width
|
||||||
|
for (int j = 0; j < widths.length; j++) {
|
||||||
|
widths[j] = Math.max(widths[j], 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render
|
||||||
|
boolean isHeader = true;
|
||||||
|
for (String[] row : rows) {
|
||||||
|
StringBuilder sb = new StringBuilder(" ");
|
||||||
|
if (isHeader) {
|
||||||
|
// Header with bold
|
||||||
|
sb.append("│ ");
|
||||||
|
for (int j = 0; j < maxCols; j++) {
|
||||||
|
String cell = j < row.length ? row[j] : "";
|
||||||
|
sb.append(AnsiStyle.bold(padRight(cell, widths[j])));
|
||||||
|
if (j < maxCols - 1) sb.append(" │ ");
|
||||||
|
}
|
||||||
|
sb.append(" │");
|
||||||
|
out.println(sb);
|
||||||
|
|
||||||
|
// Separator
|
||||||
|
StringBuilder sep = new StringBuilder(" ├─");
|
||||||
|
for (int j = 0; j < maxCols; j++) {
|
||||||
|
sep.append("─".repeat(widths[j]));
|
||||||
|
if (j < maxCols - 1) sep.append("─┼─");
|
||||||
|
}
|
||||||
|
sep.append("─┤");
|
||||||
|
out.println(AnsiStyle.dim(sep.toString()));
|
||||||
|
isHeader = false;
|
||||||
|
} else {
|
||||||
|
sb.append("│ ");
|
||||||
|
for (int j = 0; j < maxCols; j++) {
|
||||||
|
String cell = j < row.length ? row[j] : "";
|
||||||
|
sb.append(padRight(renderInline(cell), widths[j] + (renderInline(cell).length() - stripAnsi(renderInline(cell)).length())));
|
||||||
|
if (j < maxCols - 1) sb.append(" │ ");
|
||||||
|
}
|
||||||
|
sb.append(" │");
|
||||||
|
out.println(sb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Strip ANSI escape codes for width calculation */
|
||||||
|
private String stripAnsi(String s) {
|
||||||
|
return s.replaceAll("\u001B\\[[;\\d]*m", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pad string to target width (considering visible length) */
|
||||||
|
private String padRight(String s, int width) {
|
||||||
|
int visible = stripAnsi(s).length();
|
||||||
|
int padding = width - visible;
|
||||||
|
if (padding <= 0) return s;
|
||||||
|
return s + " ".repeat(padding);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 代码语法高亮 ====================
|
// ==================== 代码语法高亮 ====================
|
||||||
|
|||||||
@@ -6,33 +6,56 @@ import java.io.PrintStream;
|
|||||||
* 加载动画(Spinner)—— 对应 claude-code/src/components/Spinner.tsx。
|
* 加载动画(Spinner)—— 对应 claude-code/src/components/Spinner.tsx。
|
||||||
* <p>
|
* <p>
|
||||||
* 在等待 AI 响应时显示旋转动画。
|
* 在等待 AI 响应时显示旋转动画。
|
||||||
|
* 增强功能:多种动画样式、进度追踪、耗时计时。
|
||||||
*/
|
*/
|
||||||
public class SpinnerAnimation {
|
public class SpinnerAnimation {
|
||||||
|
|
||||||
private static final String[] FRAMES = {"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"};
|
/** 标准 braille spinner */
|
||||||
|
private static final String[] BRAILLE_FRAMES = {"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"};
|
||||||
|
/** 简约点动画 */
|
||||||
|
private static final String[] DOT_FRAMES = {"⠋", "⠙", "⠚", "⠒", "⠂", "⠂", "⠒", "⠖", "⠦", "⠖", "⠒", "⠂", "⠂", "⠒", "⠚", "⠙"};
|
||||||
|
/** 箭头动画 */
|
||||||
|
private static final String[] ARROW_FRAMES = {"▹▹▹▹▹", "▸▹▹▹▹", "▹▸▹▹▹", "▹▹▸▹▹", "▹▹▹▸▹", "▹▹▹▹▸"};
|
||||||
|
|
||||||
private static final int INTERVAL_MS = 80;
|
private static final int INTERVAL_MS = 80;
|
||||||
|
|
||||||
private final PrintStream out;
|
private final PrintStream out;
|
||||||
private volatile boolean running;
|
private volatile boolean running;
|
||||||
private Thread thread;
|
private Thread thread;
|
||||||
private String message = "Thinking";
|
private String message = "Thinking";
|
||||||
|
private long startTimeMs;
|
||||||
|
private String[] frames = BRAILLE_FRAMES;
|
||||||
|
|
||||||
public SpinnerAnimation(PrintStream out) {
|
public SpinnerAnimation(PrintStream out) {
|
||||||
this.out = out;
|
this.out = out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 设置动画样式 */
|
||||||
|
public SpinnerAnimation withStyle(Style style) {
|
||||||
|
this.frames = switch (style) {
|
||||||
|
case BRAILLE -> BRAILLE_FRAMES;
|
||||||
|
case DOT -> DOT_FRAMES;
|
||||||
|
case ARROW -> ARROW_FRAMES;
|
||||||
|
};
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/** 启动 spinner */
|
/** 启动 spinner */
|
||||||
public void start(String message) {
|
public void start(String message) {
|
||||||
if (running) return;
|
if (running) return;
|
||||||
this.message = message;
|
this.message = message;
|
||||||
this.running = true;
|
this.running = true;
|
||||||
|
this.startTimeMs = System.currentTimeMillis();
|
||||||
|
|
||||||
thread = Thread.ofVirtual().name("spinner").start(() -> {
|
thread = Thread.ofVirtual().name("spinner").start(() -> {
|
||||||
int idx = 0;
|
int idx = 0;
|
||||||
while (running) {
|
while (running) {
|
||||||
|
long elapsed = System.currentTimeMillis() - startTimeMs;
|
||||||
|
String timeStr = elapsed > 2000 ? " " + formatElapsed(elapsed) : "";
|
||||||
|
|
||||||
out.print(AnsiStyle.clearLine());
|
out.print(AnsiStyle.clearLine());
|
||||||
out.print(AnsiStyle.CYAN + " " + FRAMES[idx % FRAMES.length]
|
out.print(AnsiStyle.CYAN + " " + frames[idx % frames.length]
|
||||||
+ " " + AnsiStyle.RESET + AnsiStyle.dim(this.message));
|
+ " " + AnsiStyle.RESET + AnsiStyle.dim(this.message + timeStr));
|
||||||
out.flush();
|
out.flush();
|
||||||
idx++;
|
idx++;
|
||||||
try {
|
try {
|
||||||
@@ -61,6 +84,13 @@ public class SpinnerAnimation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 停止 spinner 并返回耗时 (ms) */
|
||||||
|
public long stopAndGetElapsed() {
|
||||||
|
long elapsed = System.currentTimeMillis() - startTimeMs;
|
||||||
|
stop();
|
||||||
|
return elapsed;
|
||||||
|
}
|
||||||
|
|
||||||
/** 更新消息 */
|
/** 更新消息 */
|
||||||
public void updateMessage(String newMessage) {
|
public void updateMessage(String newMessage) {
|
||||||
this.message = newMessage;
|
this.message = newMessage;
|
||||||
@@ -69,4 +99,20 @@ public class SpinnerAnimation {
|
|||||||
public boolean isRunning() {
|
public boolean isRunning() {
|
||||||
return running;
|
return running;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 获取已经过的时间 (ms) */
|
||||||
|
public long getElapsedMs() {
|
||||||
|
return System.currentTimeMillis() - startTimeMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatElapsed(long ms) {
|
||||||
|
if (ms < 1000) return ms + "ms";
|
||||||
|
if (ms < 60_000) return String.format("%.1fs", ms / 1000.0);
|
||||||
|
return String.format("%dm%ds", ms / 60_000, (ms % 60_000) / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 动画样式 */
|
||||||
|
public enum Style {
|
||||||
|
BRAILLE, DOT, ARROW
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import java.io.PrintStream;
|
|||||||
* 工具调用状态渲染器 —— 对应 claude-code/src/components/ToolStatus.tsx。
|
* 工具调用状态渲染器 —— 对应 claude-code/src/components/ToolStatus.tsx。
|
||||||
* <p>
|
* <p>
|
||||||
* 使用彩色 ● 圆点标识工具调用状态,配合 ⎿ 显示结果(参考 Claude Code 样式)。
|
* 使用彩色 ● 圆点标识工具调用状态,配合 ⎿ 显示结果(参考 Claude Code 样式)。
|
||||||
|
* 增强功能:进度条、权限对话框美化、计时器。
|
||||||
*/
|
*/
|
||||||
public class ToolStatusRenderer {
|
public class ToolStatusRenderer {
|
||||||
|
|
||||||
@@ -30,19 +31,48 @@ public class ToolStatusRenderer {
|
|||||||
out.println(AnsiStyle.dim(" running..."));
|
out.println(AnsiStyle.dim(" running..."));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 渲染工具调用完成 */
|
/** 渲染工具调用开始(带进度追踪) */
|
||||||
public void renderEnd(String toolName, String result) {
|
public void renderStartWithTimer(String toolName, String args) {
|
||||||
// 截断长结果
|
out.println();
|
||||||
String display = result;
|
out.print(AnsiStyle.BRIGHT_BLUE + " ● " + AnsiStyle.BOLD + toolName + AnsiStyle.RESET);
|
||||||
if (display != null && display.length() > 500) {
|
|
||||||
display = display.substring(0, 497) + "...";
|
if (args != null && !args.isBlank()) {
|
||||||
|
String summary = extractSummary(toolName, args);
|
||||||
|
if (summary != null) {
|
||||||
|
out.print(AnsiStyle.dim(" " + summary));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.println(AnsiStyle.dim(" ⏱ running..."));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 渲染工具调用完成(带耗时) */
|
||||||
|
public void renderEnd(String toolName, String result, long durationMs) {
|
||||||
|
String timeStr = durationMs > 0 ? formatDuration(durationMs) : "";
|
||||||
|
|
||||||
|
out.println(AnsiStyle.GREEN + " ● " + AnsiStyle.BOLD + toolName + AnsiStyle.RESET
|
||||||
|
+ AnsiStyle.dim(" done")
|
||||||
|
+ (timeStr.isEmpty() ? "" : AnsiStyle.dim(" (" + timeStr + ")")));
|
||||||
|
|
||||||
|
renderResultBlock(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 渲染工具调用完成 */
|
||||||
|
public void renderEnd(String toolName, String result) {
|
||||||
out.println(AnsiStyle.GREEN + " ● " + AnsiStyle.BOLD + toolName + AnsiStyle.RESET
|
out.println(AnsiStyle.GREEN + " ● " + AnsiStyle.BOLD + toolName + AnsiStyle.RESET
|
||||||
+ AnsiStyle.dim(" done"));
|
+ AnsiStyle.dim(" done"));
|
||||||
|
|
||||||
if (display != null && !display.isBlank()) {
|
renderResultBlock(result);
|
||||||
// 使用 ⎿ 前缀显示结果(Claude Code 风格)
|
}
|
||||||
|
|
||||||
|
/** 渲染结果输出块 */
|
||||||
|
private void renderResultBlock(String result) {
|
||||||
|
if (result == null || result.isBlank()) return;
|
||||||
|
|
||||||
|
String display = result;
|
||||||
|
if (display.length() > 500) {
|
||||||
|
display = display.substring(0, 497) + "...";
|
||||||
|
}
|
||||||
|
|
||||||
String[] lines = display.lines().toArray(String[]::new);
|
String[] lines = display.lines().toArray(String[]::new);
|
||||||
for (int i = 0; i < lines.length; i++) {
|
for (int i = 0; i < lines.length; i++) {
|
||||||
if (i == 0) {
|
if (i == 0) {
|
||||||
@@ -52,7 +82,6 @@ public class ToolStatusRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/** 渲染工具错误 */
|
/** 渲染工具错误 */
|
||||||
public void renderError(String toolName, String error) {
|
public void renderError(String toolName, String error) {
|
||||||
@@ -63,6 +92,60 @@ public class ToolStatusRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 渲染权限请求对话框 */
|
||||||
|
public void renderPermissionRequest(String toolName, String action, String detail) {
|
||||||
|
out.println();
|
||||||
|
out.println(AnsiStyle.YELLOW + " ┌─────────────────────────────────────────────┐" + AnsiStyle.RESET);
|
||||||
|
out.println(AnsiStyle.YELLOW + " │" + AnsiStyle.RESET + " 🔐 " + AnsiStyle.bold("Permission Required")
|
||||||
|
+ " " + AnsiStyle.YELLOW + "│" + AnsiStyle.RESET);
|
||||||
|
out.println(AnsiStyle.YELLOW + " ├─────────────────────────────────────────────┤" + AnsiStyle.RESET);
|
||||||
|
out.println(AnsiStyle.YELLOW + " │" + AnsiStyle.RESET
|
||||||
|
+ " Tool: " + AnsiStyle.bold(toolName)
|
||||||
|
+ " ".repeat(Math.max(1, 37 - toolName.length()))
|
||||||
|
+ AnsiStyle.YELLOW + "│" + AnsiStyle.RESET);
|
||||||
|
out.println(AnsiStyle.YELLOW + " │" + AnsiStyle.RESET
|
||||||
|
+ " Action: " + truncPad(action, 35)
|
||||||
|
+ AnsiStyle.YELLOW + "│" + AnsiStyle.RESET);
|
||||||
|
if (detail != null && !detail.isBlank()) {
|
||||||
|
out.println(AnsiStyle.YELLOW + " │" + AnsiStyle.RESET
|
||||||
|
+ " Detail: " + AnsiStyle.dim(truncPad(detail, 35))
|
||||||
|
+ AnsiStyle.YELLOW + "│" + AnsiStyle.RESET);
|
||||||
|
}
|
||||||
|
out.println(AnsiStyle.YELLOW + " ├─────────────────────────────────────────────┤" + AnsiStyle.RESET);
|
||||||
|
out.println(AnsiStyle.YELLOW + " │" + AnsiStyle.RESET + " "
|
||||||
|
+ AnsiStyle.green("[y] Allow") + " "
|
||||||
|
+ AnsiStyle.red("[n] Deny") + " "
|
||||||
|
+ AnsiStyle.dim("[a] Always allow")
|
||||||
|
+ " " + AnsiStyle.YELLOW + "│" + AnsiStyle.RESET);
|
||||||
|
out.println(AnsiStyle.YELLOW + " └─────────────────────────────────────────────┘" + AnsiStyle.RESET);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 渲染进度条 */
|
||||||
|
public void renderProgress(String label, int current, int total) {
|
||||||
|
if (total <= 0) return;
|
||||||
|
int width = 30;
|
||||||
|
int filled = (int) ((double) current / total * width);
|
||||||
|
filled = Math.min(filled, width);
|
||||||
|
|
||||||
|
StringBuilder bar = new StringBuilder();
|
||||||
|
bar.append(AnsiStyle.BRIGHT_BLUE);
|
||||||
|
bar.append("█".repeat(filled));
|
||||||
|
bar.append(AnsiStyle.DIM);
|
||||||
|
bar.append("░".repeat(width - filled));
|
||||||
|
bar.append(AnsiStyle.RESET);
|
||||||
|
|
||||||
|
String pct = String.format("%d%%", (int) ((double) current / total * 100));
|
||||||
|
out.print(AnsiStyle.clearLine());
|
||||||
|
out.print(" " + label + " " + bar + " " + AnsiStyle.dim(current + "/" + total + " " + pct));
|
||||||
|
out.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 渲染进度条(完成后换行) */
|
||||||
|
public void renderProgressDone(String label, int total) {
|
||||||
|
renderProgress(label, total, total);
|
||||||
|
out.println();
|
||||||
|
}
|
||||||
|
|
||||||
/** 从 JSON 参数中提取人类可读的摘要 */
|
/** 从 JSON 参数中提取人类可读的摘要 */
|
||||||
private String extractSummary(String toolName, String args) {
|
private String extractSummary(String toolName, String args) {
|
||||||
try {
|
try {
|
||||||
@@ -104,4 +187,16 @@ public class ToolStatusRenderer {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String formatDuration(long ms) {
|
||||||
|
if (ms < 1000) return ms + "ms";
|
||||||
|
if (ms < 60_000) return String.format("%.1fs", ms / 1000.0);
|
||||||
|
return String.format("%dm%ds", ms / 60_000, (ms % 60_000) / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String truncPad(String s, int width) {
|
||||||
|
if (s == null) return " ".repeat(width);
|
||||||
|
if (s.length() > width) return s.substring(0, width - 3) + "...";
|
||||||
|
return s + " ".repeat(width - s.length());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user