aliases() {
+ return List.of("diag", "check");
+ }
+
+ @Override
+ public String execute(String args, CommandContext context) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("\n");
+ sb.append(AnsiStyle.bold(" 🩺 Environment Diagnostics\n"));
+ sb.append(" ").append("═".repeat(50)).append("\n\n");
+
+ int passed = 0, warned = 0, failed = 0;
+
+ // Java Version
+ String javaVersion = System.getProperty("java.version");
+ String javaVendor = System.getProperty("java.vendor");
+ boolean javaOk = javaVersion != null && (javaVersion.startsWith("21")
+ || javaVersion.startsWith("22") || javaVersion.startsWith("23")
+ || javaVersion.startsWith("24") || javaVersion.startsWith("25"));
+ sb.append(check("Java", javaVersion + " (" + javaVendor + ")", javaOk));
+ if (javaOk) passed++; else warned++;
+
+ // OS Info
+ String osName = System.getProperty("os.name");
+ String osVersion = System.getProperty("os.version");
+ String osArch = System.getProperty("os.arch");
+ sb.append(check("OS", osName + " " + osVersion + " (" + osArch + ")", true));
+ passed++;
+
+ // Working Directory
+ String workDir = System.getProperty("user.dir");
+ sb.append(check("Working Dir", workDir, true));
+ passed++;
+
+ // Git
+ String gitVersion = runCommand("git", "--version");
+ boolean gitOk = gitVersion != null && gitVersion.contains("git version");
+ sb.append(check("Git", gitOk ? gitVersion.strip() : "not found", gitOk));
+ if (gitOk) passed++; else failed++;
+
+ // Git config
+ if (gitOk) {
+ String userName = runCommand("git", "config", "user.name");
+ String userEmail = runCommand("git", "config", "user.email");
+ boolean configOk = userName != null && !userName.isBlank()
+ && userEmail != null && !userEmail.isBlank();
+ String configInfo = configOk
+ ? userName.strip() + " <" + userEmail.strip() + ">"
+ : "user.name or user.email not set";
+ sb.append(check("Git Config", configInfo, configOk));
+ if (configOk) passed++; else warned++;
+ }
+
+ // API Key
+ boolean hasAnthropicKey = System.getenv("ANTHROPIC_API_KEY") != null;
+ boolean hasOpenAiKey = System.getenv("OPENAI_API_KEY") != null;
+ boolean hasApiKey = hasAnthropicKey || hasOpenAiKey;
+ String apiKeyInfo = hasAnthropicKey ? "ANTHROPIC_API_KEY set"
+ : hasOpenAiKey ? "OPENAI_API_KEY set" : "No API key found";
+ sb.append(check("API Key", apiKeyInfo, hasApiKey));
+ if (hasApiKey) passed++; else failed++;
+
+ // CLAUDE.md
+ boolean hasClaudeMd = new File(workDir, "CLAUDE.md").exists();
+ sb.append(check("CLAUDE.md", hasClaudeMd ? "found" : "not found (optional)", true));
+ passed++;
+
+ // Config directory
+ String userHome = System.getProperty("user.home");
+ File configDir = new File(userHome, ".claude-code");
+ boolean hasConfigDir = configDir.exists() && configDir.isDirectory();
+ sb.append(check("Config Dir", configDir.getPath()
+ + (hasConfigDir ? " (exists)" : " (will be created)"), true));
+ passed++;
+
+ // MCP config
+ boolean hasProjectMcp = new File(workDir, ".mcp.json").exists();
+ boolean hasGlobalMcp = new File(userHome, ".claude-code-java/mcp.json").exists();
+ String mcpInfo = hasProjectMcp ? ".mcp.json found (project)"
+ : hasGlobalMcp ? "mcp.json found (global)" : "no config";
+ sb.append(check("MCP Config", mcpInfo, true));
+ passed++;
+
+ // Disk Space
+ File root = new File(workDir);
+ long freeSpace = root.getFreeSpace();
+ long totalSpace = root.getTotalSpace();
+ double freeGb = freeSpace / (1024.0 * 1024.0 * 1024.0);
+ double totalGb = totalSpace / (1024.0 * 1024.0 * 1024.0);
+ boolean diskOk = freeGb > 1.0;
+ sb.append(check("Disk Space", String.format("%.1f GB free / %.1f GB total", freeGb, totalGb), diskOk));
+ if (diskOk) passed++; else warned++;
+
+ // Memory
+ Runtime runtime = Runtime.getRuntime();
+ long maxMem = runtime.maxMemory() / (1024 * 1024);
+ long usedMem = (runtime.totalMemory() - runtime.freeMemory()) / (1024 * 1024);
+ sb.append(check("JVM Memory", usedMem + " MB used / " + maxMem + " MB max", maxMem > 256));
+ passed++;
+
+ // Summary
+ sb.append("\n ").append("─".repeat(50)).append("\n");
+ sb.append(" ").append(AnsiStyle.bold("Summary: "));
+ sb.append(AnsiStyle.green(passed + " passed"));
+ if (warned > 0) sb.append(", ").append(AnsiStyle.yellow(warned + " warnings"));
+ if (failed > 0) sb.append(", ").append(AnsiStyle.red(failed + " failed"));
+ sb.append("\n");
+
+ return sb.toString();
+ }
+
+ private String check(String label, String value, boolean ok) {
+ String icon = ok ? AnsiStyle.green("✓") : AnsiStyle.red("✗");
+ return " " + icon + " " + AnsiStyle.bold(String.format("%-14s", label))
+ + " " + value + "\n";
+ }
+
+ private String runCommand(String... cmd) {
+ try {
+ ProcessBuilder pb = new ProcessBuilder(cmd);
+ pb.redirectErrorStream(true);
+ Process p = pb.start();
+ String output = new String(p.getInputStream().readAllBytes()).strip();
+ int exit = p.waitFor();
+ return exit == 0 ? output : null;
+ } catch (Exception e) {
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/com/claudecode/command/impl/RenameCommand.java b/src/main/java/com/claudecode/command/impl/RenameCommand.java
new file mode 100644
index 0000000..c564b49
--- /dev/null
+++ b/src/main/java/com/claudecode/command/impl/RenameCommand.java
@@ -0,0 +1,74 @@
+package com.claudecode.command.impl;
+
+import com.claudecode.command.CommandContext;
+import com.claudecode.command.SlashCommand;
+import com.claudecode.console.AnsiStyle;
+
+import java.util.List;
+
+/**
+ * /rename 命令 —— 会话重命名。
+ *
+ * 对应 claude-code 的 /rename 命令,给当前会话设置一个友好名称。
+ * 会话名称在 UI 标题栏和会话列表中显示。
+ */
+public class RenameCommand implements SlashCommand {
+
+ /** 当前会话名称(进程级别) */
+ private static volatile String sessionName = null;
+
+ @Override
+ public String name() {
+ return "rename";
+ }
+
+ @Override
+ public String description() {
+ return "Rename the current session";
+ }
+
+ @Override
+ public List aliases() {
+ return List.of("name", "title");
+ }
+
+ @Override
+ public String execute(String args, CommandContext context) {
+ args = args == null ? "" : args.strip();
+
+ if (args.isBlank()) {
+ if (sessionName == null) {
+ return AnsiStyle.dim(" No session name set. Use /rename to set one.");
+ }
+ return " Current session name: " + AnsiStyle.bold(sessionName);
+ }
+
+ if (args.equals("clear") || args.equals("reset")) {
+ sessionName = null;
+ return AnsiStyle.green(" ✓ Session name cleared");
+ }
+
+ // Validate name
+ if (args.length() > 100) {
+ return AnsiStyle.yellow(" ⚠ Session name too long (max 100 characters)");
+ }
+
+ String oldName = sessionName;
+ sessionName = args;
+
+ if (oldName != null) {
+ return AnsiStyle.green(" ✓ Session renamed: " + oldName + " → " + sessionName);
+ }
+ return AnsiStyle.green(" ✓ Session named: " + sessionName);
+ }
+
+ /** 获取当前会话名称(供其他组件使用) */
+ public static String getSessionName() {
+ return sessionName;
+ }
+
+ /** 设置会话名称(供程序化设置) */
+ public static void setSessionName(String name) {
+ sessionName = name;
+ }
+}
diff --git a/src/main/java/com/claudecode/command/impl/SessionCommand.java b/src/main/java/com/claudecode/command/impl/SessionCommand.java
new file mode 100644
index 0000000..bb4b7e4
--- /dev/null
+++ b/src/main/java/com/claudecode/command/impl/SessionCommand.java
@@ -0,0 +1,210 @@
+package com.claudecode.command.impl;
+
+import com.claudecode.command.CommandContext;
+import com.claudecode.command.SlashCommand;
+import com.claudecode.console.AnsiStyle;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+
+/**
+ * /session 命令 —— 会话管理。
+ *
+ * 显示当前会话信息,支持:
+ *
+ * - /session —— 显示当前会话摘要
+ * - /session save [name] —— 保存会话快照
+ * - /session list —— 列出保存的会话
+ * - /session export —— 导出会话为 JSON
+ *
+ */
+public class SessionCommand implements SlashCommand {
+
+ private static final DateTimeFormatter FMT =
+ DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
+ .withZone(ZoneId.systemDefault());
+
+ private final Instant startTime = Instant.now();
+
+ @Override
+ public String name() {
+ return "session";
+ }
+
+ @Override
+ public String description() {
+ return "View and manage the current session";
+ }
+
+ @Override
+ public List aliases() {
+ return List.of("sess");
+ }
+
+ @Override
+ public String execute(String args, CommandContext context) {
+ args = args == null ? "" : args.strip();
+
+ if (args.startsWith("save")) {
+ String sessionName = args.length() > 5 ? args.substring(5).strip() : "";
+ return handleSave(sessionName, context);
+ } else if (args.equals("list")) {
+ return handleList();
+ } else if (args.equals("export")) {
+ return handleExport(context);
+ } else {
+ return showSessionInfo(context);
+ }
+ }
+
+ private String showSessionInfo(CommandContext context) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("\n");
+ sb.append(AnsiStyle.bold(" 📋 Session Info\n"));
+ sb.append(" ").append("─".repeat(50)).append("\n");
+
+ sb.append(" ").append(AnsiStyle.dim("Started: ")).append(FMT.format(startTime)).append("\n");
+
+ // Duration
+ long seconds = java.time.Duration.between(startTime, Instant.now()).getSeconds();
+ String duration;
+ if (seconds < 60) duration = seconds + "s";
+ else if (seconds < 3600) duration = (seconds / 60) + "m " + (seconds % 60) + "s";
+ else duration = (seconds / 3600) + "h " + ((seconds % 3600) / 60) + "m";
+ sb.append(" ").append(AnsiStyle.dim("Duration: ")).append(duration).append("\n");
+
+ sb.append(" ").append(AnsiStyle.dim("Work Dir: ")).append(System.getProperty("user.dir")).append("\n");
+
+ // Token usage (from AgentLoop's TokenTracker)
+ var agentLoop = context.agentLoop();
+ if (agentLoop != null && agentLoop.getAutoCompactManager() != null) {
+ sb.append(" ").append(AnsiStyle.dim("Auto-compact: ")).append("enabled").append("\n");
+ }
+
+ // Tool count
+ int toolCount = context.toolRegistry() != null ? context.toolRegistry().size() : 0;
+ sb.append(" ").append(AnsiStyle.dim("Tools: ")).append(toolCount).append(" registered\n");
+
+ // Command count
+ int cmdCount = context.commandRegistry() != null ? context.commandRegistry().getCommands().size() : 0;
+ sb.append(" ").append(AnsiStyle.dim("Commands: ")).append(cmdCount).append(" available\n");
+
+ // Session memory
+ Path memDir = getSessionDir();
+ boolean hasMemory = memDir != null && Files.exists(memDir.resolve("SESSION_MEMORY.md"));
+ sb.append(" ").append(AnsiStyle.dim("Session Memory: "))
+ .append(hasMemory ? "active" : "not yet created").append("\n");
+
+ sb.append("\n ").append(AnsiStyle.dim("Use /session save [name] to save, /session list to view saved")).append("\n");
+
+ return sb.toString();
+ }
+
+ private String handleSave(String sessionName, CommandContext context) {
+ Path sessionsDir = getSessionsDir();
+ if (sessionsDir == null) {
+ return AnsiStyle.red(" ✗ Cannot determine sessions directory");
+ }
+
+ try {
+ Files.createDirectories(sessionsDir);
+ } catch (IOException e) {
+ return AnsiStyle.red(" ✗ Cannot create sessions directory: " + e.getMessage());
+ }
+
+ if (sessionName.isBlank()) {
+ sessionName = "session-" + FMT.format(Instant.now()).replace(" ", "_").replace(":", "-");
+ }
+
+ Path sessionFile = sessionsDir.resolve(sessionName + ".md");
+ try {
+ StringBuilder content = new StringBuilder();
+ content.append("# Session: ").append(sessionName).append("\n\n");
+ content.append("- Started: ").append(FMT.format(startTime)).append("\n");
+ content.append("- Saved: ").append(FMT.format(Instant.now())).append("\n");
+ content.append("- Working Directory: ").append(System.getProperty("user.dir")).append("\n");
+ content.append("\n## Notes\n\n");
+ content.append("(Add session notes here)\n");
+
+ Files.writeString(sessionFile, content.toString(), StandardCharsets.UTF_8);
+ return AnsiStyle.green(" ✓ Session saved: " + sessionFile);
+ } catch (IOException e) {
+ return AnsiStyle.red(" ✗ Failed to save session: " + e.getMessage());
+ }
+ }
+
+ private String handleList() {
+ Path sessionsDir = getSessionsDir();
+ if (sessionsDir == null || !Files.exists(sessionsDir)) {
+ return AnsiStyle.dim(" No saved sessions.");
+ }
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("\n");
+ sb.append(AnsiStyle.bold(" 📁 Saved Sessions\n"));
+ sb.append(" ").append("─".repeat(50)).append("\n");
+
+ try (var stream = Files.list(sessionsDir)) {
+ var files = stream
+ .filter(f -> f.toString().endsWith(".md"))
+ .sorted()
+ .toList();
+
+ if (files.isEmpty()) {
+ return AnsiStyle.dim(" No saved sessions.");
+ }
+
+ for (var file : files) {
+ String name = file.getFileName().toString().replace(".md", "");
+ long size = Files.size(file);
+ sb.append(" • ").append(name).append(" (").append(size).append(" bytes)\n");
+ }
+ } catch (IOException e) {
+ return AnsiStyle.red(" ✗ Error listing sessions: " + e.getMessage());
+ }
+
+ return sb.toString();
+ }
+
+ private String handleExport(CommandContext context) {
+ Path exportFile = Path.of(System.getProperty("user.dir"), "session-export.json");
+ try {
+ StringBuilder json = new StringBuilder();
+ json.append("{\n");
+ json.append(" \"started\": \"").append(startTime).append("\",\n");
+ json.append(" \"exported\": \"").append(Instant.now()).append("\",\n");
+ json.append(" \"workDir\": \"")
+ .append(System.getProperty("user.dir").replace("\\", "\\\\")).append("\",\n");
+ json.append(" \"tools\": ").append(context.toolRegistry() != null
+ ? context.toolRegistry().size() : 0).append(",\n");
+ json.append(" \"commands\": ").append(context.commandRegistry() != null
+ ? context.commandRegistry().getCommands().size() : 0).append("\n");
+ json.append("}\n");
+
+ Files.writeString(exportFile, json.toString(), StandardCharsets.UTF_8);
+ return AnsiStyle.green(" ✓ Session exported to: " + exportFile);
+ } catch (IOException e) {
+ return AnsiStyle.red(" ✗ Export failed: " + e.getMessage());
+ }
+ }
+
+ private Path getSessionsDir() {
+ String userHome = System.getProperty("user.home");
+ if (userHome == null) return null;
+ return Path.of(userHome, ".claude", "sessions");
+ }
+
+ private Path getSessionDir() {
+ String workDir = System.getProperty("user.dir");
+ String sanitized = Path.of(workDir).toAbsolutePath().toString()
+ .replace(":", "_").replace("\\", "_").replace("/", "_");
+ return Path.of(System.getProperty("user.home"))
+ .resolve(".claude").resolve("projects").resolve(sanitized);
+ }
+}
diff --git a/src/main/java/com/claudecode/config/AppConfig.java b/src/main/java/com/claudecode/config/AppConfig.java
index 60013d8..1bc3e2e 100644
--- a/src/main/java/com/claudecode/config/AppConfig.java
+++ b/src/main/java/com/claudecode/config/AppConfig.java
@@ -176,6 +176,11 @@ public class AppConfig {
new SecurityReviewCommand(),
new McpCommand(),
new PluginCommand(),
+ // Phase 2F 命令
+ new DoctorCommand(),
+ new SessionCommand(),
+ new AgentCommand(),
+ new RenameCommand(),
// Exit 放最后
new ExitCommand()
);