assistant_panel.rs

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