feat: 添加 Escape/Ctrl+C 中断 Agent 运行功能

- AgentLoop 添加 cancel()/resetCancel() 中断机制
- executeLoop 在每次迭代前和API调用后检查取消标志
- Escape 键和 Ctrl+C 在 Agent 运行时触发中断
- 快捷键栏更新提示

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
pull/1/head
abel533 1 month ago
parent 91190e180b
commit ffb94bb2f7
  1. 27
      src/main/java/com/claudecode/core/AgentLoop.java
  2. 14
      src/main/java/com/claudecode/tui/ClaudeCodeComponent.java

@ -66,6 +66,9 @@ public class AgentLoop {
/** 拒绝追踪器 */ /** 拒绝追踪器 */
private final DenialTracker denialTracker = new DenialTracker(); private final DenialTracker denialTracker = new DenialTracker();
/** 中断标志 —— 用于取消当前运行中的 Agent 循环 */
private volatile boolean cancelled = false;
/** 消息历史 —— 自行管理,不依赖 Spring AI ChatMemory */ /** 消息历史 —— 自行管理,不依赖 Spring AI ChatMemory */
private final List<Message> messageHistory = new ArrayList<>(); private final List<Message> messageHistory = new ArrayList<>();
@ -132,6 +135,16 @@ public class AgentLoop {
this.onThinkingContent = onThinkingContent; 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<String> onToken) { private String executeLoop(boolean streaming, Consumer<String> onToken) {
resetCancel();
List<ToolCallback> callbacks = toolRegistry.toCallbacks(toolContext); List<ToolCallback> callbacks = toolRegistry.toCallbacks(toolContext);
ChatOptions options = ToolCallingChatOptions.builder() ChatOptions options = ToolCallingChatOptions.builder()
.toolCallbacks(callbacks) .toolCallbacks(callbacks)
@ -171,6 +185,13 @@ public class AgentLoop {
String lastAssistantText = ""; String lastAssistantText = "";
while (iteration < MAX_ITERATIONS) { while (iteration < MAX_ITERATIONS) {
// 检查取消标志
if (cancelled) {
log.info("Agent loop cancelled by user at iteration {}", iteration);
lastAssistantText += "\n\n[Interrupted by user]";
break;
}
iteration++; iteration++;
log.debug("Agent loop iteration {} ({})", iteration, streaming ? "streaming" : "blocking"); log.debug("Agent loop iteration {} ({})", iteration, streaming ? "streaming" : "blocking");
@ -184,6 +205,12 @@ public class AgentLoop {
result = blockingIteration(prompt); result = blockingIteration(prompt);
} }
// 检查取消标志(API调用后)
if (cancelled) {
log.info("Agent loop cancelled by user after API call at iteration {}", iteration);
break;
}
// 记录 Token 使用量 // 记录 Token 使用量
if (result.promptTokens > 0 || result.completionTokens > 0) { if (result.promptTokens > 0 || result.completionTokens > 0) {
tokenTracker.recordUsage(result.promptTokens, result.completionTokens); tokenTracker.recordUsage(result.promptTokens, result.completionTokens);

@ -508,7 +508,7 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
} }
return Box.of( return Box.of(
Text.of("↑↓ history wheel scroll Ctrl+D exit").dimmed(), Text.of("↑↓ history Esc interrupt Ctrl+D exit").dimmed(),
Spacer.create(), Spacer.create(),
Text.of(tokenInfo).color(Color.BRIGHT_GREEN) Text.of(tokenInfo).color(Color.BRIGHT_GREEN)
).paddingX(1).height(1); ).paddingX(1).height(1);
@ -536,7 +536,7 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
// Ctrl+C: 取消当前输入或中断 Agent // Ctrl+C: 取消当前输入或中断 Agent
if (key.ctrl() && "c".equals(input)) { if (key.ctrl() && "c".equals(input)) {
if (agentRunning.get()) { if (agentRunning.get()) {
// TODO: 中断 Agent 运行 agentLoop.cancel();
addMessageInternal(new SystemMsg("^C (interrupt)", Color.BRIGHT_YELLOW), s); addMessageInternal(new SystemMsg("^C (interrupt)", Color.BRIGHT_YELLOW), s);
} else { } else {
setState(new TuiState("", s.messages, s.scrollOffset, false, "")); setState(new TuiState("", s.messages, s.scrollOffset, false, ""));
@ -554,9 +554,15 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
return; return;
} }
// AI 运行中时忽略大部分输入(但允许滚动) // AI 运行中时允许滚动和 Escape 中断
if (agentRunning.get()) { 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; return;
} }

Loading…
Cancel
Save