- /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>pull/1/head
parent
05e6e019b2
commit
95b45940fd
@ -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)); |
||||
} |
||||
} |
||||
@ -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"; |
||||
} |
||||
} |
||||
Loading…
Reference in new issue