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
@@ -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));
}
}