use anyhow::Result; use gpui::{ prelude::*, px, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView, Model, Pixels, Task, View, ViewContext, WeakView, WindowContext, }; use language_model::LanguageModelRegistry; use language_model_selector::LanguageModelSelector; use ui::{prelude::*, ButtonLike, Divider, IconButtonShape, Tab, Tooltip}; use workspace::dock::{DockPosition, Panel, PanelEvent}; use workspace::{Pane, Workspace}; use crate::message_editor::MessageEditor; use crate::thread::Thread; use crate::{NewThread, ToggleFocus, ToggleModelSelector}; pub fn init(cx: &mut AppContext) { cx.observe_new_views( |workspace: &mut Workspace, _cx: &mut ViewContext| { workspace.register_action(|workspace, _: &ToggleFocus, cx| { workspace.toggle_panel_focus::(cx); }); }, ) .detach(); } pub struct AssistantPanel { pane: View, thread: Model, message_editor: View, } impl AssistantPanel { pub fn load( workspace: WeakView, cx: AsyncWindowContext, ) -> Task>> { cx.spawn(|mut cx| async move { workspace.update(&mut cx, |workspace, cx| { cx.new_view(|cx| Self::new(workspace, cx)) }) }) } fn new(workspace: &Workspace, cx: &mut ViewContext) -> Self { let pane = cx.new_view(|cx| { let mut pane = Pane::new( workspace.weak_handle(), workspace.project().clone(), Default::default(), None, NewThread.boxed_clone(), cx, ); pane.set_can_split(false, cx); pane.set_can_navigate(true, cx); pane }); let thread = cx.new_model(Thread::new); Self { pane, thread: thread.clone(), message_editor: cx.new_view(|cx| MessageEditor::new(thread, cx)), } } } impl FocusableView for AssistantPanel { fn focus_handle(&self, cx: &AppContext) -> FocusHandle { self.pane.focus_handle(cx) } } impl EventEmitter for AssistantPanel {} impl Panel for AssistantPanel { fn persistent_name() -> &'static str { "AssistantPanel2" } fn position(&self, _cx: &WindowContext) -> DockPosition { DockPosition::Right } fn position_is_valid(&self, _: DockPosition) -> bool { true } fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext) {} fn size(&self, _cx: &WindowContext) -> Pixels { px(640.) } fn set_size(&mut self, _size: Option, _cx: &mut ViewContext) {} fn is_zoomed(&self, cx: &WindowContext) -> bool { self.pane.read(cx).is_zoomed() } fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext) { self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx)); } fn set_active(&mut self, _active: bool, _cx: &mut ViewContext) {} fn pane(&self) -> Option> { Some(self.pane.clone()) } fn remote_id() -> Option { Some(proto::PanelId::AssistantPanel) } fn icon(&self, _cx: &WindowContext) -> Option { Some(IconName::ZedAssistant) } fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> { Some("Assistant Panel") } fn toggle_action(&self) -> Box { Box::new(ToggleFocus) } } impl AssistantPanel { fn render_toolbar(&self, cx: &mut ViewContext) -> impl IntoElement { let focus_handle = self.focus_handle(cx); h_flex() .id("assistant-toolbar") .justify_between() .gap(DynamicSpacing::Base08.rems(cx)) .h(Tab::container_height(cx)) .px(DynamicSpacing::Base08.rems(cx)) .bg(cx.theme().colors().tab_bar_background) .border_b_1() .border_color(cx.theme().colors().border_variant) .child(h_flex().child(Label::new("Thread Title Goes Here"))) .child( h_flex() .gap(DynamicSpacing::Base08.rems(cx)) .child(self.render_language_model_selector(cx)) .child(Divider::vertical()) .child( IconButton::new("new-thread", IconName::Plus) .shape(IconButtonShape::Square) .icon_size(IconSize::Small) .style(ButtonStyle::Subtle) .tooltip({ let focus_handle = focus_handle.clone(); move |cx| { Tooltip::for_action_in( "New Thread", &NewThread, &focus_handle, cx, ) } }) .on_click(move |_event, _cx| { println!("New Thread"); }), ) .child( IconButton::new("open-history", IconName::HistoryRerun) .shape(IconButtonShape::Square) .icon_size(IconSize::Small) .style(ButtonStyle::Subtle) .tooltip(move |cx| Tooltip::text("Open History", cx)) .on_click(move |_event, _cx| { println!("Open History"); }), ) .child( IconButton::new("configure-assistant", IconName::Settings) .shape(IconButtonShape::Square) .icon_size(IconSize::Small) .style(ButtonStyle::Subtle) .tooltip(move |cx| Tooltip::text("Configure Assistant", cx)) .on_click(move |_event, _cx| { println!("Configure Assistant"); }), ), ) } fn render_language_model_selector(&self, cx: &mut ViewContext) -> impl IntoElement { let active_provider = LanguageModelRegistry::read_global(cx).active_provider(); let active_model = LanguageModelRegistry::read_global(cx).active_model(); LanguageModelSelector::new( |model, _cx| { println!("Selected {:?}", model.name()); }, ButtonLike::new("active-model") .style(ButtonStyle::Subtle) .child( h_flex() .w_full() .gap_0p5() .child( div() .overflow_x_hidden() .flex_grow() .whitespace_nowrap() .child(match (active_provider, active_model) { (Some(provider), Some(model)) => h_flex() .gap_1() .child( Icon::new( model.icon().unwrap_or_else(|| provider.icon()), ) .color(Color::Muted) .size(IconSize::XSmall), ) .child( Label::new(model.name().0) .size(LabelSize::Small) .color(Color::Muted), ) .into_any_element(), _ => Label::new("No model selected") .size(LabelSize::Small) .color(Color::Muted) .into_any_element(), }), ) .child( Icon::new(IconName::ChevronDown) .color(Color::Muted) .size(IconSize::XSmall), ), ) .tooltip(move |cx| Tooltip::for_action("Change Model", &ToggleModelSelector, cx)), ) } } impl Render for AssistantPanel { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { v_flex() .key_context("AssistantPanel2") .justify_between() .size_full() .on_action(cx.listener(|_this, _: &NewThread, _cx| { println!("Action: New Thread"); })) .child(self.render_toolbar(cx)) .child( v_flex() .id("message-list") .gap_2() .size_full() .p_2() .overflow_y_scroll() .bg(cx.theme().colors().panel_background) .children(self.thread.read(cx).messages.iter().map(|message| { v_flex() .p_2() .border_1() .border_color(cx.theme().colors().border_variant) .rounded_md() .child(Label::new(message.role.to_string())) .child(Label::new(message.text.clone())) })), ) .child( h_flex() .border_t_1() .border_color(cx.theme().colors().border_variant) .child(self.message_editor.clone()), ) } }