diff --git a/src/main/java/com/claudecode/tui/ClaudeCodeComponent.java b/src/main/java/com/claudecode/tui/ClaudeCodeComponent.java index ebf739e..855c87a 100644 --- a/src/main/java/com/claudecode/tui/ClaudeCodeComponent.java +++ b/src/main/java/com/claudecode/tui/ClaudeCodeComponent.java @@ -71,6 +71,7 @@ public class ClaudeCodeComponent extends Component private final Runnable onExit; // --- 内部状态 --- + private final Object stateLock = new Object(); // 保护 getState/setState 的读-改-写操作 private final List inputHistory = new ArrayList<>(); private int historyIndex = -1; private String savedInput = ""; @@ -441,78 +442,82 @@ public class ClaudeCodeComponent extends Component @Override public void onInput(String input, Key key) { - TuiState s = getState(); - - // Ctrl+D: 退出 - if (key.ctrl() && "d".equals(input)) { - if (onExit != null) onExit.run(); - return; - } + synchronized (stateLock) { + TuiState s = getState(); - // Ctrl+C: 取消当前输入或中断 Agent - if (key.ctrl() && "c".equals(input)) { - if (agentRunning.get()) { - // TODO: 中断 Agent 运行 - addMessage(new SystemMsg("^C (interrupt)", Color.BRIGHT_YELLOW)); - } else { - setState(new TuiState("", s.messages, s.scrollOffset, false, "")); + // Ctrl+D: 退出 + if (key.ctrl() && "d".equals(input)) { + if (onExit != null) onExit.run(); + return; } - return; - } - // 权限确认模式 - if (permissionCallback != null) { - handlePermissionInput(input, key, s); - return; - } + // Ctrl+C: 取消当前输入或中断 Agent + if (key.ctrl() && "c".equals(input)) { + if (agentRunning.get()) { + // TODO: 中断 Agent 运行 + addMessageInternal(new SystemMsg("^C (interrupt)", Color.BRIGHT_YELLOW), s); + } else { + setState(new TuiState("", s.messages, s.scrollOffset, false, "")); + } + return; + } - // AI 运行中时忽略大部分输入(但允许滚动) - if (agentRunning.get()) { - handleScrollInput(key, s); - return; - } + // 权限确认模式 + if (permissionCallback != null) { + handlePermissionInput(input, key, s); + return; + } - if (key.return_() && key.meta()) { - // Shift+Enter: 多行换行 - setState(new TuiState(s.inputText + "\n", s.messages, 0, false, "")); - } else if (key.return_()) { - // Enter: 发送 - if (!s.inputText.isEmpty()) { - submitInput(s.inputText, s); + // AI 运行中时忽略大部分输入(但允许滚动) + if (agentRunning.get()) { + handleScrollInput(key, s); + return; } - } else if (key.backspace()) { - if (!s.inputText.isEmpty()) { + + if (key.return_() && key.meta()) { + // Shift+Enter: 多行换行 + setState(new TuiState(s.inputText + "\n", s.messages, 0, false, "")); + } else if (key.return_()) { + // Enter: 发送 + if (!s.inputText.isEmpty()) { + submitInput(s.inputText, s); + } + } else if (key.backspace()) { + if (!s.inputText.isEmpty()) { + abandonHistoryPreview(); + String newText = s.inputText.substring(0, s.inputText.length() - 1); + setState(new TuiState(newText, s.messages, s.scrollOffset, false, "")); + } + } else if (key.upArrow()) { + browseHistoryUp(s); + } else if (key.downArrow()) { + browseHistoryDown(s); + } else if (key.scrollUp()) { + scroll(s, 3); + } else if (key.scrollDown()) { + scroll(s, -3); + } else if (key.pageUp()) { + scroll(s, 10); + } else if (key.pageDown()) { + scroll(s, -10); + } else if (key.escape()) { + // Esc: 清空输入 + setState(new TuiState("", s.messages, s.scrollOffset, false, "")); + } else if (!input.isEmpty() && isPrintableInput(input, key)) { abandonHistoryPreview(); - String newText = s.inputText.substring(0, s.inputText.length() - 1); - setState(new TuiState(newText, s.messages, s.scrollOffset, false, "")); + setState(new TuiState(s.inputText + input, s.messages, s.scrollOffset, false, "")); } - } else if (key.upArrow()) { - browseHistoryUp(s); - } else if (key.downArrow()) { - browseHistoryDown(s); - } else if (key.scrollUp()) { - scroll(s, 3); - } else if (key.scrollDown()) { - scroll(s, -3); - } else if (key.pageUp()) { - scroll(s, 10); - } else if (key.pageDown()) { - scroll(s, -10); - } else if (key.escape()) { - // Esc: 清空输入 - 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, "")); } } @Override public void onPaste(String text) { - if (agentRunning.get() || text == null || text.isEmpty()) return; - TuiState s = getState(); - abandonHistoryPreview(); - setState(new TuiState(s.inputText + text, s.messages, s.scrollOffset, false, "")); + synchronized (stateLock) { + if (agentRunning.get() || text == null || text.isEmpty()) return; + TuiState s = getState(); + abandonHistoryPreview(); + setState(new TuiState(s.inputText + text, s.messages, s.scrollOffset, false, "")); + } } /** 处理权限确认输入 */ @@ -605,8 +610,10 @@ public class ClaudeCodeComponent extends Component addMessage(new SystemMsg("Error: " + e.getMessage(), Color.BRIGHT_RED)); } finally { agentRunning.set(false); - TuiState cs = getState(); - setState(new TuiState(cs.inputText, cs.messages, 0, false, "")); + synchronized (stateLock) { + TuiState cs = getState(); + setState(new TuiState(cs.inputText, cs.messages, 0, false, "")); + } } }); } @@ -615,7 +622,13 @@ public class ClaudeCodeComponent extends Component /** 添加一条消息 */ public void addMessage(UIMessage msg) { - TuiState s = getState(); + synchronized (stateLock) { + addMessageInternal(msg, getState()); + } + } + + /** 内部添加消息(调用方需持有 stateLock) */ + private void addMessageInternal(UIMessage msg, TuiState s) { List newMsgs = new ArrayList<>(s.messages); newMsgs.add(msg); setState(new TuiState(s.inputText, Collections.unmodifiableList(newMsgs), @@ -624,46 +637,52 @@ public class ClaudeCodeComponent extends Component /** 追加 token 到当前流式助手消息 */ private void appendToStreamingMessage(String token) { - TuiState s = getState(); - List msgs = new ArrayList<>(s.messages); + synchronized (stateLock) { + TuiState s = getState(); + List msgs = new ArrayList<>(s.messages); - // 查找最后一个 streaming AssistantMsg - if (!msgs.isEmpty() && msgs.getLast() instanceof AssistantMsg am && am.streaming()) { - msgs.set(msgs.size() - 1, am.appendText(token)); - } else { - msgs.add(new AssistantMsg(token, true)); - } + // 查找最后一个 streaming AssistantMsg + if (!msgs.isEmpty() && msgs.getLast() instanceof AssistantMsg am && am.streaming()) { + msgs.set(msgs.size() - 1, am.appendText(token)); + } else { + msgs.add(new AssistantMsg(token, true)); + } - setState(new TuiState(s.inputText, Collections.unmodifiableList(msgs), - 0, s.thinking, s.thinkingText)); + setState(new TuiState(s.inputText, Collections.unmodifiableList(msgs), + 0, s.thinking, s.thinkingText)); + } } /** 完成当前流式消息(公开给 JinkReplSession 使用) */ public void finishStreamingMessage() { - TuiState s = getState(); - List msgs = new ArrayList<>(s.messages); - - if (!msgs.isEmpty() && msgs.getLast() instanceof AssistantMsg am && am.streaming()) { - msgs.set(msgs.size() - 1, am.finish()); - setState(new TuiState(s.inputText, Collections.unmodifiableList(msgs), - 0, s.thinking, s.thinkingText)); + synchronized (stateLock) { + TuiState s = getState(); + List msgs = new ArrayList<>(s.messages); + + if (!msgs.isEmpty() && msgs.getLast() instanceof AssistantMsg am && am.streaming()) { + msgs.set(msgs.size() - 1, am.finish()); + setState(new TuiState(s.inputText, Collections.unmodifiableList(msgs), + 0, s.thinking, s.thinkingText)); + } } } /** 更新最后一个工具调用消息的结果 */ public void completeLastToolCall(String result) { - TuiState s = getState(); - List msgs = new ArrayList<>(s.messages); - - for (int i = msgs.size() - 1; i >= 0; i--) { - if (msgs.get(i) instanceof ToolCallMsg tcm && tcm.running()) { - msgs.set(i, tcm.complete(result)); - break; + synchronized (stateLock) { + TuiState s = getState(); + List msgs = new ArrayList<>(s.messages); + + for (int i = msgs.size() - 1; i >= 0; i--) { + if (msgs.get(i) instanceof ToolCallMsg tcm && tcm.running()) { + msgs.set(i, tcm.complete(result)); + break; + } } - } - setState(new TuiState(s.inputText, Collections.unmodifiableList(msgs), - s.scrollOffset, s.thinking, s.thinkingText)); + setState(new TuiState(s.inputText, Collections.unmodifiableList(msgs), + s.scrollOffset, s.thinking, s.thinkingText)); + } } /** 设置权限确认回调 */ @@ -673,8 +692,10 @@ public class ClaudeCodeComponent extends Component /** 设置 thinking 状态 */ public void setThinking(boolean thinking, String text) { - TuiState s = getState(); - setState(new TuiState(s.inputText, s.messages, s.scrollOffset, thinking, text)); + synchronized (stateLock) { + TuiState s = getState(); + setState(new TuiState(s.inputText, s.messages, s.scrollOffset, thinking, text)); + } } /** 设置首次用户输入回调 */ @@ -747,20 +768,20 @@ public class ClaudeCodeComponent extends Component String[] keys = {"command", "file_path", "pattern", "query", "url"}; for (String key : keys) { String search = "\"" + key + "\""; - if (args.contains(search)) { - int start = args.indexOf(search); - int valStart = args.indexOf("\"", start + search.length()) + 1; - int valEnd = args.indexOf("\"", valStart); - if (valStart > 0 && valEnd > valStart) { - String val = args.substring(valStart, Math.min(valEnd, valStart + 60)); - return switch (key) { - case "command" -> "$ " + val; - case "pattern" -> "pattern: " + val; - case "query" -> "\"" + val + "\""; - default -> val; - }; - } - } + int start = args.indexOf(search); + if (start < 0) continue; + int colonPos = args.indexOf("\"", start + search.length()); + if (colonPos < 0) continue; + int valStart = colonPos + 1; + int valEnd = args.indexOf("\"", valStart); + if (valEnd < 0 || valEnd <= valStart) continue; + String val = args.substring(valStart, Math.min(valEnd, valStart + 60)); + return switch (key) { + case "command" -> "$ " + val; + case "pattern" -> "pattern: " + val; + case "query" -> "\"" + val + "\""; + default -> val; + }; } } catch (Exception ignored) {} return null;