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:
@@ -18,6 +18,8 @@ import java.util.concurrent.TimeUnit;
|
||||
* <li>/commit —— 自动生成 AI commit message(基于 git diff)</li>
|
||||
* <li>/commit [message] —— 使用指定的 commit message</li>
|
||||
* <li>/commit --all —— 添加所有文件并提交</li>
|
||||
* <li>/commit --push —— 提交后自动 push 到远程</li>
|
||||
* <li>/commit --pr —— 提交 + push + 创建 PR(使用 gh CLI)</li>
|
||||
* </ul>
|
||||
*/
|
||||
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<String>();
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,12 @@ import java.util.List;
|
||||
* /compact 命令 —— 用 AI 生成摘要来压缩上下文。
|
||||
* <p>
|
||||
* 对应 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 {
|
||||
|
||||
@@ -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<Message> 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<Message> compacted = fullCompact.compact(new ArrayList<>(history));
|
||||
// In aggressive mode, compact more aggressively by keeping fewer messages
|
||||
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) {
|
||||
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<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 java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* /help 命令 —— 动态展示所有已注册的斜杠命令。
|
||||
* <p>
|
||||
* 支持:
|
||||
* <ul>
|
||||
* <li>/help —— 列出所有命令</li>
|
||||
* <li>/help [keyword] —— 搜索匹配的命令</li>
|
||||
* <li>/help [command] —— 显示特定命令详情</li>
|
||||
* </ul>
|
||||
*/
|
||||
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<? extends SlashCommand> 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
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(),
|
||||
|
||||
Reference in New Issue
Block a user