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.
|
/// Tool execution status.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum ExecStatus {
|
pub enum ExecStatus {
|
||||||
|
/// 模型已发出调用、尚未执行。
|
||||||
|
/// The model issued the call; not executed yet.
|
||||||
|
Pending,
|
||||||
/// 正在运行。
|
/// 正在运行。
|
||||||
/// Running.
|
/// Running.
|
||||||
Running,
|
Running,
|
||||||
|
|
@ -527,7 +530,26 @@ fn push_content_block(
|
||||||
expanded: is_expanded,
|
expanded: is_expanded,
|
||||||
full: is_full,
|
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 running = *status == ExecStatus::Running;
|
||||||
let mark = if running {
|
let mark = match status {
|
||||||
"● 运行中".to_string()
|
ExecStatus::Running => "● 运行中".to_string(),
|
||||||
} else if *is_error {
|
ExecStatus::Done if *is_error => "✗".to_string(),
|
||||||
"✗".to_string()
|
ExecStatus::Done => "✓".to_string(),
|
||||||
} else {
|
// 待执行:不显示状态标记。
|
||||||
"✓".to_string()
|
// Pending: no status marker.
|
||||||
|
ExecStatus::Pending => String::new(),
|
||||||
};
|
};
|
||||||
let dur = duration_ms
|
let dur = duration_ms
|
||||||
.map(format_duration)
|
.map(format_duration)
|
||||||
|
|
@ -430,14 +431,23 @@ fn draw_status(frame: &mut Frame, area: Rect, app: &App) {
|
||||||
ratio,
|
ratio,
|
||||||
usage
|
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() {
|
let right = if app.is_running() {
|
||||||
// 运行中:spinner + 阶段(或状态说明,如 "compacting…")。
|
// 运行中:spinner + 阶段(或状态说明,如 "compacting…")。
|
||||||
// Running: spinner + phase (or a status note like "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)
|
format!("{} {}", spinner(app.frame), app.status)
|
||||||
} else {
|
} else {
|
||||||
format!("{} {}", spinner(app.frame), working_phase(app))
|
format!("{} {}", spinner(app.frame), working_phase(app))
|
||||||
}
|
};
|
||||||
|
format!("{}{}", base, follow_hint)
|
||||||
} else if !app.status.is_empty() {
|
} else if !app.status.is_empty() {
|
||||||
app.status.clone()
|
app.status.clone()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -511,3 +511,46 @@ fn full_flag_reaches_merged_tool_block() {
|
||||||
.expect("merged tool block");
|
.expect("merged tool block");
|
||||||
assert!(tr, "full flag must propagate");
|
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