fix(tui): show pending tool calls while streaming; pause-follow hint
1. tool calls inside a streaming assistant message were dropped entirely (push_content_block dropped the ToolCall arm after the merged-block refactor), so the call info only appeared once the whole turn finished. They now render as pending UiBlock::Tool blocks (ExecStatus::Pending, no status marker) as soon as the model issues them, then transition to the running execution blocks, then the merged request+result block. 2. with two thinkings, the first was pushed out of view by auto-follow when the second streamed; scrolling up already pauses follow, and the status bar now hints '↑跟随已暂停(滚到最底恢复)' while a run is active and the user is reviewing history. regression test: a streaming partial with a tool call renders a pending Tool block with the concrete summary.
This commit is contained in:
parent
9a2d50ee58
commit
71d83d23ed
|
|
@ -98,6 +98,9 @@ pub enum UiBlock {
|
|||
/// Tool execution status.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ExecStatus {
|
||||
/// 模型已发出调用、尚未执行。
|
||||
/// The model issued the call; not executed yet.
|
||||
Pending,
|
||||
/// 正在运行。
|
||||
/// Running.
|
||||
Running,
|
||||
|
|
@ -527,7 +530,26 @@ fn push_content_block(
|
|||
expanded: is_expanded,
|
||||
full: is_full,
|
||||
}),
|
||||
ContentBlock::Image(_) | ContentBlock::ToolCall(_) => {}
|
||||
ContentBlock::ToolCall(tc) => {
|
||||
// 流式消息中的工具调用:以「待执行」状态渲染,让用户在模型发出调用
|
||||
// 时就能看到,而不是等整个回合输出完(回归)。
|
||||
// Tool calls inside a streaming message render as pending blocks,
|
||||
// visible as soon as the model issues them rather than after the
|
||||
// whole turn finishes (regression).
|
||||
out.push(UiBlock::Tool {
|
||||
id,
|
||||
name: tc.name.clone(),
|
||||
summary: crate::format::summarize_tool(&tc.name, &tc.arguments),
|
||||
args: tc.arguments.clone(),
|
||||
status: ExecStatus::Pending,
|
||||
duration_ms: None,
|
||||
output: String::new(),
|
||||
is_error: false,
|
||||
expanded: is_expanded,
|
||||
full: is_full,
|
||||
});
|
||||
}
|
||||
ContentBlock::Image(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -296,12 +296,13 @@ fn block_lines(block: &UiBlock, width: usize) -> Vec<Line<'static>> {
|
|||
..
|
||||
} => {
|
||||
let running = *status == ExecStatus::Running;
|
||||
let mark = if running {
|
||||
"● 运行中".to_string()
|
||||
} else if *is_error {
|
||||
"✗".to_string()
|
||||
} else {
|
||||
"✓".to_string()
|
||||
let mark = match status {
|
||||
ExecStatus::Running => "● 运行中".to_string(),
|
||||
ExecStatus::Done if *is_error => "✗".to_string(),
|
||||
ExecStatus::Done => "✓".to_string(),
|
||||
// 待执行:不显示状态标记。
|
||||
// Pending: no status marker.
|
||||
ExecStatus::Pending => String::new(),
|
||||
};
|
||||
let dur = duration_ms
|
||||
.map(format_duration)
|
||||
|
|
@ -430,14 +431,23 @@ fn draw_status(frame: &mut Frame, area: Rect, app: &App) {
|
|||
ratio,
|
||||
usage
|
||||
);
|
||||
let follow_hint = if app.is_running() && !app.follow {
|
||||
// 用户上滚查看历史时已暂停自动跟随,提示如何恢复。
|
||||
// Auto-follow is paused while the user reviews history; hint how to
|
||||
// resume.
|
||||
" ↑跟随已暂停(滚到最底恢复)"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let right = if app.is_running() {
|
||||
// 运行中:spinner + 阶段(或状态说明,如 "compacting…")。
|
||||
// Running: spinner + phase (or a status note like "compacting…").
|
||||
if !app.status.is_empty() {
|
||||
let base = if !app.status.is_empty() {
|
||||
format!("{} {}", spinner(app.frame), app.status)
|
||||
} else {
|
||||
format!("{} {}", spinner(app.frame), working_phase(app))
|
||||
}
|
||||
};
|
||||
format!("{}{}", base, follow_hint)
|
||||
} else if !app.status.is_empty() {
|
||||
app.status.clone()
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -511,3 +511,46 @@ fn full_flag_reaches_merged_tool_block() {
|
|||
.expect("merged tool block");
|
||||
assert!(tr, "full flag must propagate");
|
||||
}
|
||||
|
||||
/// 流式消息中的工具调用必须以「待执行」状态渲染(回归:此前被丢弃,
|
||||
/// 工具信息要等整个回合输出完才出现)。
|
||||
/// Tool calls inside a streaming message must render as pending blocks
|
||||
/// (regression: they used to be dropped, appearing only after the turn).
|
||||
#[test]
|
||||
fn live_partial_renders_pending_tool_calls() {
|
||||
let fc = FakeClock::new();
|
||||
let mut rs = RunState::new(fc.clock());
|
||||
rs.on_agent_event(&AgentEvent::MessageStart {
|
||||
message: Message::Assistant(assistant(vec![
|
||||
thinking_block("why"),
|
||||
ContentBlock::ToolCall(ToolCall {
|
||||
id: "call_0".into(),
|
||||
name: "read".into(),
|
||||
arguments: {
|
||||
let mut a = JsonValue::obj();
|
||||
a.insert("path", "src/main.rs".into()).ok();
|
||||
a
|
||||
},
|
||||
}),
|
||||
])),
|
||||
});
|
||||
let blocks = build_blocks(
|
||||
&[],
|
||||
&HashMap::new(),
|
||||
&HashMap::new(),
|
||||
Some(&rs),
|
||||
&HashSet::new(),
|
||||
&HashSet::new(),
|
||||
);
|
||||
let tool = blocks
|
||||
.iter()
|
||||
.find_map(|b| match b {
|
||||
UiBlock::Tool {
|
||||
status, summary, ..
|
||||
} => Some((*status, summary.clone())),
|
||||
_ => None,
|
||||
})
|
||||
.expect("live tool block");
|
||||
assert_eq!(tool.0, ExecStatus::Pending);
|
||||
assert_eq!(tool.1, "read src/main.rs");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue