feat: command layer enhancements (Phase 1B.3)

- /help: keyword search + command detail view
- /compact: --stats for token statistics, --aggressive for deeper compaction
- /commit: --push to push after commit, --pr to create PR via gh CLI
- /files: directory tree with depth control, file pattern search
- /permissions: view current permission mode and rules
- /tasks: list background tasks with status, age, result preview

Total commands: 28 → 31

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
abel533
2026-04-05 09:20:44 +08:00
co-authored by Copilot
parent 05e6e019b2
commit 95b45940fd
7 changed files with 522 additions and 40 deletions
@@ -18,6 +18,8 @@ import java.util.concurrent.TimeUnit;
* <li>/commit —— 自动生成 AI commit message(基于 git diff</li> * <li>/commit —— 自动生成 AI commit message(基于 git diff</li>
* <li>/commit [message] —— 使用指定的 commit message</li> * <li>/commit [message] —— 使用指定的 commit message</li>
* <li>/commit --all —— 添加所有文件并提交</li> * <li>/commit --all —— 添加所有文件并提交</li>
* <li>/commit --push —— 提交后自动 push 到远程</li>
* <li>/commit --pr —— 提交 + push + 创建 PR(使用 gh CLI</li>
* </ul> * </ul>
*/ */
public class CommitCommand implements SlashCommand { public class CommitCommand implements SlashCommand {
@@ -29,7 +31,7 @@ public class CommitCommand implements SlashCommand {
@Override @Override
public String description() { public String description() {
return "Create a git commit (with optional AI-generated message)"; return "Create a git commit. Options: --all, --push, --pr";
} }
@Override @Override
@@ -43,9 +45,11 @@ public class CommitCommand implements SlashCommand {
try { try {
boolean addAll = args.contains("--all") || args.contains("-a"); boolean addAll = args.contains("--all") || args.contains("-a");
String message = args.replaceAll("--all|-a", "").strip(); boolean push = args.contains("--push") || args.contains("-p");
boolean pr = args.contains("--pr");
String message = args.replaceAll("--(all|push|pr)|-[ap]", "").strip();
// --all 模式:先执行 git add -A // --all: add all files
if (addAll) { if (addAll) {
String addResult = runGit(projectDir, "add", "-A"); String addResult = runGit(projectDir, "add", "-A");
if (addResult == null) { if (addResult == null) {
@@ -53,7 +57,7 @@ public class CommitCommand implements SlashCommand {
} }
} }
// 检查是否有已暂存的变更 // Check staged changes
String staged = runGit(projectDir, "diff", "--cached", "--stat"); String staged = runGit(projectDir, "diff", "--cached", "--stat");
if (staged == null || staged.isBlank()) { if (staged == null || staged.isBlank()) {
String status = runGit(projectDir, "status", "--short"); String status = runGit(projectDir, "status", "--short");
@@ -65,7 +69,7 @@ public class CommitCommand implements SlashCommand {
return AnsiStyle.green(" ✓ Working directory clean, nothing to commit"); return AnsiStyle.green(" ✓ Working directory clean, nothing to commit");
} }
// 如果没有指定 message,使用 AI 生成 // Generate message if not provided
if (message.isEmpty()) { if (message.isEmpty()) {
message = generateCommitMessage(projectDir, context); message = generateCommitMessage(projectDir, context);
if (message == null || message.isBlank()) { if (message == null || message.isBlank()) {
@@ -73,7 +77,7 @@ public class CommitCommand implements SlashCommand {
} }
} }
// 执行 git commit // Execute commit
String commitResult = runGit(projectDir, "commit", "-m", message); String commitResult = runGit(projectDir, "commit", "-m", message);
if (commitResult == null) { if (commitResult == null) {
return AnsiStyle.red(" ✗ git commit failed"); return AnsiStyle.red(" ✗ git commit failed");
@@ -83,10 +87,35 @@ public class CommitCommand implements SlashCommand {
sb.append("\n").append(AnsiStyle.green(" ✓ Commit successful\n")); sb.append("\n").append(AnsiStyle.green(" ✓ Commit successful\n"));
sb.append(" ").append("".repeat(50)).append("\n"); sb.append(" ").append("".repeat(50)).append("\n");
sb.append(" ").append(AnsiStyle.bold("Message: ")).append(message).append("\n"); sb.append(" ").append(AnsiStyle.bold("Message: ")).append(message).append("\n");
// 显示提交摘要
commitResult.lines().forEach(line -> sb.append(" ").append(AnsiStyle.dim(line)).append("\n")); commitResult.lines().forEach(line -> sb.append(" ").append(AnsiStyle.dim(line)).append("\n"));
// --push: push to remote
if (push || pr) {
String branch = runGit(projectDir, "rev-parse", "--abbrev-ref", "HEAD");
if (branch == null || branch.isBlank()) branch = "HEAD";
String pushResult = runGit(projectDir, "push", "origin", branch);
if (pushResult != null) {
sb.append("\n").append(AnsiStyle.green(" ✓ Pushed to origin/" + branch)).append("\n");
} else {
// Try push with --set-upstream for new branches
pushResult = runGit(projectDir, "push", "--set-upstream", "origin", branch);
if (pushResult != null) {
sb.append("\n").append(AnsiStyle.green(" ✓ Pushed to origin/" + branch + " (new branch)")).append("\n");
} else {
sb.append("\n").append(AnsiStyle.red(" ✗ Push failed")).append("\n");
push = false;
pr = false;
}
}
}
// --pr: create PR using gh CLI
if (pr) {
String prResult = createPullRequest(projectDir, message);
sb.append(prResult);
}
return sb.toString(); return sb.toString();
} catch (Exception e) { } catch (Exception e) {
@@ -94,19 +123,38 @@ public class CommitCommand implements SlashCommand {
} }
} }
/** 使用 AI 分析 git diff 生成 commit message */ private String createPullRequest(Path projectDir, String commitMessage) {
// Check if gh CLI is available
try {
String firstLine = commitMessage.lines().findFirst().orElse(commitMessage);
String body = commitMessage.lines().skip(1)
.reduce("", (a, b) -> a + "\n" + b).strip();
String ghResult = runCommand(projectDir, "gh", "pr", "create",
"--title", firstLine,
"--body", body.isEmpty() ? "Auto-generated PR" : body);
if (ghResult != null && ghResult.contains("http")) {
return "\n" + AnsiStyle.green(" ✓ PR created: ") + AnsiStyle.CYAN + ghResult.strip() + AnsiStyle.RESET + "\n";
} else if (ghResult != null) {
return "\n" + AnsiStyle.green(" ✓ PR: ") + ghResult.strip() + "\n";
}
} catch (Exception ignored) {}
return "\n" + AnsiStyle.yellow(" ⚠ PR creation failed — is 'gh' CLI installed?") + "\n"
+ AnsiStyle.dim(" Install: https://cli.github.com/\n");
}
/** Use AI to generate commit message from git diff */
private String generateCommitMessage(Path projectDir, CommandContext context) { private String generateCommitMessage(Path projectDir, CommandContext context) {
try { try {
// 获取暂存区的 diff
String diff = runGit(projectDir, "diff", "--cached"); String diff = runGit(projectDir, "diff", "--cached");
if (diff == null || diff.isBlank()) return null; if (diff == null || diff.isBlank()) return null;
// 截断过长的 diff
if (diff.length() > 4000) { if (diff.length() > 4000) {
diff = diff.substring(0, 4000) + "\n... (diff truncated)"; diff = diff.substring(0, 4000) + "\n... (diff truncated)";
} }
// 使用 ChatModel 生成 commit message
String prompt = """ String prompt = """
Analyze the following git diff and generate a concise commit message. Analyze the following git diff and generate a concise commit message.
Requirements: Requirements:
@@ -127,7 +175,6 @@ public class CommitCommand implements SlashCommand {
String generated = response.getResult().getOutput().getText(); String generated = response.getResult().getOutput().getText();
if (generated != null) { if (generated != null) {
// 清理:去除可能的引号和多余空行
generated = generated.strip() generated = generated.strip()
.replaceAll("^[\"'`]+|[\"'`]+$", "") .replaceAll("^[\"'`]+|[\"'`]+$", "")
.strip(); .strip();
@@ -135,18 +182,20 @@ public class CommitCommand implements SlashCommand {
return generated; return generated;
} catch (Exception e) { } catch (Exception e) {
// AI 生成失败时返回默认消息
return null; return null;
} }
} }
private String runGit(Path dir, String... args) { private String runGit(Path dir, String... args) {
try { var command = new java.util.ArrayList<String>();
var command = new java.util.ArrayList<String>(); command.add("git");
command.add("git"); command.add("--no-pager");
command.add("--no-pager"); command.addAll(java.util.List.of(args));
command.addAll(java.util.List.of(args)); return runCommand(dir, command.toArray(new String[0]));
}
private String runCommand(Path dir, String... command) {
try {
ProcessBuilder pb = new ProcessBuilder(command); ProcessBuilder pb = new ProcessBuilder(command);
pb.directory(dir.toFile()); pb.directory(dir.toFile());
pb.redirectErrorStream(true); pb.redirectErrorStream(true);
@@ -166,7 +215,7 @@ public class CommitCommand implements SlashCommand {
return null; return null;
} }
return output.toString().stripTrailing(); return process.exitValue() == 0 ? output.toString().stripTrailing() : null;
} catch (Exception e) { } catch (Exception e) {
return null; return null;
} }
@@ -16,7 +16,12 @@ import java.util.List;
* /compact 命令 —— 用 AI 生成摘要来压缩上下文。 * /compact 命令 —— 用 AI 生成摘要来压缩上下文。
* <p> * <p>
* 对应 claude-code/src/commands/compact.ts。 * 对应 claude-code/src/commands/compact.ts。
* 委托给 FullCompact 执行实际压缩逻辑 * 支持压缩级别和统计显示
* <ul>
* <li>/compact —— 标准压缩</li>
* <li>/compact --stats —— 仅显示统计信息</li>
* <li>/compact --aggressive —— 激进压缩(保留更少上下文)</li>
* </ul>
*/ */
public class CompactCommand implements SlashCommand { public class CompactCommand implements SlashCommand {
@@ -27,7 +32,7 @@ public class CompactCommand implements SlashCommand {
@Override @Override
public String description() { public String description() {
return "Compact conversation context with AI summary"; return "Compact conversation context. Use --stats for info, --aggressive for deeper compaction.";
} }
@Override @Override
@@ -36,17 +41,27 @@ public class CompactCommand implements SlashCommand {
return AnsiStyle.yellow(" ⚠ No active conversation to compact."); return AnsiStyle.yellow(" ⚠ No active conversation to compact.");
} }
String argStr = (args == null) ? "" : args.strip();
// --stats: only show statistics
if (argStr.equals("--stats") || argStr.equals("-s")) {
return showStats(context);
}
boolean aggressive = argStr.equals("--aggressive") || argStr.equals("-a");
List<Message> history = context.agentLoop().getMessageHistory(); List<Message> history = context.agentLoop().getMessageHistory();
int before = history.size(); int before = history.size();
if (before <= 3) { if (before <= 3) {
return AnsiStyle.dim(" Context is already small (" + before + " messages), no compaction needed."); return showStats(context) + "\n"
+ AnsiStyle.dim(" Context is already small, no compaction needed.");
} }
TokenTracker tracker = context.agentLoop().getTokenTracker(); TokenTracker tracker = context.agentLoop().getTokenTracker();
long tokensBefore = tracker.getInputTokens() + tracker.getOutputTokens(); long tokensBefore = tracker.getInputTokens() + tracker.getOutputTokens();
// 优先使用 AutoCompactManager 中的 FullCompact // Use FullCompact from AutoCompactManager
FullCompact fullCompact; FullCompact fullCompact;
AutoCompactManager acm = context.agentLoop().getAutoCompactManager(); AutoCompactManager acm = context.agentLoop().getAutoCompactManager();
if (acm != null) { if (acm != null) {
@@ -55,23 +70,37 @@ public class CompactCommand implements SlashCommand {
fullCompact = new FullCompact(context.agentLoop().getChatModel()); fullCompact = new FullCompact(context.agentLoop().getChatModel());
} }
// 执行全量压缩 // In aggressive mode, compact more aggressively by keeping fewer messages
List<Message> compacted = fullCompact.compact(new ArrayList<>(history)); List<Message> toCompact = new ArrayList<>(history);
if (aggressive && toCompact.size() > 5) {
// For aggressive mode, only keep the system message and last 2 exchanges
List<Message> aggressive_list = new ArrayList<>();
// Keep first message (usually system)
aggressive_list.add(toCompact.getFirst());
// Keep last 4 messages (2 exchanges)
int start = Math.max(1, toCompact.size() - 4);
aggressive_list.addAll(toCompact.subList(start, toCompact.size()));
toCompact = aggressive_list;
}
List<Message> compacted = fullCompact.compact(toCompact);
if (compacted != null) { if (compacted != null) {
context.agentLoop().replaceHistory(compacted); context.agentLoop().replaceHistory(compacted);
int after = compacted.size(); int after = compacted.size();
// 重置熔断器(手动压缩成功说明 AI 摘要功能正常)
if (acm != null) { if (acm != null) {
acm.resetCircuitBreaker(); acm.resetCircuitBreaker();
} }
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append(AnsiStyle.green(" ✅ Context compacted")).append("\n"); sb.append(AnsiStyle.green(" ✅ Context compacted")).append(
sb.append(" Messages: ").append(before).append("").append(after).append("\n"); aggressive ? AnsiStyle.yellow(" (aggressive)") : "").append("\n");
sb.append(" ").append("".repeat(40)).append("\n");
sb.append(" Messages: ").append(before).append("").append(after);
sb.append(" (").append(String.format("%.0f%%", (1.0 - (double) after / before) * 100)).append(" reduction)\n");
if (tokensBefore > 0) { if (tokensBefore > 0) {
sb.append(" Tokens before compaction: ").append(TokenTracker.formatTokens(tokensBefore)).append("\n"); sb.append(" Tokens before: ").append(TokenTracker.formatTokens(tokensBefore)).append("\n");
} }
sb.append(AnsiStyle.dim(" 📝 AI summary generated and injected into context")); sb.append(AnsiStyle.dim(" 📝 AI summary generated and injected into context"));
return sb.toString(); return sb.toString();
@@ -80,4 +109,35 @@ public class CompactCommand implements SlashCommand {
return AnsiStyle.yellow(" ⚠ Compaction failed — AI summary generation failed") + "\n" return AnsiStyle.yellow(" ⚠ Compaction failed — AI summary generation failed") + "\n"
+ AnsiStyle.dim(" The conversation history was not modified."); + AnsiStyle.dim(" The conversation history was not modified.");
} }
private String showStats(CommandContext context) {
List<Message> history = context.agentLoop().getMessageHistory();
TokenTracker tracker = context.agentLoop().getTokenTracker();
int userMsgs = 0, assistantMsgs = 0, systemMsgs = 0, toolMsgs = 0;
for (Message m : history) {
if (m instanceof UserMessage) userMsgs++;
else if (m instanceof AssistantMessage) assistantMsgs++;
else if (m instanceof SystemMessage) systemMsgs++;
else toolMsgs++;
}
StringBuilder sb = new StringBuilder();
sb.append("\n ").append(AnsiStyle.bold("📊 Context Statistics")).append("\n");
sb.append(" ").append("".repeat(40)).append("\n");
sb.append(" Total messages: ").append(history.size()).append("\n");
sb.append(" User: ").append(userMsgs).append("\n");
sb.append(" Assistant: ").append(assistantMsgs).append("\n");
sb.append(" System: ").append(systemMsgs).append("\n");
if (toolMsgs > 0) {
sb.append(" Tool: ").append(toolMsgs).append("\n");
}
sb.append(" ").append("".repeat(40)).append("\n");
sb.append(" Input tokens: ").append(TokenTracker.formatTokens(tracker.getInputTokens())).append("\n");
sb.append(" Output tokens: ").append(TokenTracker.formatTokens(tracker.getOutputTokens())).append("\n");
sb.append(" Total tokens: ").append(TokenTracker.formatTokens(
tracker.getInputTokens() + tracker.getOutputTokens())).append("\n");
return sb.toString();
}
} }
@@ -0,0 +1,136 @@
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.util.stream.Stream;
/**
* /files 命令 —— 列出当前工作目录的文件结构。
* <p>
* 对应 claude-code/src/commands/files.ts。
* 显示项目目录树(默认2层深度)。
*/
public class FilesCommand implements SlashCommand {
@Override
public String name() {
return "files";
}
@Override
public String description() {
return "List project files. Use /files [depth] to control depth (default: 2)";
}
@Override
public String execute(String args, CommandContext context) {
Path projectDir = Path.of(System.getProperty("user.dir"));
int maxDepth = 2;
if (args != null && !args.isBlank()) {
try {
maxDepth = Integer.parseInt(args.strip());
maxDepth = Math.max(1, Math.min(maxDepth, 5));
} catch (NumberFormatException ignored) {
// Treat as path filter
return listFiltered(projectDir, args.strip());
}
}
StringBuilder sb = new StringBuilder();
sb.append("\n ").append(AnsiStyle.bold("📁 " + projectDir.getFileName())).append("\n");
sb.append(" ").append("".repeat(50)).append("\n");
try {
int[] counts = {0, 0}; // [files, dirs]
listDirectory(sb, projectDir, "", maxDepth, 0, counts);
sb.append(" ").append("".repeat(50)).append("\n");
sb.append(" ").append(AnsiStyle.dim(counts[1] + " directories, " + counts[0] + " files")).append("\n");
} catch (IOException e) {
sb.append(" ").append(AnsiStyle.red("Error: " + e.getMessage())).append("\n");
}
return sb.toString();
}
private void listDirectory(StringBuilder sb, Path dir, String indent, int maxDepth, int depth, int[] counts) throws IOException {
if (depth >= maxDepth) return;
try (Stream<Path> stream = Files.list(dir).sorted()) {
var entries = stream
.filter(p -> !isHidden(p))
.toList();
for (int i = 0; i < entries.size(); i++) {
Path entry = entries.get(i);
boolean isLast = (i == entries.size() - 1);
String connector = isLast ? "└── " : "├── ";
String childIndent = indent + (isLast ? " " : "");
String name = entry.getFileName().toString();
if (Files.isDirectory(entry)) {
counts[1]++;
sb.append(" ").append(indent).append(connector)
.append(AnsiStyle.CYAN).append(name).append("/").append(AnsiStyle.RESET).append("\n");
listDirectory(sb, entry, childIndent, maxDepth, depth + 1, counts);
} else {
counts[0]++;
String sizeStr = formatSize(Files.size(entry));
sb.append(" ").append(indent).append(connector).append(name)
.append(AnsiStyle.dim(" (" + sizeStr + ")")).append("\n");
}
}
}
}
private String listFiltered(Path dir, String pattern) {
StringBuilder sb = new StringBuilder();
sb.append("\n ").append(AnsiStyle.bold("Files matching \"" + pattern + "\":")).append("\n\n");
try (Stream<Path> walk = Files.walk(dir, 5)) {
var matches = walk
.filter(Files::isRegularFile)
.filter(p -> !isHiddenPath(p, dir))
.filter(p -> p.getFileName().toString().contains(pattern))
.toList();
if (matches.isEmpty()) {
sb.append(" ").append(AnsiStyle.dim("No files found matching \"" + pattern + "\"")).append("\n");
} else {
for (Path match : matches) {
sb.append(" ").append(dir.relativize(match)).append("\n");
}
sb.append("\n ").append(AnsiStyle.dim(matches.size() + " file(s) found")).append("\n");
}
} catch (IOException e) {
sb.append(" ").append(AnsiStyle.red("Error: " + e.getMessage())).append("\n");
}
return sb.toString();
}
private boolean isHidden(Path p) {
String name = p.getFileName().toString();
return name.startsWith(".") || name.equals("node_modules") || name.equals("target")
|| name.equals("build") || name.equals("__pycache__") || name.equals(".git");
}
private boolean isHiddenPath(Path p, Path root) {
Path rel = root.relativize(p);
for (Path part : rel) {
if (isHidden(part)) return true;
}
return false;
}
private String formatSize(long bytes) {
if (bytes < 1024) return bytes + "B";
if (bytes < 1024 * 1024) return String.format("%.1fK", bytes / 1024.0);
return String.format("%.1fM", bytes / (1024.0 * 1024));
}
}
@@ -5,9 +5,17 @@ import com.claudecode.command.SlashCommand;
import com.claudecode.console.AnsiStyle; import com.claudecode.console.AnsiStyle;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
/** /**
* /help 命令 —— 动态展示所有已注册的斜杠命令。 * /help 命令 —— 动态展示所有已注册的斜杠命令。
* <p>
* 支持:
* <ul>
* <li>/help —— 列出所有命令</li>
* <li>/help [keyword] —— 搜索匹配的命令</li>
* <li>/help [command] —— 显示特定命令详情</li>
* </ul>
*/ */
public class HelpCommand implements SlashCommand { public class HelpCommand implements SlashCommand {
@@ -18,7 +26,7 @@ public class HelpCommand implements SlashCommand {
@Override @Override
public String description() { public String description() {
return "Show available commands"; return "Show available commands. Use /help [keyword] to search or /help [command] for details.";
} }
@Override @Override
@@ -28,13 +36,52 @@ public class HelpCommand implements SlashCommand {
@Override @Override
public String execute(String args, CommandContext context) { public String execute(String args, CommandContext context) {
String query = (args == null) ? "" : args.strip();
var allCommands = context.commandRegistry().getCommands();
// If a specific command name is given, show details
if (!query.isEmpty()) {
String queryClean = query.startsWith("/") ? query.substring(1) : query;
// Exact match → show detail
var exactMatch = allCommands.stream()
.filter(cmd -> cmd.name().equalsIgnoreCase(queryClean)
|| cmd.aliases().stream().anyMatch(a -> a.equalsIgnoreCase(queryClean)))
.findFirst();
if (exactMatch.isPresent()) {
return formatCommandDetail(exactMatch.get());
}
// Fuzzy search by keyword
String queryLower = queryClean.toLowerCase();
var matches = allCommands.stream()
.filter(cmd -> cmd.name().toLowerCase().contains(queryLower)
|| cmd.description().toLowerCase().contains(queryLower))
.collect(Collectors.toList());
if (matches.isEmpty()) {
return AnsiStyle.yellow(" No commands matching \"" + query + "\"\n")
+ AnsiStyle.dim(" Use /help to see all commands");
}
StringBuilder sb = new StringBuilder();
sb.append(AnsiStyle.bold("\n Commands matching \"" + query + "\":\n\n"));
appendCommandList(sb, matches);
return sb.toString();
}
// No args → show all
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append(AnsiStyle.bold("\n Available Commands:\n\n")); sb.append(AnsiStyle.bold("\n Available Commands:\n\n"));
appendCommandList(sb, allCommands);
sb.append("\n");
sb.append(AnsiStyle.dim(" 💡 Use /help [command] for details, /help [keyword] to search\n"));
sb.append(AnsiStyle.dim(" Shortcuts: Tab to autocomplete, ↑↓ to browse history, Ctrl+D to exit\n"));
return sb.toString();
}
// 从 CommandRegistry 动态获取所有已注册命令 private void appendCommandList(StringBuilder sb, List<? extends SlashCommand> commands) {
var commands = context.commandRegistry().getCommands();
// 计算最长命令名称用于对齐
int maxNameLen = commands.stream() int maxNameLen = commands.stream()
.mapToInt(cmd -> cmd.name().length()) .mapToInt(cmd -> cmd.name().length())
.max().orElse(12); .max().orElse(12);
@@ -42,7 +89,6 @@ public class HelpCommand implements SlashCommand {
for (SlashCommand cmd : commands) { for (SlashCommand cmd : commands) {
String nameStr = "/" + cmd.name(); String nameStr = "/" + cmd.name();
// 如果有别名,附加显示
String aliasStr = ""; String aliasStr = "";
if (!cmd.aliases().isEmpty()) { if (!cmd.aliases().isEmpty()) {
aliasStr = AnsiStyle.DIM + " (also: " aliasStr = AnsiStyle.DIM + " (also: "
@@ -52,10 +98,17 @@ public class HelpCommand implements SlashCommand {
sb.append(String.format(" %s%-" + (maxNameLen + 2) + "s%s %s%s%n", sb.append(String.format(" %s%-" + (maxNameLen + 2) + "s%s %s%s%n",
AnsiStyle.CYAN, nameStr, AnsiStyle.RESET, cmd.description(), aliasStr)); AnsiStyle.CYAN, nameStr, AnsiStyle.RESET, cmd.description(), aliasStr));
} }
}
sb.append("\n"); private String formatCommandDetail(SlashCommand cmd) {
sb.append(AnsiStyle.dim(" Shortcuts: Tab to autocomplete, ↑↓ to browse history, Ctrl+D to exit\n")); StringBuilder sb = new StringBuilder();
sb.append("\n ").append(AnsiStyle.bold("/" + cmd.name())).append("\n");
sb.append(" ").append("".repeat(40)).append("\n");
sb.append(" ").append(cmd.description()).append("\n");
if (!cmd.aliases().isEmpty()) {
sb.append(" ").append(AnsiStyle.dim("Aliases: "
+ cmd.aliases().stream().map(a -> "/" + a).collect(Collectors.joining(", ")))).append("\n");
}
return sb.toString(); return sb.toString();
} }
} }
@@ -0,0 +1,67 @@
package com.claudecode.command.impl;
import com.claudecode.command.CommandContext;
import com.claudecode.command.SlashCommand;
import com.claudecode.console.AnsiStyle;
import com.claudecode.permission.PermissionSettings;
import com.claudecode.permission.PermissionTypes;
import java.util.List;
/**
* /permissions 命令 —— 查看和管理权限设置。
* <p>
* 对应 claude-code/src/commands/permissions.ts。
* 显示当前权限模式和规则列表。
*/
public class PermissionsCommand implements SlashCommand {
private final PermissionSettings settings;
public PermissionsCommand(PermissionSettings settings) {
this.settings = settings;
}
@Override
public String name() {
return "permissions";
}
@Override
public String description() {
return "View and manage permission settings";
}
@Override
public String execute(String args, CommandContext context) {
StringBuilder sb = new StringBuilder();
sb.append("\n ").append(AnsiStyle.bold("🔐 Permission Settings")).append("\n");
sb.append(" ").append("".repeat(50)).append("\n");
// Current mode
PermissionTypes.PermissionMode mode = settings.getCurrentMode();
String modeStr = mode != null ? mode.name() : "DEFAULT";
String modeColor = switch (modeStr) {
case "AUTO_ALLOW" -> AnsiStyle.green(modeStr);
case "DENY_ALL" -> AnsiStyle.red(modeStr);
default -> AnsiStyle.yellow(modeStr);
};
sb.append(" Mode: ").append(modeColor).append("\n\n");
// All rules
List<String> rules = settings.listRules();
sb.append(" ").append(AnsiStyle.bold("Rules")).append(" (").append(rules.size()).append("):\n");
if (rules.isEmpty()) {
sb.append(" ").append(AnsiStyle.dim("(no rules configured)")).append("\n");
} else {
for (String rule : rules) {
String icon = rule.contains("ALLOW") ? AnsiStyle.green("") : AnsiStyle.red("");
sb.append(" ").append(icon).append(" ").append(rule).append("\n");
}
}
sb.append("\n ").append(AnsiStyle.dim("Use /config to change permission settings")).append("\n");
return sb.toString();
}
}
@@ -0,0 +1,113 @@
package com.claudecode.command.impl;
import com.claudecode.command.CommandContext;
import com.claudecode.command.SlashCommand;
import com.claudecode.console.AnsiStyle;
import com.claudecode.core.TaskManager;
import com.claudecode.core.TaskManager.TaskInfo;
import com.claudecode.core.TaskManager.TaskStatus;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
/**
* /tasks 命令 —— 列出所有后台任务状态。
* <p>
* 对应 claude-code 中的任务管理 UI。
* 显示所有任务的状态、创建时间和结果摘要。
*/
public class TasksCommand implements SlashCommand {
private final TaskManager taskManager;
public TasksCommand(TaskManager taskManager) {
this.taskManager = taskManager;
}
@Override
public String name() {
return "tasks";
}
@Override
public String description() {
return "List all background tasks and their status";
}
@Override
public String execute(String args, CommandContext context) {
List<TaskInfo> tasks;
String filter = (args == null) ? "" : args.strip();
// Optional status filter
if (!filter.isEmpty()) {
try {
TaskStatus statusFilter = TaskStatus.valueOf(filter.toUpperCase());
tasks = taskManager.listTasks(statusFilter);
} catch (IllegalArgumentException e) {
return AnsiStyle.yellow(" ⚠ Invalid status filter: " + filter) + "\n"
+ AnsiStyle.dim(" Valid values: PENDING, RUNNING, COMPLETED, FAILED, CANCELLED");
}
} else {
tasks = taskManager.listTasks();
}
if (tasks.isEmpty()) {
return AnsiStyle.dim(" No tasks" + (filter.isEmpty() ? "" : " with status " + filter));
}
StringBuilder sb = new StringBuilder();
sb.append("\n ").append(AnsiStyle.bold("📋 Tasks")).append(" (").append(tasks.size()).append(")\n");
sb.append(" ").append("".repeat(60)).append("\n");
for (TaskInfo task : tasks) {
String icon = switch (task.status()) {
case PENDING -> "";
case RUNNING -> "🔄";
case COMPLETED -> "";
case FAILED -> "";
case CANCELLED -> "🚫";
};
String statusColor = switch (task.status()) {
case COMPLETED -> AnsiStyle.green(task.status().name());
case FAILED -> AnsiStyle.red(task.status().name());
case RUNNING -> AnsiStyle.CYAN + task.status().name() + AnsiStyle.RESET;
case CANCELLED -> AnsiStyle.yellow(task.status().name());
default -> task.status().name();
};
sb.append(" ").append(icon).append(" ")
.append(AnsiStyle.bold(task.id())).append(" ")
.append(statusColor).append(" ")
.append(task.description()).append("\n");
// Time info
String age = formatDuration(Duration.between(task.createdAt(), Instant.now()));
sb.append(" ").append(AnsiStyle.dim("Created " + age + " ago"));
// Result preview for completed/failed
if (task.result() != null) {
String preview = task.result().length() > 60
? task.result().substring(0, 57) + "..."
: task.result();
sb.append(" ").append(AnsiStyle.dim("" + preview));
}
sb.append("\n");
}
// Summary
sb.append(" ").append("".repeat(60)).append("\n");
sb.append(" ").append(AnsiStyle.dim(taskManager.getSummary())).append("\n");
return sb.toString();
}
private String formatDuration(Duration d) {
if (d.toMinutes() < 1) return d.toSeconds() + "s";
if (d.toHours() < 1) return d.toMinutes() + "m";
if (d.toDays() < 1) return d.toHours() + "h";
return d.toDays() + "d";
}
}
@@ -127,7 +127,8 @@ public class AppConfig {
} }
@Bean @Bean
public CommandRegistry commandRegistry(PluginManager pluginManager, PermissionSettings permissionSettings) { public CommandRegistry commandRegistry(PluginManager pluginManager, PermissionSettings permissionSettings,
TaskManager taskManager) {
ConfigCommand configCommand = new ConfigCommand(permissionSettings); ConfigCommand configCommand = new ConfigCommand(permissionSettings);
CommandRegistry registry = new CommandRegistry(); CommandRegistry registry = new CommandRegistry();
registry.registerAll( registry.registerAll(
@@ -152,6 +153,9 @@ public class AppConfig {
new ResumeCommand(), new ResumeCommand(),
new ExportCommand(), new ExportCommand(),
new CommitCommand(), new CommitCommand(),
new FilesCommand(),
new PermissionsCommand(permissionSettings),
new TasksCommand(taskManager),
// P2 命令 // P2 命令
new HooksCommand(), new HooksCommand(),
new ReviewCommand(), new ReviewCommand(),