From d09809e9240d299879576b7ccbf9d6d057da3f4c Mon Sep 17 00:00:00 2001 From: abel533 Date: Sun, 5 Apr 2026 11:31:22 +0800 Subject: [PATCH] feat: 12 UX commands - brief, vim, theme, usage, tips, env, perf, etc (Phase 4B) New commands: - /brief: toggle concise output mode - /vim: toggle vi-mode editing - /theme: switch color theme (dark/light/auto) - /usage: detailed token/cost breakdown with visual bar - /tips: usage tips (random or /tips all) - /output-style: output format (markdown/plain/json) - /env: environment variables, config paths, system info - /performance (/perf): memory, threads, GC, session metrics - /privacy: telemetry/logging/memory opt-out settings - /feedback: save feedback to ~/.claude-code-java/feedback/ - /release-notes (/changelog): version history - /keybindings (/keys): keyboard shortcuts reference Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../claudecode/command/impl/BriefCommand.java | 40 +++++++ .../claudecode/command/impl/EnvCommand.java | 93 ++++++++++++++++ .../command/impl/FeedbackCommand.java | 56 ++++++++++ .../command/impl/KeybindingsCommand.java | 60 ++++++++++ .../command/impl/OutputStyleCommand.java | 49 +++++++++ .../command/impl/PerformanceCommand.java | 104 ++++++++++++++++++ .../command/impl/PrivacyCommand.java | 70 ++++++++++++ .../command/impl/ReleaseNotesCommand.java | 66 +++++++++++ .../claudecode/command/impl/ThemeCommand.java | 52 +++++++++ .../claudecode/command/impl/TipsCommand.java | 65 +++++++++++ .../claudecode/command/impl/UsageCommand.java | 71 ++++++++++++ .../claudecode/command/impl/VimCommand.java | 43 ++++++++ .../java/com/claudecode/config/AppConfig.java | 13 +++ 13 files changed, 782 insertions(+) create mode 100644 src/main/java/com/claudecode/command/impl/BriefCommand.java create mode 100644 src/main/java/com/claudecode/command/impl/EnvCommand.java create mode 100644 src/main/java/com/claudecode/command/impl/FeedbackCommand.java create mode 100644 src/main/java/com/claudecode/command/impl/KeybindingsCommand.java create mode 100644 src/main/java/com/claudecode/command/impl/OutputStyleCommand.java create mode 100644 src/main/java/com/claudecode/command/impl/PerformanceCommand.java create mode 100644 src/main/java/com/claudecode/command/impl/PrivacyCommand.java create mode 100644 src/main/java/com/claudecode/command/impl/ReleaseNotesCommand.java create mode 100644 src/main/java/com/claudecode/command/impl/ThemeCommand.java create mode 100644 src/main/java/com/claudecode/command/impl/TipsCommand.java create mode 100644 src/main/java/com/claudecode/command/impl/UsageCommand.java create mode 100644 src/main/java/com/claudecode/command/impl/VimCommand.java diff --git a/src/main/java/com/claudecode/command/impl/BriefCommand.java b/src/main/java/com/claudecode/command/impl/BriefCommand.java new file mode 100644 index 0000000..13faf7d --- /dev/null +++ b/src/main/java/com/claudecode/command/impl/BriefCommand.java @@ -0,0 +1,40 @@ +package com.claudecode.command.impl; + +import com.claudecode.command.CommandContext; +import com.claudecode.command.SlashCommand; +import com.claudecode.console.AnsiStyle; + +/** + * /brief 命令 —— 切换简洁输出模式。 + */ +public class BriefCommand implements SlashCommand { + + @Override + public String name() { return "brief"; } + + @Override + public String description() { return "Toggle brief output mode"; } + + @Override + public String execute(String args, CommandContext context) { + if (context.agentLoop() == null) { + return AnsiStyle.red(" ✗ No active session"); + } + + var toolCtx = context.agentLoop().getToolContext(); + boolean current = Boolean.TRUE.equals(toolCtx.get("BRIEF_MODE")); + + String trimmed = (args == null) ? "" : args.trim(); + boolean newMode = switch (trimmed) { + case "on", "enable", "true" -> true; + case "off", "disable", "false" -> false; + default -> !current; // toggle + }; + + toolCtx.set("BRIEF_MODE", newMode); + + return newMode + ? AnsiStyle.green(" ✓ Brief mode ON") + " — responses will be concise" + : AnsiStyle.green(" ✓ Brief mode OFF") + " — responses will be detailed"; + } +} diff --git a/src/main/java/com/claudecode/command/impl/EnvCommand.java b/src/main/java/com/claudecode/command/impl/EnvCommand.java new file mode 100644 index 0000000..f158b4d --- /dev/null +++ b/src/main/java/com/claudecode/command/impl/EnvCommand.java @@ -0,0 +1,93 @@ +package com.claudecode.command.impl; + +import com.claudecode.command.CommandContext; +import com.claudecode.command.SlashCommand; +import com.claudecode.console.AnsiStyle; + +import java.io.File; +import java.lang.management.ManagementFactory; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** + * /env 命令 —— 显示环境变量和配置信息。 + */ +public class EnvCommand implements SlashCommand { + + @Override + public String name() { return "env"; } + + @Override + public String description() { return "Show environment variables and configuration"; } + + @Override + public String execute(String args, CommandContext context) { + String trimmed = (args == null) ? "" : args.trim(); + + StringBuilder sb = new StringBuilder(); + sb.append("\n").append(AnsiStyle.bold(" 🔧 Environment\n")); + sb.append(" ").append("─".repeat(50)).append("\n\n"); + + // System info + sb.append(AnsiStyle.bold(" System\n")); + sb.append(" OS: ").append(System.getProperty("os.name")).append(" ") + .append(System.getProperty("os.version")).append("\n"); + sb.append(" Java: ").append(System.getProperty("java.version")) + .append(" (").append(System.getProperty("java.vendor")).append(")\n"); + sb.append(" JVM: ").append(System.getProperty("java.vm.name")).append("\n"); + sb.append(" Heap: ").append(formatBytes(Runtime.getRuntime().totalMemory())) + .append(" / ").append(formatBytes(Runtime.getRuntime().maxMemory())).append("\n"); + sb.append(" PID: ").append(ProcessHandle.current().pid()).append("\n\n"); + + // Work directory + sb.append(AnsiStyle.bold(" Paths\n")); + sb.append(" WorkDir: ").append(System.getProperty("user.dir")).append("\n"); + sb.append(" Home: ").append(System.getProperty("user.home")).append("\n"); + sb.append(" Config: ").append(System.getProperty("user.home")) + .append(File.separator).append(".claude-code-java").append("\n\n"); + + // Relevant env vars + sb.append(AnsiStyle.bold(" Environment Variables\n")); + List relevantVars = List.of( + "ANTHROPIC_API_KEY", "OPENAI_API_KEY", "CLAUDE_CODE_", + "JAVA_HOME", "PATH", "SHELL", "TERM", "EDITOR" + ); + + Map env = new TreeMap<>(System.getenv()); + for (Map.Entry entry : env.entrySet()) { + String key = entry.getKey(); + boolean show = false; + for (String prefix : relevantVars) { + if (key.startsWith(prefix) || key.equals(prefix)) { + show = true; + break; + } + } + if (!show && !trimmed.equals("all")) continue; + + String value = entry.getValue(); + // Mask secrets + if (key.contains("KEY") || key.contains("SECRET") || key.contains("TOKEN")) { + value = value.length() > 8 ? value.substring(0, 4) + "****" + value.substring(value.length() - 4) : "****"; + } + // Truncate long values + if (value.length() > 80) { + value = value.substring(0, 77) + "..."; + } + sb.append(" ").append(AnsiStyle.cyan(key)).append("=").append(value).append("\n"); + } + + if (!trimmed.equals("all")) { + sb.append("\n").append(AnsiStyle.dim(" Run /env all to show all environment variables")); + } + + return sb.toString(); + } + + private String formatBytes(long bytes) { + if (bytes >= 1_073_741_824) return String.format("%.1fGB", bytes / 1_073_741_824.0); + if (bytes >= 1_048_576) return String.format("%.0fMB", bytes / 1_048_576.0); + return String.format("%.0fKB", bytes / 1_024.0); + } +} diff --git a/src/main/java/com/claudecode/command/impl/FeedbackCommand.java b/src/main/java/com/claudecode/command/impl/FeedbackCommand.java new file mode 100644 index 0000000..67e40a9 --- /dev/null +++ b/src/main/java/com/claudecode/command/impl/FeedbackCommand.java @@ -0,0 +1,56 @@ +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.file.Files; +import java.nio.file.Path; +import java.time.Instant; + +/** + * /feedback 命令 —— 提交反馈(本地保存)。 + */ +public class FeedbackCommand implements SlashCommand { + + @Override + public String name() { return "feedback"; } + + @Override + public String description() { return "Submit feedback (saved locally)"; } + + @Override + public String execute(String args, CommandContext context) { + String trimmed = (args == null) ? "" : args.trim(); + + if (trimmed.isEmpty()) { + return AnsiStyle.yellow(" Usage: /feedback ") + + "\n" + AnsiStyle.dim(" Feedback is saved locally to ~/.claude-code-java/feedback/"); + } + + try { + Path feedbackDir = Path.of(System.getProperty("user.home"), + ".claude-code-java", "feedback"); + Files.createDirectories(feedbackDir); + + String timestamp = Instant.now().toString().replaceAll("[:]", "-"); + Path feedbackFile = feedbackDir.resolve("feedback-" + timestamp + ".txt"); + + StringBuilder content = new StringBuilder(); + content.append("Timestamp: ").append(Instant.now()).append("\n"); + content.append("OS: ").append(System.getProperty("os.name")).append("\n"); + content.append("Java: ").append(System.getProperty("java.version")).append("\n"); + content.append("WorkDir: ").append(System.getProperty("user.dir")).append("\n"); + content.append("---\n"); + content.append(trimmed).append("\n"); + + Files.writeString(feedbackFile, content.toString()); + + return AnsiStyle.green(" ✓ Feedback saved: " + feedbackFile.getFileName()) + + "\n" + AnsiStyle.dim(" Thank you for your feedback!"); + } catch (IOException e) { + return AnsiStyle.red(" ✗ Failed to save feedback: " + e.getMessage()); + } + } +} diff --git a/src/main/java/com/claudecode/command/impl/KeybindingsCommand.java b/src/main/java/com/claudecode/command/impl/KeybindingsCommand.java new file mode 100644 index 0000000..2bb4930 --- /dev/null +++ b/src/main/java/com/claudecode/command/impl/KeybindingsCommand.java @@ -0,0 +1,60 @@ +package com.claudecode.command.impl; + +import com.claudecode.command.CommandContext; +import com.claudecode.command.SlashCommand; +import com.claudecode.console.AnsiStyle; + +import java.util.List; + +/** + * /keybindings 命令 —— 显示和配置快捷键。 + */ +public class KeybindingsCommand implements SlashCommand { + + @Override + public String name() { return "keybindings"; } + + @Override + public String description() { return "Show keyboard shortcuts"; } + + @Override + public List aliases() { + return List.of("keys", "shortcuts"); + } + + @Override + public String execute(String args, CommandContext context) { + StringBuilder sb = new StringBuilder(); + sb.append("\n").append(AnsiStyle.bold(" ⌨️ Keyboard Shortcuts\n")); + sb.append(" ").append("─".repeat(50)).append("\n\n"); + + sb.append(AnsiStyle.bold(" Input\n")); + sb.append(" ").append(AnsiStyle.cyan("Enter ")).append(" Submit message\n"); + sb.append(" ").append(AnsiStyle.cyan("Shift+Enter ")).append(" New line\n"); + sb.append(" ").append(AnsiStyle.cyan("Tab ")).append(" Command completion\n"); + sb.append(" ").append(AnsiStyle.cyan("↑/↓ ")).append(" History navigation\n"); + sb.append(" ").append(AnsiStyle.cyan("Ctrl+A ")).append(" Move to line start\n"); + sb.append(" ").append(AnsiStyle.cyan("Ctrl+E ")).append(" Move to line end\n"); + sb.append(" ").append(AnsiStyle.cyan("Ctrl+W ")).append(" Delete word backward\n"); + sb.append(" ").append(AnsiStyle.cyan("Ctrl+U ")).append(" Delete to line start\n"); + sb.append(" ").append(AnsiStyle.cyan("Ctrl+K ")).append(" Delete to line end\n\n"); + + sb.append(AnsiStyle.bold(" Control\n")); + sb.append(" ").append(AnsiStyle.cyan("Ctrl+C ")).append(" Interrupt current operation\n"); + sb.append(" ").append(AnsiStyle.cyan("Ctrl+D ")).append(" Exit (when input is empty)\n"); + sb.append(" ").append(AnsiStyle.cyan("Ctrl+L ")).append(" Clear screen\n"); + sb.append(" ").append(AnsiStyle.cyan("Ctrl+R ")).append(" Reverse history search\n\n"); + + sb.append(AnsiStyle.bold(" Vim Mode (/vim to enable)\n")); + sb.append(" ").append(AnsiStyle.cyan("Esc ")).append(" Normal mode\n"); + sb.append(" ").append(AnsiStyle.cyan("i ")).append(" Insert mode\n"); + sb.append(" ").append(AnsiStyle.cyan("a ")).append(" Append after cursor\n"); + sb.append(" ").append(AnsiStyle.cyan("dd ")).append(" Delete line\n"); + sb.append(" ").append(AnsiStyle.cyan("yy ")).append(" Yank line\n"); + sb.append(" ").append(AnsiStyle.cyan("p ")).append(" Paste\n"); + sb.append(" ").append(AnsiStyle.cyan("w/b ")).append(" Word forward/backward\n"); + sb.append(" ").append(AnsiStyle.cyan("0/$ ")).append(" Line start/end\n"); + + return sb.toString(); + } +} diff --git a/src/main/java/com/claudecode/command/impl/OutputStyleCommand.java b/src/main/java/com/claudecode/command/impl/OutputStyleCommand.java new file mode 100644 index 0000000..b982603 --- /dev/null +++ b/src/main/java/com/claudecode/command/impl/OutputStyleCommand.java @@ -0,0 +1,49 @@ +package com.claudecode.command.impl; + +import com.claudecode.command.CommandContext; +import com.claudecode.command.SlashCommand; +import com.claudecode.console.AnsiStyle; + +/** + * /output-style 命令 —— 输出风格设置。 + */ +public class OutputStyleCommand implements SlashCommand { + + @Override + public String name() { return "output-style"; } + + @Override + public String description() { return "Set output format (markdown/plain/json)"; } + + @Override + public String execute(String args, CommandContext context) { + if (context.agentLoop() == null) { + return AnsiStyle.red(" ✗ No active session"); + } + + var toolCtx = context.agentLoop().getToolContext(); + String current = (String) toolCtx.get("OUTPUT_STYLE"); + if (current == null) current = "markdown"; + + String trimmed = (args == null) ? "" : args.trim().toLowerCase(); + + if (trimmed.isEmpty()) { + return "\n" + AnsiStyle.bold(" 📝 Output Style\n") + + " " + "─".repeat(30) + "\n\n" + + " Current: " + AnsiStyle.cyan(current) + "\n\n" + + " Options:\n" + + " " + AnsiStyle.bold("markdown") + " — Rich formatting with code blocks\n" + + " " + AnsiStyle.bold("plain") + " — Plain text, no formatting\n" + + " " + AnsiStyle.bold("json") + " — JSON structured output\n" + + "\n" + AnsiStyle.dim(" Usage: /output-style "); + } + + if (!trimmed.equals("markdown") && !trimmed.equals("plain") && !trimmed.equals("json")) { + return AnsiStyle.yellow(" Unknown style: " + trimmed) + + "\n" + AnsiStyle.dim(" Options: markdown, plain, json"); + } + + toolCtx.set("OUTPUT_STYLE", trimmed); + return AnsiStyle.green(" ✓ Output style set to " + trimmed); + } +} diff --git a/src/main/java/com/claudecode/command/impl/PerformanceCommand.java b/src/main/java/com/claudecode/command/impl/PerformanceCommand.java new file mode 100644 index 0000000..a6eb78b --- /dev/null +++ b/src/main/java/com/claudecode/command/impl/PerformanceCommand.java @@ -0,0 +1,104 @@ +package com.claudecode.command.impl; + +import com.claudecode.command.CommandContext; +import com.claudecode.command.SlashCommand; +import com.claudecode.console.AnsiStyle; +import com.claudecode.telemetry.MetricsCollector; + +/** + * /performance 命令 —— 性能统计。 + */ +public class PerformanceCommand implements SlashCommand { + + @Override + public String name() { return "performance"; } + + @Override + public String description() { return "Show performance statistics"; } + + @Override + public java.util.List aliases() { + return java.util.List.of("perf"); + } + + @Override + public String execute(String args, CommandContext context) { + StringBuilder sb = new StringBuilder(); + sb.append("\n").append(AnsiStyle.bold(" ⚡ Performance Statistics\n")); + sb.append(" ").append("─".repeat(40)).append("\n\n"); + + // JVM stats + Runtime runtime = Runtime.getRuntime(); + long totalMem = runtime.totalMemory(); + long freeMem = runtime.freeMemory(); + long usedMem = totalMem - freeMem; + long maxMem = runtime.maxMemory(); + + sb.append(AnsiStyle.bold(" Memory\n")); + sb.append(" Used: ").append(formatBytes(usedMem)).append("\n"); + sb.append(" Allocated: ").append(formatBytes(totalMem)).append("\n"); + sb.append(" Max: ").append(formatBytes(maxMem)).append("\n"); + sb.append(" Usage: ").append(memBar(usedMem, maxMem)).append("\n\n"); + + // Thread stats + int threadCount = Thread.activeCount(); + sb.append(AnsiStyle.bold(" Threads\n")); + sb.append(" Active: ").append(threadCount).append("\n"); + sb.append(" Available: ").append(runtime.availableProcessors()).append(" CPUs\n\n"); + + // GC stats + long gcCount = 0; + long gcTime = 0; + for (var gc : java.lang.management.ManagementFactory.getGarbageCollectorMXBeans()) { + gcCount += gc.getCollectionCount(); + gcTime += gc.getCollectionTime(); + } + sb.append(AnsiStyle.bold(" GC\n")); + sb.append(" Collections: ").append(gcCount).append("\n"); + sb.append(" Total time: ").append(gcTime).append("ms\n\n"); + + // Metrics if available + if (context.agentLoop() != null) { + Object metricsObj = context.agentLoop().getToolContext().get("METRICS_COLLECTOR"); + if (metricsObj instanceof MetricsCollector metrics) { + sb.append(AnsiStyle.bold(" Session Metrics\n")); + sb.append(" Duration: ").append(formatDuration(metrics.getSessionDurationSeconds())).append("\n"); + var toolUsage = metrics.getToolUsage(); + if (!toolUsage.isEmpty()) { + sb.append(" Tool calls: ").append(toolUsage.values().stream().mapToLong(Long::longValue).sum()).append("\n"); + sb.append(" Top tools: "); + toolUsage.entrySet().stream() + .sorted(java.util.Map.Entry.comparingByValue().reversed()) + .limit(3) + .forEach(e -> sb.append(e.getKey()).append("(").append(e.getValue()).append(") ")); + sb.append("\n"); + } + } + } + + return sb.toString(); + } + + private String memBar(long used, long max) { + int barWidth = 20; + double ratio = (double) used / max; + int filled = (int) (ratio * barWidth); + String color = ratio > 0.8 ? AnsiStyle.red("█".repeat(filled)) + : ratio > 0.5 ? AnsiStyle.yellow("█".repeat(filled)) + : AnsiStyle.green("█".repeat(filled)); + return "[" + color + "░".repeat(barWidth - filled) + "] " + + String.format("%.0f%%", ratio * 100); + } + + private String formatBytes(long bytes) { + if (bytes >= 1_073_741_824) return String.format("%.1fGB", bytes / 1_073_741_824.0); + if (bytes >= 1_048_576) return String.format("%.0fMB", bytes / 1_048_576.0); + return String.format("%.0fKB", bytes / 1_024.0); + } + + private String formatDuration(long seconds) { + if (seconds < 60) return seconds + "s"; + if (seconds < 3600) return (seconds / 60) + "m " + (seconds % 60) + "s"; + return (seconds / 3600) + "h " + ((seconds % 3600) / 60) + "m"; + } +} diff --git a/src/main/java/com/claudecode/command/impl/PrivacyCommand.java b/src/main/java/com/claudecode/command/impl/PrivacyCommand.java new file mode 100644 index 0000000..8ffd4fc --- /dev/null +++ b/src/main/java/com/claudecode/command/impl/PrivacyCommand.java @@ -0,0 +1,70 @@ +package com.claudecode.command.impl; + +import com.claudecode.command.CommandContext; +import com.claudecode.command.SlashCommand; +import com.claudecode.console.AnsiStyle; + +/** + * /privacy 命令 —— 隐私设置查看和修改。 + */ +public class PrivacyCommand implements SlashCommand { + + @Override + public String name() { return "privacy"; } + + @Override + public String description() { return "View/modify privacy settings"; } + + @Override + public String execute(String args, CommandContext context) { + if (context.agentLoop() == null) { + return AnsiStyle.red(" ✗ No active session"); + } + + var toolCtx = context.agentLoop().getToolContext(); + boolean telemetryEnabled = Boolean.TRUE.equals(toolCtx.get("TELEMETRY_ENABLED")); + boolean sessionLogging = !Boolean.FALSE.equals(toolCtx.get("SESSION_LOGGING")); + boolean memoryPersist = !Boolean.FALSE.equals(toolCtx.get("MEMORY_PERSIST")); + + String trimmed = (args == null) ? "" : args.trim().toLowerCase(); + + if (trimmed.isEmpty()) { + StringBuilder sb = new StringBuilder(); + sb.append("\n").append(AnsiStyle.bold(" 🔒 Privacy Settings\n")); + sb.append(" ").append("─".repeat(40)).append("\n\n"); + sb.append(" ").append(statusIcon(telemetryEnabled)).append(" Telemetry: ") + .append(telemetryEnabled ? "Enabled" : "Disabled").append("\n"); + sb.append(" ").append(statusIcon(sessionLogging)).append(" Session Logging: ") + .append(sessionLogging ? "Enabled" : "Disabled").append("\n"); + sb.append(" ").append(statusIcon(memoryPersist)).append(" Memory Persist: ") + .append(memoryPersist ? "Enabled" : "Disabled").append("\n"); + sb.append("\n").append(AnsiStyle.dim(" Toggle: /privacy ")); + return sb.toString(); + } + + String[] parts = trimmed.split("\\s+", 2); + String setting = parts[0]; + boolean enable = parts.length > 1 && ("on".equals(parts[1]) || "enable".equals(parts[1]) || "true".equals(parts[1])); + + return switch (setting) { + case "telemetry" -> { + toolCtx.set("TELEMETRY_ENABLED", enable); + yield AnsiStyle.green(" ✓ Telemetry " + (enable ? "enabled" : "disabled")); + } + case "logging" -> { + toolCtx.set("SESSION_LOGGING", enable); + yield AnsiStyle.green(" ✓ Session logging " + (enable ? "enabled" : "disabled")); + } + case "memory" -> { + toolCtx.set("MEMORY_PERSIST", enable); + yield AnsiStyle.green(" ✓ Memory persistence " + (enable ? "enabled" : "disabled")); + } + default -> AnsiStyle.yellow(" Unknown setting: " + setting) + + "\n" + AnsiStyle.dim(" Options: telemetry, logging, memory"); + }; + } + + private String statusIcon(boolean enabled) { + return enabled ? AnsiStyle.green("●") : AnsiStyle.red("○"); + } +} diff --git a/src/main/java/com/claudecode/command/impl/ReleaseNotesCommand.java b/src/main/java/com/claudecode/command/impl/ReleaseNotesCommand.java new file mode 100644 index 0000000..9859418 --- /dev/null +++ b/src/main/java/com/claudecode/command/impl/ReleaseNotesCommand.java @@ -0,0 +1,66 @@ +package com.claudecode.command.impl; + +import com.claudecode.command.CommandContext; +import com.claudecode.command.SlashCommand; +import com.claudecode.console.AnsiStyle; + +import java.util.List; + +/** + * /release-notes 命令 —— 显示版本更新日志。 + */ +public class ReleaseNotesCommand implements SlashCommand { + + @Override + public String name() { return "release-notes"; } + + @Override + public String description() { return "Show version release notes"; } + + @Override + public List aliases() { + return List.of("changelog", "whatsnew"); + } + + @Override + public String execute(String args, CommandContext context) { + StringBuilder sb = new StringBuilder(); + sb.append("\n").append(AnsiStyle.bold(" 📋 Release Notes\n")); + sb.append(" ").append("─".repeat(50)).append("\n\n"); + + sb.append(AnsiStyle.bold(" v0.4.0 — Phase 4: Commands, Tools & Services\n")); + sb.append(" ").append("─".repeat(45)).append("\n"); + sb.append(" • LSPTool: code navigation via Language Server Protocol\n"); + sb.append(" • BriefTool: output verbosity control\n"); + sb.append(" • NotificationTool: cross-platform desktop notifications\n"); + sb.append(" • 12 new commands: /brief, /vim, /theme, /usage, /tips, etc.\n"); + sb.append(" • RateLimiter, TokenEstimation, InternalLogger services\n"); + sb.append(" • Debug commands: /debug, /heapdump, /trace, /ctx-viz\n\n"); + + sb.append(AnsiStyle.bold(" v0.3.0 — Phase 3: Advanced Infrastructure\n")); + sb.append(" ").append("─".repeat(45)).append("\n"); + sb.append(" • Server Mode: WebSocket direct connect for SDK integration\n"); + sb.append(" • Git Worktree: parallel branch isolation for agent tasks\n"); + sb.append(" • LSP Integration: JSON-RPC client, multi-server, diagnostics\n"); + sb.append(" • Telemetry: Feature flags, metrics, feature gates\n"); + sb.append(" • Plugin Marketplace: install, search, auto-update plugins\n\n"); + + sb.append(AnsiStyle.bold(" v0.2.0 — Phase 2: Core Features\n")); + sb.append(" ").append("─".repeat(45)).append("\n"); + sb.append(" • Plan Mode for multi-step task planning\n"); + sb.append(" • Skills execution system with /skill command\n"); + sb.append(" • Session Memory with CLAUDE.md auto-persist\n"); + sb.append(" • Coordinator Mode with multi-agent messaging\n"); + sb.append(" • MCP enhancements: HTTP+SSE, resources, env vars\n\n"); + + sb.append(AnsiStyle.bold(" v0.1.0 — Phase 1: Foundation\n")); + sb.append(" ").append("─".repeat(45)).append("\n"); + sb.append(" • Enhanced system prompts (7 security/style sections)\n"); + sb.append(" • 8 tool description improvements\n"); + sb.append(" • New tools: TaskStop, TaskOutput, Sleep, ToolSearch\n"); + sb.append(" • Command enhancements: /help search, /compact stats\n"); + sb.append(" • UI: markdown tables, spinner styles, tool status\n"); + + return sb.toString(); + } +} diff --git a/src/main/java/com/claudecode/command/impl/ThemeCommand.java b/src/main/java/com/claudecode/command/impl/ThemeCommand.java new file mode 100644 index 0000000..84356e3 --- /dev/null +++ b/src/main/java/com/claudecode/command/impl/ThemeCommand.java @@ -0,0 +1,52 @@ +package com.claudecode.command.impl; + +import com.claudecode.command.CommandContext; +import com.claudecode.command.SlashCommand; +import com.claudecode.console.AnsiStyle; + +/** + * /theme 命令 —— 切换主题(dark/light/auto)。 + */ +public class ThemeCommand implements SlashCommand { + + @Override + public String name() { return "theme"; } + + @Override + public String description() { return "Switch color theme (dark/light/auto)"; } + + @Override + public String execute(String args, CommandContext context) { + if (context.agentLoop() == null) { + return AnsiStyle.red(" ✗ No active session"); + } + + var toolCtx = context.agentLoop().getToolContext(); + String current = (String) toolCtx.get("THEME"); + if (current == null) current = "dark"; + + String trimmed = (args == null) ? "" : args.trim().toLowerCase(); + + if (trimmed.isEmpty()) { + return "\n" + AnsiStyle.bold(" 🎨 Theme Settings\n") + + " " + "─".repeat(30) + "\n\n" + + " Current: " + AnsiStyle.cyan(current) + "\n" + + " Options: " + AnsiStyle.dim("dark, light, auto") + "\n" + + "\n" + AnsiStyle.dim(" Usage: /theme "); + } + + if (!trimmed.equals("dark") && !trimmed.equals("light") && !trimmed.equals("auto")) { + return AnsiStyle.yellow(" Unknown theme: " + trimmed) + + "\n" + AnsiStyle.dim(" Options: dark, light, auto"); + } + + toolCtx.set("THEME", trimmed); + String icon = switch (trimmed) { + case "dark" -> "🌙"; + case "light" -> "☀️"; + case "auto" -> "🔄"; + default -> "🎨"; + }; + return AnsiStyle.green(" ✓ Theme set to " + trimmed + " " + icon); + } +} diff --git a/src/main/java/com/claudecode/command/impl/TipsCommand.java b/src/main/java/com/claudecode/command/impl/TipsCommand.java new file mode 100644 index 0000000..08d98b6 --- /dev/null +++ b/src/main/java/com/claudecode/command/impl/TipsCommand.java @@ -0,0 +1,65 @@ +package com.claudecode.command.impl; + +import com.claudecode.command.CommandContext; +import com.claudecode.command.SlashCommand; +import com.claudecode.console.AnsiStyle; + +import java.util.List; +import java.util.Random; + +/** + * /tips 命令 —— 显示使用技巧和建议。 + */ +public class TipsCommand implements SlashCommand { + + private static final List TIPS = List.of( + "Use /compact when the conversation gets long to reduce token usage", + "Use /brief to toggle concise output mode for faster responses", + "Chain commands: 'run tests && fix any failures' lets the agent iterate", + "Use /plan to enter plan mode — great for complex multi-step tasks", + "Use /memory to save important context that persists across sessions", + "Press Ctrl+C to interrupt a long-running tool execution", + "Use /status to check token usage and session health", + "Use /commit --push to commit and push in one command", + "Agent tasks (/tasks) run in parallel — great for concurrent work", + "Use /vim to enable vi-mode editing in the input field", + "Use /theme to switch between dark and light themes", + "Use /diff to review uncommitted changes before committing", + "CLAUDE.md files in your project root customize agent behavior", + "Use /skills to discover and load skill templates", + "Use /plugin search to find community plugins", + "Use /export to save the conversation as markdown", + "Use /usage to see detailed token and cost breakdown", + "Use /doctor to diagnose configuration issues", + "Use /context to see what files are loaded in context", + "Use /keybindings to see all keyboard shortcuts" + ); + + private final Random random = new Random(); + + @Override + public String name() { return "tips"; } + + @Override + public String description() { return "Show usage tips and suggestions"; } + + @Override + public String execute(String args, CommandContext context) { + String trimmed = (args == null) ? "" : args.trim(); + + if ("all".equals(trimmed)) { + StringBuilder sb = new StringBuilder(); + sb.append("\n").append(AnsiStyle.bold(" 💡 All Tips\n")); + sb.append(" ").append("─".repeat(50)).append("\n\n"); + for (int i = 0; i < TIPS.size(); i++) { + sb.append(String.format(" %2d. %s%n", i + 1, TIPS.get(i))); + } + return sb.toString(); + } + + // Show random tip + String tip = TIPS.get(random.nextInt(TIPS.size())); + return "\n 💡 " + AnsiStyle.bold("Tip: ") + tip + "\n\n" + + AnsiStyle.dim(" Run /tips all to see all tips"); + } +} diff --git a/src/main/java/com/claudecode/command/impl/UsageCommand.java b/src/main/java/com/claudecode/command/impl/UsageCommand.java new file mode 100644 index 0000000..f8836dd --- /dev/null +++ b/src/main/java/com/claudecode/command/impl/UsageCommand.java @@ -0,0 +1,71 @@ +package com.claudecode.command.impl; + +import com.claudecode.command.CommandContext; +import com.claudecode.command.SlashCommand; +import com.claudecode.console.AnsiStyle; +import com.claudecode.core.TokenTracker; + +/** + * /usage 命令 —— 详细 token 和费用统计。 + */ +public class UsageCommand implements SlashCommand { + + @Override + public String name() { return "usage"; } + + @Override + public String description() { return "Show detailed token usage and cost breakdown"; } + + @Override + public String execute(String args, CommandContext context) { + if (context.agentLoop() == null) { + return AnsiStyle.red(" ✗ No active session"); + } + + TokenTracker tracker = context.agentLoop().getTokenTracker(); + + long inputTokens = tracker.getInputTokens(); + long outputTokens = tracker.getOutputTokens(); + long totalTokens = inputTokens + outputTokens; + + // Approximate costs (Claude 3.5 Sonnet pricing) + double inputCost = inputTokens / 1_000_000.0 * 3.0; // $3/M input + double outputCost = outputTokens / 1_000_000.0 * 15.0; // $15/M output + double totalCost = inputCost + outputCost; + + StringBuilder sb = new StringBuilder(); + sb.append("\n"); + sb.append(AnsiStyle.bold(" 📊 Token Usage & Cost\n")); + sb.append(" ").append("─".repeat(40)).append("\n\n"); + + sb.append(" ").append(AnsiStyle.bold("Model: ")).append(AnsiStyle.cyan(tracker.getModelName())).append("\n\n"); + + sb.append(" ").append(AnsiStyle.bold("Input Tokens: ")).append(formatNumber(inputTokens)).append("\n"); + sb.append(" ").append(AnsiStyle.bold("Output Tokens: ")).append(formatNumber(outputTokens)).append("\n"); + sb.append(" ").append(AnsiStyle.bold("Total Tokens: ")).append(formatNumber(totalTokens)).append("\n\n"); + + sb.append(" ").append(AnsiStyle.bold("Estimated Cost")).append("\n"); + sb.append(" Input: $").append(String.format("%.4f", inputCost)).append(" (").append(formatNumber(inputTokens)).append(" × $3/M)\n"); + sb.append(" Output: $").append(String.format("%.4f", outputCost)).append(" (").append(formatNumber(outputTokens)).append(" × $15/M)\n"); + sb.append(" ").append(AnsiStyle.bold("Total: $" + String.format("%.4f", totalCost))).append("\n\n"); + + // Usage bar + if (totalTokens > 0) { + int barWidth = 30; + int inputBar = (int) ((double) inputTokens / totalTokens * barWidth); + int outputBar = barWidth - inputBar; + sb.append(" [").append(AnsiStyle.blue("█".repeat(inputBar))) + .append(AnsiStyle.green("█".repeat(outputBar))).append("]\n"); + sb.append(" ").append(AnsiStyle.blue("■")).append(" Input ") + .append(AnsiStyle.green("■")).append(" Output\n"); + } + + return sb.toString(); + } + + private String formatNumber(long n) { + if (n >= 1_000_000) return String.format("%.1fM", n / 1_000_000.0); + if (n >= 1_000) return String.format("%.1fK", n / 1_000.0); + return String.valueOf(n); + } +} diff --git a/src/main/java/com/claudecode/command/impl/VimCommand.java b/src/main/java/com/claudecode/command/impl/VimCommand.java new file mode 100644 index 0000000..1c6e901 --- /dev/null +++ b/src/main/java/com/claudecode/command/impl/VimCommand.java @@ -0,0 +1,43 @@ +package com.claudecode.command.impl; + +import com.claudecode.command.CommandContext; +import com.claudecode.command.SlashCommand; +import com.claudecode.console.AnsiStyle; + +/** + * /vim 命令 —— 切换 Vi 编辑模式(JLine vi-mode)。 + */ +public class VimCommand implements SlashCommand { + + @Override + public String name() { return "vim"; } + + @Override + public String description() { return "Toggle vim editing mode"; } + + @Override + public String execute(String args, CommandContext context) { + if (context.agentLoop() == null) { + return AnsiStyle.red(" ✗ No active session"); + } + + var toolCtx = context.agentLoop().getToolContext(); + boolean current = Boolean.TRUE.equals(toolCtx.get("VIM_MODE")); + + String trimmed = (args == null) ? "" : args.trim(); + boolean newMode = switch (trimmed) { + case "on", "enable" -> true; + case "off", "disable" -> false; + default -> !current; + }; + + toolCtx.set("VIM_MODE", newMode); + + if (newMode) { + return AnsiStyle.green(" ✓ Vim mode ON") + "\n" + + AnsiStyle.dim(" Key bindings: ESC → normal mode, i → insert, dd → delete line"); + } else { + return AnsiStyle.green(" ✓ Vim mode OFF") + " — standard editing"; + } + } +} diff --git a/src/main/java/com/claudecode/config/AppConfig.java b/src/main/java/com/claudecode/config/AppConfig.java index 4b11a03..992b0de 100644 --- a/src/main/java/com/claudecode/config/AppConfig.java +++ b/src/main/java/com/claudecode/config/AppConfig.java @@ -187,6 +187,19 @@ public class AppConfig { new SessionCommand(), new AgentCommand(), new RenameCommand(), + // Phase 4B 命令 + new BriefCommand(), + new VimCommand(), + new ThemeCommand(), + new UsageCommand(), + new TipsCommand(), + new OutputStyleCommand(), + new EnvCommand(), + new PerformanceCommand(), + new PrivacyCommand(), + new FeedbackCommand(), + new ReleaseNotesCommand(), + new KeybindingsCommand(), // Exit 放最后 new ExitCommand() );