From 54c3cb41a9cb050fc617dd41d74aa20a0392d107 Mon Sep 17 00:00:00 2001 From: abel533 Date: Sat, 4 Apr 2026 19:22:35 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=B7=A5=E5=85=B7=E6=89=A7=E8=A1=8C?= =?UTF-8?q?=E6=B5=81=E5=BC=8F=E8=BE=93=E5=87=BA=E9=A2=84=E8=A7=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ToolEvent 新增 PROGRESS 阶段,工具可在执行中报告进度 - ToolContext 添加 progressCallback,工具可通过 reportProgress() 报告输出行 - BashTool 在读取每行输出时实时报告进度 - AgentLoop 在工具执行前设置进度回调,执行后清除 - ToolCallMsg 支持 outputLines 字段,保留最后 5 行流式预览 - 运行中的工具在消息区显示实时输出(灰色,截断至 120 字符) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../java/com/claudecode/core/AgentLoop.java | 15 ++++++++-- .../java/com/claudecode/tool/ToolContext.java | 15 ++++++++++ .../com/claudecode/tool/impl/BashTool.java | 2 ++ .../claudecode/tui/ClaudeCodeComponent.java | 28 +++++++++++++++++++ .../com/claudecode/tui/JinkReplSession.java | 4 +++ .../java/com/claudecode/tui/UIMessage.java | 24 ++++++++++++++-- 6 files changed, 83 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/claudecode/core/AgentLoop.java b/src/main/java/com/claudecode/core/AgentLoop.java index bfb1270..33a6730 100644 --- a/src/main/java/com/claudecode/core/AgentLoop.java +++ b/src/main/java/com/claudecode/core/AgentLoop.java @@ -413,7 +413,18 @@ public class AgentLoop { } if (permitted) { - result = adapter.call(toolArgs); + // 设置进度回调,将工具输出行转发为 PROGRESS 事件 + final String tn = toolName; + final String ta = toolArgs; + if (onToolEvent != null) { + toolContext.setProgressCallback(line -> + onToolEvent.accept(new ToolEvent(tn, ToolEvent.Phase.PROGRESS, ta, line))); + } + try { + result = adapter.call(toolArgs); + } finally { + toolContext.setProgressCallback(null); + } } else { result = "Permission denied: User rejected this operation"; log.info("[{}] User denied tool execution", toolName); @@ -561,6 +572,6 @@ public class AgentLoop { /** 工具事件,用于 UI 展示 */ public record ToolEvent(String toolName, Phase phase, String arguments, String result) { - public enum Phase { START, END } + public enum Phase { START, PROGRESS, END } } } diff --git a/src/main/java/com/claudecode/tool/ToolContext.java b/src/main/java/com/claudecode/tool/ToolContext.java index ae4409f..7413e82 100644 --- a/src/main/java/com/claudecode/tool/ToolContext.java +++ b/src/main/java/com/claudecode/tool/ToolContext.java @@ -2,6 +2,7 @@ package com.claudecode.tool; import java.nio.file.Path; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; /** * 工具执行上下文 —— 对应 claude-code 中 ToolUseContext。 @@ -13,6 +14,7 @@ public class ToolContext { private final Path workDir; private final String model; private final ConcurrentHashMap state; + private volatile Consumer progressCallback; // 工具执行进度回调(流式输出行) public ToolContext(Path workDir, String model) { this.workDir = workDir; @@ -51,4 +53,17 @@ public class ToolContext { public boolean has(String key) { return state.containsKey(key); } + + /** 设置进度回调(工具可在执行过程中报告输出行) */ + public void setProgressCallback(Consumer progressCallback) { + this.progressCallback = progressCallback; + } + + /** 报告进度(如果有回调注册) */ + public void reportProgress(String line) { + Consumer cb = progressCallback; + if (cb != null) { + cb.accept(line); + } + } } diff --git a/src/main/java/com/claudecode/tool/impl/BashTool.java b/src/main/java/com/claudecode/tool/impl/BashTool.java index 3832b9c..7d30556 100644 --- a/src/main/java/com/claudecode/tool/impl/BashTool.java +++ b/src/main/java/com/claudecode/tool/impl/BashTool.java @@ -118,6 +118,8 @@ public class BashTool implements Tool { String line; while ((line = reader.readLine()) != null) { output.append(line).append("\n"); + // 报告流式进度 + context.reportProgress(line); } } diff --git a/src/main/java/com/claudecode/tui/ClaudeCodeComponent.java b/src/main/java/com/claudecode/tui/ClaudeCodeComponent.java index cb51d47..7e2e830 100644 --- a/src/main/java/com/claudecode/tui/ClaudeCodeComponent.java +++ b/src/main/java/com/claudecode/tui/ClaudeCodeComponent.java @@ -417,6 +417,16 @@ public class ClaudeCodeComponent extends Component argSummary != null ? Text.of("(" + argSummary + ")").dimmed() : Text.of(""), Text.of(" running...").dimmed() )); + // 流式输出预览(最后几行) + if (m.outputLines() != null && !m.outputLines().isEmpty()) { + for (String line : m.outputLines()) { + lines.add(Text.of( + Text.of(" ⎿ ").dimmed(), + Text.of(line.length() > 120 ? line.substring(0, 117) + "..." : line) + .color(Color.BRIGHT_BLACK) + )); + } + } } else { lines.add(Text.of( Text.of(" ● ").color(Color.BRIGHT_GREEN), @@ -1247,6 +1257,24 @@ public class ClaudeCodeComponent extends Component } } + /** 追加工具执行的流式输出行到最后一个运行中的 ToolCallMsg */ + public void appendToolOutput(String line) { + 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.appendOutput(line)); + break; + } + } + + setState(new TuiState(s.inputText, Collections.unmodifiableList(msgs), + s.scrollOffset, s.thinking, s.thinkingText)); + } + } + /** 设置简单文本输入回调(用于无选项的 AskUser) */ public void requestTextInput(Consumer callback) { this.askOptions = null; diff --git a/src/main/java/com/claudecode/tui/JinkReplSession.java b/src/main/java/com/claudecode/tui/JinkReplSession.java index bc5c01c..f5e48cc 100644 --- a/src/main/java/com/claudecode/tui/JinkReplSession.java +++ b/src/main/java/com/claudecode/tui/JinkReplSession.java @@ -120,6 +120,10 @@ public class JinkReplSession { true )); } + case PROGRESS -> { + // 工具执行中的流式输出行 + component.appendToolOutput(event.result()); + } case END -> { component.completeLastToolCall(event.result()); } diff --git a/src/main/java/com/claudecode/tui/UIMessage.java b/src/main/java/com/claudecode/tui/UIMessage.java index 35355ac..bfc7fa2 100644 --- a/src/main/java/com/claudecode/tui/UIMessage.java +++ b/src/main/java/com/claudecode/tui/UIMessage.java @@ -25,10 +25,28 @@ public sealed interface UIMessage { } } - /** 工具调用消息 */ - record ToolCallMsg(String toolName, String args, String result, boolean running) implements UIMessage { + /** 工具调用消息(支持流式输出预览) */ + record ToolCallMsg(String toolName, String args, String result, boolean running, + List outputLines) implements UIMessage { + /** 创建运行中的工具消息(无输出) */ + public ToolCallMsg(String toolName, String args, String result, boolean running) { + this(toolName, args, result, running, List.of()); + } + + /** 完成工具调用 */ public ToolCallMsg complete(String result) { - return new ToolCallMsg(toolName, args, result, false); + return new ToolCallMsg(toolName, args, result, false, List.of()); + } + + /** 追加一行流式输出 */ + public ToolCallMsg appendOutput(String line) { + var newLines = new java.util.ArrayList<>(outputLines); + newLines.add(line); + // 只保留最后 5 行预览 + while (newLines.size() > 5) { + newLines.removeFirst(); + } + return new ToolCallMsg(toolName, args, result, running, List.copyOf(newLines)); } }