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>
This commit is contained in:
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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<String> relevantVars = List.of(
|
||||
"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "CLAUDE_CODE_",
|
||||
"JAVA_HOME", "PATH", "SHELL", "TERM", "EDITOR"
|
||||
);
|
||||
|
||||
Map<String, String> env = new TreeMap<>(System.getenv());
|
||||
for (Map.Entry<String, String> 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);
|
||||
}
|
||||
}
|
||||
@@ -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 <your feedback text>")
|
||||
+ "\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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String> 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();
|
||||
}
|
||||
}
|
||||
@@ -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 <markdown|plain|json>");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<String> 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.<String, Long>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";
|
||||
}
|
||||
}
|
||||
@@ -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 <telemetry|logging|memory> <on|off>"));
|
||||
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("○");
|
||||
}
|
||||
}
|
||||
@@ -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<String> 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();
|
||||
}
|
||||
}
|
||||
@@ -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 <dark|light|auto>");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<String> 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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user