diff --git a/crates/op-editor-ui/src/widgets/ai_chat_panel.rs b/crates/op-editor-ui/src/widgets/ai_chat_panel.rs
index 9798bad53..147b849d1 100644
--- a/crates/op-editor-ui/src/widgets/ai_chat_panel.rs
+++ b/crates/op-editor-ui/src/widgets/ai_chat_panel.rs
@@ -585,13 +585,9 @@ impl<'a> Widget for AIChatPlaceholder<'a> {
self.theme.muted_foreground,
1.4,
);
-<<<<<<< HEAD
- model_x += 18.0;
// Concurrency chip — `Zap` + `{n}x`. A solo 1x team rests as a
// faint ghost; a staffed team (>1) gets a primary-tinted chip so
// the parallel mode reads as active.
-=======
->>>>>>> 926f84b6 (feat(editor-ui): canvas + panel widget batch for TS parity)
let chip = footer.agent_team;
let team_size = self.state.agent_team_size;
let team_active = team_size > 1;
@@ -782,13 +778,10 @@ pub(crate) fn to_jian_color(c: Color) -> jian_core::scene::Color {
#[cfg(test)]
#[path = "ai_chat_panel/tests.rs"]
mod tests;
-<<<<<<< HEAD
#[cfg(test)]
#[path = "ai_chat_panel/tests_paint.rs"]
mod tests_paint;
-=======
#[cfg(test)]
#[path = "ai_chat_panel/tests_transcript.rs"]
mod tests_transcript;
->>>>>>> 926f84b6 (feat(editor-ui): canvas + panel widget batch for TS parity)
diff --git a/crates/op-editor-ui/src/widgets/ai_chat_panel/tests.rs b/crates/op-editor-ui/src/widgets/ai_chat_panel/tests.rs
index 00416738a..163dad552 100644
--- a/crates/op-editor-ui/src/widgets/ai_chat_panel/tests.rs
+++ b/crates/op-editor-ui/src/widgets/ai_chat_panel/tests.rs
@@ -2,6 +2,8 @@
//! Split into a sibling file to keep `ai_chat_panel.rs` under the
//! 800-line cap.
+#[allow(unused_imports)]
+use super::tests_paint::{assert_close, color_close, rect_close};
use super::*;
use crate::widgets::ai_chat_hit::{AIChatHit, ChatResizeEdge};
@@ -243,38 +245,6 @@ fn footer_agent_team_chip_is_clickable_and_hoverable() {
);
}
-#[test]
-<<<<<<< HEAD
-=======
-fn footer_selection_count_sits_close_to_agent_team_chip() {
- let mut s = EditorState::new();
- seed_available_model(&mut s);
- s.selection.set = vec![op_editor_core::NodeId::new("n1")];
- s.selection.anchor = op_editor_core::NodeId::new("n1");
- let panel = AIChatPlaceholder::from_editor(&s);
- let rect = Rect::xywh(0.0, 0.0, AI_CHAT_WIDTH, AI_CHAT_HEIGHT);
- let input = panel.input_rect(rect);
- let toolbar_top = input.origin.y + INPUT_AREA_HEIGHT;
- let footer = panel.footer_layout(rect, input, toolbar_top);
- let mut backend = PanelPaintBackend::default();
- let mut cx = PaintCx {
- backend: &mut backend,
- };
-
- panel.paint(&mut cx, rect);
-
- let selected_text =
- op_i18n::translate(panel.locale, "common.selected").replace("{{count}}", "1");
- let (_, _, _, origin) = backend
- .texts
- .iter()
- .find(|(text, _, _, _)| text == &selected_text)
- .expect("footer should paint selected-count label");
- let gap = origin.x - (footer.agent_team.origin.x + footer.agent_team.size.x);
- assert_close(gap, 4.0);
-}
-
-#[test]
fn multiline_input_expands_above_footer_toolbar() {
let mut s = EditorState::new();
s.chat.input =
@@ -289,140 +259,6 @@ fn multiline_input_expands_above_footer_toolbar() {
assert!(panel.input_height() > INPUT_BASE_HEIGHT);
}
-#[test]
-fn paint_send_button_hover_adds_visible_feedback() {
- let mut s = EditorState::new();
- seed_available_model(&mut s);
- s.chat.input = "design a login page".into();
- s.editor_ui.chat_footer_hover = Some(op_editor_core::ChatFooterButton::Send);
- let panel = AIChatPlaceholder::from_editor(&s);
- let rect = Rect::xywh(0.0, 0.0, AI_CHAT_WIDTH, AI_CHAT_HEIGHT);
- let mut backend = PanelPaintBackend::default();
- let mut cx = PaintCx {
- backend: &mut backend,
- };
-
- panel.paint(&mut cx, rect);
-
- let send_rect = Rect {
- origin: Point2D::new(AI_CHAT_WIDTH - PAD - 24.0, toolbar_center_y() - 24.0 / 2.0),
- size: Point2D::new(24.0, 24.0),
- };
- let fills: Vec<_> = backend
- .round_rects
- .iter()
- .filter(|(r, _, _)| rect_close(*r, send_rect))
- .collect();
-
- assert!(
- fills.len() >= 2,
- "hovered send button should paint feedback over its base fill"
- );
-}
-
-#[test]
-fn paint_footer_neutral_hovers_use_visible_feedback() {
- let cases = [
- op_editor_core::ChatFooterButton::ModelPicker,
- op_editor_core::ChatFooterButton::AgentTeam,
- op_editor_core::ChatFooterButton::AddAttachment,
- op_editor_core::ChatFooterButton::Send,
- ];
-
- for hover in cases {
- let mut s = EditorState::new();
- seed_available_model(&mut s);
- s.editor_ui.chat_footer_hover = Some(hover);
- let panel = AIChatPlaceholder::from_editor(&s);
- let rect = Rect::xywh(0.0, 0.0, AI_CHAT_WIDTH, AI_CHAT_HEIGHT);
- let input = panel.input_rect(rect);
- let toolbar_top = input.origin.y + INPUT_AREA_HEIGHT;
- let footer = panel.footer_layout(rect, input, toolbar_top);
- let target = match hover {
- op_editor_core::ChatFooterButton::ModelPicker => footer.model,
- op_editor_core::ChatFooterButton::AgentTeam => footer.agent_team,
- op_editor_core::ChatFooterButton::AddAttachment => footer.attach,
- op_editor_core::ChatFooterButton::Send => footer.send,
- op_editor_core::ChatFooterButton::Stop => unreachable!(),
- };
- let mut backend = PanelPaintBackend::default();
- let mut cx = PaintCx {
- backend: &mut backend,
- };
-
- panel.paint(&mut cx, rect);
-
- assert!(
- backend.round_rects.iter().any(|(r, _, color)| {
- rect_close(*r, target)
- && !color_close(*color, panel.theme.muted)
- && color.a > panel.theme.button_hover.a + 0.01
- }),
- "{hover:?} hover should paint a visible neutral wash"
- );
- }
-}
-
-#[test]
-fn paint_model_picker_hover_stays_inside_model_chip() {
- let mut s = EditorState::new();
- seed_available_model(&mut s);
- s.editor_ui.chat_footer_hover = Some(op_editor_core::ChatFooterButton::ModelPicker);
- let panel = AIChatPlaceholder::from_editor(&s);
- let rect = Rect::xywh(0.0, 0.0, AI_CHAT_WIDTH, AI_CHAT_HEIGHT);
- let input = panel.input_rect(rect);
- let toolbar_top = input.origin.y + INPUT_AREA_HEIGHT;
- let footer = panel.footer_layout(rect, input, toolbar_top);
- let mut backend = PanelPaintBackend::default();
- let mut cx = PaintCx {
- backend: &mut backend,
- };
-
- panel.paint(&mut cx, rect);
-
- let hover = backend
- .round_rects
- .iter()
- .find(|(r, _, color)| {
- rect_close(*r, footer.model)
- && color_close(*color, chat_neutral_hover_color(&panel.theme))
- })
- .expect("model picker hover should paint a visible wash");
-
- assert!(
- hover.0.origin.x + hover.0.size.x <= footer.agent_team.origin.x - 6.0,
- "model hover should leave visible spacing before the Agent Team chip"
- );
-}
-
-#[test]
-fn paint_expanded_header_title_hover_adds_visible_feedback_across_label() {
- let mut s = EditorState::new();
- s.editor_ui.chat_header_hover = Some(op_editor_core::ChatHeaderButton::ToggleCollapse);
- let panel = AIChatPlaceholder::from_editor(&s);
- let rect = Rect::xywh(0.0, 0.0, AI_CHAT_WIDTH, AI_CHAT_HEIGHT);
- let mut backend = PanelPaintBackend::default();
- let mut cx = PaintCx {
- backend: &mut backend,
- };
-
- panel.paint(&mut cx, rect);
-
- assert!(
- backend.round_rects.iter().any(|(r, _, color)| {
- r.origin.x <= PAD
- && r.origin.y <= 6.0
- && r.size.x >= 108.0
- && r.size.y >= 28.0
- && r.size.y <= 34.0
- && color_close(*color, chat_neutral_hover_color(&panel.theme))
- }),
- "expanded New Chat title hover should cover the label, not only the chevron"
- );
-}
-
-#[test]
->>>>>>> 926f84b6 (feat(editor-ui): canvas + panel widget batch for TS parity)
fn hit_test_resolves_model_search_clear_button() {
let mut s = EditorState::new();
seed_available_model(&mut s);
@@ -599,184 +435,6 @@ fn hit_test_resolves_header_new_chat_button() {
assert_eq!(panel.hit_test(rect, p), Some(AIChatHit::NewChat));
}
-<<<<<<< HEAD
-#[test]
-fn body_rect_reserves_space_for_fixed_step_checklist() {
- let mut s = EditorState::new();
- let mut message = op_editor_core::ChatMessage::assistant_streaming();
- message.content =
- r#"Analyzing request..."#
- .into();
- s.chat.messages.push(message);
-
- let panel = AIChatPlaceholder::from_editor(&s);
- let rect = Rect::xywh(0.0, 0.0, AI_CHAT_WIDTH, AI_CHAT_HEIGHT);
- let body = panel.body_rect(rect);
- let legacy_bottom = rect.origin.y + rect.size.y - INPUT_BASE_HEIGHT - PAD - 8.0;
-
- assert!(
- body.origin.y + body.size.y < legacy_bottom - 1.0,
- "fixed step checklist should reserve bottom space outside transcript"
- );
-}
-
-#[test]
-fn default_height_preserves_transcript_space_above_full_checklist() {
- let mut s = EditorState::new();
- let mut message = op_editor_core::ChatMessage::assistant_streaming();
- message.content = (0..17)
- .map(|idx| {
- format!(r#"Generated section {idx}"#)
- })
- .collect::>()
- .join("\n");
- s.chat.messages.push(message);
-
- let panel = AIChatPlaceholder::from_editor(&s);
- let rect = Rect::xywh(0.0, 0.0, AI_CHAT_WIDTH, AI_CHAT_HEIGHT);
- let body = panel.body_rect(rect);
-
- assert!(
- body.size.y >= 150.0,
- "default chat panel height should leave room for prior chat above the pinned checklist, got {}",
- body.size.y
- );
-}
-
-#[test]
-fn body_rect_reserves_less_space_when_fixed_step_checklist_collapsed() {
- let mut expanded_state = EditorState::new();
- let mut message = op_editor_core::ChatMessage::assistant_streaming();
- message.content = r#"
-"#
- .into();
- expanded_state.chat.messages.push(message.clone());
-
- let mut collapsed_state = EditorState::new();
- collapsed_state.chat.messages.push(message);
- collapsed_state.chat.checklist_collapsed = true;
-
- let rect = Rect::xywh(0.0, 0.0, AI_CHAT_WIDTH, AI_CHAT_HEIGHT);
- let expanded = AIChatPlaceholder::from_editor(&expanded_state).body_rect(rect);
- let collapsed = AIChatPlaceholder::from_editor(&collapsed_state).body_rect(rect);
-
- assert!(collapsed.size.y > expanded.size.y);
-}
-
-#[test]
-fn hit_test_resolves_fixed_checklist_header_toggle() {
- let mut s = EditorState::new();
- let mut message = op_editor_core::ChatMessage::assistant_streaming();
- message.content = r#"
-"#
- .into();
- s.chat.messages.push(message);
-
- let panel = AIChatPlaceholder::from_editor(&s);
- let rect = Rect::xywh(0.0, 0.0, AI_CHAT_WIDTH, AI_CHAT_HEIGHT);
- let checklist_h = fixed_checklist_height(&s.chat.messages, s.chat.checklist_collapsed);
- let checklist = fixed_checklist_rect(rect, INPUT_BASE_HEIGHT, checklist_h);
- let p = Point2D::new(
- checklist.origin.x + checklist.size.x / 2.0,
- checklist.origin.y + 2.0 + 32.0 / 2.0,
- );
-
- assert_eq!(panel.hit_test(rect, p), Some(AIChatHit::ToggleChecklist));
-}
-
-#[test]
-fn hit_test_resolves_individual_tool_card_header_toggle() {
- let mut s = EditorState::new();
- let mut message = op_editor_core::ChatMessage::assistant("answer");
- message.tools_collapsed = false;
- message.tool_calls.push(op_editor_core::ChatToolCall {
- name: "snapshot_layout".into(),
- args: r#"{"args":{"pageId":"page-1"}}"#.into(),
- });
- s.chat.messages.push(message);
-
- let panel = AIChatPlaceholder::from_editor(&s);
- let rect = Rect::xywh(0.0, 0.0, AI_CHAT_WIDTH, AI_CHAT_HEIGHT);
- let card_header = crate::widgets::ai_chat_transcript::build_transcript(
- &s.chat.messages,
- panel.body_rect(rect),
- panel.locale,
- )[0]
- .tools
- .as_ref()
- .unwrap()
- .cards[0]
- .header;
- let p = Point2D::new(
- card_header.origin.x + card_header.size.x / 2.0,
- card_header.origin.y + card_header.size.y / 2.0,
- );
-
- assert_eq!(
- panel.hit_test(rect, p),
- Some(AIChatHit::SetToolCallCardExpanded(0, 0, true))
- );
-}
-
-#[test]
-fn hit_test_resolves_design_block_header_toggle() {
- let mut s = EditorState::new();
- s.chat.messages.push(op_editor_core::ChatMessage::assistant(
- r#"```json
-[{"id":"frame-1","type":"Frame"}]
-```"#,
- ));
-
- let panel = AIChatPlaceholder::from_editor(&s);
- let rect = Rect::xywh(0.0, 0.0, AI_CHAT_WIDTH, AI_CHAT_HEIGHT);
- let header = crate::widgets::ai_chat_transcript::build_transcript(
- &s.chat.messages,
- panel.body_rect(rect),
- panel.locale,
- )[0]
- .design_blocks[0]
- .header;
- let p = Point2D::new(
- header.origin.x + header.size.x / 2.0,
- header.origin.y + header.size.y / 2.0,
- );
-
- assert_eq!(
- panel.hit_test(rect, p),
- Some(AIChatHit::SetDesignBlockExpanded(0, 0, true))
- );
-}
-
-#[test]
-fn hit_test_resolves_design_block_copy_button() {
- let code = r#"[{"id":"frame-1","type":"Frame"}]"#;
- let mut s = EditorState::new();
- s.chat
- .messages
- .push(op_editor_core::ChatMessage::assistant(format!(
- r#"```json
-{code}
-```"#
- )));
-
- let panel = AIChatPlaceholder::from_editor(&s);
- let rect = Rect::xywh(0.0, 0.0, AI_CHAT_WIDTH, AI_CHAT_HEIGHT);
- let block = &crate::widgets::ai_chat_transcript::build_transcript(
- &s.chat.messages,
- panel.body_rect(rect),
- panel.locale,
- )[0]
- .design_blocks[0];
- let p = Point2D::new(
- block.header.origin.x + block.header.size.x - 38.0,
- block.header.origin.y + block.header.size.y / 2.0,
- );
-
- assert_eq!(
- panel.hit_test(rect, p),
- Some(AIChatHit::CopyDesignBlock(code.to_string()))
- );
-=======
// Shared paint-assertion infrastructure — also used by the
// sibling `tests_transcript` module (split at the 800-line cap).
#[derive(Default)]
@@ -835,5 +493,4 @@ pub(in super::super) fn has_fill_rect(fills: &[(Rect, crate::Color)], expected:
&& (rect.size.x - expected.size.x).abs() < 1e-4
&& (rect.size.y - expected.size.y).abs() < 1e-4
})
->>>>>>> 926f84b6 (feat(editor-ui): canvas + panel widget batch for TS parity)
}
diff --git a/crates/op-editor-ui/src/widgets/ai_chat_panel/tests_paint.rs b/crates/op-editor-ui/src/widgets/ai_chat_panel/tests_paint.rs
index 64e16882a..be05b7435 100644
--- a/crates/op-editor-ui/src/widgets/ai_chat_panel/tests_paint.rs
+++ b/crates/op-editor-ui/src/widgets/ai_chat_panel/tests_paint.rs
@@ -6,21 +6,21 @@
use super::tests::{seed_available_model, toolbar_center_y};
use super::*;
-fn assert_close(actual: f32, expected: f32) {
+pub(in super::super) fn assert_close(actual: f32, expected: f32) {
assert!(
(actual - expected).abs() < 1e-4,
"expected {actual} to be close to {expected}"
);
}
-fn rect_close(actual: Rect, expected: Rect) -> bool {
+pub(in super::super) fn rect_close(actual: Rect, expected: Rect) -> bool {
(actual.origin.x - expected.origin.x).abs() < 0.01
&& (actual.origin.y - expected.origin.y).abs() < 0.01
&& (actual.size.x - expected.size.x).abs() < 0.01
&& (actual.size.y - expected.size.y).abs() < 0.01
}
-fn color_close(actual: crate::Color, expected: crate::Color) -> bool {
+pub(in super::super) fn color_close(actual: crate::Color, expected: crate::Color) -> bool {
(actual.r - expected.r).abs() < 0.001
&& (actual.g - expected.g).abs() < 0.001
&& (actual.b - expected.b).abs() < 0.001
@@ -217,6 +217,35 @@ fn paint_model_picker_hover_stays_inside_model_chip() {
);
}
+#[test]
+fn footer_selection_count_sits_close_to_agent_team_chip() {
+ let mut s = EditorState::new();
+ seed_available_model(&mut s);
+ s.selection.set = vec![op_editor_core::NodeId::new("n1")];
+ s.selection.anchor = op_editor_core::NodeId::new("n1");
+ let panel = AIChatPlaceholder::from_editor(&s);
+ let rect = Rect::xywh(0.0, 0.0, AI_CHAT_WIDTH, AI_CHAT_HEIGHT);
+ let input = panel.input_rect(rect);
+ let toolbar_top = input.origin.y + INPUT_AREA_HEIGHT;
+ let footer = panel.footer_layout(rect, input, toolbar_top);
+ let mut backend = PanelPaintBackend::default();
+ let mut cx = PaintCx {
+ backend: &mut backend,
+ };
+
+ panel.paint(&mut cx, rect);
+
+ let selected_text =
+ op_i18n::translate(panel.locale, "common.selected").replace("{{count}}", "1");
+ let (_, _, _, origin) = backend
+ .texts
+ .iter()
+ .find(|(text, _, _, _)| text == &selected_text)
+ .expect("footer should paint selected-count label");
+ let gap = origin.x - (footer.agent_team.origin.x + footer.agent_team.size.x);
+ assert_close(gap, 4.0);
+}
+
#[test]
fn paint_expanded_header_title_hover_adds_visible_feedback_across_label() {
let mut s = EditorState::new();