From 95b45940fd5f8de397414d38949f06fb5e442c11 Mon Sep 17 00:00:00 2001 From: abel533 Date: Sun, 5 Apr 2026 09:20:44 +0800 Subject: [PATCH] feat: command layer enhancements (Phase 1B.3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /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> --- .../command/impl/CommitCommand.java | 89 +++++++++--- .../command/impl/CompactCommand.java | 80 +++++++++-- .../claudecode/command/impl/FilesCommand.java | 136 ++++++++++++++++++ .../claudecode/command/impl/HelpCommand.java | 71 +++++++-- .../command/impl/PermissionsCommand.java | 67 +++++++++ .../claudecode/command/impl/TasksCommand.java | 113 +++++++++++++++ .../java/com/claudecode/config/AppConfig.java | 6 +- 7 files changed, 522 insertions(+), 40 deletions(-) create mode 100644 src/main/java/com/claudecode/command/impl/FilesCommand.java create mode 100644 src/main/java/com/claudecode/command/impl/PermissionsCommand.java create mode 100644 src/main/java/com/claudecode/command/impl/TasksCommand.java diff --git a/src/main/java/com/claudecode/command/impl/CommitCommand.java b/src/main/java/com/claudecode/command/impl/CommitCommand.java index 23c90a1..8c9dd89 100644 --- a/src/main/java/com/claudecode/command/impl/CommitCommand.java +++ b/src/main/java/com/claudecode/command/impl/CommitCommand.java @@ -18,6 +18,8 @@ import java.util.concurrent.TimeUnit; *
  • /commit —— 自动生成 AI commit message(基于 git diff)
  • *
  • /commit [message] —— 使用指定的 commit message
  • *
  • /commit --all —— 添加所有文件并提交
  • + *
  • /commit --push —— 提交后自动 push 到远程
  • + *
  • /commit --pr —— 提交 + push + 创建 PR(使用 gh CLI)
  • * */ public class CommitCommand implements SlashCommand { @@ -29,7 +31,7 @@ public class CommitCommand implements SlashCommand { @Override public String description() { - return "Create a git commit (with optional AI-generated message)"; + return "Create a git commit. Options: --all, --push, --pr"; } @Override @@ -43,9 +45,11 @@ public class CommitCommand implements SlashCommand { try { 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) { String addResult = runGit(projectDir, "add", "-A"); if (addResult == null) { @@ -53,7 +57,7 @@ public class CommitCommand implements SlashCommand { } } - // 检查是否有已暂存的变更 + // Check staged changes String staged = runGit(projectDir, "diff", "--cached", "--stat"); if (staged == null || staged.isBlank()) { String status = runGit(projectDir, "status", "--short"); @@ -65,7 +69,7 @@ public class CommitCommand implements SlashCommand { return AnsiStyle.green(" ✓ Working directory clean, nothing to commit"); } - // 如果没有指定 message,使用 AI 生成 + // Generate message if not provided if (message.isEmpty()) { message = generateCommitMessage(projectDir, context); 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); if (commitResult == null) { 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(" ").append("─".repeat(50)).append("\n"); sb.append(" ").append(AnsiStyle.bold("Message: ")).append(message).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(); } 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) { try { - // 获取暂存区的 diff String diff = runGit(projectDir, "diff", "--cached"); if (diff == null || diff.isBlank()) return null; - // 截断过长的 diff if (diff.length() > 4000) { diff = diff.substring(0, 4000) + "\n... (diff truncated)"; } - // 使用 ChatModel 生成 commit message String prompt = """ Analyze the following git diff and generate a concise commit message. Requirements: @@ -127,7 +175,6 @@ public class CommitCommand implements SlashCommand { String generated = response.getResult().getOutput().getText(); if (generated != null) { - // 清理:去除可能的引号和多余空行 generated = generated.strip() .replaceAll("^[\"'`]+|[\"'`]+$", "") .strip(); @@ -135,18 +182,20 @@ public class CommitCommand implements SlashCommand { return generated; } catch (Exception e) { - // AI 生成失败时返回默认消息 return null; } } private String runGit(Path dir, String... args) { - try { - var command = new java.util.ArrayList(); - command.add("git"); - command.add("--no-pager"); - command.addAll(java.util.List.of(args)); + var command = new java.util.ArrayList(); + command.add("git"); + command.add("--no-pager"); + 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); pb.directory(dir.toFile()); pb.redirectErrorStream(true); @@ -166,7 +215,7 @@ public class CommitCommand implements SlashCommand { return null; } - return output.toString().stripTrailing(); + return process.exitValue() == 0 ? output.toString().stripTrailing() : null; } catch (Exception e) { return null; } diff --git a/src/main/java/com/claudecode/command/impl/CompactCommand.java b/src/main/java/com/claudecode/command/impl/CompactCommand.java index b4ea5dc..50b087d 100644 --- a/src/main/java/com/claudecode/command/impl/CompactCommand.java +++ b/src/main/java/com/claudecode/command/impl/CompactCommand.java @@ -16,7 +16,12 @@ import java.util.List; * /compact 命令 —— 用 AI 生成摘要来压缩上下文。 *

    * 对应 claude-code/src/commands/compact.ts。 - * 委托给 FullCompact 执行实际压缩逻辑。 + * 支持压缩级别和统计显示。 + *

      + *
    • /compact —— 标准压缩
    • + *
    • /compact --stats —— 仅显示统计信息
    • + *
    • /compact --aggressive —— 激进压缩(保留更少上下文)
    • + *
    */ public class CompactCommand implements SlashCommand { @@ -27,7 +32,7 @@ public class CompactCommand implements SlashCommand { @Override public String description() { - return "Compact conversation context with AI summary"; + return "Compact conversation context. Use --stats for info, --aggressive for deeper compaction."; } @Override @@ -36,17 +41,27 @@ public class CompactCommand implements SlashCommand { 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 history = context.agentLoop().getMessageHistory(); int before = history.size(); 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(); long tokensBefore = tracker.getInputTokens() + tracker.getOutputTokens(); - // 优先使用 AutoCompactManager 中的 FullCompact + // Use FullCompact from AutoCompactManager FullCompact fullCompact; AutoCompactManager acm = context.agentLoop().getAutoCompactManager(); if (acm != null) { @@ -55,23 +70,37 @@ public class CompactCommand implements SlashCommand { fullCompact = new FullCompact(context.agentLoop().getChatModel()); } - // 执行全量压缩 - List compacted = fullCompact.compact(new ArrayList<>(history)); + // In aggressive mode, compact more aggressively by keeping fewer messages + List toCompact = new ArrayList<>(history); + if (aggressive && toCompact.size() > 5) { + // For aggressive mode, only keep the system message and last 2 exchanges + List 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 compacted = fullCompact.compact(toCompact); if (compacted != null) { context.agentLoop().replaceHistory(compacted); int after = compacted.size(); - // 重置熔断器(手动压缩成功说明 AI 摘要功能正常) if (acm != null) { acm.resetCircuitBreaker(); } StringBuilder sb = new StringBuilder(); - sb.append(AnsiStyle.green(" ✅ Context compacted")).append("\n"); - sb.append(" Messages: ").append(before).append(" → ").append(after).append("\n"); + sb.append(AnsiStyle.green(" ✅ Context compacted")).append( + 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) { - 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")); return sb.toString(); @@ -80,4 +109,35 @@ public class CompactCommand implements SlashCommand { return AnsiStyle.yellow(" ⚠ Compaction failed — AI summary generation failed") + "\n" + AnsiStyle.dim(" The conversation history was not modified."); } + + private String showStats(CommandContext context) { + List 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(); + } } diff --git a/src/main/java/com/claudecode/command/impl/FilesCommand.java b/src/main/java/com/claudecode/command/impl/FilesCommand.java new file mode 100644 index 0000000..fcb4e62 --- /dev/null +++ b/src/main/java/com/claudecode/command/impl/FilesCommand.java @@ -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 命令 —— 列出当前工作目录的文件结构。 + *

    + * 对应 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 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 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)); + } +} diff --git a/src/main/java/com/claudecode/command/impl/HelpCommand.java b/src/main/java/com/claudecode/command/impl/HelpCommand.java index b767524..ca8c663 100644 --- a/src/main/java/com/claudecode/command/impl/HelpCommand.java +++ b/src/main/java/com/claudecode/command/impl/HelpCommand.java @@ -5,9 +5,17 @@ import com.claudecode.command.SlashCommand; import com.claudecode.console.AnsiStyle; import java.util.List; +import java.util.stream.Collectors; /** * /help 命令 —— 动态展示所有已注册的斜杠命令。 + *

    + * 支持: + *

      + *
    • /help —— 列出所有命令
    • + *
    • /help [keyword] —— 搜索匹配的命令
    • + *
    • /help [command] —— 显示特定命令详情
    • + *
    */ public class HelpCommand implements SlashCommand { @@ -18,7 +26,7 @@ public class HelpCommand implements SlashCommand { @Override public String description() { - return "Show available commands"; + return "Show available commands. Use /help [keyword] to search or /help [command] for details."; } @Override @@ -28,13 +36,52 @@ public class HelpCommand implements SlashCommand { @Override 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(); 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 动态获取所有已注册命令 - var commands = context.commandRegistry().getCommands(); - - // 计算最长命令名称用于对齐 + private void appendCommandList(StringBuilder sb, List commands) { int maxNameLen = commands.stream() .mapToInt(cmd -> cmd.name().length()) .max().orElse(12); @@ -42,7 +89,6 @@ public class HelpCommand implements SlashCommand { for (SlashCommand cmd : commands) { String nameStr = "/" + cmd.name(); - // 如果有别名,附加显示 String aliasStr = ""; if (!cmd.aliases().isEmpty()) { 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", AnsiStyle.CYAN, nameStr, AnsiStyle.RESET, cmd.description(), aliasStr)); } + } - sb.append("\n"); - sb.append(AnsiStyle.dim(" Shortcuts: Tab to autocomplete, ↑↓ to browse history, Ctrl+D to exit\n")); - + private String formatCommandDetail(SlashCommand cmd) { + 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(); } } diff --git a/src/main/java/com/claudecode/command/impl/PermissionsCommand.java b/src/main/java/com/claudecode/command/impl/PermissionsCommand.java new file mode 100644 index 0000000..5245b53 --- /dev/null +++ b/src/main/java/com/claudecode/command/impl/PermissionsCommand.java @@ -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 命令 —— 查看和管理权限设置。 + *

    + * 对应 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 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(); + } +} diff --git a/src/main/java/com/claudecode/command/impl/TasksCommand.java b/src/main/java/com/claudecode/command/impl/TasksCommand.java new file mode 100644 index 0000000..dd31fa0 --- /dev/null +++ b/src/main/java/com/claudecode/command/impl/TasksCommand.java @@ -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 命令 —— 列出所有后台任务状态。 + *

    + * 对应 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 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"; + } +} diff --git a/src/main/java/com/claudecode/config/AppConfig.java b/src/main/java/com/claudecode/config/AppConfig.java index f15a7f3..11b6a88 100644 --- a/src/main/java/com/claudecode/config/AppConfig.java +++ b/src/main/java/com/claudecode/config/AppConfig.java @@ -127,7 +127,8 @@ public class AppConfig { } @Bean - public CommandRegistry commandRegistry(PluginManager pluginManager, PermissionSettings permissionSettings) { + public CommandRegistry commandRegistry(PluginManager pluginManager, PermissionSettings permissionSettings, + TaskManager taskManager) { ConfigCommand configCommand = new ConfigCommand(permissionSettings); CommandRegistry registry = new CommandRegistry(); registry.registerAll( @@ -152,6 +153,9 @@ public class AppConfig { new ResumeCommand(), new ExportCommand(), new CommitCommand(), + new FilesCommand(), + new PermissionsCommand(permissionSettings), + new TasksCommand(taskManager), // P2 命令 new HooksCommand(), new ReviewCommand(),