fix(ai): show chat step details
This commit is contained in:
parent
9498e8d37d
commit
8b28d7e51f
|
|
@ -37,6 +37,10 @@ const MSG_GAP: f32 = 10.0;
|
|||
const SUB_GAP: f32 = 4.0;
|
||||
/// Height of one compact design-progress step row.
|
||||
const ACTION_STEP_H: f32 = 28.0;
|
||||
/// Height of one detail line under a progress step.
|
||||
const ACTION_DETAIL_LINE_H: f32 = 14.0;
|
||||
/// Gap between the progress title row and detail lines.
|
||||
const ACTION_DETAIL_GAP: f32 = 4.0;
|
||||
/// Vertical gap between compact design-progress rows.
|
||||
const ACTION_STEP_GAP: f32 = 4.0;
|
||||
/// Side length of an image thumbnail box.
|
||||
|
|
@ -75,6 +79,7 @@ pub(crate) struct Collapsible {
|
|||
pub(crate) struct ActionStep {
|
||||
pub rect: Rect,
|
||||
pub label: String,
|
||||
pub details: Vec<String>,
|
||||
pub done: bool,
|
||||
pub active: bool,
|
||||
pub failed: bool,
|
||||
|
|
@ -209,14 +214,21 @@ fn build_item(
|
|||
let total_steps = progress_steps.len();
|
||||
for (i, step) in progress_steps.iter().enumerate() {
|
||||
let (done, active, failed) = step_state(step, msg.streaming, i, total_steps);
|
||||
let details: Vec<String> = step
|
||||
.details
|
||||
.iter()
|
||||
.flat_map(|line| wrap_units(line, budget.saturating_sub(4)))
|
||||
.collect();
|
||||
let step_h = action_step_height(details.len());
|
||||
steps.push(ActionStep {
|
||||
rect: Rect::xywh(x, y, bubble_w, ACTION_STEP_H),
|
||||
rect: Rect::xywh(x, y, bubble_w, step_h),
|
||||
label: step.title.clone(),
|
||||
details,
|
||||
done,
|
||||
active,
|
||||
failed,
|
||||
});
|
||||
y += ACTION_STEP_H + ACTION_STEP_GAP;
|
||||
y += step_h + ACTION_STEP_GAP;
|
||||
}
|
||||
|
||||
let thinking = build_collapsible(
|
||||
|
|
@ -303,6 +315,14 @@ fn build_item(
|
|||
)
|
||||
}
|
||||
|
||||
fn action_step_height(detail_count: usize) -> f32 {
|
||||
if detail_count == 0 {
|
||||
ACTION_STEP_H
|
||||
} else {
|
||||
ACTION_STEP_H + ACTION_DETAIL_GAP + detail_count as f32 * ACTION_DETAIL_LINE_H
|
||||
}
|
||||
}
|
||||
|
||||
/// Lay out the tail of `messages` that fits inside `body_rect`,
|
||||
/// top-aligned. Each item carries absolute rects ready for paint and
|
||||
/// hit-test.
|
||||
|
|
@ -523,6 +543,23 @@ fn paint_action_step(cx: &mut PaintCx<'_>, theme: &Theme, step: &ActionStep) {
|
|||
11.0,
|
||||
label_color,
|
||||
);
|
||||
if !step.details.is_empty() {
|
||||
cx.backend.save();
|
||||
cx.backend.clip_rect(step.rect);
|
||||
let mut baseline = step.rect.origin.y + ACTION_STEP_H + ACTION_DETAIL_GAP + 9.0;
|
||||
for line in &step.details {
|
||||
draw_line(
|
||||
cx,
|
||||
line,
|
||||
step.rect.origin.x + 32.0,
|
||||
baseline,
|
||||
10.0,
|
||||
theme.muted_foreground,
|
||||
);
|
||||
baseline += ACTION_DETAIL_LINE_H;
|
||||
}
|
||||
cx.backend.restore();
|
||||
}
|
||||
}
|
||||
|
||||
/// Paint a collapsible block — header row (chevron + label) plus,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ pub(crate) enum ParsedStepStatus {
|
|||
pub(crate) struct ParsedStep {
|
||||
pub title: String,
|
||||
pub status: Option<ParsedStepStatus>,
|
||||
pub details: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
|
@ -28,6 +29,7 @@ pub(crate) fn split_design_progress(thinking: &str) -> (Vec<ParsedStep>, String)
|
|||
steps.push(ParsedStep {
|
||||
title: label.to_string(),
|
||||
status: None,
|
||||
details: Vec::new(),
|
||||
});
|
||||
}
|
||||
} else if !trimmed.is_empty() {
|
||||
|
|
@ -81,7 +83,7 @@ fn finish_extraction(steps: Vec<ParsedStep>, visible: String) -> StepExtraction
|
|||
}
|
||||
}
|
||||
|
||||
fn parsed_step(attrs: &str, _content: &str, partial: bool) -> ParsedStep {
|
||||
fn parsed_step(attrs: &str, content: &str, partial: bool) -> ParsedStep {
|
||||
let default_title = if partial { "Design" } else { "Processing" };
|
||||
ParsedStep {
|
||||
title: attr_value(attrs, "title")
|
||||
|
|
@ -89,9 +91,19 @@ fn parsed_step(attrs: &str, _content: &str, partial: bool) -> ParsedStep {
|
|||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or_else(|| default_title.to_string()),
|
||||
status: attr_value(attrs, "status").and_then(|s| parse_status(s.trim())),
|
||||
details: step_details(content),
|
||||
}
|
||||
}
|
||||
|
||||
fn step_details(content: &str) -> Vec<String> {
|
||||
content
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|line| !line.is_empty())
|
||||
.map(str::to_string)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn parse_status(value: &str) -> Option<ParsedStepStatus> {
|
||||
match value.to_ascii_lowercase().as_str() {
|
||||
"pending" => Some(ParsedStepStatus::Pending),
|
||||
|
|
@ -163,6 +175,7 @@ mod tests {
|
|||
vec![ParsedStep {
|
||||
title: "Check".into(),
|
||||
status: Some(ParsedStepStatus::Done),
|
||||
details: vec!["ok".into()],
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -154,6 +154,35 @@ fn step_tag_content_renders_as_progress_not_raw_bubble() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn step_tag_content_surfaces_as_progress_details() {
|
||||
let mut message = ChatMessage::assistant_streaming();
|
||||
message.content = r#"<step title="Validate design" status="streaming">
|
||||
lint: fixed spacing
|
||||
render: captured frame
|
||||
</step>"#
|
||||
.into();
|
||||
|
||||
let items = build_transcript(
|
||||
std::slice::from_ref(&message),
|
||||
body(),
|
||||
op_editor_core::Locale::EnUs,
|
||||
);
|
||||
|
||||
assert_eq!(items[0].steps.len(), 1);
|
||||
assert_eq!(
|
||||
items[0].steps[0].details,
|
||||
vec![
|
||||
"lint: fixed spacing".to_string(),
|
||||
"render: captured frame".to_string()
|
||||
]
|
||||
);
|
||||
assert!(
|
||||
items[0].steps[0].rect.size.y > 28.0,
|
||||
"step details should reserve space instead of being dropped"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_calls_block_header_label_counts_the_calls() {
|
||||
let mut m = ChatMessage::assistant("done");
|
||||
|
|
|
|||
Loading…
Reference in a new issue