assistant_panel.rs

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