From 6c5580ccd74a7f2c04a9cc550a7c94ec5cfa0f56 Mon Sep 17 00:00:00 2001 From: abel533 Date: Sat, 4 Apr 2026 19:13:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=96=9C=E6=9D=A0?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=20Tab=20=E8=87=AA=E5=8A=A8=E8=A1=A5=E5=85=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 输入 / 开头时,前缀匹配所有已注册命令 - Tab 键补全唯一匹配,多个匹配时循环选择 - 输入区显示 ghost text(灰色补全后缀提示) - 快捷键栏显示匹配命令列表(最多5个) - 当前 Tab 选中项高亮显示 - Backspace/Esc/Submit 时同步更新建议列表 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../claudecode/tui/ClaudeCodeComponent.java | 128 +++++++++++++++++- 1 file changed, 121 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/claudecode/tui/ClaudeCodeComponent.java b/src/main/java/com/claudecode/tui/ClaudeCodeComponent.java index 3060bc8..1b36b22 100644 --- a/src/main/java/com/claudecode/tui/ClaudeCodeComponent.java +++ b/src/main/java/com/claudecode/tui/ClaudeCodeComponent.java @@ -107,6 +107,10 @@ public class ClaudeCodeComponent extends Component /** Thinking 开始时间(用于显示耗时) */ private volatile long thinkingStartTime = 0; + /** Tab 自动补全状态 */ + private volatile List commandSuggestions = List.of(); // 当前匹配的命令名列表 + private volatile int tabCompletionIndex = -1; // 当前 Tab 循环索引,-1 表示未开始 + /** 首次用户输入回调(用于 conversation summary) */ private Consumer onFirstUserInput; @@ -520,13 +524,22 @@ public class ClaudeCodeComponent extends Component Text.of(" Type a message, / for commands").dimmed() ); } else { - // 有文字 — 文字 + 块光标 + // 有文字 — 文字 + 块光标 + ghost text(命令补全提示) String indent = " ".repeat(PROMPT_WIDTH); String displayText = s.inputText.replace("\n", "\n" + indent); - content = Text.of( - Text.of(displayText).color(Color.WHITE), - Text.of("█").color(Color.BRIGHT_WHITE) - ); + String ghost = getGhostText(s.inputText); + if (!ghost.isEmpty()) { + content = Text.of( + Text.of(displayText).color(Color.WHITE), + Text.of(ghost).dimmed(), + Text.of("█").color(Color.BRIGHT_WHITE) + ); + } else { + content = Text.of( + Text.of(displayText).color(Color.WHITE), + Text.of("█").color(Color.BRIGHT_WHITE) + ); + } } return Box.of( @@ -611,7 +624,29 @@ public class ClaudeCodeComponent extends Component } else if (agentRunning.get()) { leftText = Text.of("esc to interrupt").dimmed(); } else { - leftText = Text.of("↑↓ history Esc interrupt").dimmed(); + // 检查是否在输入斜杠命令 — 显示匹配的命令列表 + List suggestions = commandSuggestions; + if (!suggestions.isEmpty()) { + // 在快捷键栏显示匹配命令(最多显示 5 个) + int maxShow = Math.min(suggestions.size(), 5); + List parts = new ArrayList<>(); + parts.add(Text.of("Tab ").color(Color.BRIGHT_CYAN)); + for (int i = 0; i < maxShow; i++) { + if (i > 0) parts.add(Text.of(" ").dimmed()); + String cmd = "/" + suggestions.get(i); + if (i == tabCompletionIndex) { + parts.add(Text.of(cmd).color(Color.BRIGHT_CYAN).bold()); + } else { + parts.add(Text.of(cmd).dimmed()); + } + } + if (suggestions.size() > maxShow) { + parts.add(Text.of(" +" + (suggestions.size() - maxShow) + " more").dimmed()); + } + leftText = Text.of(parts.toArray(new Renderable[0])); + } else { + leftText = Text.of("↑↓ history Esc interrupt").dimmed(); + } } return Box.of( @@ -705,6 +740,9 @@ public class ClaudeCodeComponent extends Component if (key.return_() && key.meta()) { // Shift+Enter: 多行换行 setState(new TuiState(s.inputText + "\n", s.messages, 0, false, "")); + } else if (key.tab() && !key.shift()) { + // Tab: 命令自动补全 + handleTabCompletion(s); } else if (key.return_()) { // Enter: 发送 if (!s.inputText.isEmpty()) { @@ -714,6 +752,7 @@ public class ClaudeCodeComponent extends Component if (!s.inputText.isEmpty()) { abandonHistoryPreview(); String newText = s.inputText.substring(0, s.inputText.length() - 1); + updateCommandSuggestions(newText); setState(new TuiState(newText, s.messages, s.scrollOffset, false, "")); } } else if (key.upArrow()) { @@ -736,10 +775,13 @@ public class ClaudeCodeComponent extends Component scroll(s, -10); } else if (key.escape()) { // Esc: 清空输入 + updateCommandSuggestions(""); setState(new TuiState("", s.messages, s.scrollOffset, false, "")); } else if (!input.isEmpty() && isPrintableInput(input, key)) { abandonHistoryPreview(); - setState(new TuiState(s.inputText + input, s.messages, s.scrollOffset, false, "")); + String newText = s.inputText + input; + updateCommandSuggestions(newText); + setState(new TuiState(newText, s.messages, s.scrollOffset, false, "")); } } } @@ -904,11 +946,37 @@ public class ClaudeCodeComponent extends Component else if (key.pageDown()) scroll(s, -10); } + /** Tab 自动补全处理 */ + private void handleTabCompletion(TuiState s) { + if (!s.inputText.startsWith("/")) return; + + List suggestions = commandSuggestions; + if (suggestions.isEmpty()) { + // 第一次按 Tab 时可能还没计算建议 + updateCommandSuggestions(s.inputText); + suggestions = commandSuggestions; + if (suggestions.isEmpty()) return; + } + + if (suggestions.size() == 1) { + // 唯一匹配 → 直接补全 + 空格(准备输入参数) + String completed = "/" + suggestions.getFirst(); + updateCommandSuggestions(completed); + setState(new TuiState(completed, s.messages, s.scrollOffset, false, "")); + } else { + // 多个匹配 → 循环选择 + tabCompletionIndex = (tabCompletionIndex + 1) % suggestions.size(); + String completed = "/" + suggestions.get(tabCompletionIndex); + setState(new TuiState(completed, s.messages, s.scrollOffset, false, "")); + } + } + /** 提交用户输入 */ private void submitInput(String text, TuiState s) { inputHistory.add(text); historyIndex = -1; savedInput = ""; + updateCommandSuggestions(""); // 清除命令建议 // 斜杠命令 if (commandRegistry != null && commandRegistry.isCommand(text)) { @@ -1204,6 +1272,52 @@ public class ClaudeCodeComponent extends Component // ==================== 工具方法 ==================== + /** 计算匹配的斜杠命令(前缀匹配) */ + private List computeCommandSuggestions(String inputText) { + if (commandRegistry == null || !inputText.startsWith("/")) { + return List.of(); + } + // 如果已经有空格,说明在输入参数,不再补全命令名 + if (inputText.contains(" ")) { + return List.of(); + } + String query = inputText.substring(1).toLowerCase(); + if (query.isEmpty()) { + // 只输入了 "/",返回所有命令 + return commandRegistry.getCommands().stream() + .map(cmd -> cmd.name()) + .distinct() + .sorted() + .toList(); + } + // 前缀匹配 + return commandRegistry.getCommandNames().stream() + .filter(name -> name.startsWith(query)) + .sorted() + .toList(); + } + + /** 更新命令建议列表(输入变化时调用) */ + private void updateCommandSuggestions(String inputText) { + commandSuggestions = computeCommandSuggestions(inputText); + tabCompletionIndex = -1; // 重置 Tab 循环 + } + + /** 获取第一个建议的补全后缀(用于 ghost text 显示) */ + private String getGhostText(String inputText) { + if (!inputText.startsWith("/") || inputText.contains(" ")) return ""; + String query = inputText.substring(1).toLowerCase(); + if (query.isEmpty()) return ""; + List suggestions = commandSuggestions; + if (suggestions.isEmpty()) return ""; + // 返回第一个匹配项的剩余部分 + String firstMatch = suggestions.getFirst(); + if (firstMatch.startsWith(query) && firstMatch.length() > query.length()) { + return firstMatch.substring(query.length()); + } + return ""; + } + private boolean isPrintableInput(String input, Key key) { if (key.ctrl() || key.meta()) return false; if (key.upArrow() || key.downArrow() || key.leftArrow() || key.rightArrow()) return false;