Get the chat panel wired up again

This commit is contained in:
Max Brunsfeld 2023-12-08 11:53:13 -08:00
parent 213ed2028c
commit c7d8169cab
6 changed files with 211 additions and 226 deletions

View file

@ -8,8 +8,8 @@ use db::kvp::KEY_VALUE_STORE;
use editor::Editor; use editor::Editor;
use gpui::{ use gpui::{
actions, div, list, prelude::*, px, serde_json, AnyElement, AppContext, AsyncWindowContext, actions, div, list, prelude::*, px, serde_json, AnyElement, AppContext, AsyncWindowContext,
Div, EventEmitter, FocusableView, ListState, Model, Render, Subscription, Task, View, ClickEvent, Div, EventEmitter, FocusableView, ListOffset, ListScrollEvent, ListState, Model,
ViewContext, VisualContext, WeakView, Render, SharedString, Subscription, Task, View, ViewContext, VisualContext, WeakView,
}; };
use language::LanguageRegistry; use language::LanguageRegistry;
use menu::Confirm; use menu::Confirm;
@ -20,7 +20,10 @@ use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore}; use settings::{Settings, SettingsStore};
use std::sync::Arc; use std::sync::Arc;
use time::{OffsetDateTime, UtcOffset}; use time::{OffsetDateTime, UtcOffset};
use ui::prelude::WindowContext; use ui::{
h_stack, prelude::WindowContext, v_stack, Avatar, Button, ButtonCommon as _, Clickable, Icon,
IconButton, Label, Tooltip,
};
use util::{ResultExt, TryFutureExt}; use util::{ResultExt, TryFutureExt};
use workspace::{ use workspace::{
dock::{DockPosition, Panel, PanelEvent}, dock::{DockPosition, Panel, PanelEvent},
@ -47,7 +50,6 @@ pub struct ChatPanel {
subscriptions: Vec<gpui::Subscription>, subscriptions: Vec<gpui::Subscription>,
workspace: WeakView<Workspace>, workspace: WeakView<Workspace>,
is_scrolled_to_bottom: bool, is_scrolled_to_bottom: bool,
has_focus: bool,
markdown_data: HashMap<ChannelMessageId, RichText>, markdown_data: HashMap<ChannelMessageId, RichText>,
} }
@ -63,11 +65,10 @@ pub enum Event {
Dismissed, Dismissed,
} }
actions!(LoadMoreMessages, ToggleFocus, OpenChannelNotes, JoinCall); actions!(ToggleFocus, OpenChannelNotes, JoinCall);
// pub fn init(cx: &mut AppContext) { // pub fn init(cx: &mut AppContext) {
// cx.add_action(ChatPanel::send); // cx.add_action(ChatPanel::send);
// cx.add_action(ChatPanel::load_more_messages);
// cx.add_action(ChatPanel::open_notes); // cx.add_action(ChatPanel::open_notes);
// cx.add_action(ChatPanel::join_call); // cx.add_action(ChatPanel::join_call);
// } // }
@ -121,12 +122,12 @@ impl ChatPanel {
view.update(cx, |view, cx| view.render_message(ix, cx)) view.update(cx, |view, cx| view.render_message(ix, cx))
}); });
// message_list.set_scroll_handler(cx.listener(|this, event: &ListScrollEvent, cx| { message_list.set_scroll_handler(cx.listener(|this, event: &ListScrollEvent, cx| {
// if event.visible_range.start < MESSAGE_LOADING_THRESHOLD { if event.visible_range.start < MESSAGE_LOADING_THRESHOLD {
// this.load_more_messages(cx); this.load_more_messages(cx);
// } }
// this.is_scrolled_to_bottom = event.visible_range.end == event.count; this.is_scrolled_to_bottom = event.visible_range.end == event.count;
// })); }));
let mut this = Self { let mut this = Self {
fs, fs,
@ -138,7 +139,6 @@ impl ChatPanel {
pending_serialization: Task::ready(None), pending_serialization: Task::ready(None),
input_editor, input_editor,
local_timezone: cx.local_timezone(), local_timezone: cx.local_timezone(),
has_focus: false,
subscriptions: Vec::new(), subscriptions: Vec::new(),
workspace: workspace_handle, workspace: workspace_handle,
is_scrolled_to_bottom: true, is_scrolled_to_bottom: true,
@ -311,19 +311,39 @@ impl ChatPanel {
} }
fn render_channel(&self, cx: &mut ViewContext<Self>) -> AnyElement { fn render_channel(&self, cx: &mut ViewContext<Self>) -> AnyElement {
todo!() v_stack()
// v_stack() .full()
// .child(Label::new( .on_action(cx.listener(Self::send))
// self.active_chat.map_or(Default::default(), |c| { .child(
// c.0.read(cx).channel(cx)?.name.clone() h_stack()
// }), .w_full()
// )) .justify_between()
// .child(self.render_active_channel_messages(cx)) .child(Label::new(
// .child(self.input_editor.clone()) self.active_chat
// .into_any() .as_ref()
.and_then(|c| Some(c.0.read(cx).channel(cx)?.name.clone()))
.unwrap_or_default(),
))
.child(
h_stack()
.child(
IconButton::new("notes", Icon::File)
.on_click(cx.listener(Self::open_notes))
.tooltip(|cx| Tooltip::text("Open notes", cx)),
)
.child(
IconButton::new("call", Icon::AudioOn)
.on_click(cx.listener(Self::join_call))
.tooltip(|cx| Tooltip::text("Join call", cx)),
),
),
)
.child(div().grow().child(self.render_active_channel_messages(cx)))
.child(self.input_editor.clone())
.into_any()
} }
fn render_active_channel_messages(&self, cx: &mut ViewContext<Self>) -> AnyElement { fn render_active_channel_messages(&self, _cx: &mut ViewContext<Self>) -> AnyElement {
if self.active_chat.is_some() { if self.active_chat.is_some() {
list(self.message_list.clone()).into_any_element() list(self.message_list.clone()).into_any_element()
} else { } else {
@ -332,88 +352,81 @@ impl ChatPanel {
} }
fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement { fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
todo!() let active_chat = &self.active_chat.as_ref().unwrap().0;
// let (message, is_continuation, is_last, is_admin) = self let (message, is_continuation, is_admin) = active_chat.update(cx, |active_chat, cx| {
// .active_chat let is_admin = self
// .as_ref() .channel_store
// .unwrap() .read(cx)
// .0 .is_channel_admin(active_chat.channel_id);
// .update(cx, |active_chat, cx| {
// let is_admin = self
// .channel_store
// .read(cx)
// .is_channel_admin(active_chat.channel_id);
// let last_message = active_chat.message(ix.saturating_sub(1)); let last_message = active_chat.message(ix.saturating_sub(1));
// let this_message = active_chat.message(ix).clone(); let this_message = active_chat.message(ix).clone();
// let is_continuation = last_message.id != this_message.id let is_continuation = last_message.id != this_message.id
// && this_message.sender.id == last_message.sender.id; && this_message.sender.id == last_message.sender.id;
// if let ChannelMessageId::Saved(id) = this_message.id { if let ChannelMessageId::Saved(id) = this_message.id {
// if this_message if this_message
// .mentions .mentions
// .iter() .iter()
// .any(|(_, user_id)| Some(*user_id) == self.client.user_id()) .any(|(_, user_id)| Some(*user_id) == self.client.user_id())
// { {
// active_chat.acknowledge_message(id); active_chat.acknowledge_message(id);
// } }
// } }
// ( (this_message, is_continuation, is_admin)
// this_message, });
// is_continuation,
// active_chat.message_count() == ix + 1,
// is_admin,
// )
// });
// let is_pending = message.is_pending(); let _is_pending = message.is_pending();
// let text = self.markdown_data.entry(message.id).or_insert_with(|| { let text = self.markdown_data.entry(message.id).or_insert_with(|| {
// Self::render_markdown_with_mentions(&self.languages, self.client.id(), &message) Self::render_markdown_with_mentions(&self.languages, self.client.id(), &message)
// }); });
// let now = OffsetDateTime::now_utc(); let now = OffsetDateTime::now_utc();
// let belongs_to_user = Some(message.sender.id) == self.client.user_id(); let belongs_to_user = Some(message.sender.id) == self.client.user_id();
// let message_id_to_remove = if let (ChannelMessageId::Saved(id), true) = let message_id_to_remove = if let (ChannelMessageId::Saved(id), true) =
// (message.id, belongs_to_user || is_admin) (message.id, belongs_to_user || is_admin)
// { {
// Some(id) Some(id)
// } else { } else {
// None None
// }; };
// if is_continuation { // todo!("render the text with markdown formatting")
// h_stack() if is_continuation {
// .child(text.element(cx)) h_stack()
// .child(render_remove(message_id_to_remove, cx)) .child(SharedString::from(text.text.clone()))
// .mb_1() .child(render_remove(message_id_to_remove, cx))
// .into_any() .mb_1()
// } else { .into_any()
// v_stack() } else {
// .child( v_stack()
// h_stack() .child(
// .child(Avatar::data(message.sender.avatar.clone())) h_stack()
// .child(Label::new(message.sender.github_login.clone())) .children(
// .child( message
// Label::new(format_timestamp( .sender
// message.timestamp, .avatar
// now, .clone()
// self.local_timezone, .map(|avatar| Avatar::data(avatar)),
// )) )
// .flex(1., true), .child(Label::new(message.sender.github_login.clone()))
// ) .child(Label::new(format_timestamp(
// .child(render_remove(message_id_to_remove, cx)) message.timestamp,
// .align_children_center(), now,
// ) self.local_timezone,
// .child( )))
// h_stack() .child(render_remove(message_id_to_remove, cx)),
// .child(text.element(cx)) )
// .child(render_remove(None, cx)), .child(
// ) h_stack()
// .mb_1() .child(SharedString::from(text.text.clone()))
// .into_any() .child(render_remove(None, cx)),
// } )
.mb_1()
.into_any()
}
} }
fn render_markdown_with_mentions( fn render_markdown_with_mentions(
@ -509,31 +522,25 @@ impl ChatPanel {
// } // }
fn render_sign_in_prompt(&self, cx: &mut ViewContext<Self>) -> AnyElement { fn render_sign_in_prompt(&self, cx: &mut ViewContext<Self>) -> AnyElement {
todo!() Button::new("sign-in", "Sign in to use chat")
// enum SignInPromptLabel {} .on_click(cx.listener(move |this, _, cx| {
let client = this.client.clone();
// Button::new("sign-in", "Sign in to use chat") cx.spawn(|this, mut cx| async move {
// .on_click(move |_, this, cx| { if client
// let client = this.client.clone(); .authenticate_and_connect(true, &cx)
// cx.spawn(|this, mut cx| async move { .log_err()
// if client .await
// .authenticate_and_connect(true, &cx) .is_some()
// .log_err() {
// .await this.update(&mut cx, |_, cx| {
// .is_some() cx.focus_self();
// { })
// this.update(&mut cx, |this, cx| { .ok();
// if cx.handle().is_focused(cx) { }
// cx.focus(&this.input_editor); })
// } .detach();
// }) }))
// .ok(); .into_any_element()
// }
// })
// .detach();
// })
// .aligned()
// .into_any()
} }
fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) { fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
@ -557,7 +564,7 @@ impl ChatPanel {
} }
} }
fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext<Self>) { fn load_more_messages(&mut self, cx: &mut ViewContext<Self>) {
if let Some((chat, _)) = self.active_chat.as_ref() { if let Some((chat, _)) = self.active_chat.as_ref() {
chat.update(cx, |channel, cx| { chat.update(cx, |channel, cx| {
if let Some(task) = channel.load_more_messages(cx) { if let Some(task) = channel.load_more_messages(cx) {
@ -573,48 +580,47 @@ impl ChatPanel {
scroll_to_message_id: Option<u64>, scroll_to_message_id: Option<u64>,
cx: &mut ViewContext<ChatPanel>, cx: &mut ViewContext<ChatPanel>,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
todo!() let open_chat = self
// let open_chat = self .active_chat
// .active_chat .as_ref()
// .as_ref() .and_then(|(chat, _)| {
// .and_then(|(chat, _)| { (chat.read(cx).channel_id == selected_channel_id)
// (chat.read(cx).channel_id == selected_channel_id) .then(|| Task::ready(anyhow::Ok(chat.clone())))
// .then(|| Task::ready(anyhow::Ok(chat.clone()))) })
// }) .unwrap_or_else(|| {
// .unwrap_or_else(|| { self.channel_store.update(cx, |store, cx| {
// self.channel_store.update(cx, |store, cx| { store.open_channel_chat(selected_channel_id, cx)
// store.open_channel_chat(selected_channel_id, cx) })
// }) });
// });
// cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {
// let chat = open_chat.await?; let chat = open_chat.await?;
// this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
// this.set_active_chat(chat.clone(), cx); this.set_active_chat(chat.clone(), cx);
// })?; })?;
// if let Some(message_id) = scroll_to_message_id { if let Some(message_id) = scroll_to_message_id {
// if let Some(item_ix) = if let Some(item_ix) =
// ChannelChat::load_history_since_message(chat.clone(), message_id, cx.clone()) ChannelChat::load_history_since_message(chat.clone(), message_id, (*cx).clone())
// .await .await
// { {
// this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
// if this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) { if this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) {
// this.message_list.scroll_to(ListOffset { this.message_list.scroll_to(ListOffset {
// item_ix, item_ix,
// offset_in_item: px(0.0), offset_in_item: px(0.0),
// }); });
// cx.notify(); cx.notify();
// } }
// })?; })?;
// } }
// } }
// Ok(()) Ok(())
// }) })
} }
fn open_notes(&mut self, _: &OpenChannelNotes, cx: &mut ViewContext<Self>) { fn open_notes(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
if let Some((chat, _)) = &self.active_chat { if let Some((chat, _)) = &self.active_chat {
let channel_id = chat.read(cx).channel_id; let channel_id = chat.read(cx).channel_id;
if let Some(workspace) = self.workspace.upgrade() { if let Some(workspace) = self.workspace.upgrade() {
@ -623,7 +629,7 @@ impl ChatPanel {
} }
} }
fn join_call(&mut self, _: &JoinCall, cx: &mut ViewContext<Self>) { fn join_call(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
if let Some((chat, _)) = &self.active_chat { if let Some((chat, _)) = &self.active_chat {
let channel_id = chat.read(cx).channel_id; let channel_id = chat.read(cx).channel_id;
ActiveCall::global(cx) ActiveCall::global(cx)
@ -634,40 +640,15 @@ impl ChatPanel {
} }
fn render_remove(message_id_to_remove: Option<u64>, cx: &mut ViewContext<ChatPanel>) -> AnyElement { fn render_remove(message_id_to_remove: Option<u64>, cx: &mut ViewContext<ChatPanel>) -> AnyElement {
todo!() if let Some(message_id) = message_id_to_remove {
// enum DeleteMessage {} IconButton::new(("remove", message_id), Icon::XCircle)
.on_click(cx.listener(move |this, _, cx| {
// message_id_to_remove this.remove_message(message_id, cx);
// .map(|id| { }))
// MouseEventHandler::new::<DeleteMessage, _>(id as usize, cx, |mouse_state, _| { .into_any_element()
// let button_style = theme.chat_panel.icon_button.style_for(mouse_state); } else {
// render_icon_button(button_style, "icons/x.svg") div().into_any_element()
// .aligned() }
// .into_any()
// })
// .with_padding(Padding::uniform(2.))
// .with_cursor_style(CursorStyle::PointingHand)
// .on_click(MouseButton::Left, move |_, this, cx| {
// this.remove_message(id, cx);
// })
// .flex_float()
// .into_any()
// })
// .unwrap_or_else(|| {
// let style = theme.chat_panel.icon_button.default;
// Empty::new()
// .constrained()
// .with_width(style.icon_width)
// .aligned()
// .constrained()
// .with_width(style.button_width)
// .with_height(style.button_width)
// .contained()
// .with_uniform_padding(2.)
// .flex_float()
// .into_any()
// })
} }
impl EventEmitter<Event> for ChatPanel {} impl EventEmitter<Event> for ChatPanel {}
@ -677,6 +658,7 @@ impl Render for ChatPanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element { fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
div() div()
.full()
.child(if self.client.user_id().is_some() { .child(if self.client.user_id().is_some() {
self.render_channel(cx) self.render_channel(cx)
} else { } else {
@ -729,7 +711,7 @@ impl Panel for ChatPanel {
} }
fn persistent_name() -> &'static str { fn persistent_name() -> &'static str {
todo!() "ChatPanel"
} }
fn icon(&self, _cx: &WindowContext) -> Option<ui::Icon> { fn icon(&self, _cx: &WindowContext) -> Option<ui::Icon> {
@ -737,7 +719,7 @@ impl Panel for ChatPanel {
} }
fn toggle_action(&self) -> Box<dyn gpui::Action> { fn toggle_action(&self) -> Box<dyn gpui::Action> {
todo!() Box::new(ToggleFocus)
} }
} }

View file

@ -3,7 +3,8 @@ use client::UserId;
use collections::HashMap; use collections::HashMap;
use editor::{AnchorRangeExt, Editor}; use editor::{AnchorRangeExt, Editor};
use gpui::{ use gpui::{
AnyView, AsyncWindowContext, Model, Render, SharedString, Task, View, ViewContext, WeakView, AnyView, AsyncWindowContext, FocusableView, Model, Render, SharedString, Task, View,
ViewContext, WeakView,
}; };
use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry}; use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry};
use lazy_static::lazy_static; use lazy_static::lazy_static;
@ -52,8 +53,7 @@ impl MessageEditor {
let markdown = markdown.await?; let markdown = markdown.await?;
buffer.update(&mut cx, |buffer, cx| { buffer.update(&mut cx, |buffer, cx| {
buffer.set_language(Some(markdown), cx) buffer.set_language(Some(markdown), cx)
}); })
anyhow::Ok(())
}) })
.detach_and_log_err(cx); .detach_and_log_err(cx);
@ -191,14 +191,14 @@ impl MessageEditor {
} }
pub(crate) fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle { pub(crate) fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
todo!() self.editor.read(cx).focus_handle(cx)
} }
} }
impl Render for MessageEditor { impl Render for MessageEditor {
type Element = AnyView; type Element = AnyView;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element { fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
self.editor.to_any() self.editor.to_any()
} }
} }

View file

@ -192,6 +192,7 @@ use workspace::{
}; };
use crate::channel_view::ChannelView; use crate::channel_view::ChannelView;
use crate::chat_panel::ChatPanel;
use crate::{face_pile::FacePile, CollaborationPanelSettings}; use crate::{face_pile::FacePile, CollaborationPanelSettings};
use self::channel_modal::ChannelModal; use self::channel_modal::ChannelModal;
@ -2102,14 +2103,13 @@ impl CollabPanel {
}; };
cx.window_context().defer(move |cx| { cx.window_context().defer(move |cx| {
workspace.update(cx, |workspace, cx| { workspace.update(cx, |workspace, cx| {
todo!(); if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
// if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) { panel.update(cx, |panel, cx| {
// panel.update(cx, |panel, cx| { panel
// panel .select_channel(channel_id, None, cx)
// .select_channel(channel_id, None, cx) .detach_and_log_err(cx);
// .detach_and_log_err(cx); });
// }); }
// }
}); });
}); });
} }
@ -2603,9 +2603,14 @@ impl CollabPanel {
Color::Default Color::Default
} else { } else {
Color::Muted Color::Muted
})
.on_click(cx.listener(move |this, _, cx| {
this.join_channel_chat(channel_id, cx)
}))
.tooltip(|cx| {
Tooltip::text("Open channel chat", cx)
}), }),
) ),
.tooltip(|cx| Tooltip::text("Open channel chat", cx)),
) )
.child( .child(
div() div()

View file

@ -132,7 +132,7 @@ impl ListState {
} }
pub fn set_scroll_handler( pub fn set_scroll_handler(
&mut self, &self,
handler: impl FnMut(&ListScrollEvent, &mut WindowContext) + 'static, handler: impl FnMut(&ListScrollEvent, &mut WindowContext) + 'static,
) { ) {
self.0.borrow_mut().scroll_handler = Some(Box::new(handler)) self.0.borrow_mut().scroll_handler = Some(Box::new(handler))

View file

@ -1,12 +1,10 @@
use std::any::TypeId;
use crate::{ItemHandle, Pane}; use crate::{ItemHandle, Pane};
use gpui::{ use gpui::{
div, AnyView, Div, IntoElement, ParentElement, Render, Styled, Subscription, View, ViewContext, div, AnyView, Div, IntoElement, ParentElement, Render, Styled, Subscription, View, ViewContext,
WindowContext, WindowContext,
}; };
use ui::h_stack; use std::any::TypeId;
use ui::prelude::*; use ui::{h_stack, prelude::*};
use util::ResultExt; use util::ResultExt;
pub trait StatusItemView: Render { pub trait StatusItemView: Render {
@ -47,8 +45,8 @@ impl Render for StatusBar {
.w_full() .w_full()
.h_8() .h_8()
.bg(cx.theme().colors().status_bar_background) .bg(cx.theme().colors().status_bar_background)
.child(h_stack().gap_1().child(self.render_left_tools(cx))) .child(self.render_left_tools(cx))
.child(h_stack().gap_4().child(self.render_right_tools(cx))) .child(self.render_right_tools(cx))
} }
} }

View file

@ -160,8 +160,8 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone()); let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
let channels_panel = let channels_panel =
collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone()); collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
// let chat_panel = let chat_panel =
// collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone()); collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone());
// let notification_panel = collab_ui::notification_panel::NotificationPanel::load( // let notification_panel = collab_ui::notification_panel::NotificationPanel::load(
// workspace_handle.clone(), // workspace_handle.clone(),
// cx.clone(), // cx.clone(),
@ -171,14 +171,14 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
terminal_panel, terminal_panel,
assistant_panel, assistant_panel,
channels_panel, channels_panel,
// chat_panel, chat_panel,
// notification_panel, // notification_panel,
) = futures::try_join!( ) = futures::try_join!(
project_panel, project_panel,
terminal_panel, terminal_panel,
assistant_panel, assistant_panel,
channels_panel, channels_panel,
// chat_panel, chat_panel,
// notification_panel, // notification_panel,
)?; )?;
@ -188,7 +188,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
workspace.add_panel(terminal_panel, cx); workspace.add_panel(terminal_panel, cx);
workspace.add_panel(assistant_panel, cx); workspace.add_panel(assistant_panel, cx);
workspace.add_panel(channels_panel, cx); workspace.add_panel(channels_panel, cx);
// workspace.add_panel(chat_panel, cx); workspace.add_panel(chat_panel, cx);
// workspace.add_panel(notification_panel, cx); // workspace.add_panel(notification_panel, cx);
// if !was_deserialized // if !was_deserialized