feat: 工具执行流式输出预览
- ToolEvent 新增 PROGRESS 阶段,工具可在执行中报告进度 - ToolContext 添加 progressCallback,工具可通过 reportProgress() 报告输出行 - BashTool 在读取每行输出时实时报告进度 - AgentLoop 在工具执行前设置进度回调,执行后清除 - ToolCallMsg 支持 outputLines 字段,保留最后 5 行流式预览 - 运行中的工具在消息区显示实时输出(灰色,截断至 120 字符) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -413,7 +413,18 @@ public class AgentLoop {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (permitted) {
|
if (permitted) {
|
||||||
|
// 设置进度回调,将工具输出行转发为 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);
|
result = adapter.call(toolArgs);
|
||||||
|
} finally {
|
||||||
|
toolContext.setProgressCallback(null);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
result = "Permission denied: User rejected this operation";
|
result = "Permission denied: User rejected this operation";
|
||||||
log.info("[{}] User denied tool execution", toolName);
|
log.info("[{}] User denied tool execution", toolName);
|
||||||
@@ -561,6 +572,6 @@ public class AgentLoop {
|
|||||||
|
|
||||||
/** 工具事件,用于 UI 展示 */
|
/** 工具事件,用于 UI 展示 */
|
||||||
public record ToolEvent(String toolName, Phase phase, String arguments, String result) {
|
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.nio.file.Path;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工具执行上下文 —— 对应 claude-code 中 ToolUseContext。
|
* 工具执行上下文 —— 对应 claude-code 中 ToolUseContext。
|
||||||
@@ -13,6 +14,7 @@ public class ToolContext {
|
|||||||
private final Path workDir;
|
private final Path workDir;
|
||||||
private final String model;
|
private final String model;
|
||||||
private final ConcurrentHashMap<String, Object> state;
|
private final ConcurrentHashMap<String, Object> state;
|
||||||
|
private volatile Consumer<String> progressCallback; // 工具执行进度回调(流式输出行)
|
||||||
|
|
||||||
public ToolContext(Path workDir, String model) {
|
public ToolContext(Path workDir, String model) {
|
||||||
this.workDir = workDir;
|
this.workDir = workDir;
|
||||||
@@ -51,4 +53,17 @@ public class ToolContext {
|
|||||||
public boolean has(String key) {
|
public boolean has(String key) {
|
||||||
return state.containsKey(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;
|
String line;
|
||||||
while ((line = reader.readLine()) != null) {
|
while ((line = reader.readLine()) != null) {
|
||||||
output.append(line).append("\n");
|
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(""),
|
argSummary != null ? Text.of("(" + argSummary + ")").dimmed() : Text.of(""),
|
||||||
Text.of(" running...").dimmed()
|
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 {
|
} else {
|
||||||
lines.add(Text.of(
|
lines.add(Text.of(
|
||||||
Text.of(" ● ").color(Color.BRIGHT_GREEN),
|
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) */
|
/** 设置简单文本输入回调(用于无选项的 AskUser) */
|
||||||
public void requestTextInput(Consumer<String> callback) {
|
public void requestTextInput(Consumer<String> callback) {
|
||||||
this.askOptions = null;
|
this.askOptions = null;
|
||||||
|
|||||||
@@ -120,6 +120,10 @@ public class JinkReplSession {
|
|||||||
true
|
true
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
case PROGRESS -> {
|
||||||
|
// 工具执行中的流式输出行
|
||||||
|
component.appendToolOutput(event.result());
|
||||||
|
}
|
||||||
case END -> {
|
case END -> {
|
||||||
component.completeLastToolCall(event.result());
|
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) {
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user