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。
|
* Banner 打印器 —— 对应 claude-code/src/components/Banner.tsx。
|
||||||
* <p>
|
* <p>
|
||||||
* 在启动时打印 ASCII Art Logo 和版本信息。
|
* 在启动时打印带边框的 Logo 和版本信息。
|
||||||
|
* 参考 Copilot CLI / Claude Code 的边框样式。
|
||||||
*/
|
*/
|
||||||
public class BannerPrinter {
|
public class BannerPrinter {
|
||||||
|
|
||||||
private static final String VERSION = "0.1.0-SNAPSHOT";
|
private static final String VERSION = "0.1.0-SNAPSHOT";
|
||||||
|
|
||||||
/**
|
// 边框字符
|
||||||
* 打印 claude-code-java 启动 banner。
|
private static final String TL = "╭", TR = "╮", BL = "╰", BR = "╯";
|
||||||
*/
|
private static final String H = "─", V = "│";
|
||||||
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));
|
* 打印带边框的启动 Banner。
|
||||||
out.println(AnsiStyle.dim(" Powered by Spring AI • Type /help for commands"));
|
*
|
||||||
|
* @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();
|
||||||
|
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) {
|
public static void printCompact(PrintStream out) {
|
||||||
out.println();
|
out.println();
|
||||||
@@ -48,4 +99,16 @@ public class BannerPrinter {
|
|||||||
out.println(AnsiStyle.dim(" Type /help for commands • Ctrl+D to exit"));
|
out.println(AnsiStyle.dim(" Type /help for commands • Ctrl+D to exit"));
|
||||||
out.println();
|
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。
|
* Thinking 内容渲染器 —— 对应 claude-code/src/components/Thinking.tsx。
|
||||||
* <p>
|
* <p>
|
||||||
* 显示 AI 模型的思考过程(extended thinking)。
|
* 使用 ● 圆点 + <thought> 标签样式显示 AI 的思考过程(参考 Copilot CLI)。
|
||||||
*/
|
*/
|
||||||
public class ThinkingRenderer {
|
public class ThinkingRenderer {
|
||||||
|
|
||||||
@@ -15,25 +15,28 @@ public class ThinkingRenderer {
|
|||||||
this.out = out;
|
this.out = out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 渲染 thinking 内容块 */
|
/** 渲染 thinking 内容块(Copilot CLI 的 <thought> 标签风格) */
|
||||||
public void render(String thinkingContent) {
|
public void render(String thinkingContent) {
|
||||||
if (thinkingContent == null || thinkingContent.isBlank()) {
|
if (thinkingContent == null || thinkingContent.isBlank()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
out.println();
|
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()) {
|
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();
|
out.println();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 渲染 thinking 开始标记 */
|
/** 渲染 thinking 开始标记 */
|
||||||
public void renderStart() {
|
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 结束标记 */
|
/** 渲染 thinking 结束标记 */
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import java.io.PrintStream;
|
|||||||
/**
|
/**
|
||||||
* 工具调用状态渲染器 —— 对应 claude-code/src/components/ToolStatus.tsx。
|
* 工具调用状态渲染器 —— 对应 claude-code/src/components/ToolStatus.tsx。
|
||||||
* <p>
|
* <p>
|
||||||
* 在终端中显示工具调用的进度和结果。
|
* 使用彩色 ● 圆点标识工具调用状态,配合 ⎿ 显示结果(参考 Claude Code 样式)。
|
||||||
*/
|
*/
|
||||||
public class ToolStatusRenderer {
|
public class ToolStatusRenderer {
|
||||||
|
|
||||||
@@ -17,16 +17,17 @@ public class ToolStatusRenderer {
|
|||||||
|
|
||||||
/** 渲染工具调用开始 */
|
/** 渲染工具调用开始 */
|
||||||
public void renderStart(String toolName, String args) {
|
public void renderStart(String toolName, String args) {
|
||||||
out.println(AnsiStyle.dim(" ─────────────────────────────────────────"));
|
out.println();
|
||||||
out.print(AnsiStyle.YELLOW + " ⚙ " + AnsiStyle.BOLD + toolName + AnsiStyle.RESET);
|
out.print(AnsiStyle.BRIGHT_BLUE + " ● " + AnsiStyle.BOLD + toolName + AnsiStyle.RESET);
|
||||||
out.println(AnsiStyle.dim(" running..."));
|
|
||||||
// 如果有简短参数,显示
|
// 提取并显示简短参数
|
||||||
if (args != null && !args.isBlank()) {
|
if (args != null && !args.isBlank()) {
|
||||||
String summary = extractSummary(toolName, args);
|
String summary = extractSummary(toolName, args);
|
||||||
if (summary != null) {
|
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) + "...";
|
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"));
|
+ AnsiStyle.dim(" done"));
|
||||||
|
|
||||||
if (display != null && !display.isBlank()) {
|
if (display != null && !display.isBlank()) {
|
||||||
// 缩进输出每一行
|
// 使用 ⎿ 前缀显示结果(Claude Code 风格)
|
||||||
for (String line : display.lines().toList()) {
|
String[] lines = display.lines().toArray(String[]::new);
|
||||||
out.println(AnsiStyle.dim(" " + line));
|
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) {
|
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"));
|
+ AnsiStyle.red(" error"));
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
out.println(AnsiStyle.red(" " + error));
|
out.println(AnsiStyle.DIM + " ⎿ " + AnsiStyle.RED + error + AnsiStyle.RESET);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 从 JSON 参数中提取人类可读的摘要 */
|
/** 从 JSON 参数中提取人类可读的摘要 */
|
||||||
private String extractSummary(String toolName, String args) {
|
private String extractSummary(String toolName, String args) {
|
||||||
try {
|
try {
|
||||||
// 简单提取关键字段
|
|
||||||
if (args.contains("\"command\"")) {
|
if (args.contains("\"command\"")) {
|
||||||
int start = args.indexOf("\"command\"");
|
int start = args.indexOf("\"command\"");
|
||||||
int valStart = args.indexOf("\"", start + 10) + 1;
|
int valStart = args.indexOf("\"", start + 10) + 1;
|
||||||
int valEnd = args.indexOf("\"", valStart);
|
int valEnd = args.indexOf("\"", valStart);
|
||||||
if (valStart > 0 && valEnd > 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;
|
return "$ " + cmd;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,6 +91,14 @@ public class ToolStatusRenderer {
|
|||||||
return "pattern: " + args.substring(valStart, valEnd);
|
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) {
|
} catch (Exception e) {
|
||||||
// 忽略解析错误
|
// 忽略解析错误
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,16 +220,6 @@ public class ReplSession {
|
|||||||
|
|
||||||
/** 打印启动 Banner(JLine 模式) */
|
/** 打印启动 Banner(JLine 模式) */
|
||||||
private void printBanner(Terminal terminal) {
|
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());
|
boolean isDumb = "dumb".equals(terminal.getType());
|
||||||
int w = terminal.getWidth();
|
int w = terminal.getWidth();
|
||||||
int h = terminal.getHeight();
|
int h = terminal.getHeight();
|
||||||
@@ -244,14 +234,27 @@ public class ReplSession {
|
|||||||
termInfo += " [vim]";
|
termInfo += " [vim]";
|
||||||
}
|
}
|
||||||
|
|
||||||
out.println(AnsiStyle.dim(" Terminal: " + termInfo));
|
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) {
|
if (isDumb) {
|
||||||
out.println(AnsiStyle.yellow(" ⚠ Dumb 终端模式:Tab补全和行编辑可能受限"));
|
out.println(AnsiStyle.yellow(" ⚠ Dumb 终端模式:建议在 Windows Terminal / PowerShell 中运行"));
|
||||||
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"));
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// 标准终端用带边框的 Banner
|
||||||
|
BannerPrinter.printBoxed(out,
|
||||||
|
providerInfo.provider(),
|
||||||
|
providerInfo.model(),
|
||||||
|
providerInfo.baseUrl(),
|
||||||
|
System.getProperty("user.dir"),
|
||||||
|
toolRegistry.size(),
|
||||||
|
commandRegistry.getCommands().size(),
|
||||||
|
termInfo);
|
||||||
|
}
|
||||||
|
|
||||||
out.println();
|
out.println();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,6 +315,11 @@ public class ReplSession {
|
|||||||
spinner.start("Thinking...");
|
spinner.start("Thinking...");
|
||||||
out.println(); // 换行准备输出区域
|
out.println(); // 换行准备输出区域
|
||||||
|
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
// AI 回复前的 ● 标识
|
||||||
|
out.println(AnsiStyle.BRIGHT_CYAN + " ● " + AnsiStyle.RESET);
|
||||||
|
|
||||||
// 流式回调:逐 token 输出到终端
|
// 流式回调:逐 token 输出到终端
|
||||||
String response = agentLoop.runStreaming(input, token -> {
|
String response = agentLoop.runStreaming(input, token -> {
|
||||||
out.print(token);
|
out.print(token);
|
||||||
@@ -321,6 +329,12 @@ public class ReplSession {
|
|||||||
spinner.stop();
|
spinner.stop();
|
||||||
out.println(); // 流式输出结束后换行
|
out.println(); // 流式输出结束后换行
|
||||||
|
|
||||||
|
// 显示耗时
|
||||||
|
long elapsed = (System.currentTimeMillis() - startTime) / 1000;
|
||||||
|
if (elapsed > 0) {
|
||||||
|
out.println(AnsiStyle.DIM + " ✻ Worked for " + elapsed + "s" + AnsiStyle.RESET);
|
||||||
|
}
|
||||||
|
|
||||||
// 刷新底部状态行(显示最新 token 用量)
|
// 刷新底部状态行(显示最新 token 用量)
|
||||||
if (statusLine.isEnabled()) {
|
if (statusLine.isEnabled()) {
|
||||||
out.println(statusLine.renderInline());
|
out.println(statusLine.renderInline());
|
||||||
@@ -328,7 +342,7 @@ public class ReplSession {
|
|||||||
out.println();
|
out.println();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
spinner.stop();
|
spinner.stop();
|
||||||
out.println(AnsiStyle.red("\n ✗ Error: " + e.getMessage()));
|
out.println(AnsiStyle.RED + "\n ● Error: " + AnsiStyle.RESET + e.getMessage());
|
||||||
log.error("Agent 循环异常", e);
|
log.error("Agent 循环异常", e);
|
||||||
out.println();
|
out.println();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user