From 2fcfb686a632c84f69770517f903df0d69fe8586 Mon Sep 17 00:00:00 2001 From: Fini Date: Mon, 1 Jun 2026 04:52:38 +0800 Subject: [PATCH] fix(ai): collapse completed design steps --- .../src/widgets/ai_chat_transcript.rs | 26 ++++++-- .../src/widgets/ai_chat_transcript_tests.rs | 61 +++++++++++++++++++ 2 files changed, 83 insertions(+), 4 deletions(-) diff --git a/crates/op-editor-ui/src/widgets/ai_chat_transcript.rs b/crates/op-editor-ui/src/widgets/ai_chat_transcript.rs index 436515e22..3d459dc0e 100644 --- a/crates/op-editor-ui/src/widgets/ai_chat_transcript.rs +++ b/crates/op-editor-ui/src/widgets/ai_chat_transcript.rs @@ -93,6 +93,7 @@ pub(crate) struct ActionStep { pub rect: Rect, pub label: String, pub details: Vec, + pub expanded: bool, pub done: bool, pub active: bool, pub failed: bool, @@ -258,11 +259,13 @@ fn build_item( .iter() .flat_map(|line| wrap_units(line, budget.saturating_sub(4))) .collect(); - let step_h = action_step_height(details.len()); + let expanded = active; + let step_h = action_step_height(expanded, details.len()); steps.push(ActionStep { rect: Rect::xywh(x, y, bubble_w, step_h), label: step.title.clone(), details, + expanded, done, active, failed, @@ -395,8 +398,8 @@ fn build_item( ) } -fn action_step_height(detail_count: usize) -> f32 { - if detail_count == 0 { +fn action_step_height(expanded: bool, detail_count: usize) -> f32 { + if !expanded || detail_count == 0 { ACTION_STEP_H } else { ACTION_STEP_H + ACTION_DETAIL_GAP + detail_count as f32 * ACTION_DETAIL_LINE_H @@ -528,7 +531,22 @@ fn paint_action_step(cx: &mut PaintCx<'_>, theme: &Theme, step: &ActionStep) { 11.0, label_color, ); - if !step.details.is_empty() { + draw_icon( + cx.backend, + if step.expanded { + Icon::ChevronDown + } else { + Icon::ChevronRight + }, + Point2D::new( + step.rect.origin.x + step.rect.size.x - 20.0, + step.rect.origin.y + 7.0, + ), + 14.0, + theme.muted_foreground, + 1.5, + ); + if step.expanded && !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; diff --git a/crates/op-editor-ui/src/widgets/ai_chat_transcript_tests.rs b/crates/op-editor-ui/src/widgets/ai_chat_transcript_tests.rs index 2f291eb65..4607b3901 100644 --- a/crates/op-editor-ui/src/widgets/ai_chat_transcript_tests.rs +++ b/crates/op-editor-ui/src/widgets/ai_chat_transcript_tests.rs @@ -344,6 +344,67 @@ render: captured frame ); } +#[test] +fn completed_step_with_content_defaults_collapsed_like_ts_accordion() { + let mut message = ChatMessage::assistant( + r#" +lint: fixed spacing +render: captured frame +"#, + ); + message.streaming = false; + + let items = build_transcript( + std::slice::from_ref(&message), + body(), + op_editor_core::Locale::EnUs, + ); + + assert_eq!(items[0].steps.len(), 1); + assert!(items[0].steps[0].done); + assert!(!items[0].steps[0].active); + assert!( + (items[0].steps[0].rect.size.y - ACTION_STEP_H).abs() < 1e-4, + "TS ActionStepItem defaults completed accordions closed" + ); +} + +#[test] +fn paint_completed_step_hides_details_like_collapsed_ts_accordion() { + let message = ChatMessage::assistant( + r#" +lint: fixed spacing +render: captured frame +"#, + ); + let mut backend = TranscriptPaintBackend::default(); + let mut cx = PaintCx { + backend: &mut backend, + }; + + paint_transcript( + &mut cx, + &crate::Theme::dark(), + body(), + &[message], + 0, + op_editor_core::Locale::EnUs, + ); + + assert!( + backend.texts.iter().any(|text| text == "Validate design"), + "collapsed accordion still paints its title" + ); + assert!( + !backend + .texts + .iter() + .any(|text| text.contains("lint: fixed spacing") + || text.contains("render: captured frame")), + "collapsed TS accordions hide details until opened" + ); +} + #[test] fn assistant_design_json_code_fence_renders_compact_design_block() { let message = ChatMessage::assistant(