fix(ai): add chat panel resize handles

This commit is contained in:
Fini 2026-06-01 02:03:38 +08:00
parent 31df6308d0
commit 6fc604a184
13 changed files with 341 additions and 13 deletions

View file

@ -285,6 +285,9 @@ impl ChatAnchor {
}
}
pub const DEFAULT_CHAT_PANEL_WIDTH: f32 = 360.0;
pub const DEFAULT_CHAT_PANEL_HEIGHT: f32 = 400.0;
/// Floating AI chat panel state — mirrors shell-core's `ChatState`
/// (messages, input draft, focused flag, panel anchor, model catalog).
#[derive(Debug, Clone)]
@ -294,6 +297,16 @@ pub struct ChatState {
pub focused: bool,
/// Which canvas corner the floating chat panel snaps to.
pub anchor: ChatAnchor,
/// Non-maximized panel width. TS persists this as
/// `panelWidth` in the AI UI store.
pub panel_width: f32,
/// Non-maximized panel height. TS persists this as
/// `panelHeight` in the AI UI store.
pub panel_height: f32,
/// Absolute top-left while the user has resized from an edge
/// that moves the panel origin. `None` falls back to the snapped
/// corner anchor.
pub panel_position: Option<(f32, f32)>,
/// Collapsed state — when true the panel paints only its header.
pub collapsed: bool,
/// Maximized state — when true the host lays the panel out across
@ -370,6 +383,9 @@ impl Default for ChatState {
input: String::new(),
focused: false,
anchor: ChatAnchor::BottomLeft,
panel_width: DEFAULT_CHAT_PANEL_WIDTH,
panel_height: DEFAULT_CHAT_PANEL_HEIGHT,
panel_position: None,
collapsed: false,
maximized: false,
checklist_collapsed: false,

View file

@ -14,6 +14,8 @@ pub enum AIChatHit {
/// Click landed on the header / margin — host should start a
/// drag so the user can move the panel between canvas corners.
DragHandle,
/// Press landed on one of the invisible TS-style resize handles.
Resize(ChatResizeEdge),
/// Click on the chevron at the top-left of the header — host
/// flips the `ChatState::collapsed` flag.
ToggleCollapse,
@ -66,6 +68,18 @@ pub enum AIChatHit {
ToggleChecklist,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChatResizeEdge {
N,
S,
E,
W,
Ne,
Nw,
Se,
Sw,
}
impl From<super::ai_chat_transcript::TranscriptHit> for AIChatHit {
fn from(hit: super::ai_chat_transcript::TranscriptHit) -> Self {
match hit {

View file

@ -2,7 +2,7 @@ use crate::theme::Theme;
use crate::widgets::ai_chat_checklist::{
fixed_checklist_height, fixed_checklist_rect, paint_fixed_checklist, HEADER_H, PROGRESS_H,
};
use crate::widgets::ai_chat_hit::AIChatHit;
use crate::widgets::ai_chat_hit::{AIChatHit, ChatResizeEdge};
use crate::widgets::ai_chat_panel_controls::{
attachment_row_hit, paint_attachment_row, ATTACHMENT_ROW_HEIGHT,
};
@ -16,12 +16,17 @@ use crate::{Color, Point2D, Rect, TextLayout};
use op_editor_core::chat::ChatState;
use op_editor_core::EditorState;
pub const AI_CHAT_WIDTH: f32 = 360.0;
pub const AI_CHAT_HEIGHT: f32 = 400.0;
pub const AI_CHAT_WIDTH: f32 = op_editor_core::chat::DEFAULT_CHAT_PANEL_WIDTH;
pub const AI_CHAT_HEIGHT: f32 = op_editor_core::chat::DEFAULT_CHAT_PANEL_HEIGHT;
pub const AI_CHAT_MIN_WIDTH: f32 = 280.0;
pub const AI_CHAT_MIN_HEIGHT: f32 = 250.0;
pub const AI_CHAT_MAX_RATIO: f32 = 0.8;
pub const AI_CHAT_COLLAPSED_WIDTH: f32 = 150.0;
pub const AI_CHAT_COLLAPSED_HEIGHT: f32 = 36.0;
pub(crate) const PAD: f32 = 16.0;
pub(crate) const HEADER_HEIGHT: f32 = 36.0;
const RESIZE_GUTTER: f32 = 4.0;
const RESIZE_CORNER: f32 = 12.0;
const INPUT_AREA_HEIGHT: f32 = 56.0;
const INPUT_TOOLBAR_HEIGHT: f32 = 40.0;
const MODEL_CHIP_W: f32 = 150.0;
@ -194,6 +199,9 @@ impl<'a> AIChatPlaceholder<'a> {
}
pub fn hit_test(&self, rect: Rect, point: Point2D) -> Option<AIChatHit> {
if let Some(edge) = self.resize_edge_at(rect, point) {
return Some(AIChatHit::Resize(edge));
}
if !rect_contains(rect, point) {
return None;
}
@ -362,6 +370,58 @@ impl<'a> AIChatPlaceholder<'a> {
Some(AIChatHit::DragHandle)
}
pub fn resize_edge_at(&self, rect: Rect, point: Point2D) -> Option<ChatResizeEdge> {
if self.state.collapsed || self.state.maximized {
return None;
}
let left = rect.origin.x;
let right = rect.origin.x + rect.size.x;
let top = rect.origin.y;
let bottom = rect.origin.y + rect.size.y;
let outer = Rect::xywh(
left - RESIZE_GUTTER,
top - RESIZE_GUTTER,
rect.size.x + RESIZE_GUTTER * 2.0,
rect.size.y + RESIZE_GUTTER * 2.0,
);
if !rect_contains(outer, point) {
return None;
}
let near_top = (point.y - top).abs() <= RESIZE_GUTTER;
let near_bottom = (point.y - bottom).abs() <= RESIZE_GUTTER;
let near_left = (point.x - left).abs() <= RESIZE_GUTTER;
let near_right = (point.x - right).abs() <= RESIZE_GUTTER;
let in_left_corner = point.x <= left + RESIZE_CORNER;
let in_right_corner = point.x >= right - RESIZE_CORNER;
let in_top_corner = point.y <= top + RESIZE_CORNER;
let in_bottom_corner = point.y >= bottom - RESIZE_CORNER;
match (
near_top && in_left_corner,
near_top && in_right_corner,
near_bottom && in_left_corner,
near_bottom && in_right_corner,
) {
(true, _, _, _) => return Some(ChatResizeEdge::Nw),
(_, true, _, _) => return Some(ChatResizeEdge::Ne),
(_, _, true, _) => return Some(ChatResizeEdge::Sw),
(_, _, _, true) => return Some(ChatResizeEdge::Se),
_ => {}
}
if near_top {
Some(ChatResizeEdge::N)
} else if near_bottom {
Some(ChatResizeEdge::S)
} else if near_left && !in_top_corner && !in_bottom_corner {
Some(ChatResizeEdge::W)
} else if near_right && !in_top_corner && !in_bottom_corner {
Some(ChatResizeEdge::E)
} else {
None
}
}
pub fn design_block_hover_at(&self, rect: Rect, point: Point2D) -> Option<(usize, usize)> {
if self.state.messages.is_empty() {
return None;
@ -392,7 +452,7 @@ impl<'a> Widget for AIChatPlaceholder<'a> {
LayoutBox {
rect: Rect {
origin: Point2D::new(0.0, 0.0),
size: Point2D::new(AI_CHAT_WIDTH, AI_CHAT_HEIGHT),
size: Point2D::new(self.state.panel_width, self.state.panel_height),
},
}
}

View file

@ -249,6 +249,42 @@ fn hit_test_header_returns_drag_handle() {
assert_eq!(panel.hit_test(rect, p), Some(AIChatHit::DragHandle));
}
#[test]
fn resize_edge_at_resolves_all_ts_handles_when_not_maximized() {
let s = EditorState::new();
let panel = AIChatPlaceholder::from_editor(&s);
let rect = Rect::xywh(100.0, 80.0, AI_CHAT_WIDTH, AI_CHAT_HEIGHT);
let mid_x = rect.origin.x + rect.size.x / 2.0;
let mid_y = rect.origin.y + rect.size.y / 2.0;
let right = rect.origin.x + rect.size.x;
let bottom = rect.origin.y + rect.size.y;
let cases = [
(Point2D::new(mid_x, rect.origin.y + 2.0), ChatResizeEdge::N),
(Point2D::new(mid_x, bottom - 2.0), ChatResizeEdge::S),
(Point2D::new(right - 2.0, mid_y), ChatResizeEdge::E),
(Point2D::new(rect.origin.x + 2.0, mid_y), ChatResizeEdge::W),
(
Point2D::new(right - 2.0, rect.origin.y + 2.0),
ChatResizeEdge::Ne,
),
(
Point2D::new(rect.origin.x + 2.0, rect.origin.y + 2.0),
ChatResizeEdge::Nw,
),
(Point2D::new(right - 2.0, bottom - 2.0), ChatResizeEdge::Se),
(
Point2D::new(rect.origin.x + 2.0, bottom - 2.0),
ChatResizeEdge::Sw,
),
];
for (point, edge) in cases {
assert_eq!(panel.resize_edge_at(rect, point), Some(edge));
assert_eq!(panel.hit_test(rect, point), Some(AIChatHit::Resize(edge)));
}
}
#[test]
fn hit_test_resolves_header_maximize_button() {
let s = EditorState::new();

View file

@ -177,10 +177,10 @@ pub use canvas_viewport::{
pub use icons::{draw_icon, draw_icon_catalog_entry, draw_icon_data, Icon, IconPathData};
pub use ai_chat_hit::AIChatHit;
pub use ai_chat_hit::{AIChatHit, ChatResizeEdge};
pub use ai_chat_panel::{
AIChatPlaceholder, AI_CHAT_COLLAPSED_HEIGHT, AI_CHAT_COLLAPSED_WIDTH, AI_CHAT_HEIGHT,
AI_CHAT_WIDTH,
AI_CHAT_MAX_RATIO, AI_CHAT_MIN_HEIGHT, AI_CHAT_MIN_WIDTH, AI_CHAT_WIDTH,
};
pub use align_toolbar::{AlignToolbar, AlignToolbarHit, ALIGN_TOOLBAR_HEIGHT, ALIGN_TOOLBAR_WIDTH};
pub use component_browser_panel::{

View file

@ -153,6 +153,9 @@ pub struct WidgetHostNative {
/// host computes the nearest corner via `ChatAnchor::nearest`
/// and snaps.
pub(in crate::widget_host) chat_drag: Option<ChatDragState>,
/// Active chat-panel resize — mirrors the TS panel's invisible
/// edge/corner handles.
pub(in crate::widget_host) chat_resize: Option<ChatResizeState>,
/// Active Design-MD panel drag — present while the user drags the
/// floating panel by its header bar. The live top-left is written
/// straight back into `editor_ui.design_md_panel_pos`.
@ -404,6 +407,14 @@ pub(in crate::widget_host) struct ChatDragState {
pub(in crate::widget_host) pos_y: f32,
}
#[derive(Debug, Clone, Copy)]
pub(in crate::widget_host) struct ChatResizeState {
pub(in crate::widget_host) edge: op_editor_ui::widgets::ChatResizeEdge,
pub(in crate::widget_host) start_x: f32,
pub(in crate::widget_host) start_y: f32,
pub(in crate::widget_host) start_rect: Rect,
}
#[derive(Debug, Clone, Copy)]
pub(in crate::widget_host) struct DesignMdDragState {
/// Pointer offset within the panel rect when the drag began —
@ -439,6 +450,7 @@ impl WidgetHostNative {
theme: Theme::dark(),
drag: None,
chat_drag: None,
chat_resize: None,
design_md_drag: None,
component_browser_drag: None,
icon_picker_drag: None,

View file

@ -4,7 +4,7 @@ use super::helpers::{AICHAT_INSET_BOTTOM, AICHAT_INSET_LEFT};
use super::WidgetHostNative;
use op_editor_core::ChatAnchor;
use op_editor_ui::widgets::{
AI_CHAT_COLLAPSED_HEIGHT, AI_CHAT_COLLAPSED_WIDTH, AI_CHAT_HEIGHT, AI_CHAT_WIDTH,
AI_CHAT_COLLAPSED_HEIGHT, AI_CHAT_COLLAPSED_WIDTH, AI_CHAT_MIN_HEIGHT, AI_CHAT_MIN_WIDTH,
};
use op_editor_ui::{Point2D, Rect};
@ -13,7 +13,10 @@ impl WidgetHostNative {
if self.editor_state.chat.collapsed {
(AI_CHAT_COLLAPSED_WIDTH, AI_CHAT_COLLAPSED_HEIGHT)
} else {
(AI_CHAT_WIDTH, AI_CHAT_HEIGHT)
(
self.editor_state.chat.panel_width.max(AI_CHAT_MIN_WIDTH),
self.editor_state.chat.panel_height.max(AI_CHAT_MIN_HEIGHT),
)
}
}
@ -43,6 +46,12 @@ impl WidgetHostNative {
size: Point2D::new(panel_w, panel_h),
});
}
if let Some((x, y)) = self.editor_state.chat.panel_position {
return Some(Rect {
origin: Point2D::new(x, y),
size: Point2D::new(panel_w, panel_h),
});
}
let (x, y) = match self.editor_state.chat.anchor {
ChatAnchor::TopLeft => (cx0 + AICHAT_INSET_LEFT, cy0 + AICHAT_INSET_BOTTOM),
ChatAnchor::TopRight => (

View file

@ -162,6 +162,10 @@ impl WidgetHostNative {
// this; reaching here is a path bypass.
return false;
}
AIChatHit::Resize(_) => {
// Resize handles are press-drag only.
return false;
}
AIChatHit::ToggleCollapse => {
self.editor_state.chat.collapsed = !self.editor_state.chat.collapsed;
self.mark_dirty();

View file

@ -10,8 +10,8 @@
use super::helpers::{rect_contains, PANEL_RESIZE_GUTTER, STATUS_INSET};
use super::{CursorHint, PanelResizeKind, WidgetHostNative};
use op_editor_ui::widgets::{
rotation_corner_at_point, selection_handle_at_point, STATUS_BAR_HEIGHT, STATUS_BAR_WIDTH,
TOP_BAR_HEIGHT,
rotation_corner_at_point, selection_handle_at_point, AIChatPlaceholder, ChatResizeEdge,
STATUS_BAR_HEIGHT, STATUS_BAR_WIDTH, TOP_BAR_HEIGHT,
};
use op_editor_ui::{Point2D, Rect};
@ -58,6 +58,27 @@ impl WidgetHostNative {
self.panel_resize.is_some()
}
pub fn chat_resize_hover(
&self,
x: f32,
y: f32,
viewport_w: f32,
viewport_h: f32,
) -> Option<ChatResizeEdge> {
let rect = self.ai_chat_rect(viewport_w, viewport_h)?;
let panel = AIChatPlaceholder::from_editor(&self.editor_state);
panel.resize_edge_at(rect, Point2D::new(x, y))
}
fn chat_resize_cursor(edge: ChatResizeEdge) -> CursorHint {
match edge {
ChatResizeEdge::E | ChatResizeEdge::W => CursorHint::ResizeEw,
ChatResizeEdge::N | ChatResizeEdge::S => CursorHint::ResizeNs,
ChatResizeEdge::Nw | ChatResizeEdge::Se => CursorHint::ResizeNwse,
ChatResizeEdge::Ne | ChatResizeEdge::Sw => CursorHint::ResizeNesw,
}
}
/// Clear every lower-overlay hover highlight — file menu, locale
/// / shape pickers, align toolbar, layer-context menu. Called
/// when the cursor moves over the top-most Design-MD panel so a
@ -356,6 +377,12 @@ impl WidgetHostNative {
if self.over_disabled_init_card(x, y, viewport_w, viewport_h) {
return CursorHint::NotAllowed;
}
if let Some(resize) = self.chat_resize {
return Self::chat_resize_cursor(resize.edge);
}
if let Some(edge) = self.chat_resize_hover(x, y, viewport_w, viewport_h) {
return Self::chat_resize_cursor(edge);
}
// Any floating overlay (panels, Git popover, Toolbar /
// StatusBar / chat, open dropdowns) — a neutral cursor over
// them, never a canvas action cursor (Move / Crosshair)

View file

@ -2,6 +2,9 @@
use super::helpers::{resize_bounds, PANEL_MAX_WIDTH, PANEL_MIN_WIDTH};
use super::{PanelResizeKind, WidgetHostNative};
use op_editor_ui::widgets::{
ChatResizeEdge, AI_CHAT_MAX_RATIO, AI_CHAT_MIN_HEIGHT, AI_CHAT_MIN_WIDTH,
};
use op_editor_ui::{Point2D, Rect};
/// Minimum cursor travel (logical px) from the node-drag press point
@ -536,6 +539,95 @@ impl WidgetHostNative {
self.mark_dirty();
return true;
}
if let Some(resize) = self.chat_resize {
let (cx0, cy0, cw, ch) = self.canvas_region(self.last_viewport_w, self.last_viewport_h);
let max_w = (cw * AI_CHAT_MAX_RATIO).max(AI_CHAT_MIN_WIDTH);
let max_h = (ch * AI_CHAT_MAX_RATIO).max(AI_CHAT_MIN_HEIGHT);
let dx = x - resize.start_x;
let dy = y - resize.start_y;
let mut new_w = resize.start_rect.size.x;
let mut new_h = resize.start_rect.size.y;
let mut new_left = resize.start_rect.origin.x;
let mut new_top = resize.start_rect.origin.y;
if matches!(
resize.edge,
ChatResizeEdge::E | ChatResizeEdge::Ne | ChatResizeEdge::Se
) {
new_w = resize.start_rect.size.x + dx;
}
if matches!(
resize.edge,
ChatResizeEdge::W | ChatResizeEdge::Nw | ChatResizeEdge::Sw
) {
new_w = resize.start_rect.size.x - dx;
new_left = resize.start_rect.origin.x + dx;
}
if matches!(
resize.edge,
ChatResizeEdge::S | ChatResizeEdge::Se | ChatResizeEdge::Sw
) {
new_h = resize.start_rect.size.y + dy;
}
if matches!(
resize.edge,
ChatResizeEdge::N | ChatResizeEdge::Ne | ChatResizeEdge::Nw
) {
new_h = resize.start_rect.size.y - dy;
new_top = resize.start_rect.origin.y + dy;
}
if new_w < AI_CHAT_MIN_WIDTH {
let diff = AI_CHAT_MIN_WIDTH - new_w;
new_w = AI_CHAT_MIN_WIDTH;
if matches!(
resize.edge,
ChatResizeEdge::W | ChatResizeEdge::Nw | ChatResizeEdge::Sw
) {
new_left -= diff;
}
}
if new_w > max_w {
let diff = new_w - max_w;
new_w = max_w;
if matches!(
resize.edge,
ChatResizeEdge::W | ChatResizeEdge::Nw | ChatResizeEdge::Sw
) {
new_left += diff;
}
}
if new_h < AI_CHAT_MIN_HEIGHT {
let diff = AI_CHAT_MIN_HEIGHT - new_h;
new_h = AI_CHAT_MIN_HEIGHT;
if matches!(
resize.edge,
ChatResizeEdge::N | ChatResizeEdge::Ne | ChatResizeEdge::Nw
) {
new_top -= diff;
}
}
if new_h > max_h {
let diff = new_h - max_h;
new_h = max_h;
if matches!(
resize.edge,
ChatResizeEdge::N | ChatResizeEdge::Ne | ChatResizeEdge::Nw
) {
new_top += diff;
}
}
let max_left = cx0 + cw - new_w;
let max_top = cy0 + ch - new_h;
new_left = new_left.clamp(cx0, max_left.max(cx0));
new_top = new_top.clamp(cy0, max_top.max(cy0));
self.editor_state.chat.panel_width = new_w.round();
self.editor_state.chat.panel_height = new_h.round();
self.editor_state.chat.panel_position = Some((new_left.round(), new_top.round()));
self.mark_dirty();
return true;
}
if let Some(d) = self.chat_drag.as_mut() {
d.pos_x = x - d.grab_dx;
d.pos_y = y - d.grab_dy;
@ -584,6 +676,9 @@ impl WidgetHostNative {
if self.panel_resize.take().is_some() {
return true;
}
if self.chat_resize.take().is_some() {
return true;
}
if self.rotate_drag.take().is_some() {
return true;
}
@ -644,6 +739,7 @@ impl WidgetHostNative {
let (cx0, cy0, cw, ch) = self.canvas_region(viewport_w, viewport_h);
self.editor_state.chat.anchor =
op_editor_core::ChatAnchor::nearest(center, cx0, cy0, cw, ch);
self.editor_state.chat.panel_position = None;
self.mark_dirty();
return true;
}
@ -657,6 +753,9 @@ impl WidgetHostNative {
if self.panel_resize.take().is_some() {
return true;
}
if self.chat_resize.take().is_some() {
return true;
}
if self.rotate_drag.take().is_some() {
return true;
}

View file

@ -5,7 +5,7 @@
//! `host.editor_state` from canonical-schema JSON and assert against
//! `editor_state` + the derived `LayoutScene` render scene.
use super::WidgetHostNative;
use super::{CursorHint, WidgetHostNative};
use op_editor_core::ui_draft::PropertyFocus;
use op_editor_core::NodeId;
use op_editor_core::PenNodeExt;
@ -76,6 +76,40 @@ fn ai_chat_new_chat_click_clears_transcript_and_queues_abort() {
assert!(host.editor_state().chat.pending_new_chat);
}
#[test]
fn ai_chat_east_edge_shows_resize_cursor() {
let host = WidgetHostNative::new();
let rect = host
.ai_chat_rect(1200.0, 800.0)
.expect("chat panel visible");
let x = rect.origin.x + rect.size.x - 2.0;
let y = rect.origin.y + rect.size.y / 2.0;
assert_eq!(host.cursor_hint(x, y, 1200.0, 800.0), CursorHint::ResizeEw);
}
#[test]
fn ai_chat_east_edge_drag_resizes_panel_width() {
let mut host = WidgetHostNative::new();
let before = host
.ai_chat_rect(1200.0, 800.0)
.expect("chat panel visible");
let x = before.origin.x + before.size.x - 2.0;
let y = before.origin.y + before.size.y / 2.0;
assert!(host.apply_press(x, y, 1200.0, 800.0));
assert!(host.apply_cursor_move(x + 72.0, y));
let after = host
.ai_chat_rect(1200.0, 800.0)
.expect("chat panel visible after resize");
assert!(
after.size.x > before.size.x + 60.0,
"dragging the east edge should grow chat width; before={before:?}, after={after:?}"
);
assert_eq!(after.origin.x, before.origin.x);
}
#[test]
fn ai_chat_stop_click_keeps_transcript_and_queues_abort() {
let mut host = WidgetHostNative::new();

View file

@ -9,8 +9,8 @@
use super::helpers::{rect_contains, TOOLBAR_INSET_X, TOOLBAR_INSET_Y};
use super::{
ChatDragState, CreateDragState, DragState, HandleDragState, NodeDragState, PanelResize,
PanelResizeKind, RotateDragState, WidgetHostNative,
ChatDragState, ChatResizeState, CreateDragState, DragState, HandleDragState, NodeDragState,
PanelResize, PanelResizeKind, RotateDragState, WidgetHostNative,
};
use op_editor_ui::widgets::{
rotation_corner_at_point, selection_handle_at_point, AIChatHit, AIChatPlaceholder, LayoutCx,
@ -530,6 +530,17 @@ impl WidgetHostNative {
if let Some(chat_rect) = self.ai_chat_rect(viewport_width, viewport_height) {
let panel = AIChatPlaceholder::from_editor(&self.editor_state);
if let Some(hit) = panel.hit_test(chat_rect, Point2D::new(x, y)) {
if let AIChatHit::Resize(edge) = hit {
self.chat_resize = Some(ChatResizeState {
edge,
start_x: x,
start_y: y,
start_rect: chat_rect,
});
self.editor_state.chat.focused = false;
self.mark_dirty();
return true;
}
if matches!(hit, AIChatHit::DragHandle) {
self.chat_drag = Some(ChatDragState {
grab_dx: x - chat_rect.origin.x,

View file

@ -405,6 +405,9 @@ impl WidgetHost {
if let Some(chat_rect) = self.ai_chat_rect(viewport_width, viewport_height) {
let panel = AIChatPlaceholder::from_editor_at(&self.editor_state, self.now_ms);
if let Some(hit) = panel.hit_test(chat_rect, Point2D::new(x, y)) {
if matches!(hit, AIChatHit::Resize(_)) {
return true;
}
if matches!(hit, AIChatHit::DragHandle) {
self.chat_drag = Some(ChatDragState {
grab_dx: x - chat_rect.origin.x,
@ -627,6 +630,9 @@ impl WidgetHost {
AIChatHit::DragHandle => {
return false;
}
AIChatHit::Resize(_) => {
return false;
}
AIChatHit::ToggleCollapse => {
let v = &mut self.editor_state.chat.collapsed;
*v = !*v;