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
parent
d65d63038f
commit
d09809e924
@ -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,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,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"; |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue