assistant_panel.rs

  1use anyhow::Result;
  2use gpui::{
  3    prelude::*, px, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle,
  4    FocusableView, Pixels, Task, View, ViewContext, WeakView, WindowContext,
  5};
  6use language_model::LanguageModelRegistry;
  7use language_model_selector::LanguageModelSelector;
  8use ui::{prelude::*, ButtonLike, Divider, IconButtonShape, Tab, Tooltip};
  9use workspace::dock::{DockPosition, Panel, PanelEvent};
 10use workspace::{Pane, Workspace};
 11
 12use crate::message_editor::MessageEditor;
 13use crate::{NewThread, ToggleFocus, ToggleModelSelector};
 14
 15pub fn init(cx: &mut AppContext) {
 16    cx.observe_new_views(
 17        |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
 18            workspace.register_action(|workspace, _: &ToggleFocus, cx| {
 19                workspace.toggle_panel_focus::<AssistantPanel>(cx);
 20            });
 21        },
 22    )
 23    .detach();
 24}
 25
 26pub struct AssistantPanel {
 27    pane: View<Pane>,
 28    message_editor: View<MessageEditor>,
 29}
 30
 31impl AssistantPanel {
 32    pub fn load(
 33        workspace: WeakView<Workspace>,
 34        cx: AsyncWindowContext,
 35    ) -> Task<Result<View<Self>>> {
 36        cx.spawn(|mut cx| async move {
 37            workspace.update(&mut cx, |workspace, cx| {
 38                cx.new_view(|cx| Self::new(workspace, cx))
 39            })
 40        })
 41    }
 42
 43    fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
 44        let pane = cx.new_view(|cx| {
 45            let mut pane = Pane::new(
 46                workspace.weak_handle(),
 47                workspace.project().clone(),
 48                Default::default(),
 49                None,
 50                NewThread.boxed_clone(),
 51                cx,
 52            );
 53            pane.set_can_split(false, cx);
 54            pane.set_can_navigate(true, cx);
 55
 56            pane
 57        });
 58
 59        Self {
 60            pane,
 61            message_editor: cx.new_view(MessageEditor::new),
 62        }
 63    }
 64}
 65
 66impl FocusableView for AssistantPanel {
 67    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
 68        self.pane.focus_handle(cx)
 69    }
 70}
 71
 72impl EventEmitter<PanelEvent> for AssistantPanel {}
 73
 74impl Panel for AssistantPanel {
 75    fn persistent_name() -> &'static str {
 76        "AssistantPanel2"
 77    }
 78
 79    fn position(&self, _cx: &WindowContext) -> DockPosition {
 80        DockPosition::Right
 81    }
 82
 83    fn position_is_valid(&self, _: DockPosition) -> bool {
 84        true
 85    }
 86
 87    fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext<Self>) {}
 88
 89    fn size(&self, _cx: &WindowContext) -> Pixels {
 90        px(640.)
 91    }
 92
 93    fn set_size(&mut self, _size: Option<Pixels>, _cx: &mut ViewContext<Self>) {}
 94
 95    fn is_zoomed(&self, cx: &WindowContext) -> bool {
 96        self.pane.read(cx).is_zoomed()
 97    }
 98
 99    fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
100        self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx));
101    }
102
103    fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
104
105    fn pane(&self) -> Option<View<Pane>> {
106        Some(self.pane.clone())
107    }
108
109    fn remote_id() -> Option<proto::PanelId> {
110        Some(proto::PanelId::AssistantPanel)
111    }
112
113    fn icon(&self, _cx: &WindowContext) -> Option<IconName> {
114        Some(IconName::ZedAssistant)
115    }
116
117    fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
118        Some("Assistant Panel")
119    }
120
121    fn toggle_action(&self) -> Box<dyn Action> {
122        Box::new(ToggleFocus)
123    }
124}
125
126impl AssistantPanel {
127    fn render_toolbar(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
128        let focus_handle = self.focus_handle(cx);
129
130        h_flex()
131            .id("assistant-toolbar")
132            .justify_between()
133            .gap(DynamicSpacing::Base08.rems(cx))
134            .h(Tab::container_height(cx))
135            .px(DynamicSpacing::Base08.rems(cx))
136            .bg(cx.theme().colors().tab_bar_background)
137            .border_b_1()
138            .border_color(cx.theme().colors().border_variant)
139            .child(h_flex().child(Label::new("Thread Title Goes Here")))
140            .child(
141                h_flex()
142                    .gap(DynamicSpacing::Base08.rems(cx))
143                    .child(self.render_language_model_selector(cx))
144                    .child(Divider::vertical())
145                    .child(
146                        IconButton::new("new-thread", IconName::Plus)
147                            .shape(IconButtonShape::Square)
148                            .icon_size(IconSize::Small)
149                            .style(ButtonStyle::Subtle)
150                            .tooltip({
151                                let focus_handle = focus_handle.clone();
152                                move |cx| {
153                                    Tooltip::for_action_in(
154                                        "New Thread",
155                                        &NewThread,
156                                        &focus_handle,
157                                        cx,
158                                    )
159                                }
160                            })
161                            .on_click(move |_event, _cx| {
162                                println!("New Thread");
163                            }),
164                    )
165                    .child(
166                        IconButton::new("open-history", IconName::HistoryRerun)
167                            .shape(IconButtonShape::Square)
168                            .icon_size(IconSize::Small)
169                            .style(ButtonStyle::Subtle)
170                            .tooltip(move |cx| Tooltip::text("Open History", cx))
171                            .on_click(move |_event, _cx| {
172                                println!("Open History");
173                            }),
174                    )
175                    .child(
176                        IconButton::new("configure-assistant", IconName::Settings)
177                            .shape(IconButtonShape::Square)
178                            .icon_size(IconSize::Small)
179                            .style(ButtonStyle::Subtle)
180                            .tooltip(move |cx| Tooltip::text("Configure Assistant", cx))
181                            .on_click(move |_event, _cx| {
182                                println!("Configure Assistant");
183                            }),
184                    ),
185            )
186    }
187
188    fn render_language_model_selector(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
189        let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
190        let active_model = LanguageModelRegistry::read_global(cx).active_model();
191
192        LanguageModelSelector::new(
193            |model, _cx| {
194                println!("Selected {:?}", model.name());
195            },
196            ButtonLike::new("active-model")
197                .style(ButtonStyle::Subtle)
198                .child(
199                    h_flex()
200                        .w_full()
201                        .gap_0p5()
202                        .child(
203                            div()
204                                .overflow_x_hidden()
205                                .flex_grow()
206                                .whitespace_nowrap()
207                                .child(match (active_provider, active_model) {
208                                    (Some(provider), Some(model)) => h_flex()
209                                        .gap_1()
210                                        .child(
211                                            Icon::new(
212                                                model.icon().unwrap_or_else(|| provider.icon()),
213                                            )
214                                            .color(Color::Muted)
215                                            .size(IconSize::XSmall),
216                                        )
217                                        .child(
218                                            Label::new(model.name().0)
219                                                .size(LabelSize::Small)
220                                                .color(Color::Muted),
221                                        )
222                                        .into_any_element(),
223                                    _ => Label::new("No model selected")
224                                        .size(LabelSize::Small)
225                                        .color(Color::Muted)
226                                        .into_any_element(),
227                                }),
228                        )
229                        .child(
230                            Icon::new(IconName::ChevronDown)
231                                .color(Color::Muted)
232                                .size(IconSize::XSmall),
233                        ),
234                )
235                .tooltip(move |cx| Tooltip::for_action("Change Model", &ToggleModelSelector, cx)),
236        )
237    }
238}
239
240impl Render for AssistantPanel {
241    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
242        v_flex()
243            .key_context("AssistantPanel2")
244            .justify_between()
245            .size_full()
246            .on_action(cx.listener(|_this, _: &NewThread, _cx| {
247                println!("Action: New Thread");
248            }))
249            .child(self.render_toolbar(cx))
250            .child(v_flex().bg(cx.theme().colors().panel_background))
251            .child(
252                h_flex()
253                    .border_t_1()
254                    .border_color(cx.theme().colors().border_variant)
255                    .child(self.message_editor.clone()),
256            )
257    }
258}