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>
pull/1/head
abel533 1 month ago
parent d65d63038f
commit d09809e924
  1. 40
      src/main/java/com/claudecode/command/impl/BriefCommand.java
  2. 93
      src/main/java/com/claudecode/command/impl/EnvCommand.java
  3. 56
      src/main/java/com/claudecode/command/impl/FeedbackCommand.java
  4. 60
      src/main/java/com/claudecode/command/impl/KeybindingsCommand.java
  5. 49
      src/main/java/com/claudecode/command/impl/OutputStyleCommand.java
  6. 104
      src/main/java/com/claudecode/command/impl/PerformanceCommand.java
  7. 70
      src/main/java/com/claudecode/command/impl/PrivacyCommand.java
  8. 66
      src/main/java/com/claudecode/command/impl/ReleaseNotesCommand.java
  9. 52
      src/main/java/com/claudecode/command/impl/ThemeCommand.java
  10. 65
      src/main/java/com/claudecode/command/impl/TipsCommand.java
  11. 71
      src/main/java/com/claudecode/command/impl/UsageCommand.java
  12. 43
      src/main/java/com/claudecode/command/impl/VimCommand.java
  13. 13
      src/main/java/com/claudecode/config/AppConfig.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";
}
}

@ -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 SessionCommand(),
new AgentCommand(), new AgentCommand(),
new RenameCommand(), 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 放最后 // Exit 放最后
new ExitCommand() new ExitCommand()
); );

Loading…
Cancel
Save