feat: Phase 4C infrastructure services + 4D debug commands
4C Services: - RateLimiter: sliding window rate limiting, concurrent semaphore, cooldowns - TokenEstimationService: approximate token counting, cost estimation - NotificationService: cross-platform desktop notifications - InternalLogger: structured session logging with export 4D Debug Commands: - /debug: toggle debug mode, view internal logs - /heapdump: JVM heap dump and memory pool info (Java advantage) - /trace: request/response tracing - /ctx-viz: context window token distribution visualization - /reset-limits: reset rate limits and cooldowns - /sandbox: sandbox mode toggle (strict/permissive/none) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
package com.claudecode.command.impl;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
import com.claudecode.core.TokenEstimationService;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* /ctx-viz 命令 —— 上下文可视化(token 分布、消息结构)。
|
||||
*/
|
||||
public class ContextVizCommand implements SlashCommand {
|
||||
|
||||
@Override
|
||||
public String name() { return "ctx-viz"; }
|
||||
|
||||
@Override
|
||||
public String description() { return "Visualize context window token distribution"; }
|
||||
|
||||
@Override
|
||||
public List<String> aliases() { return List.of("context", "ctx"); }
|
||||
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\n").append(AnsiStyle.bold(" 📊 Context Window Visualization\n"));
|
||||
sb.append(" ").append("─".repeat(45)).append("\n\n");
|
||||
|
||||
if (context.agentLoop() == null) {
|
||||
return sb.append(" No active agent loop\n").toString();
|
||||
}
|
||||
|
||||
var toolCtx = context.agentLoop().getToolContext();
|
||||
|
||||
// Get or create token estimation service
|
||||
TokenEstimationService estimator = new TokenEstimationService();
|
||||
|
||||
// Model context limit
|
||||
String modelName = "claude-sonnet-4-20250514";
|
||||
Object modelObj = toolCtx.get("MODEL_NAME");
|
||||
if (modelObj instanceof String m) modelName = m;
|
||||
|
||||
int contextLimit = getContextLimit(modelName);
|
||||
|
||||
// Estimate system prompt tokens
|
||||
int systemPromptTokens = 0;
|
||||
Object sysMsgObj = toolCtx.get("SYSTEM_PROMPT_CACHE");
|
||||
if (sysMsgObj instanceof String sysMsg) {
|
||||
systemPromptTokens = estimator.estimateTokens(sysMsg);
|
||||
} else {
|
||||
systemPromptTokens = 4000; // typical estimate
|
||||
}
|
||||
|
||||
// Tool definitions estimate
|
||||
int toolDefTokens = 0;
|
||||
Object toolCountObj = toolCtx.get("TOOL_REGISTRY");
|
||||
if (toolCountObj != null) {
|
||||
toolDefTokens = 2000; // ~65 tokens per tool × 30 tools
|
||||
}
|
||||
|
||||
// Token tracker data
|
||||
long inputTokens = 0;
|
||||
long outputTokens = 0;
|
||||
var tokenTracker = context.agentLoop().getTokenTracker();
|
||||
if (tokenTracker != null) {
|
||||
inputTokens = tokenTracker.getInputTokens();
|
||||
outputTokens = tokenTracker.getOutputTokens();
|
||||
}
|
||||
|
||||
// Calculate remaining
|
||||
long usedTokens = systemPromptTokens + toolDefTokens + inputTokens;
|
||||
long remainingTokens = Math.max(0, contextLimit - usedTokens);
|
||||
double usagePercent = (double) usedTokens / contextLimit * 100;
|
||||
|
||||
// Display context bar
|
||||
sb.append(AnsiStyle.bold(" Context Usage\n"));
|
||||
int barWidth = 40;
|
||||
int filled = (int) (usagePercent / 100 * barWidth);
|
||||
filled = Math.min(filled, barWidth);
|
||||
|
||||
String barColor;
|
||||
if (usagePercent > 90) barColor = AnsiStyle.red("█".repeat(filled));
|
||||
else if (usagePercent > 70) barColor = AnsiStyle.yellow("█".repeat(filled));
|
||||
else barColor = AnsiStyle.green("█".repeat(filled));
|
||||
|
||||
sb.append(" [").append(barColor).append("░".repeat(barWidth - filled)).append("] ");
|
||||
sb.append(String.format("%.1f%%\n\n", usagePercent));
|
||||
|
||||
// Token breakdown
|
||||
sb.append(AnsiStyle.bold(" Token Breakdown\n"));
|
||||
sb.append(" ┌────────────────────────┬────────────┬───────┐\n");
|
||||
sb.append(" │ Component │ Tokens │ % │\n");
|
||||
sb.append(" ├────────────────────────┼────────────┼───────┤\n");
|
||||
|
||||
appendRow(sb, "System Prompt", systemPromptTokens, contextLimit);
|
||||
appendRow(sb, "Tool Definitions", toolDefTokens, contextLimit);
|
||||
appendRow(sb, "Conversation (input)", (int) inputTokens, contextLimit);
|
||||
appendRow(sb, "Generated (output)", (int) outputTokens, contextLimit);
|
||||
sb.append(" ├────────────────────────┼────────────┼───────┤\n");
|
||||
appendRow(sb, "Total Used", (int) usedTokens, contextLimit);
|
||||
appendRow(sb, "Remaining", (int) remainingTokens, contextLimit);
|
||||
sb.append(" └────────────────────────┴────────────┴───────┘\n\n");
|
||||
|
||||
// Model info
|
||||
sb.append(AnsiStyle.bold(" Model Info\n"));
|
||||
sb.append(" Model: ").append(modelName).append("\n");
|
||||
sb.append(" Context: ").append(estimator.formatTokenCount(contextLimit)).append(" tokens\n");
|
||||
sb.append(" Cost est: $").append(String.format("%.4f",
|
||||
estimator.estimateCost(inputTokens, outputTokens, modelName))).append("\n");
|
||||
|
||||
// Recommendations
|
||||
if (usagePercent > 80) {
|
||||
sb.append("\n ⚠️ ").append(AnsiStyle.yellow("Context is getting full. Consider /compact to free space.")).append("\n");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private void appendRow(StringBuilder sb, String label, int tokens, int total) {
|
||||
double pct = (double) tokens / total * 100;
|
||||
sb.append(String.format(" │ %-22s │ %10s │ %5.1f │\n",
|
||||
label, formatTokens(tokens), pct));
|
||||
}
|
||||
|
||||
private String formatTokens(int tokens) {
|
||||
if (tokens >= 1_000_000) return String.format("%.1fM", tokens / 1_000_000.0);
|
||||
if (tokens >= 1_000) return String.format("%.1fK", tokens / 1_000.0);
|
||||
return String.valueOf(tokens);
|
||||
}
|
||||
|
||||
private int getContextLimit(String model) {
|
||||
if (model.contains("opus")) return 200_000;
|
||||
if (model.contains("haiku")) return 200_000;
|
||||
return 200_000; // All Claude 3+ models: 200K
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package com.claudecode.command.impl;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
import com.claudecode.core.InternalLogger;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* /debug 命令 —— 调试模式开关 + 工具调用追踪。
|
||||
*/
|
||||
public class DebugCommand implements SlashCommand {
|
||||
|
||||
@Override
|
||||
public String name() { return "debug"; }
|
||||
|
||||
@Override
|
||||
public String description() { return "Toggle debug mode and view internal logs"; }
|
||||
|
||||
@Override
|
||||
public List<String> aliases() { return List.of("dbg"); }
|
||||
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
String trimmed = (args == null) ? "" : args.trim();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\n").append(AnsiStyle.bold(" 🐛 Debug Mode\n"));
|
||||
sb.append(" ").append("─".repeat(40)).append("\n\n");
|
||||
|
||||
if (context.agentLoop() == null) {
|
||||
return sb.append(" No active agent loop\n").toString();
|
||||
}
|
||||
|
||||
var toolCtx = context.agentLoop().getToolContext();
|
||||
|
||||
if (trimmed.equals("on") || trimmed.equals("enable")) {
|
||||
toolCtx.set("DEBUG_MODE", true);
|
||||
sb.append(" Debug mode: ").append(AnsiStyle.green("ENABLED")).append("\n");
|
||||
sb.append(" Tool call tracing is now active.\n");
|
||||
|
||||
// Set InternalLogger to DEBUG level if available
|
||||
Object loggerObj = toolCtx.get("INTERNAL_LOGGER");
|
||||
if (loggerObj instanceof InternalLogger logger) {
|
||||
logger.setLevel(InternalLogger.Level.DEBUG);
|
||||
sb.append(" Internal log level: DEBUG\n");
|
||||
}
|
||||
|
||||
} else if (trimmed.equals("off") || trimmed.equals("disable")) {
|
||||
toolCtx.set("DEBUG_MODE", false);
|
||||
sb.append(" Debug mode: ").append(AnsiStyle.red("DISABLED")).append("\n");
|
||||
|
||||
Object loggerObj = toolCtx.get("INTERNAL_LOGGER");
|
||||
if (loggerObj instanceof InternalLogger logger) {
|
||||
logger.setLevel(InternalLogger.Level.NORMAL);
|
||||
sb.append(" Internal log level: NORMAL\n");
|
||||
}
|
||||
|
||||
} else if (trimmed.startsWith("log")) {
|
||||
// /debug logs [N] — show recent internal logs
|
||||
int count = 20;
|
||||
String[] parts = trimmed.split("\\s+");
|
||||
if (parts.length > 1) {
|
||||
try { count = Integer.parseInt(parts[1]); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
Object loggerObj = toolCtx.get("INTERNAL_LOGGER");
|
||||
if (loggerObj instanceof InternalLogger logger) {
|
||||
sb.append(AnsiStyle.bold(" Recent Logs (last " + count + ")\n\n"));
|
||||
String logs = logger.getRecent(count);
|
||||
if (logs.isEmpty()) {
|
||||
sb.append(" No logs recorded yet.\n");
|
||||
} else {
|
||||
sb.append(AnsiStyle.dim(logs));
|
||||
}
|
||||
} else {
|
||||
sb.append(" InternalLogger not available.\n");
|
||||
}
|
||||
|
||||
} else if (trimmed.equals("tools")) {
|
||||
// /debug tools — show tool call stats from ToolContext
|
||||
sb.append(AnsiStyle.bold(" Tool Call Tracing\n\n"));
|
||||
Object metrics = toolCtx.get("METRICS_COLLECTOR");
|
||||
if (metrics != null) {
|
||||
sb.append(" Use /performance for detailed tool stats.\n");
|
||||
} else {
|
||||
sb.append(" No metrics collector available.\n");
|
||||
}
|
||||
|
||||
} else {
|
||||
// Status
|
||||
boolean debugOn = Boolean.TRUE.equals(toolCtx.get("DEBUG_MODE"));
|
||||
sb.append(" Status: ").append(debugOn
|
||||
? AnsiStyle.green("ENABLED") : AnsiStyle.dim("disabled")).append("\n\n");
|
||||
|
||||
sb.append(AnsiStyle.bold(" Subcommands\n"));
|
||||
sb.append(" /debug on Enable debug mode\n");
|
||||
sb.append(" /debug off Disable debug mode\n");
|
||||
sb.append(" /debug logs Show recent internal logs\n");
|
||||
sb.append(" /debug logs 50 Show last 50 log entries\n");
|
||||
sb.append(" /debug tools Show tool call tracing info\n");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
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.lang.management.MemoryMXBean;
|
||||
import java.lang.management.MemoryPoolMXBean;
|
||||
import java.lang.management.MemoryUsage;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* /heapdump 命令 —— JVM 堆转储(Java 独有优势)。
|
||||
* 使用 HotSpotDiagnosticMXBean 进行堆转储。
|
||||
*/
|
||||
public class HeapdumpCommand implements SlashCommand {
|
||||
|
||||
@Override
|
||||
public String name() { return "heapdump"; }
|
||||
|
||||
@Override
|
||||
public String description() { return "Generate JVM heap dump (Java advantage)"; }
|
||||
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
String trimmed = (args == null) ? "" : args.trim();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\n").append(AnsiStyle.bold(" 📦 JVM Heap Dump\n"));
|
||||
sb.append(" ").append("─".repeat(40)).append("\n\n");
|
||||
|
||||
if (trimmed.equals("info") || trimmed.isEmpty()) {
|
||||
// Show memory pool details
|
||||
MemoryMXBean memBean = ManagementFactory.getMemoryMXBean();
|
||||
MemoryUsage heap = memBean.getHeapMemoryUsage();
|
||||
MemoryUsage nonHeap = memBean.getNonHeapMemoryUsage();
|
||||
|
||||
sb.append(AnsiStyle.bold(" Heap Memory\n"));
|
||||
sb.append(" Used: ").append(formatBytes(heap.getUsed())).append("\n");
|
||||
sb.append(" Committed: ").append(formatBytes(heap.getCommitted())).append("\n");
|
||||
sb.append(" Max: ").append(formatBytes(heap.getMax())).append("\n\n");
|
||||
|
||||
sb.append(AnsiStyle.bold(" Non-Heap Memory\n"));
|
||||
sb.append(" Used: ").append(formatBytes(nonHeap.getUsed())).append("\n");
|
||||
sb.append(" Committed: ").append(formatBytes(nonHeap.getCommitted())).append("\n\n");
|
||||
|
||||
sb.append(AnsiStyle.bold(" Memory Pools\n"));
|
||||
for (MemoryPoolMXBean pool : ManagementFactory.getMemoryPoolMXBeans()) {
|
||||
MemoryUsage usage = pool.getUsage();
|
||||
if (usage != null && usage.getUsed() > 0) {
|
||||
sb.append(" ").append(String.format("%-25s", pool.getName()))
|
||||
.append(formatBytes(usage.getUsed())).append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
sb.append("\n").append(AnsiStyle.dim(" Run /heapdump dump to generate a heap dump file"));
|
||||
|
||||
} else if (trimmed.startsWith("dump")) {
|
||||
// Determine output path
|
||||
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss"));
|
||||
String filename = trimmed.length() > 5 ? trimmed.substring(5).trim() : "";
|
||||
if (filename.isEmpty()) {
|
||||
filename = "heapdump-" + timestamp + ".hprof";
|
||||
}
|
||||
Path dumpPath = Path.of(System.getProperty("user.dir"), filename);
|
||||
|
||||
try {
|
||||
var hotspot = ManagementFactory.getPlatformMXBean(
|
||||
com.sun.management.HotSpotDiagnosticMXBean.class);
|
||||
hotspot.dumpHeap(dumpPath.toString(), true);
|
||||
long fileSize = dumpPath.toFile().length();
|
||||
sb.append(" ✅ Heap dump saved to:\n");
|
||||
sb.append(" ").append(AnsiStyle.cyan(dumpPath.toString())).append("\n");
|
||||
sb.append(" Size: ").append(formatBytes(fileSize)).append("\n\n");
|
||||
sb.append(AnsiStyle.dim(" Analyze with: jhat, MAT, or VisualVM"));
|
||||
} catch (Exception e) {
|
||||
sb.append(" ❌ Failed to create heap dump: ").append(e.getMessage()).append("\n");
|
||||
sb.append(AnsiStyle.dim(" Requires HotSpot JVM (OpenJDK or Oracle JDK)"));
|
||||
}
|
||||
|
||||
} else if (trimmed.equals("gc")) {
|
||||
// Trigger GC and report
|
||||
long beforeUsed = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
|
||||
System.gc();
|
||||
long afterUsed = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
|
||||
long freed = beforeUsed - afterUsed;
|
||||
|
||||
sb.append(" 🗑 Garbage collection triggered\n");
|
||||
sb.append(" Before: ").append(formatBytes(beforeUsed)).append("\n");
|
||||
sb.append(" After: ").append(formatBytes(afterUsed)).append("\n");
|
||||
sb.append(" Freed: ").append(AnsiStyle.green(formatBytes(Math.max(0, freed)))).append("\n");
|
||||
|
||||
} else {
|
||||
sb.append(AnsiStyle.bold(" Subcommands\n"));
|
||||
sb.append(" /heapdump Show memory pool info\n");
|
||||
sb.append(" /heapdump dump Generate .hprof file\n");
|
||||
sb.append(" /heapdump gc Trigger garbage collection\n");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String formatBytes(long bytes) {
|
||||
if (bytes < 0) return "N/A";
|
||||
if (bytes >= 1_073_741_824) return String.format("%.1fGB", bytes / 1_073_741_824.0);
|
||||
if (bytes >= 1_048_576) return String.format("%.1fMB", bytes / 1_048_576.0);
|
||||
return String.format("%.0fKB", bytes / 1_024.0);
|
||||
}
|
||||
}
|
||||
@@ -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 com.claudecode.core.RateLimiter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* /reset-limits 命令 —— 重置速率限制。
|
||||
*/
|
||||
public class ResetLimitsCommand implements SlashCommand {
|
||||
|
||||
@Override
|
||||
public String name() { return "reset-limits"; }
|
||||
|
||||
@Override
|
||||
public String description() { return "Reset rate limits and cooldowns"; }
|
||||
|
||||
@Override
|
||||
public List<String> aliases() { return List.of("rl"); }
|
||||
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\n").append(AnsiStyle.bold(" 🔄 Rate Limit Reset\n"));
|
||||
sb.append(" ").append("─".repeat(40)).append("\n\n");
|
||||
|
||||
if (context.agentLoop() == null) {
|
||||
return sb.append(" No active agent loop\n").toString();
|
||||
}
|
||||
|
||||
var toolCtx = context.agentLoop().getToolContext();
|
||||
Object limiterObj = toolCtx.get("RATE_LIMITER");
|
||||
|
||||
if (limiterObj instanceof RateLimiter limiter) {
|
||||
// Show current state first
|
||||
sb.append(AnsiStyle.bold(" Before Reset\n"));
|
||||
sb.append(" Remaining (api): ").append(limiter.getRemaining("api")).append("\n");
|
||||
sb.append(" Remaining (tool): ").append(limiter.getRemaining("tool")).append("\n\n");
|
||||
|
||||
// Reset
|
||||
limiter.resetAll();
|
||||
|
||||
sb.append(AnsiStyle.bold(" After Reset\n"));
|
||||
sb.append(" Remaining (api): ").append(AnsiStyle.green(
|
||||
String.valueOf(limiter.getRemaining("api")))).append("\n");
|
||||
sb.append(" Remaining (tool): ").append(AnsiStyle.green(
|
||||
String.valueOf(limiter.getRemaining("tool")))).append("\n");
|
||||
sb.append(" Concurrent slots: ").append(AnsiStyle.green("all available")).append("\n\n");
|
||||
sb.append(" ✅ Rate limits have been reset.\n");
|
||||
} else {
|
||||
sb.append(" No rate limiter configured.\n");
|
||||
sb.append(AnsiStyle.dim(" Rate limiting is not active in the current session.\n"));
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.claudecode.command.impl;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* /sandbox 命令 —— 沙箱模式切换。
|
||||
* 控制工具执行的安全隔离级别。
|
||||
*/
|
||||
public class SandboxCommand implements SlashCommand {
|
||||
|
||||
@Override
|
||||
public String name() { return "sandbox"; }
|
||||
|
||||
@Override
|
||||
public String description() { return "Toggle sandbox mode for tool execution"; }
|
||||
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
String trimmed = (args == null) ? "" : args.trim();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\n").append(AnsiStyle.bold(" 🏖 Sandbox Mode\n"));
|
||||
sb.append(" ").append("─".repeat(40)).append("\n\n");
|
||||
|
||||
if (context.agentLoop() == null) {
|
||||
return sb.append(" No active agent loop\n").toString();
|
||||
}
|
||||
|
||||
var toolCtx = context.agentLoop().getToolContext();
|
||||
|
||||
if (trimmed.equals("on") || trimmed.equals("enable") || trimmed.equals("strict")) {
|
||||
toolCtx.set("SANDBOX_MODE", "strict");
|
||||
sb.append(" Sandbox: ").append(AnsiStyle.green("STRICT")).append("\n\n");
|
||||
sb.append(" Restrictions:\n");
|
||||
sb.append(" • File writes limited to work directory\n");
|
||||
sb.append(" • Network access: disabled\n");
|
||||
sb.append(" • Shell commands: require approval\n");
|
||||
sb.append(" • System calls: blocked\n");
|
||||
|
||||
} else if (trimmed.equals("off") || trimmed.equals("disable") || trimmed.equals("none")) {
|
||||
toolCtx.set("SANDBOX_MODE", "none");
|
||||
sb.append(" Sandbox: ").append(AnsiStyle.red("DISABLED")).append("\n");
|
||||
sb.append(" ⚠️ All tool operations are unrestricted.\n");
|
||||
|
||||
} else if (trimmed.equals("permissive")) {
|
||||
toolCtx.set("SANDBOX_MODE", "permissive");
|
||||
sb.append(" Sandbox: ").append(AnsiStyle.yellow("PERMISSIVE")).append("\n\n");
|
||||
sb.append(" Restrictions:\n");
|
||||
sb.append(" • File writes: allowed with logging\n");
|
||||
sb.append(" • Network access: allowed\n");
|
||||
sb.append(" • Shell commands: allowed with logging\n");
|
||||
sb.append(" • System calls: require approval\n");
|
||||
|
||||
} else {
|
||||
// Show current status
|
||||
Object mode = toolCtx.get("SANDBOX_MODE");
|
||||
String current = (mode instanceof String m) ? m : "permissive";
|
||||
|
||||
sb.append(" Current mode: ");
|
||||
sb.append(switch (current) {
|
||||
case "strict" -> AnsiStyle.green("STRICT");
|
||||
case "none" -> AnsiStyle.red("NONE");
|
||||
default -> AnsiStyle.yellow("PERMISSIVE");
|
||||
}).append("\n\n");
|
||||
|
||||
sb.append(AnsiStyle.bold(" Available Modes\n"));
|
||||
sb.append(" ┌─────────────┬────────────┬─────────┬──────────┐\n");
|
||||
sb.append(" │ Mode │ File Write │ Network │ Shell │\n");
|
||||
sb.append(" ├─────────────┼────────────┼─────────┼──────────┤\n");
|
||||
sb.append(" │ strict │ work dir │ blocked │ approval │\n");
|
||||
sb.append(" │ permissive │ logged │ allowed │ logged │\n");
|
||||
sb.append(" │ none │ unlimited │ allowed │ allowed │\n");
|
||||
sb.append(" └─────────────┴────────────┴─────────┴──────────┘\n\n");
|
||||
|
||||
sb.append(AnsiStyle.bold(" Usage\n"));
|
||||
sb.append(" /sandbox strict Enable strict sandbox\n");
|
||||
sb.append(" /sandbox permissive Enable permissive sandbox\n");
|
||||
sb.append(" /sandbox off Disable sandbox\n");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.claudecode.command.impl;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||
|
||||
/**
|
||||
* /trace 命令 —— 请求/响应追踪。
|
||||
* 显示 API 调用追踪信息(模型调用、tool 调用链等)。
|
||||
*/
|
||||
public class TraceCommand implements SlashCommand {
|
||||
|
||||
@Override
|
||||
public String name() { return "trace"; }
|
||||
|
||||
@Override
|
||||
public String description() { return "Show request/response tracing"; }
|
||||
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
String trimmed = (args == null) ? "" : args.trim();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\n").append(AnsiStyle.bold(" 🔍 Request Tracing\n"));
|
||||
sb.append(" ").append("─".repeat(40)).append("\n\n");
|
||||
|
||||
if (context.agentLoop() == null) {
|
||||
return sb.append(" No active agent loop\n").toString();
|
||||
}
|
||||
|
||||
var toolCtx = context.agentLoop().getToolContext();
|
||||
|
||||
if (trimmed.equals("on") || trimmed.equals("enable")) {
|
||||
toolCtx.set("TRACE_ENABLED", true);
|
||||
sb.append(" Tracing: ").append(AnsiStyle.green("ENABLED")).append("\n");
|
||||
sb.append(" API calls and tool executions will be traced.\n");
|
||||
|
||||
} else if (trimmed.equals("off") || trimmed.equals("disable")) {
|
||||
toolCtx.set("TRACE_ENABLED", false);
|
||||
sb.append(" Tracing: ").append(AnsiStyle.red("DISABLED")).append("\n");
|
||||
|
||||
} else if (trimmed.equals("clear")) {
|
||||
toolCtx.set("TRACE_LOG", null);
|
||||
sb.append(" Trace log cleared.\n");
|
||||
|
||||
} else {
|
||||
// Show current trace info
|
||||
boolean traceOn = Boolean.TRUE.equals(toolCtx.get("TRACE_ENABLED"));
|
||||
sb.append(" Status: ").append(traceOn
|
||||
? AnsiStyle.green("ENABLED") : AnsiStyle.dim("disabled")).append("\n\n");
|
||||
|
||||
// Show thread info
|
||||
sb.append(AnsiStyle.bold(" Active Threads\n"));
|
||||
Thread.getAllStackTraces().entrySet().stream()
|
||||
.filter(e -> e.getKey().getName().startsWith("agent")
|
||||
|| e.getKey().getName().contains("tool")
|
||||
|| e.getKey().getName().contains("http"))
|
||||
.limit(10)
|
||||
.forEach(e -> {
|
||||
Thread t = e.getKey();
|
||||
sb.append(" ").append(String.format("%-30s", t.getName()))
|
||||
.append(AnsiStyle.dim(t.getState().toString())).append("\n");
|
||||
});
|
||||
|
||||
// Show recent conversation turns
|
||||
sb.append("\n").append(AnsiStyle.bold(" Conversation State\n"));
|
||||
sb.append(" Session ID: ").append(context.agentLoop().getToolContext()
|
||||
.get("SESSION_ID") != null ? toolCtx.get("SESSION_ID") : "default").append("\n");
|
||||
|
||||
sb.append("\n").append(AnsiStyle.bold(" Subcommands\n"));
|
||||
sb.append(" /trace on Enable tracing\n");
|
||||
sb.append(" /trace off Disable tracing\n");
|
||||
sb.append(" /trace clear Clear trace log\n");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user