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(
106                                tool_use.assistant_message_id,
107                                tool_use.id.clone(),
108                                task,
109                                cx,
110                            );
111                        });
112                    }
113                }
114            }
115            ThreadEvent::ToolFinished { .. } => {}
116        }
117    }
118}
119
120impl FocusableView for AssistantPanel {
121    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
122        self.message_editor.focus_handle(cx)
123    }
124}
125
126impl EventEmitter<PanelEvent> for AssistantPanel {}
127
128impl Panel for AssistantPanel {
129    fn persistent_name() -> &'static str {
130        "AssistantPanel2"
131    }
132
133    fn position(&self, _cx: &WindowContext) -> DockPosition {
134        DockPosition::Right
135    }
136
137    fn position_is_valid(&self, _: DockPosition) -> bool {
138        true
139    }
140
141    fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext<Self>) {}
142
143    fn size(&self, _cx: &WindowContext) -> Pixels {
144        px(640.)
145    }
146
147    fn set_size(&mut self, _size: Option<Pixels>, _cx: &mut ViewContext<Self>) {}
148
149    fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
150
151    fn remote_id() -> Option<proto::PanelId> {
152        Some(proto::PanelId::AssistantPanel)
153    }
154
155    fn icon(&self, _cx: &WindowContext) -> Option<IconName> {
156        Some(IconName::ZedAssistant)
157    }
158
159    fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
160        Some("Assistant Panel")
161    }
162
163    fn toggle_action(&self) -> Box<dyn Action> {
164        Box::new(ToggleFocus)
165    }
166}
167
168impl AssistantPanel {
169    fn render_toolbar(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
170        let focus_handle = self.focus_handle(cx);
171
172        h_flex()
173            .id("assistant-toolbar")
174            .justify_between()
175            .gap(DynamicSpacing::Base08.rems(cx))
176            .h(Tab::container_height(cx))
177            .px(DynamicSpacing::Base08.rems(cx))
178            .bg(cx.theme().colors().tab_bar_background)
179            .border_b_1()
180            .border_color(cx.theme().colors().border_variant)
181            .child(h_flex().child(Label::new("Thread Title Goes Here")))
182            .child(
183                h_flex()
184                    .gap(DynamicSpacing::Base08.rems(cx))
185                    .child(self.render_language_model_selector(cx))
186                    .child(Divider::vertical())
187                    .child(
188                        IconButton::new("new-thread", IconName::Plus)
189                            .shape(IconButtonShape::Square)
190                            .icon_size(IconSize::Small)
191                            .style(ButtonStyle::Subtle)
192                            .tooltip({
193                                let focus_handle = focus_handle.clone();
194                                move |cx| {
195                                    Tooltip::for_action_in(
196                                        "New Thread",
197                                        &NewThread,
198                                        &focus_handle,
199                                        cx,
200                                    )
201                                }
202                            })
203                            .on_click(move |_event, _cx| {
204                                println!("New Thread");
205                            }),
206                    )
207                    .child(
208                        IconButton::new("open-history", IconName::HistoryRerun)
209                            .shape(IconButtonShape::Square)
210                            .icon_size(IconSize::Small)
211                            .style(ButtonStyle::Subtle)
212                            .tooltip(move |cx| Tooltip::text("Open History", cx))
213                            .on_click(move |_event, _cx| {
214                                println!("Open History");
215                            }),
216                    )
217                    .child(
218                        IconButton::new("configure-assistant", IconName::Settings)
219                            .shape(IconButtonShape::Square)
220                            .icon_size(IconSize::Small)
221                            .style(ButtonStyle::Subtle)
222                            .tooltip(move |cx| Tooltip::text("Configure Assistant", cx))
223                            .on_click(move |_event, _cx| {
224                                println!("Configure Assistant");
225                            }),
226                    ),
227            )
228    }
229
230    fn render_language_model_selector(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
231        let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
232        let active_model = LanguageModelRegistry::read_global(cx).active_model();
233
234        LanguageModelSelector::new(
235            |model, _cx| {
236                println!("Selected {:?}", model.name());
237            },
238            ButtonLike::new("active-model")
239                .style(ButtonStyle::Subtle)
240                .child(
241                    h_flex()
242                        .w_full()
243                        .gap_0p5()
244                        .child(
245                            div()
246                                .overflow_x_hidden()
247                                .flex_grow()
248                                .whitespace_nowrap()
249                                .child(match (active_provider, active_model) {
250                                    (Some(provider), Some(model)) => h_flex()
251                                        .gap_1()
252                                        .child(
253                                            Icon::new(
254                                                model.icon().unwrap_or_else(|| provider.icon()),
255                                            )
256                                            .color(Color::Muted)
257                                            .size(IconSize::XSmall),
258                                        )
259                                        .child(
260                                            Label::new(model.name().0)
261                                                .size(LabelSize::Small)
262                                                .color(Color::Muted),
263                                        )
264                                        .into_any_element(),
265                                    _ => Label::new("No model selected")
266                                        .size(LabelSize::Small)
267                                        .color(Color::Muted)
268                                        .into_any_element(),
269                                }),
270                        )
271                        .child(
272                            Icon::new(IconName::ChevronDown)
273                                .color(Color::Muted)
274                                .size(IconSize::XSmall),
275                        ),
276                )
277                .tooltip(move |cx| Tooltip::for_action("Change Model", &ToggleModelSelector, cx)),
278        )
279    }
280
281    fn render_message(&self, message: Message, cx: &mut ViewContext<Self>) -> impl IntoElement {
282        let (role_icon, role_name) = match message.role {
283            Role::User => (IconName::Person, "You"),
284            Role::Assistant => (IconName::ZedAssistant, "Assistant"),
285            Role::System => (IconName::Settings, "System"),
286        };
287
288        v_flex()
289            .border_1()
290            .border_color(cx.theme().colors().border_variant)
291            .rounded_md()
292            .child(
293                h_flex()
294                    .justify_between()
295                    .p_1p5()
296                    .border_b_1()
297                    .border_color(cx.theme().colors().border_variant)
298                    .child(
299                        h_flex()
300                            .gap_2()
301                            .child(Icon::new(role_icon).size(IconSize::Small))
302                            .child(Label::new(role_name).size(LabelSize::Small)),
303                    ),
304            )
305            .child(v_flex().p_1p5().child(Label::new(message.text.clone())))
306    }
307}
308
309impl Render for AssistantPanel {
310    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
311        let messages = self.thread.read(cx).messages().cloned().collect::<Vec<_>>();
312
313        v_flex()
314            .key_context("AssistantPanel2")
315            .justify_between()
316            .size_full()
317            .on_action(cx.listener(|this, _: &NewThread, cx| {
318                this.new_thread(cx);
319            }))
320            .child(self.render_toolbar(cx))
321            .child(
322                v_flex()
323                    .id("message-list")
324                    .gap_2()
325                    .size_full()
326                    .p_2()
327                    .overflow_y_scroll()
328                    .bg(cx.theme().colors().panel_background)
329                    .children(
330                        messages
331                            .into_iter()
332                            .map(|message| self.render_message(message, cx)),
333                    ),
334            )
335            .child(
336                h_flex()
337                    .border_t_1()
338                    .border_color(cx.theme().colors().border_variant)
339                    .child(self.message_editor.clone()),
340            )
341    }
342}