Overhaul assistant panel (#2610)

Closes
https://linear.app/zed-industries/issue/Z-2368/use-a-different-icon-for-the-assistant-panel
Closes
https://linear.app/zed-industries/issue/Z-2363/ship-the-assistant-only-on-preview
Closes
https://linear.app/zed-industries/issue/Z-2331/scrolling-makes-it-hard-to-read
Closes
https://linear.app/zed-industries/issue/Z-2306/allow-undo-and-collaboration-in-assistant

This pull request is a significant overhaul of the assistant panel,
which now uses a simple `Buffer` as opposed to a `MultiBuffer` to show
messages. Specifically, we track the start of each message with an
anchor located right after the newline (or `Anchor::MIN` for the first
message). When the anchor becomes invalid (that is, the newline is
deleted), we merge the message with the preceding ones. Crucially,
messages don't actually get deleted so that, if the newline anchor
becomes valid again (such as when undoing/redoing), we can restore the
messages as well.

As part of this overhaul, we are also improving the scrolling behavior
to maintain the viewport stable only when editing or moving the cursor,
but otherwise leave the scroll position unchanged when manually
scrolling up or down.

Note that with these changes, we are limiting access to the assistant to
users on preview (and dev), as we want to polish the behavior a little
more before shipping to the general public. Users on stable will still
be able to see the default settings/keybindings of the assistant, but I
think that's okay, as they won't be able to do anything with them.

Release Notes:

- Added support for undo/redo in the assistant (preview-only)
- Improved the scrolling behavior of the assistant when it was
generating responses. Now Zed will keep the viewport stable only when
editing or moving the cursor, but otherwise leave the scroll position
unchanged when manually scrolling up or down (preview-only)
- Changed the icon of the assistant panel (preview-only)

**Note for @JosephTLyons: given that we're feature flagging this, let's
make sure things on stable look reasonable and work correctly. Things to
look out for: ensure a stock installation works, changing the settings
on stable works, changing the keybinding on stable works.**
This commit is contained in:
Antonio Scandurra 2023-06-14 14:09:09 +02:00 committed by GitHub
commit 2b8b954c3e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 679 additions and 565 deletions

View File

@ -0,0 +1,4 @@
<svg width="14" height="16" viewBox="0 0 14 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.5 4C2.5 2.89531 3.39688 2 4.5 2H9.5C10.6031 2 11.5 2.89531 11.5 4V8C11.5 9.10312 10.6031 10 9.5 10H4.5C3.39688 10 2.5 9.10312 2.5 8V4ZM5 4C4.44687 4 4 4.44687 4 5C4 5.55313 4.44687 6 5 6C5.55313 6 6 5.55313 6 5C6 4.44687 5.55313 4 5 4ZM9 6C9.55313 6 10 5.55313 10 5C10 4.44687 9.55313 4 9 4C8.44687 4 8 4.44687 8 5C8 5.55313 8.44687 6 9 6ZM5 8.5C5.275 8.5 5.5 8.275 5.5 8C5.5 7.725 5.275 7.5 5 7.5C4.725 7.5 4.5 7.725 4.5 8C4.5 8.275 4.725 8.5 5 8.5ZM7 7.5C6.725 7.5 6.5 7.725 6.5 8C6.5 8.275 6.725 8.5 7 8.5C7.275 8.5 7.5 8.275 7.5 8C7.5 7.725 7.275 7.5 7 7.5ZM9 8.5C9.275 8.5 9.5 8.275 9.5 8C9.5 7.725 9.275 7.5 9 7.5C8.725 7.5 8.5 7.725 8.5 8C8.5 8.275 8.725 8.5 9 8.5ZM0 14C0 12.3156 1.34312 11 3 11H11C12.6562 11 14 12.3156 14 14V15C14 15.5531 13.5531 16 13 16H11V14C11 13.4469 10.5531 13 10 13H4C3.44687 13 3 13.4469 3 14V16H1C0.447812 16 0 15.5531 0 15V14Z" fill="#808080"/>
<path d="M7.5 2H6.5V0.5C6.5 0.22375 6.725 0 7 0C7.275 0 7.5 0.22375 7.5 0.5V2ZM1.5 4.5V7.5C1.5 7.775 1.27625 8 1 8C0.72375 8 0.5 7.775 0.5 7.5V4.5C0.5 4.225 0.72375 4 1 4C1.27625 4 1.5 4.225 1.5 4.5ZM5.5 16H4.5V14.5C4.5 14.225 4.725 14 5 14C5.275 14 5.5 14.225 5.5 14.5V16ZM7.5 16H6.5V14.5C6.5 14.225 6.725 14 7 14C7.275 14 7.5 14.225 7.5 14.5V16ZM9 14C9.275 14 9.5 14.225 9.5 14.5V16H8.5V14.5C8.5 14.225 8.725 14 9 14ZM13.5 7.5C13.5 7.775 13.275 8 13 8C12.725 8 12.5 7.775 12.5 7.5V4.5C12.5 4.225 12.725 4 13 4C13.275 4 13.5 4.225 13.5 4.5V7.5Z" fill="#808080"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

File diff suppressed because it is too large Load Diff

View File

@ -430,7 +430,7 @@ impl ProjectDiagnosticsEditor {
});
self.editor.update(cx, |editor, cx| {
editor.remove_blocks(blocks_to_remove, cx);
editor.remove_blocks(blocks_to_remove, None, cx);
let block_ids = editor.insert_blocks(
blocks_to_add.into_iter().map(|block| {
let (excerpt_id, text_anchor) = block.position;
@ -442,6 +442,7 @@ impl ProjectDiagnosticsEditor {
disposition: block.disposition,
}
}),
Some(Autoscroll::fit()),
cx,
);

View File

@ -31,13 +31,11 @@ use copilot::Copilot;
pub use display_map::DisplayPoint;
use display_map::*;
pub use editor_settings::EditorSettings;
pub use element::RenderExcerptHeaderParams;
pub use element::{
Cursor, EditorElement, HighlightedRange, HighlightedRangeLine, LineWithInvisibles,
};
use futures::FutureExt;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::LayoutContext;
use gpui::{
actions,
color::Color,
@ -511,7 +509,6 @@ pub struct Editor {
mode: EditorMode,
show_gutter: bool,
placeholder_text: Option<Arc<str>>,
render_excerpt_header: Option<element::RenderExcerptHeader>,
highlighted_rows: Option<Range<u32>>,
#[allow(clippy::type_complexity)]
background_highlights: BTreeMap<TypeId, (fn(&Theme) -> Color, Vec<Range<Anchor>>)>,
@ -1317,7 +1314,6 @@ impl Editor {
mode,
show_gutter: mode == EditorMode::Full,
placeholder_text: None,
render_excerpt_header: None,
highlighted_rows: None,
background_highlights: Default::default(),
nav_history: None,
@ -6272,6 +6268,7 @@ impl Editor {
}),
disposition: BlockDisposition::Below,
}],
Some(Autoscroll::fit()),
cx,
)[0];
this.pending_rename = Some(RenameState {
@ -6338,7 +6335,11 @@ impl Editor {
cx: &mut ViewContext<Self>,
) -> Option<RenameState> {
let rename = self.pending_rename.take()?;
self.remove_blocks([rename.block_id].into_iter().collect(), cx);
self.remove_blocks(
[rename.block_id].into_iter().collect(),
Some(Autoscroll::fit()),
cx,
);
self.clear_text_highlights::<Rename>(cx);
self.show_local_selections = true;
@ -6724,29 +6725,43 @@ impl Editor {
pub fn insert_blocks(
&mut self,
blocks: impl IntoIterator<Item = BlockProperties<Anchor>>,
autoscroll: Option<Autoscroll>,
cx: &mut ViewContext<Self>,
) -> Vec<BlockId> {
let blocks = self
.display_map
.update(cx, |display_map, cx| display_map.insert_blocks(blocks, cx));
self.request_autoscroll(Autoscroll::fit(), cx);
if let Some(autoscroll) = autoscroll {
self.request_autoscroll(autoscroll, cx);
}
blocks
}
pub fn replace_blocks(
&mut self,
blocks: HashMap<BlockId, RenderBlock>,
autoscroll: Option<Autoscroll>,
cx: &mut ViewContext<Self>,
) {
self.display_map
.update(cx, |display_map, _| display_map.replace_blocks(blocks));
self.request_autoscroll(Autoscroll::fit(), cx);
if let Some(autoscroll) = autoscroll {
self.request_autoscroll(autoscroll, cx);
}
}
pub fn remove_blocks(&mut self, block_ids: HashSet<BlockId>, cx: &mut ViewContext<Self>) {
pub fn remove_blocks(
&mut self,
block_ids: HashSet<BlockId>,
autoscroll: Option<Autoscroll>,
cx: &mut ViewContext<Self>,
) {
self.display_map.update(cx, |display_map, cx| {
display_map.remove_blocks(block_ids, cx)
});
if let Some(autoscroll) = autoscroll {
self.request_autoscroll(autoscroll, cx);
}
}
pub fn longest_row(&self, cx: &mut AppContext) -> u32 {
@ -6827,20 +6842,6 @@ impl Editor {
cx.notify();
}
pub fn set_render_excerpt_header(
&mut self,
render_excerpt_header: impl 'static
+ Fn(
&mut Editor,
RenderExcerptHeaderParams,
&mut LayoutContext<Editor>,
) -> AnyElement<Editor>,
cx: &mut ViewContext<Self>,
) {
self.render_excerpt_header = Some(Arc::new(render_excerpt_header));
cx.notify();
}
pub fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext<Self>) {
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
@ -7448,6 +7449,7 @@ pub enum Event {
},
ScrollPositionChanged {
local: bool,
autoscroll: bool,
},
Closed,
}
@ -7479,12 +7481,8 @@ impl View for Editor {
});
}
let mut editor = EditorElement::new(style.clone());
if let Some(render_excerpt_header) = self.render_excerpt_header.clone() {
editor = editor.with_render_excerpt_header(render_excerpt_header);
}
Stack::new()
.with_child(editor)
.with_child(EditorElement::new(style.clone()))
.with_child(ChildView::new(&self.mouse_context_menu, cx))
.into_any()
}

View File

@ -2495,6 +2495,7 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
height: 1,
render: Arc::new(|_| Empty::new().into_any()),
}],
Some(Autoscroll::fit()),
cx,
);
editor.change_selections(None, cx, |s| {

View File

@ -91,41 +91,17 @@ impl SelectionLayout {
}
}
pub struct RenderExcerptHeaderParams<'a> {
pub id: crate::ExcerptId,
pub buffer: &'a language::BufferSnapshot,
pub range: &'a crate::ExcerptRange<text::Anchor>,
pub starts_new_buffer: bool,
pub gutter_padding: f32,
pub editor_style: &'a EditorStyle,
}
pub type RenderExcerptHeader = Arc<
dyn Fn(
&mut Editor,
RenderExcerptHeaderParams,
&mut LayoutContext<Editor>,
) -> AnyElement<Editor>,
>;
pub struct EditorElement {
style: Arc<EditorStyle>,
render_excerpt_header: RenderExcerptHeader,
}
impl EditorElement {
pub fn new(style: EditorStyle) -> Self {
Self {
style: Arc::new(style),
render_excerpt_header: Arc::new(render_excerpt_header),
}
}
pub fn with_render_excerpt_header(mut self, render: RenderExcerptHeader) -> Self {
self.render_excerpt_header = render;
self
}
fn attach_mouse_handlers(
scene: &mut SceneBuilder,
position_map: &Arc<PositionMap>,
@ -1531,18 +1507,117 @@ impl EditorElement {
range,
starts_new_buffer,
..
} => (self.render_excerpt_header)(
editor,
RenderExcerptHeaderParams {
id: *id,
buffer,
range,
starts_new_buffer: *starts_new_buffer,
gutter_padding,
editor_style: style,
},
cx,
),
} => {
let tooltip_style = theme::current(cx).tooltip.clone();
let include_root = editor
.project
.as_ref()
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
.unwrap_or_default();
let jump_icon = project::File::from_dyn(buffer.file()).map(|file| {
let jump_path = ProjectPath {
worktree_id: file.worktree_id(cx),
path: file.path.clone(),
};
let jump_anchor = range
.primary
.as_ref()
.map_or(range.context.start, |primary| primary.start);
let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
enum JumpIcon {}
MouseEventHandler::<JumpIcon, _>::new((*id).into(), cx, |state, _| {
let style = style.jump_icon.style_for(state, false);
Svg::new("icons/arrow_up_right_8.svg")
.with_color(style.color)
.constrained()
.with_width(style.icon_width)
.aligned()
.contained()
.with_style(style.container)
.constrained()
.with_width(style.button_width)
.with_height(style.button_width)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, editor, cx| {
if let Some(workspace) = editor
.workspace
.as_ref()
.and_then(|(workspace, _)| workspace.upgrade(cx))
{
workspace.update(cx, |workspace, cx| {
Editor::jump(
workspace,
jump_path.clone(),
jump_position,
jump_anchor,
cx,
);
});
}
})
.with_tooltip::<JumpIcon>(
(*id).into(),
"Jump to Buffer".to_string(),
Some(Box::new(crate::OpenExcerpts)),
tooltip_style.clone(),
cx,
)
.aligned()
.flex_float()
});
if *starts_new_buffer {
let editor_font_size = style.text.font_size;
let style = &style.diagnostic_path_header;
let font_size = (style.text_scale_factor * editor_font_size).round();
let path = buffer.resolve_file_path(cx, include_root);
let mut filename = None;
let mut parent_path = None;
// Can't use .and_then() because `.file_name()` and `.parent()` return references :(
if let Some(path) = path {
filename = path.file_name().map(|f| f.to_string_lossy().to_string());
parent_path =
path.parent().map(|p| p.to_string_lossy().to_string() + "/");
}
Flex::row()
.with_child(
Label::new(
filename.unwrap_or_else(|| "untitled".to_string()),
style.filename.text.clone().with_font_size(font_size),
)
.contained()
.with_style(style.filename.container)
.aligned(),
)
.with_children(parent_path.map(|path| {
Label::new(path, style.path.text.clone().with_font_size(font_size))
.contained()
.with_style(style.path.container)
.aligned()
}))
.with_children(jump_icon)
.contained()
.with_style(style.container)
.with_padding_left(gutter_padding)
.with_padding_right(gutter_padding)
.expanded()
.into_any_named("path header block")
} else {
let text_style = style.text.clone();
Flex::row()
.with_child(Label::new("", text_style))
.with_children(jump_icon)
.contained()
.with_padding_left(gutter_padding)
.with_padding_right(gutter_padding)
.expanded()
.into_any_named("collapsed context")
}
}
};
element.layout(
@ -2679,121 +2754,6 @@ impl HighlightedRange {
}
}
fn render_excerpt_header(
editor: &mut Editor,
RenderExcerptHeaderParams {
id,
buffer,
range,
starts_new_buffer,
gutter_padding,
editor_style,
}: RenderExcerptHeaderParams,
cx: &mut LayoutContext<Editor>,
) -> AnyElement<Editor> {
let tooltip_style = theme::current(cx).tooltip.clone();
let include_root = editor
.project
.as_ref()
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
.unwrap_or_default();
let jump_icon = project::File::from_dyn(buffer.file()).map(|file| {
let jump_path = ProjectPath {
worktree_id: file.worktree_id(cx),
path: file.path.clone(),
};
let jump_anchor = range
.primary
.as_ref()
.map_or(range.context.start, |primary| primary.start);
let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
enum JumpIcon {}
MouseEventHandler::<JumpIcon, _>::new(id.into(), cx, |state, _| {
let style = editor_style.jump_icon.style_for(state, false);
Svg::new("icons/arrow_up_right_8.svg")
.with_color(style.color)
.constrained()
.with_width(style.icon_width)
.aligned()
.contained()
.with_style(style.container)
.constrained()
.with_width(style.button_width)
.with_height(style.button_width)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, editor, cx| {
if let Some(workspace) = editor
.workspace
.as_ref()
.and_then(|(workspace, _)| workspace.upgrade(cx))
{
workspace.update(cx, |workspace, cx| {
Editor::jump(workspace, jump_path.clone(), jump_position, jump_anchor, cx);
});
}
})
.with_tooltip::<JumpIcon>(
id.into(),
"Jump to Buffer".to_string(),
Some(Box::new(crate::OpenExcerpts)),
tooltip_style.clone(),
cx,
)
.aligned()
.flex_float()
});
if starts_new_buffer {
let style = &editor_style.diagnostic_path_header;
let font_size = (style.text_scale_factor * editor_style.text.font_size).round();
let path = buffer.resolve_file_path(cx, include_root);
let mut filename = None;
let mut parent_path = None;
// Can't use .and_then() because `.file_name()` and `.parent()` return references :(
if let Some(path) = path {
filename = path.file_name().map(|f| f.to_string_lossy().to_string());
parent_path = path.parent().map(|p| p.to_string_lossy().to_string() + "/");
}
Flex::row()
.with_child(
Label::new(
filename.unwrap_or_else(|| "untitled".to_string()),
style.filename.text.clone().with_font_size(font_size),
)
.contained()
.with_style(style.filename.container)
.aligned(),
)
.with_children(parent_path.map(|path| {
Label::new(path, style.path.text.clone().with_font_size(font_size))
.contained()
.with_style(style.path.container)
.aligned()
}))
.with_children(jump_icon)
.contained()
.with_style(style.container)
.with_padding_left(gutter_padding)
.with_padding_right(gutter_padding)
.expanded()
.into_any_named("path header block")
} else {
let text_style = editor_style.text.clone();
Flex::row()
.with_child(Label::new("", text_style))
.with_children(jump_icon)
.contained()
.with_padding_left(gutter_padding)
.with_padding_right(gutter_padding)
.expanded()
.into_any_named("collapsed context")
}
}
fn position_to_display_point(
position: Vector2F,
text_bounds: RectF,
@ -2923,6 +2883,7 @@ mod tests {
position: Anchor::min(),
render: Arc::new(|_| Empty::new().into_any()),
}],
None,
cx,
);

View File

@ -294,7 +294,7 @@ impl FollowableItem for Editor {
match event {
Event::Edited => true,
Event::SelectionsChanged { local } => *local,
Event::ScrollPositionChanged { local } => *local,
Event::ScrollPositionChanged { local, .. } => *local,
_ => false,
}
}

View File

@ -173,6 +173,7 @@ impl ScrollManager {
scroll_position: Vector2F,
map: &DisplaySnapshot,
local: bool,
autoscroll: bool,
workspace_id: Option<i64>,
cx: &mut ViewContext<Editor>,
) {
@ -203,7 +204,7 @@ impl ScrollManager {
)
};
self.set_anchor(new_anchor, top_row, local, workspace_id, cx);
self.set_anchor(new_anchor, top_row, local, autoscroll, workspace_id, cx);
}
fn set_anchor(
@ -211,11 +212,12 @@ impl ScrollManager {
anchor: ScrollAnchor,
top_row: u32,
local: bool,
autoscroll: bool,
workspace_id: Option<i64>,
cx: &mut ViewContext<Editor>,
) {
self.anchor = anchor;
cx.emit(Event::ScrollPositionChanged { local });
cx.emit(Event::ScrollPositionChanged { local, autoscroll });
self.show_scrollbar(cx);
self.autoscroll_request.take();
if let Some(workspace_id) = workspace_id {
@ -296,21 +298,28 @@ impl Editor {
}
pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext<Self>) {
self.set_scroll_position_internal(scroll_position, true, cx);
self.set_scroll_position_internal(scroll_position, true, false, cx);
}
pub(crate) fn set_scroll_position_internal(
&mut self,
scroll_position: Vector2F,
local: bool,
autoscroll: bool,
cx: &mut ViewContext<Self>,
) {
let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
hide_hover(self, cx);
let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
self.scroll_manager
.set_scroll_position(scroll_position, &map, local, workspace_id, cx);
self.scroll_manager.set_scroll_position(
scroll_position,
&map,
local,
autoscroll,
workspace_id,
cx,
);
}
pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F {
@ -326,7 +335,7 @@ impl Editor {
.to_point(&self.buffer().read(cx).snapshot(cx))
.row;
self.scroll_manager
.set_anchor(scroll_anchor, top_row, true, workspace_id, cx);
.set_anchor(scroll_anchor, top_row, true, false, workspace_id, cx);
}
pub(crate) fn set_scroll_anchor_remote(
@ -341,7 +350,7 @@ impl Editor {
.to_point(&self.buffer().read(cx).snapshot(cx))
.row;
self.scroll_manager
.set_anchor(scroll_anchor, top_row, false, workspace_id, cx);
.set_anchor(scroll_anchor, top_row, false, false, workspace_id, cx);
}
pub fn scroll_screen(&mut self, amount: &ScrollAmount, cx: &mut ViewContext<Self>) {

View File

@ -136,23 +136,23 @@ impl Editor {
if target_top < start_row {
scroll_position.set_y(target_top);
self.set_scroll_position_internal(scroll_position, local, cx);
self.set_scroll_position_internal(scroll_position, local, true, cx);
} else if target_bottom >= end_row {
scroll_position.set_y(target_bottom - visible_lines);
self.set_scroll_position_internal(scroll_position, local, cx);
self.set_scroll_position_internal(scroll_position, local, true, cx);
}
}
AutoscrollStrategy::Center => {
scroll_position.set_y((first_cursor_top - margin).max(0.0));
self.set_scroll_position_internal(scroll_position, local, cx);
self.set_scroll_position_internal(scroll_position, local, true, cx);
}
AutoscrollStrategy::Top => {
scroll_position.set_y((first_cursor_top).max(0.0));
self.set_scroll_position_internal(scroll_position, local, cx);
self.set_scroll_position_internal(scroll_position, local, true, cx);
}
AutoscrollStrategy::Bottom => {
scroll_position.set_y((last_cursor_bottom - visible_lines).max(0.0));
self.set_scroll_position_internal(scroll_position, local, cx);
self.set_scroll_position_internal(scroll_position, local, true, cx);
}
}

View File

@ -254,13 +254,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
workspace.toggle_panel_focus::<TerminalPanel>(cx);
},
);
cx.add_action(
|workspace: &mut Workspace,
_: &ai::assistant::ToggleFocus,
cx: &mut ViewContext<Workspace>| {
workspace.toggle_panel_focus::<AssistantPanel>(cx);
},
);
cx.add_global_action({
let app_state = Arc::downgrade(&app_state);
move |_: &NewWindow, cx: &mut AppContext| {
@ -368,9 +361,12 @@ pub fn initialize_workspace(
let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
let (project_panel, terminal_panel, assistant_panel) =
futures::try_join!(project_panel, terminal_panel, assistant_panel)?;
let assistant_panel = if *util::channel::RELEASE_CHANNEL == ReleaseChannel::Stable {
None
} else {
Some(AssistantPanel::load(workspace_handle.clone(), cx.clone()).await?)
};
let (project_panel, terminal_panel) = futures::try_join!(project_panel, terminal_panel)?;
workspace_handle.update(&mut cx, |workspace, cx| {
let project_panel_position = project_panel.position(cx);
workspace.add_panel(project_panel, cx);
@ -389,7 +385,9 @@ pub fn initialize_workspace(
}
workspace.add_panel(terminal_panel, cx);
workspace.add_panel(assistant_panel, cx);
if let Some(assistant_panel) = assistant_panel {
workspace.add_panel(assistant_panel, cx);
}
})?;
Ok(())
})