feat: 工具执行流式输出预览

- ToolEvent 新增 PROGRESS 阶段,工具可在执行中报告进度
- ToolContext 添加 progressCallback,工具可通过 reportProgress() 报告输出行
- BashTool 在读取每行输出时实时报告进度
- AgentLoop 在工具执行前设置进度回调,执行后清除
- ToolCallMsg 支持 outputLines 字段,保留最后 5 行流式预览
- 运行中的工具在消息区显示实时输出(灰色,截断至 120 字符)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
pull/1/head
abel533 1 month ago
parent 012d5dfae6
commit 54c3cb41a9
  1. 15
      src/main/java/com/claudecode/core/AgentLoop.java
  2. 15
      src/main/java/com/claudecode/tool/ToolContext.java
  3. 2
      src/main/java/com/claudecode/tool/impl/BashTool.java
  4. 28
      src/main/java/com/claudecode/tui/ClaudeCodeComponent.java
  5. 4
      src/main/java/com/claudecode/tui/JinkReplSession.java
  6. 24
      src/main/java/com/claudecode/tui/UIMessage.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 }
}
}

@ -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<String, Object> state;
private volatile Consumer<String> 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<String> progressCallback) {
this.progressCallback = progressCallback;
}
/** 报告进度(如果有回调注册) */
public void reportProgress(String line) {
Consumer<String> cb = progressCallback;
if (cb != null) {
cb.accept(line);
}
}
}

@ -118,6 +118,8 @@ public class BashTool implements Tool {
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
// 报告流式进度
context.reportProgress(line);
}
}

@ -417,6 +417,16 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
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<ClaudeCodeComponent.TuiState>
}
}
/** 追加工具执行的流式输出行到最后一个运行中的 ToolCallMsg */
public void appendToolOutput(String line) {
synchronized (stateLock) {
TuiState s = getState();
List<UIMessage> 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<String> callback) {
this.askOptions = null;

@ -120,6 +120,10 @@ public class JinkReplSession {
true
));
}
case PROGRESS -> {
// 工具执行中的流式输出行
component.appendToolOutput(event.result());
}
case END -> {
component.completeLastToolCall(event.result());
}

@ -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<String> 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));
}
}

Loading…
Cancel
Save