feat: 添加斜杠命令 Tab 自动补全
- 输入 / 开头时,前缀匹配所有已注册命令 - Tab 键补全唯一匹配,多个匹配时循环选择 - 输入区显示 ghost text(灰色补全后缀提示) - 快捷键栏显示匹配命令列表(最多5个) - 当前 Tab 选中项高亮显示 - Backspace/Esc/Submit 时同步更新建议列表 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -107,6 +107,10 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
/** Thinking 开始时间(用于显示耗时) */
|
/** Thinking 开始时间(用于显示耗时) */
|
||||||
private volatile long thinkingStartTime = 0;
|
private volatile long thinkingStartTime = 0;
|
||||||
|
|
||||||
|
/** Tab 自动补全状态 */
|
||||||
|
private volatile List<String> commandSuggestions = List.of(); // 当前匹配的命令名列表
|
||||||
|
private volatile int tabCompletionIndex = -1; // 当前 Tab 循环索引,-1 表示未开始
|
||||||
|
|
||||||
/** 首次用户输入回调(用于 conversation summary) */
|
/** 首次用户输入回调(用于 conversation summary) */
|
||||||
private Consumer<String> onFirstUserInput;
|
private Consumer<String> onFirstUserInput;
|
||||||
|
|
||||||
@@ -520,13 +524,22 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
Text.of(" Type a message, / for commands").dimmed()
|
Text.of(" Type a message, / for commands").dimmed()
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// 有文字 — 文字 + 块光标
|
// 有文字 — 文字 + 块光标 + ghost text(命令补全提示)
|
||||||
String indent = " ".repeat(PROMPT_WIDTH);
|
String indent = " ".repeat(PROMPT_WIDTH);
|
||||||
String displayText = s.inputText.replace("\n", "\n" + indent);
|
String displayText = s.inputText.replace("\n", "\n" + indent);
|
||||||
content = Text.of(
|
String ghost = getGhostText(s.inputText);
|
||||||
Text.of(displayText).color(Color.WHITE),
|
if (!ghost.isEmpty()) {
|
||||||
Text.of("█").color(Color.BRIGHT_WHITE)
|
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(
|
return Box.of(
|
||||||
@@ -611,7 +624,29 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
} else if (agentRunning.get()) {
|
} else if (agentRunning.get()) {
|
||||||
leftText = Text.of("esc to interrupt").dimmed();
|
leftText = Text.of("esc to interrupt").dimmed();
|
||||||
} else {
|
} else {
|
||||||
leftText = Text.of("↑↓ history Esc interrupt").dimmed();
|
// 检查是否在输入斜杠命令 — 显示匹配的命令列表
|
||||||
|
List<String> suggestions = commandSuggestions;
|
||||||
|
if (!suggestions.isEmpty()) {
|
||||||
|
// 在快捷键栏显示匹配命令(最多显示 5 个)
|
||||||
|
int maxShow = Math.min(suggestions.size(), 5);
|
||||||
|
List<Renderable> 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(
|
return Box.of(
|
||||||
@@ -705,6 +740,9 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
if (key.return_() && key.meta()) {
|
if (key.return_() && key.meta()) {
|
||||||
// Shift+Enter: 多行换行
|
// Shift+Enter: 多行换行
|
||||||
setState(new TuiState(s.inputText + "\n", s.messages, 0, false, ""));
|
setState(new TuiState(s.inputText + "\n", s.messages, 0, false, ""));
|
||||||
|
} else if (key.tab() && !key.shift()) {
|
||||||
|
// Tab: 命令自动补全
|
||||||
|
handleTabCompletion(s);
|
||||||
} else if (key.return_()) {
|
} else if (key.return_()) {
|
||||||
// Enter: 发送
|
// Enter: 发送
|
||||||
if (!s.inputText.isEmpty()) {
|
if (!s.inputText.isEmpty()) {
|
||||||
@@ -714,6 +752,7 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
if (!s.inputText.isEmpty()) {
|
if (!s.inputText.isEmpty()) {
|
||||||
abandonHistoryPreview();
|
abandonHistoryPreview();
|
||||||
String newText = s.inputText.substring(0, s.inputText.length() - 1);
|
String newText = s.inputText.substring(0, s.inputText.length() - 1);
|
||||||
|
updateCommandSuggestions(newText);
|
||||||
setState(new TuiState(newText, s.messages, s.scrollOffset, false, ""));
|
setState(new TuiState(newText, s.messages, s.scrollOffset, false, ""));
|
||||||
}
|
}
|
||||||
} else if (key.upArrow()) {
|
} else if (key.upArrow()) {
|
||||||
@@ -736,10 +775,13 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
scroll(s, -10);
|
scroll(s, -10);
|
||||||
} else if (key.escape()) {
|
} else if (key.escape()) {
|
||||||
// Esc: 清空输入
|
// Esc: 清空输入
|
||||||
|
updateCommandSuggestions("");
|
||||||
setState(new TuiState("", s.messages, s.scrollOffset, false, ""));
|
setState(new TuiState("", s.messages, s.scrollOffset, false, ""));
|
||||||
} else if (!input.isEmpty() && isPrintableInput(input, key)) {
|
} else if (!input.isEmpty() && isPrintableInput(input, key)) {
|
||||||
abandonHistoryPreview();
|
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<ClaudeCodeComponent.TuiState>
|
|||||||
else if (key.pageDown()) scroll(s, -10);
|
else if (key.pageDown()) scroll(s, -10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Tab 自动补全处理 */
|
||||||
|
private void handleTabCompletion(TuiState s) {
|
||||||
|
if (!s.inputText.startsWith("/")) return;
|
||||||
|
|
||||||
|
List<String> 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) {
|
private void submitInput(String text, TuiState s) {
|
||||||
inputHistory.add(text);
|
inputHistory.add(text);
|
||||||
historyIndex = -1;
|
historyIndex = -1;
|
||||||
savedInput = "";
|
savedInput = "";
|
||||||
|
updateCommandSuggestions(""); // 清除命令建议
|
||||||
|
|
||||||
// 斜杠命令
|
// 斜杠命令
|
||||||
if (commandRegistry != null && commandRegistry.isCommand(text)) {
|
if (commandRegistry != null && commandRegistry.isCommand(text)) {
|
||||||
@@ -1204,6 +1272,52 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
|
|
||||||
// ==================== 工具方法 ====================
|
// ==================== 工具方法 ====================
|
||||||
|
|
||||||
|
/** 计算匹配的斜杠命令(前缀匹配) */
|
||||||
|
private List<String> 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<String> 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) {
|
private boolean isPrintableInput(String input, Key key) {
|
||||||
if (key.ctrl() || key.meta()) return false;
|
if (key.ctrl() || key.meta()) return false;
|
||||||
if (key.upArrow() || key.downArrow() || key.leftArrow() || key.rightArrow()) return false;
|
if (key.upArrow() || key.downArrow() || key.leftArrow() || key.rightArrow()) return false;
|
||||||
|
|||||||
Reference in New Issue
Block a user