From ffb94bb2f7ae9b1737d047e05f8e2d83687a8dab Mon Sep 17 00:00:00 2001 From: abel533 Date: Sat, 4 Apr 2026 17:34:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20Escape/Ctrl+C=20?= =?UTF-8?q?=E4=B8=AD=E6=96=AD=20Agent=20=E8=BF=90=E8=A1=8C=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AgentLoop 添加 cancel()/resetCancel() 中断机制 - executeLoop 在每次迭代前和API调用后检查取消标志 - Escape 键和 Ctrl+C 在 Agent 运行时触发中断 - 快捷键栏更新提示 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../java/com/claudecode/core/AgentLoop.java | 27 +++++++++++++++++++ .../claudecode/tui/ClaudeCodeComponent.java | 14 +++++++--- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/claudecode/core/AgentLoop.java b/src/main/java/com/claudecode/core/AgentLoop.java index 14f0698..780498e 100644 --- a/src/main/java/com/claudecode/core/AgentLoop.java +++ b/src/main/java/com/claudecode/core/AgentLoop.java @@ -66,6 +66,9 @@ public class AgentLoop { /** 拒绝追踪器 */ private final DenialTracker denialTracker = new DenialTracker(); + /** 中断标志 —— 用于取消当前运行中的 Agent 循环 */ + private volatile boolean cancelled = false; + /** 消息历史 —— 自行管理,不依赖 Spring AI ChatMemory */ private final List messageHistory = new ArrayList<>(); @@ -132,6 +135,16 @@ public class AgentLoop { this.onThinkingContent = onThinkingContent; } + /** 取消当前运行中的 Agent 循环 */ + public void cancel() { + cancelled = true; + } + + /** 重置取消标志(每次新的循环开始时调用) */ + private void resetCancel() { + cancelled = false; + } + // ==================== 阻塞模式 ==================== /** @@ -161,6 +174,7 @@ public class AgentLoop { // ==================== 核心循环(统一阻塞/流式) ==================== private String executeLoop(boolean streaming, Consumer onToken) { + resetCancel(); List callbacks = toolRegistry.toCallbacks(toolContext); ChatOptions options = ToolCallingChatOptions.builder() .toolCallbacks(callbacks) @@ -171,6 +185,13 @@ public class AgentLoop { String lastAssistantText = ""; while (iteration < MAX_ITERATIONS) { + // 检查取消标志 + if (cancelled) { + log.info("Agent loop cancelled by user at iteration {}", iteration); + lastAssistantText += "\n\n[Interrupted by user]"; + break; + } + iteration++; log.debug("Agent loop iteration {} ({})", iteration, streaming ? "streaming" : "blocking"); @@ -184,6 +205,12 @@ public class AgentLoop { result = blockingIteration(prompt); } + // 检查取消标志(API调用后) + if (cancelled) { + log.info("Agent loop cancelled by user after API call at iteration {}", iteration); + break; + } + // 记录 Token 使用量 if (result.promptTokens > 0 || result.completionTokens > 0) { tokenTracker.recordUsage(result.promptTokens, result.completionTokens); diff --git a/src/main/java/com/claudecode/tui/ClaudeCodeComponent.java b/src/main/java/com/claudecode/tui/ClaudeCodeComponent.java index cee8301..6d72e93 100644 --- a/src/main/java/com/claudecode/tui/ClaudeCodeComponent.java +++ b/src/main/java/com/claudecode/tui/ClaudeCodeComponent.java @@ -508,7 +508,7 @@ public class ClaudeCodeComponent extends Component } return Box.of( - Text.of("↑↓ history wheel scroll Ctrl+D exit").dimmed(), + Text.of("↑↓ history Esc interrupt Ctrl+D exit").dimmed(), Spacer.create(), Text.of(tokenInfo).color(Color.BRIGHT_GREEN) ).paddingX(1).height(1); @@ -536,7 +536,7 @@ public class ClaudeCodeComponent extends Component // Ctrl+C: 取消当前输入或中断 Agent if (key.ctrl() && "c".equals(input)) { if (agentRunning.get()) { - // TODO: 中断 Agent 运行 + agentLoop.cancel(); addMessageInternal(new SystemMsg("^C (interrupt)", Color.BRIGHT_YELLOW), s); } else { setState(new TuiState("", s.messages, s.scrollOffset, false, "")); @@ -554,9 +554,15 @@ public class ClaudeCodeComponent extends Component return; } - // AI 运行中时忽略大部分输入(但允许滚动) + // AI 运行中时允许滚动和 Escape 中断 if (agentRunning.get()) { - handleScrollInput(key, s); + if (key.escape()) { + // Esc: 中断 Agent 运行 + agentLoop.cancel(); + addMessageInternal(new SystemMsg("⚡ Interrupted", Color.BRIGHT_YELLOW), s); + } else { + handleScrollInput(key, s); + } return; }