diff --git a/src/main/java/com/claudecode/tui/ClaudeCodeComponent.java b/src/main/java/com/claudecode/tui/ClaudeCodeComponent.java index 1b36b22..cb51d47 100644 --- a/src/main/java/com/claudecode/tui/ClaudeCodeComponent.java +++ b/src/main/java/com/claudecode/tui/ClaudeCodeComponent.java @@ -111,6 +111,12 @@ public class ClaudeCodeComponent extends Component private volatile List commandSuggestions = List.of(); // 当前匹配的命令名列表 private volatile int tabCompletionIndex = -1; // 当前 Tab 循环索引,-1 表示未开始 + /** Ctrl+R 反向历史搜索状态 */ + private volatile boolean historySearchMode = false; // 是否在搜索模式 + private volatile String historySearchQuery = ""; // 搜索关键词 + private volatile String historySearchResult = ""; // 当前匹配结果 + private volatile int historySearchIndex = -1; // 匹配到的历史记录索引 + /** 首次用户输入回调(用于 conversation summary) */ private Consumer onFirstUserInput; @@ -185,6 +191,12 @@ public class ClaudeCodeComponent extends Component // 权限选择模式:光标在选中选项的 ❯ 位置 int permCursorRow = h - 2 - (snapPermOptions.size() - snapPermSelected); setCursorPosition(Math.max(0, permCursorRow), 3); + } else if (historySearchMode) { + // 搜索模式:光标在搜索词 █ 的位置 + // "(reverse-i-search)`" = 20 chars, then query, then "█" + int cursorRow = Math.max(0, h - 3); + int cursorCol = 1 + 20 + StringWidth.width(historySearchQuery); + setCursorPosition(cursorRow, cursorCol); } else { // 正常模式:光标隐藏在块光标 █ 的位置 int cursorRow = Math.max(0, h - 3); @@ -511,6 +523,20 @@ public class ClaudeCodeComponent extends Component return renderPermissionSelectArea(s, w); } + // Ctrl+R 历史搜索模式 + if (historySearchMode) { + String query = historySearchQuery; + String result = historySearchResult; + Text searchPrompt = Text.of( + Text.of("(reverse-i-search)`").color(Color.BRIGHT_CYAN), + Text.of(query).color(Color.BRIGHT_YELLOW), + Text.of("█").color(Color.BRIGHT_WHITE), + Text.of("': ").color(Color.BRIGHT_CYAN), + Text.of(result.isEmpty() ? "" : result).color(Color.WHITE) + ); + return Box.of(searchPrompt).paddingX(1); + } + Text prompt = Text.of("❯ ").color(Color.BRIGHT_GREEN).bold(); Text content; @@ -621,6 +647,8 @@ public class ClaudeCodeComponent extends Component Renderable leftText; if (ctrlCPending) { leftText = Text.of("Press Ctrl-C again to exit").color(Color.BRIGHT_YELLOW); + } else if (historySearchMode) { + leftText = Text.of("Ctrl+R next · Enter select · Esc cancel").dimmed(); } else if (agentRunning.get()) { leftText = Text.of("esc to interrupt").dimmed(); } else { @@ -725,6 +753,22 @@ public class ClaudeCodeComponent extends Component return; } + // Ctrl+R: 进入历史搜索模式 + if (key.ctrl() && "r".equals(input) && !agentRunning.get()) { + historySearchMode = true; + historySearchQuery = ""; + historySearchResult = ""; + historySearchIndex = -1; + setState(new TuiState("", s.messages, s.scrollOffset, false, "")); + return; + } + + // 历史搜索模式的输入处理 + if (historySearchMode) { + handleHistorySearchInput(input, key, s); + return; + } + // AI 运行中时允许滚动和 Escape 中断 if (agentRunning.get()) { if (key.escape()) { @@ -946,6 +990,56 @@ public class ClaudeCodeComponent extends Component else if (key.pageDown()) scroll(s, -10); } + /** Ctrl+R 反向历史搜索输入处理 */ + private void handleHistorySearchInput(String input, Key key, TuiState s) { + if (key.escape() || (key.ctrl() && "c".equals(input))) { + // Esc/Ctrl+C: 退出搜索,恢复原输入 + historySearchMode = false; + setState(new TuiState(s.inputText, s.messages, s.scrollOffset, false, "")); + } else if (key.return_()) { + // Enter: 选定搜索结果,放入输入框 + String result = historySearchResult; + historySearchMode = false; + setState(new TuiState(result, s.messages, s.scrollOffset, false, "")); + } else if (key.ctrl() && "r".equals(input)) { + // 再次 Ctrl+R: 搜索下一个匹配(更旧的) + searchHistoryBackward(historySearchQuery, historySearchIndex - 1); + setState(new TuiState(s.inputText, s.messages, s.scrollOffset, false, "")); + } else if (key.backspace()) { + // 退格: 缩短搜索词 + if (!historySearchQuery.isEmpty()) { + historySearchQuery = historySearchQuery.substring(0, historySearchQuery.length() - 1); + searchHistoryBackward(historySearchQuery, inputHistory.size() - 1); + } + setState(new TuiState(s.inputText, s.messages, s.scrollOffset, false, "")); + } else if (!input.isEmpty() && isPrintableInput(input, key)) { + // 输入字符: 追加到搜索词并搜索 + historySearchQuery += input; + searchHistoryBackward(historySearchQuery, inputHistory.size() - 1); + setState(new TuiState(s.inputText, s.messages, s.scrollOffset, false, "")); + } + } + + /** 从指定位置向前搜索历史记录 */ + private void searchHistoryBackward(String query, int startIdx) { + if (query.isEmpty()) { + historySearchResult = ""; + historySearchIndex = -1; + return; + } + String lowerQuery = query.toLowerCase(); + for (int i = Math.min(startIdx, inputHistory.size() - 1); i >= 0; i--) { + if (inputHistory.get(i).toLowerCase().contains(lowerQuery)) { + historySearchResult = inputHistory.get(i); + historySearchIndex = i; + return; + } + } + // 没找到 — 保留之前的结果(或清空) + historySearchResult = ""; + historySearchIndex = -1; + } + /** Tab 自动补全处理 */ private void handleTabCompletion(TuiState s) { if (!s.inputText.startsWith("/")) return;