fix(tui): merged tool request+result blocks; fix wheel-down bottom jump
1. wheel-down jumped to the bottom because max_scroll() derived the limit from
geometry, which only covers expandable blocks — a text-only conversation
left it at 0, so one down-notch clamped scroll to 0 with follow=true and
the next draw snapped to the real bottom. The limit now uses
content_lines (the full rendered line count, stored per draw).
2. tool request and result are now one block ('requested where it returns'):
UiBlock::ToolCall/ToolResult/ToolExec are unified into UiBlock::Tool, built
by merging each assistant tool call with its matching tool_result message
(looked up by call_id). Collapsed shows the concrete summary + duration +
status; expanded shows human-readable args, then '── 结果 ──', then the
output (two-level: partial preview with remaining-line hint, then full).
Live running executions reuse the same block. ToolResult messages are no
longer rendered standalone.
tests updated for the merged model; wheel test now drives content_lines.
This commit is contained in:
parent
acddf7be1b
commit
7d289580b5
|
|
@ -214,6 +214,10 @@ pub struct App {
|
|||
/// 消息区视图高度(由 draw 填充)。
|
||||
/// Messages-area view height (filled by draw).
|
||||
pub view_height: u16,
|
||||
/// 消息区总内容行数(由 draw 填充;滚动上限依据)。
|
||||
/// Total content lines of the message area (filled by draw; the scroll
|
||||
/// limit is based on it).
|
||||
pub content_lines: u16,
|
||||
/// 帧计数(供 spinner 动画)。
|
||||
/// Frame counter (drives the spinner animation).
|
||||
pub frame: u64,
|
||||
|
|
@ -249,6 +253,7 @@ impl App {
|
|||
last_usage: None,
|
||||
geometry: Vec::new(),
|
||||
view_height: 0,
|
||||
content_lines: 0,
|
||||
frame: 0,
|
||||
quit: false,
|
||||
}
|
||||
|
|
@ -892,26 +897,18 @@ impl App {
|
|||
}
|
||||
}
|
||||
|
||||
/// 最大滚动偏移(由最近渲染的几何决定)。
|
||||
/// Max scroll offset (derived from the last render's geometry).
|
||||
/// 最大滚动偏移(由最近渲染的总行数决定;几何只含可展开块,不能用作依据)。
|
||||
/// Max scroll offset (based on the last render's total line count; the
|
||||
/// geometry only covers expandable blocks and is unusable for this).
|
||||
fn max_scroll(&self) -> u16 {
|
||||
let total = self.geometry.last().map(|g| g.end).unwrap_or(0);
|
||||
total.saturating_sub(self.view_height.max(1))
|
||||
self.content_lines.saturating_sub(self.view_height.max(1))
|
||||
}
|
||||
|
||||
/// 切换最近一个可折叠块。
|
||||
/// Toggle the most recent collapsible block.
|
||||
/// 切换最近一个可折叠块(几何只含可展开块:思考与工具)。
|
||||
/// Toggle the most recent collapsible block (geometry only holds
|
||||
/// expandable blocks: thinking and tools).
|
||||
fn toggle_latest_collapsible(&mut self) {
|
||||
let id = self
|
||||
.geometry
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|g| {
|
||||
let id = &g.id;
|
||||
id.0.starts_with("live-") || id.0.starts_with("exec-") || id.0.contains("-r")
|
||||
})
|
||||
.map(|g| g.id.clone());
|
||||
if let Some(id) = id {
|
||||
if let Some(id) = self.geometry.last().map(|g| g.id.clone()) {
|
||||
cycle_expansion(&mut self.expanded, &mut self.full, &id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,41 +63,30 @@ pub enum UiBlock {
|
|||
/// partial preview).
|
||||
full: bool,
|
||||
},
|
||||
/// assistant 发出的工具调用。
|
||||
/// A tool call issued by the assistant.
|
||||
ToolCall {
|
||||
id: BlockId,
|
||||
name: String,
|
||||
summary: String,
|
||||
args: JsonValue,
|
||||
expanded: bool,
|
||||
},
|
||||
/// 工具执行结果消息(来自已提交的 transcript)。
|
||||
/// A tool-result message (from the committed transcript).
|
||||
ToolResult {
|
||||
id: BlockId,
|
||||
name: String,
|
||||
text: String,
|
||||
is_error: bool,
|
||||
duration_ms: Option<u64>,
|
||||
expanded: bool,
|
||||
full: bool,
|
||||
},
|
||||
/// 运行错误(assistant 消息的 error_message,红色显示)。
|
||||
/// A run error (an assistant message's error_message, shown in red).
|
||||
Error { id: BlockId, text: String },
|
||||
/// 工具执行过程(运行中 / 完成,含耗时与输出)。
|
||||
/// A tool execution (running / done, with duration and output).
|
||||
ToolExec {
|
||||
/// 一次工具执行:请求(参数)与结果合并为一块——在那请求,在那返回。
|
||||
/// One tool execution: the request (arguments) and its result are merged
|
||||
/// into a single block — requested where it returns.
|
||||
Tool {
|
||||
id: BlockId,
|
||||
name: String,
|
||||
/// 折叠摘要(具体内容,如 `read /path` / `shell: cmd`)。
|
||||
/// Collapsed summary (concrete content, e.g. `read /path`).
|
||||
summary: String,
|
||||
/// 参数(展开时以人类可读键值对展示)。
|
||||
/// Arguments (shown human-readably when expanded).
|
||||
args: JsonValue,
|
||||
status: ExecStatus,
|
||||
duration_ms: Option<u64>,
|
||||
/// 结果文本(或运行中的流式输出)。
|
||||
/// Result text (or live streamed output while running).
|
||||
output: String,
|
||||
result_text: String,
|
||||
is_error: bool,
|
||||
expanded: bool,
|
||||
/// 二级展开:true 显示全部(false 只显示部分预览)。
|
||||
/// Second-level expansion: true shows everything.
|
||||
full: bool,
|
||||
},
|
||||
/// 系统提示(欢迎、帮助、状态说明等)。
|
||||
|
|
@ -376,6 +365,16 @@ pub fn build_blocks(
|
|||
full: &HashSet<BlockId>,
|
||||
) -> Vec<UiBlock> {
|
||||
let mut out = Vec::new();
|
||||
// 预收集 tool_result:call_id → (结果消息, 消息索引),用于把结果合并到
|
||||
// 对应的工具调用块里(在那请求,在那返回)。
|
||||
// Pre-collect tool_results: call_id → (result message, index), so each
|
||||
// result merges into its tool-call block (requested where it returns).
|
||||
let mut results: HashMap<&str, (&ToolResultMessage, usize)> = HashMap::new();
|
||||
for (mi, msg) in transcript.iter().enumerate() {
|
||||
if let Message::ToolResult(t) = msg {
|
||||
results.entry(t.tool_call_id.as_str()).or_insert((t, mi));
|
||||
}
|
||||
}
|
||||
for (mi, msg) in transcript.iter().enumerate() {
|
||||
match msg {
|
||||
Message::User(u) => {
|
||||
|
|
@ -393,14 +392,43 @@ pub fn build_blocks(
|
|||
stop_reason: a.stop_reason,
|
||||
});
|
||||
for (bi, block) in a.content.iter().enumerate() {
|
||||
push_content_block(
|
||||
&mut out,
|
||||
BlockId(format!("m{}-{}", mi, bi)),
|
||||
block,
|
||||
thinking_durations.get(&(mi, bi)).copied(),
|
||||
expanded,
|
||||
full,
|
||||
);
|
||||
match block {
|
||||
ContentBlock::ToolCall(tc) => {
|
||||
// 工具调用与其结果合并为一块。
|
||||
// The tool call merges with its result.
|
||||
let id = BlockId(format!("m{}-{}", mi, bi));
|
||||
let result = results.get(tc.id.as_str()).copied();
|
||||
let (status, duration, output, is_error) = match result {
|
||||
Some((r, r_idx)) => (
|
||||
ExecStatus::Done,
|
||||
tool_durations.get(&(r_idx, 0)).copied(),
|
||||
join_text(&r.content),
|
||||
r.is_error,
|
||||
),
|
||||
None => (ExecStatus::Done, None, String::new(), false),
|
||||
};
|
||||
out.push(UiBlock::Tool {
|
||||
id,
|
||||
name: tc.name.clone(),
|
||||
summary: crate::format::summarize_tool(&tc.name, &tc.arguments),
|
||||
args: tc.arguments.clone(),
|
||||
status,
|
||||
duration_ms: duration,
|
||||
output,
|
||||
is_error,
|
||||
expanded: expanded.contains(&BlockId(format!("m{}-{}", mi, bi))),
|
||||
full: full.contains(&BlockId(format!("m{}-{}", mi, bi))),
|
||||
});
|
||||
}
|
||||
other => push_content_block(
|
||||
&mut out,
|
||||
BlockId(format!("m{}-{}", mi, bi)),
|
||||
other,
|
||||
thinking_durations.get(&(mi, bi)).copied(),
|
||||
expanded,
|
||||
full,
|
||||
),
|
||||
}
|
||||
}
|
||||
// 具体错误文本(如 provider 的 HTTP 400 详情)。
|
||||
// The concrete error text (e.g. a provider HTTP 400 detail).
|
||||
|
|
@ -411,17 +439,9 @@ pub fn build_blocks(
|
|||
});
|
||||
}
|
||||
}
|
||||
Message::ToolResult(t) => {
|
||||
let id = BlockId(format!("m{}-r", mi));
|
||||
out.push(UiBlock::ToolResult {
|
||||
id: id.clone(),
|
||||
name: t.tool_name.clone(),
|
||||
text: join_text(&t.content),
|
||||
is_error: t.is_error,
|
||||
duration_ms: tool_durations.get(&(mi, 0)).copied(),
|
||||
expanded: expanded.contains(&id),
|
||||
full: full.contains(&id),
|
||||
});
|
||||
Message::ToolResult(_) => {
|
||||
// 已合并进对应的工具调用块,不再单独渲染。
|
||||
// Already merged into its tool-call block; not rendered alone.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -464,14 +484,14 @@ pub fn build_blocks(
|
|||
.filter(|(_, e)| e.status == ExecStatus::Running)
|
||||
{
|
||||
let id = BlockId(format!("exec-{}", ei));
|
||||
out.push(UiBlock::ToolExec {
|
||||
out.push(UiBlock::Tool {
|
||||
id: id.clone(),
|
||||
name: exec.name.clone(),
|
||||
summary: exec.summary.clone(),
|
||||
args: JsonValue::obj(),
|
||||
status: exec.status,
|
||||
duration_ms: exec.duration_ms(),
|
||||
output: exec.output.clone(),
|
||||
result_text: exec.result_text.clone(),
|
||||
is_error: exec.is_error,
|
||||
expanded: expanded.contains(&id),
|
||||
full: full.contains(&id),
|
||||
|
|
@ -507,14 +527,7 @@ fn push_content_block(
|
|||
expanded: is_expanded,
|
||||
full: is_full,
|
||||
}),
|
||||
ContentBlock::ToolCall(tc) => out.push(UiBlock::ToolCall {
|
||||
id,
|
||||
name: tc.name.clone(),
|
||||
summary: crate::format::summarize_tool(&tc.name, &tc.arguments),
|
||||
args: tc.arguments.clone(),
|
||||
expanded: is_expanded,
|
||||
}),
|
||||
ContentBlock::Image(_) => {}
|
||||
ContentBlock::Image(_) | ContentBlock::ToolCall(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -547,9 +560,7 @@ impl UiBlock {
|
|||
| UiBlock::AssistantHeader { id, .. }
|
||||
| UiBlock::Text { id, .. }
|
||||
| UiBlock::Thinking { id, .. }
|
||||
| UiBlock::ToolCall { id, .. }
|
||||
| UiBlock::ToolResult { id, .. }
|
||||
| UiBlock::ToolExec { id, .. }
|
||||
| UiBlock::Tool { id, .. }
|
||||
| UiBlock::Error { id, .. } => Some(id),
|
||||
UiBlock::Note { .. } => None,
|
||||
}
|
||||
|
|
@ -558,12 +569,6 @@ impl UiBlock {
|
|||
/// 是否可展开/折叠(点击或 Tab)。
|
||||
/// Whether the block is expandable (click or Tab).
|
||||
pub fn is_expandable(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
UiBlock::Thinking { .. }
|
||||
| UiBlock::ToolCall { .. }
|
||||
| UiBlock::ToolResult { .. }
|
||||
| UiBlock::ToolExec { .. }
|
||||
)
|
||||
matches!(self, UiBlock::Thinking { .. } | UiBlock::Tool { .. })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -164,6 +164,7 @@ fn draw_messages(frame: &mut Frame, area: Rect, app: &mut App) {
|
|||
}
|
||||
|
||||
let total = lines.len() as u16;
|
||||
app.content_lines = total;
|
||||
let max_scroll = total.saturating_sub(area.height.max(1));
|
||||
if app.follow {
|
||||
app.scroll = max_scroll;
|
||||
|
|
@ -281,94 +282,55 @@ fn block_lines(block: &UiBlock, width: usize) -> Vec<Line<'static>> {
|
|||
)));
|
||||
}
|
||||
}
|
||||
UiBlock::ToolCall {
|
||||
name,
|
||||
|
||||
UiBlock::Tool {
|
||||
name: _,
|
||||
summary,
|
||||
args,
|
||||
expanded,
|
||||
..
|
||||
} => {
|
||||
if *expanded {
|
||||
out.push(Line::from(Span::styled(
|
||||
format!("▾ 🔧 {}", name),
|
||||
Style::new().fg(Color::Magenta).bold(),
|
||||
)));
|
||||
// 人类可读的参数(非原始 JSON)。
|
||||
// Human-readable arguments (not raw JSON).
|
||||
for line in format_args_human(args) {
|
||||
for wrapped in wrap_text(&line, width.saturating_sub(2)) {
|
||||
out.push(Line::from(Span::styled(format!(" {}", wrapped), dim)));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
out.push(Line::from(Span::styled(
|
||||
format!("▸ 🔧 {}(点击/Tab 展开)", summary),
|
||||
Style::new().fg(Color::Magenta),
|
||||
)));
|
||||
}
|
||||
}
|
||||
UiBlock::ToolResult {
|
||||
name,
|
||||
text,
|
||||
is_error,
|
||||
duration_ms,
|
||||
expanded,
|
||||
full,
|
||||
..
|
||||
} => {
|
||||
let mark = if *is_error { "✗" } else { "✓" };
|
||||
let dur = duration_ms
|
||||
.map(format_duration)
|
||||
.map(|d| format!(" · {}", d))
|
||||
.unwrap_or_default();
|
||||
if *expanded {
|
||||
out.push(Line::from(Span::styled(
|
||||
format!("▾ 📦 {} {}{}", mark, name, dur),
|
||||
if *is_error {
|
||||
Style::new().fg(Color::Red).bold()
|
||||
} else {
|
||||
Style::new().fg(Color::Green).bold()
|
||||
},
|
||||
)));
|
||||
out.extend(preview_lines(text, *full, width, " ", dim));
|
||||
} else {
|
||||
out.push(Line::from(Span::styled(
|
||||
format!("▸ 📦 {} {}{}(点击/Tab 展开)", mark, name, dur),
|
||||
if *is_error {
|
||||
Style::new().fg(Color::Red)
|
||||
} else {
|
||||
Style::new().fg(Color::Green)
|
||||
},
|
||||
)));
|
||||
}
|
||||
}
|
||||
UiBlock::ToolExec {
|
||||
summary,
|
||||
status,
|
||||
duration_ms,
|
||||
output,
|
||||
result_text,
|
||||
is_error,
|
||||
expanded,
|
||||
full,
|
||||
..
|
||||
} => {
|
||||
let running = *status == ExecStatus::Running;
|
||||
let mark = if running {
|
||||
"● 运行中".to_string()
|
||||
} else if *is_error {
|
||||
"✗".to_string()
|
||||
} else {
|
||||
"✓".to_string()
|
||||
};
|
||||
let dur = duration_ms
|
||||
.map(format_duration)
|
||||
.map(|d| format!(" · {}", d))
|
||||
.unwrap_or_default();
|
||||
if *expanded {
|
||||
let state = if running { "● 运行中" } else { "✓" };
|
||||
out.push(Line::from(Span::styled(
|
||||
format!("▾ 🔧 {} · {}", summary, state),
|
||||
Style::new().fg(Color::Magenta).bold(),
|
||||
)));
|
||||
// 运行中:流式输出实时预览;结束后:结果文本(部分/全部)。
|
||||
// Running: live streaming preview; done: result text
|
||||
// (partial/full).
|
||||
let body = if running {
|
||||
output.clone()
|
||||
let header = format!("▾ 🔧 {} · {} {}", summary, mark, dur);
|
||||
let style = if *is_error {
|
||||
Style::new().fg(Color::Red).bold()
|
||||
} else {
|
||||
result_text.clone()
|
||||
Style::new().fg(Color::Magenta).bold()
|
||||
};
|
||||
out.extend(preview_lines(&body, *full, width, " ", dim));
|
||||
out.push(Line::from(Span::styled(header, style)));
|
||||
// 参数:人类可读键值对(在那请求)。
|
||||
// Arguments: human-readable key-value lines (the request).
|
||||
for line in format_args_human(args) {
|
||||
for wrapped in wrap_text(&line, width.saturating_sub(2)) {
|
||||
out.push(Line::from(Span::styled(format!(" {}", wrapped), dim)));
|
||||
}
|
||||
}
|
||||
if !output.is_empty() {
|
||||
out.push(Line::from(Span::styled(
|
||||
" ── 结果 ──",
|
||||
Style::new().fg(Color::DarkGray),
|
||||
)));
|
||||
// 结果:部分/全部(在那返回)。
|
||||
// Result: partial/full (the return).
|
||||
out.extend(preview_lines(output, *full, width, " ", dim));
|
||||
}
|
||||
if !running && *is_error {
|
||||
out.push(Line::from(Span::styled(
|
||||
" ✗ 工具执行失败",
|
||||
|
|
|
|||
|
|
@ -93,17 +93,10 @@ fn new_session_resets_state() {
|
|||
/// back, killing the wheel).
|
||||
#[test]
|
||||
fn wheel_scrolls_line_by_line_in_correct_direction() {
|
||||
use focus_tui::app::BlockGeometry;
|
||||
use focus_tui::state::BlockId;
|
||||
|
||||
let mut app = app_on(&temp_data_dir("wheel"));
|
||||
// 模拟内容 100 行、视口 10 行的几何。
|
||||
// Simulate geometry: 100 content lines, 10-line viewport.
|
||||
app.geometry = vec![BlockGeometry {
|
||||
id: BlockId("x".into()),
|
||||
start: 0,
|
||||
end: 100,
|
||||
}];
|
||||
app.content_lines = 100;
|
||||
app.view_height = 10;
|
||||
|
||||
// 贴底时向上滚:偏移减 1、取消 follow。
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ fn blocks_are_collapsed_by_default_and_summarized() {
|
|||
assert_eq!(text, "secret reasoning");
|
||||
assert_eq!(*duration_ms, None);
|
||||
}
|
||||
UiBlock::ToolCall {
|
||||
UiBlock::Tool {
|
||||
summary, expanded, ..
|
||||
} => {
|
||||
assert!(!expanded);
|
||||
|
|
@ -202,7 +202,7 @@ fn blocks_are_collapsed_by_default_and_summarized() {
|
|||
);
|
||||
let mut expanded_count = 0;
|
||||
for b in &blocks {
|
||||
if let UiBlock::Thinking { expanded, .. } | UiBlock::ToolCall { expanded, .. } = b {
|
||||
if let UiBlock::Thinking { expanded, .. } | UiBlock::Tool { expanded, .. } = b {
|
||||
assert!(*expanded);
|
||||
expanded_count += 1;
|
||||
}
|
||||
|
|
@ -346,20 +346,27 @@ fn commit_partial_clears_live_message() {
|
|||
assert!(rs.partial.is_none());
|
||||
}
|
||||
|
||||
/// 已提交 tool_result 消息的耗时进入摘要行。
|
||||
/// A committed tool_result's duration reaches its summary.
|
||||
/// 工具结果与调用合并为一块时,耗时进入该块。
|
||||
/// When a tool result merges with its call, the duration reaches the block.
|
||||
#[test]
|
||||
fn tool_duration_shows_in_tool_result_block() {
|
||||
let transcript = vec![Message::ToolResult(ToolResultMessage {
|
||||
tool_call_id: "c1".into(),
|
||||
tool_name: "shell".into(),
|
||||
content: vec![ContentBlock::text("ok")],
|
||||
details: JsonValue::obj(),
|
||||
is_error: false,
|
||||
timestamp: 0,
|
||||
})];
|
||||
fn tool_duration_shows_in_merged_tool_block() {
|
||||
let transcript = vec![
|
||||
Message::Assistant(assistant(vec![ContentBlock::ToolCall(ToolCall {
|
||||
id: "c1".into(),
|
||||
name: "shell".into(),
|
||||
arguments: JsonValue::obj(),
|
||||
})])),
|
||||
Message::ToolResult(ToolResultMessage {
|
||||
tool_call_id: "c1".into(),
|
||||
tool_name: "shell".into(),
|
||||
content: vec![ContentBlock::text("ok")],
|
||||
details: JsonValue::obj(),
|
||||
is_error: false,
|
||||
timestamp: 0,
|
||||
}),
|
||||
];
|
||||
let mut durations = HashMap::new();
|
||||
durations.insert((0usize, 0usize), 12345u64);
|
||||
durations.insert((1usize, 0usize), 12345u64); // tool_result 在索引 1 / at index 1
|
||||
let blocks = build_blocks(
|
||||
&transcript,
|
||||
&HashMap::new(),
|
||||
|
|
@ -369,7 +376,7 @@ fn tool_duration_shows_in_tool_result_block() {
|
|||
&HashSet::new(),
|
||||
);
|
||||
let tr = blocks.iter().find_map(|b| match b {
|
||||
UiBlock::ToolResult { duration_ms, .. } => Some(*duration_ms),
|
||||
UiBlock::Tool { duration_ms, .. } => Some(*duration_ms),
|
||||
_ => None,
|
||||
});
|
||||
assert_eq!(tr, Some(Some(12_345)));
|
||||
|
|
@ -406,7 +413,7 @@ fn live_renders_only_running_execs() {
|
|||
&HashSet::new(),
|
||||
);
|
||||
assert!(
|
||||
!blocks.iter().any(|b| matches!(b, UiBlock::ToolExec { .. })),
|
||||
!blocks.iter().any(|b| matches!(b, UiBlock::Tool { .. })),
|
||||
"finished execs must not render live"
|
||||
);
|
||||
|
||||
|
|
@ -430,7 +437,7 @@ fn live_renders_only_running_execs() {
|
|||
assert!(
|
||||
blocks.iter().any(|b| matches!(
|
||||
b,
|
||||
UiBlock::ToolExec {
|
||||
UiBlock::Tool {
|
||||
status: ExecStatus::Running,
|
||||
..
|
||||
}
|
||||
|
|
@ -466,20 +473,27 @@ fn expansion_cycles_three_states() {
|
|||
assert!(!full.contains(&id));
|
||||
}
|
||||
|
||||
/// 二级展开标志进入 ToolResult 块。
|
||||
/// The full-level flag reaches ToolResult blocks.
|
||||
/// 二级展开标志进入合并后的工具块。
|
||||
/// The full-level flag reaches the merged tool block.
|
||||
#[test]
|
||||
fn full_flag_reaches_blocks() {
|
||||
let transcript = vec![Message::ToolResult(ToolResultMessage {
|
||||
tool_call_id: "c1".into(),
|
||||
tool_name: "shell".into(),
|
||||
content: vec![ContentBlock::text("out")],
|
||||
details: JsonValue::obj(),
|
||||
is_error: false,
|
||||
timestamp: 0,
|
||||
})];
|
||||
fn full_flag_reaches_merged_tool_block() {
|
||||
let transcript = vec![
|
||||
Message::Assistant(assistant(vec![ContentBlock::ToolCall(ToolCall {
|
||||
id: "c1".into(),
|
||||
name: "shell".into(),
|
||||
arguments: JsonValue::obj(),
|
||||
})])),
|
||||
Message::ToolResult(ToolResultMessage {
|
||||
tool_call_id: "c1".into(),
|
||||
tool_name: "shell".into(),
|
||||
content: vec![ContentBlock::text("out")],
|
||||
details: JsonValue::obj(),
|
||||
is_error: false,
|
||||
timestamp: 0,
|
||||
}),
|
||||
];
|
||||
let mut full = HashSet::new();
|
||||
full.insert(BlockId("m0-r".into()));
|
||||
full.insert(BlockId("m0-0".into())); // 工具调用在 assistant 的索引 0
|
||||
let blocks = build_blocks(
|
||||
&transcript,
|
||||
&HashMap::new(),
|
||||
|
|
@ -491,9 +505,9 @@ fn full_flag_reaches_blocks() {
|
|||
let tr = blocks
|
||||
.iter()
|
||||
.find_map(|b| match b {
|
||||
UiBlock::ToolResult { full, .. } => Some(*full),
|
||||
UiBlock::Tool { full, .. } => Some(*full),
|
||||
_ => None,
|
||||
})
|
||||
.expect("tool result block");
|
||||
.expect("merged tool block");
|
||||
assert!(tr, "full flag must propagate");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue