1use std::sync::Arc;
2
3use anyhow::Result;
4use assistant_tool::ToolWorkingSet;
5use client::zed_urls;
6use gpui::{
7 prelude::*, px, svg, Action, AnyElement, AppContext, AsyncWindowContext, EventEmitter,
8 FocusHandle, FocusableView, FontWeight, Model, Pixels, Task, View, ViewContext, WeakView,
9 WindowContext,
10};
11use language::LanguageRegistry;
12use time::UtcOffset;
13use ui::{prelude::*, Divider, IconButtonShape, KeyBinding, Tab, Tooltip};
14use workspace::dock::{DockPosition, Panel, PanelEvent};
15use workspace::Workspace;
16
17use crate::active_thread::ActiveThread;
18use crate::message_editor::MessageEditor;
19use crate::thread::{ThreadError, ThreadId};
20use crate::thread_history::{PastThread, ThreadHistory};
21use crate::thread_store::ThreadStore;
22use crate::{NewThread, OpenHistory, ToggleFocus};
23
24pub fn init(cx: &mut AppContext) {
25 cx.observe_new_views(
26 |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
27 workspace.register_action(|workspace, _: &ToggleFocus, cx| {
28 workspace.toggle_panel_focus::<AssistantPanel>(cx);
29 });
30 },
31 )
32 .detach();
33}
34
35enum ActiveView {
36 Thread,
37 History,
38}
39
40pub struct AssistantPanel {
41 workspace: WeakView<Workspace>,
42 language_registry: Arc<LanguageRegistry>,
43 thread_store: Model<ThreadStore>,
44 thread: View<ActiveThread>,
45 message_editor: View<MessageEditor>,
46 tools: Arc<ToolWorkingSet>,
47 local_timezone: UtcOffset,
48 active_view: ActiveView,
49 history: View<ThreadHistory>,
50}
51
52impl AssistantPanel {
53 pub fn load(
54 workspace: WeakView<Workspace>,
55 cx: AsyncWindowContext,
56 ) -> Task<Result<View<Self>>> {
57 cx.spawn(|mut cx| async move {
58 let tools = Arc::new(ToolWorkingSet::default());
59 let thread_store = workspace
60 .update(&mut cx, |workspace, cx| {
61 let project = workspace.project().clone();
62 ThreadStore::new(project, tools.clone(), cx)
63 })?
64 .await?;
65
66 workspace.update(&mut cx, |workspace, cx| {
67 cx.new_view(|cx| Self::new(workspace, thread_store, tools, cx))
68 })
69 })
70 }
71
72 fn new(
73 workspace: &Workspace,
74 thread_store: Model<ThreadStore>,
75 tools: Arc<ToolWorkingSet>,
76 cx: &mut ViewContext<Self>,
77 ) -> Self {
78 let thread = thread_store.update(cx, |this, cx| this.create_thread(cx));
79 let language_registry = workspace.project().read(cx).languages().clone();
80 let workspace = workspace.weak_handle();
81 let weak_self = cx.view().downgrade();
82
83 Self {
84 active_view: ActiveView::Thread,
85 workspace: workspace.clone(),
86 language_registry: language_registry.clone(),
87 thread_store: thread_store.clone(),
88 thread: cx.new_view(|cx| {
89 ActiveThread::new(
90 thread.clone(),
91 workspace.clone(),
92 language_registry,
93 tools.clone(),
94 cx,
95 )
96 }),
97 message_editor: cx.new_view(|cx| MessageEditor::new(workspace, thread.clone(), cx)),
98 tools,
99 local_timezone: UtcOffset::from_whole_seconds(
100 chrono::Local::now().offset().local_minus_utc(),
101 )
102 .unwrap(),
103 history: cx.new_view(|cx| ThreadHistory::new(weak_self, thread_store, cx)),
104 }
105 }
106
107 pub(crate) fn local_timezone(&self) -> UtcOffset {
108 self.local_timezone
109 }
110
111 fn new_thread(&mut self, cx: &mut ViewContext<Self>) {
112 let thread = self
113 .thread_store
114 .update(cx, |this, cx| this.create_thread(cx));
115
116 self.active_view = ActiveView::Thread;
117 self.thread = cx.new_view(|cx| {
118 ActiveThread::new(
119 thread.clone(),
120 self.workspace.clone(),
121 self.language_registry.clone(),
122 self.tools.clone(),
123 cx,
124 )
125 });
126 self.message_editor =
127 cx.new_view(|cx| MessageEditor::new(self.workspace.clone(), thread, cx));
128 self.message_editor.focus_handle(cx).focus(cx);
129 }
130
131 pub(crate) fn open_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext<Self>) {
132 let Some(thread) = self
133 .thread_store
134 .update(cx, |this, cx| this.open_thread(thread_id, cx))
135 else {
136 return;
137 };
138
139 self.active_view = ActiveView::Thread;
140 self.thread = cx.new_view(|cx| {
141 ActiveThread::new(
142 thread.clone(),
143 self.workspace.clone(),
144 self.language_registry.clone(),
145 self.tools.clone(),
146 cx,
147 )
148 });
149 self.message_editor =
150 cx.new_view(|cx| MessageEditor::new(self.workspace.clone(), thread, cx));
151 self.message_editor.focus_handle(cx).focus(cx);
152 }
153
154 pub(crate) fn delete_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext<Self>) {
155 self.thread_store
156 .update(cx, |this, cx| this.delete_thread(thread_id, cx));
157 }
158}
159
160impl FocusableView for AssistantPanel {
161 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
162 match self.active_view {
163 ActiveView::Thread => self.message_editor.focus_handle(cx),
164 ActiveView::History => self.history.focus_handle(cx),
165 }
166 }
167}
168
169impl EventEmitter<PanelEvent> for AssistantPanel {}
170
171impl Panel for AssistantPanel {
172 fn persistent_name() -> &'static str {
173 "AssistantPanel2"
174 }
175
176 fn position(&self, _cx: &WindowContext) -> DockPosition {
177 DockPosition::Right
178 }
179
180 fn position_is_valid(&self, _: DockPosition) -> bool {
181 true
182 }
183
184 fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext<Self>) {}
185
186 fn size(&self, _cx: &WindowContext) -> Pixels {
187 px(640.)
188 }
189
190 fn set_size(&mut self, _size: Option<Pixels>, _cx: &mut ViewContext<Self>) {}
191
192 fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
193
194 fn remote_id() -> Option<proto::PanelId> {
195 Some(proto::PanelId::AssistantPanel)
196 }
197
198 fn icon(&self, _cx: &WindowContext) -> Option<IconName> {
199 Some(IconName::ZedAssistant)
200 }
201
202 fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
203 Some("Assistant Panel")
204 }
205
206 fn toggle_action(&self) -> Box<dyn Action> {
207 Box::new(ToggleFocus)
208 }
209}
210
211impl AssistantPanel {
212 fn render_toolbar(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
213 let focus_handle = self.focus_handle(cx);
214
215 h_flex()
216 .id("assistant-toolbar")
217 .justify_between()
218 .gap(DynamicSpacing::Base08.rems(cx))
219 .h(Tab::container_height(cx))
220 .px(DynamicSpacing::Base08.rems(cx))
221 .bg(cx.theme().colors().tab_bar_background)
222 .border_b_1()
223 .border_color(cx.theme().colors().border_variant)
224 .child(h_flex().children(self.thread.read(cx).summary(cx).map(Label::new)))
225 .child(
226 h_flex()
227 .gap(DynamicSpacing::Base08.rems(cx))
228 .child(Divider::vertical())
229 .child(
230 IconButton::new("new-thread", IconName::Plus)
231 .shape(IconButtonShape::Square)
232 .icon_size(IconSize::Small)
233 .style(ButtonStyle::Subtle)
234 .tooltip({
235 let focus_handle = focus_handle.clone();
236 move |cx| {
237 Tooltip::for_action_in(
238 "New Thread",
239 &NewThread,
240 &focus_handle,
241 cx,
242 )
243 }
244 })
245 .on_click(move |_event, cx| {
246 cx.dispatch_action(NewThread.boxed_clone());
247 }),
248 )
249 .child(
250 IconButton::new("open-history", IconName::HistoryRerun)
251 .shape(IconButtonShape::Square)
252 .icon_size(IconSize::Small)
253 .style(ButtonStyle::Subtle)
254 .tooltip({
255 let focus_handle = focus_handle.clone();
256 move |cx| {
257 Tooltip::for_action_in(
258 "Open History",
259 &OpenHistory,
260 &focus_handle,
261 cx,
262 )
263 }
264 })
265 .on_click(move |_event, cx| {
266 cx.dispatch_action(OpenHistory.boxed_clone());
267 }),
268 )
269 .child(
270 IconButton::new("configure-assistant", IconName::Settings)
271 .shape(IconButtonShape::Square)
272 .icon_size(IconSize::Small)
273 .style(ButtonStyle::Subtle)
274 .tooltip(move |cx| Tooltip::text("Configure Assistant", cx))
275 .on_click(move |_event, _cx| {
276 println!("Configure Assistant");
277 }),
278 ),
279 )
280 }
281
282 fn render_active_thread_or_empty_state(&self, cx: &mut ViewContext<Self>) -> AnyElement {
283 if self.thread.read(cx).is_empty() {
284 return self.render_thread_empty_state(cx).into_any_element();
285 }
286
287 self.thread.clone().into_any()
288 }
289
290 fn render_thread_empty_state(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
291 let recent_threads = self
292 .thread_store
293 .update(cx, |this, cx| this.recent_threads(3, cx));
294
295 v_flex()
296 .gap_2()
297 .mx_auto()
298 .child(
299 v_flex().w_full().child(
300 svg()
301 .path("icons/logo_96.svg")
302 .text_color(cx.theme().colors().text)
303 .w(px(40.))
304 .h(px(40.))
305 .mx_auto()
306 .mb_4(),
307 ),
308 )
309 .when(!recent_threads.is_empty(), |parent| {
310 parent
311 .child(
312 h_flex()
313 .w_full()
314 .justify_center()
315 .child(Label::new("Recent Threads:").size(LabelSize::Small)),
316 )
317 .child(
318 v_flex().gap_2().children(
319 recent_threads
320 .into_iter()
321 .map(|thread| PastThread::new(thread, cx.view().downgrade())),
322 ),
323 )
324 .child(
325 h_flex().w_full().justify_center().child(
326 Button::new("view-all-past-threads", "View All Past Threads")
327 .style(ButtonStyle::Subtle)
328 .label_size(LabelSize::Small)
329 .key_binding(KeyBinding::for_action_in(
330 &OpenHistory,
331 &self.focus_handle(cx),
332 cx,
333 ))
334 .on_click(move |_event, cx| {
335 cx.dispatch_action(OpenHistory.boxed_clone());
336 }),
337 ),
338 )
339 })
340 }
341
342 fn render_last_error(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
343 let last_error = self.thread.read(cx).last_error()?;
344
345 Some(
346 div()
347 .absolute()
348 .right_3()
349 .bottom_12()
350 .max_w_96()
351 .py_2()
352 .px_3()
353 .elevation_2(cx)
354 .occlude()
355 .child(match last_error {
356 ThreadError::PaymentRequired => self.render_payment_required_error(cx),
357 ThreadError::MaxMonthlySpendReached => {
358 self.render_max_monthly_spend_reached_error(cx)
359 }
360 ThreadError::Message(error_message) => {
361 self.render_error_message(&error_message, cx)
362 }
363 })
364 .into_any(),
365 )
366 }
367
368 fn render_payment_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
369 const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
370
371 v_flex()
372 .gap_0p5()
373 .child(
374 h_flex()
375 .gap_1p5()
376 .items_center()
377 .child(Icon::new(IconName::XCircle).color(Color::Error))
378 .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
379 )
380 .child(
381 div()
382 .id("error-message")
383 .max_h_24()
384 .overflow_y_scroll()
385 .child(Label::new(ERROR_MESSAGE)),
386 )
387 .child(
388 h_flex()
389 .justify_end()
390 .mt_1()
391 .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
392 |this, _, cx| {
393 this.thread.update(cx, |this, _cx| {
394 this.clear_last_error();
395 });
396
397 cx.open_url(&zed_urls::account_url(cx));
398 cx.notify();
399 },
400 )))
401 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
402 |this, _, cx| {
403 this.thread.update(cx, |this, _cx| {
404 this.clear_last_error();
405 });
406
407 cx.notify();
408 },
409 ))),
410 )
411 .into_any()
412 }
413
414 fn render_max_monthly_spend_reached_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
415 const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
416
417 v_flex()
418 .gap_0p5()
419 .child(
420 h_flex()
421 .gap_1p5()
422 .items_center()
423 .child(Icon::new(IconName::XCircle).color(Color::Error))
424 .child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
425 )
426 .child(
427 div()
428 .id("error-message")
429 .max_h_24()
430 .overflow_y_scroll()
431 .child(Label::new(ERROR_MESSAGE)),
432 )
433 .child(
434 h_flex()
435 .justify_end()
436 .mt_1()
437 .child(
438 Button::new("subscribe", "Update Monthly Spend Limit").on_click(
439 cx.listener(|this, _, cx| {
440 this.thread.update(cx, |this, _cx| {
441 this.clear_last_error();
442 });
443
444 cx.open_url(&zed_urls::account_url(cx));
445 cx.notify();
446 }),
447 ),
448 )
449 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
450 |this, _, cx| {
451 this.thread.update(cx, |this, _cx| {
452 this.clear_last_error();
453 });
454
455 cx.notify();
456 },
457 ))),
458 )
459 .into_any()
460 }
461
462 fn render_error_message(
463 &self,
464 error_message: &SharedString,
465 cx: &mut ViewContext<Self>,
466 ) -> AnyElement {
467 v_flex()
468 .gap_0p5()
469 .child(
470 h_flex()
471 .gap_1p5()
472 .items_center()
473 .child(Icon::new(IconName::XCircle).color(Color::Error))
474 .child(
475 Label::new("Error interacting with language model")
476 .weight(FontWeight::MEDIUM),
477 ),
478 )
479 .child(
480 div()
481 .id("error-message")
482 .max_h_32()
483 .overflow_y_scroll()
484 .child(Label::new(error_message.clone())),
485 )
486 .child(
487 h_flex()
488 .justify_end()
489 .mt_1()
490 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
491 |this, _, cx| {
492 this.thread.update(cx, |this, _cx| {
493 this.clear_last_error();
494 });
495
496 cx.notify();
497 },
498 ))),
499 )
500 .into_any()
501 }
502}
503
504impl Render for AssistantPanel {
505 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
506 v_flex()
507 .key_context("AssistantPanel2")
508 .justify_between()
509 .size_full()
510 .on_action(cx.listener(|this, _: &NewThread, cx| {
511 this.new_thread(cx);
512 }))
513 .on_action(cx.listener(|this, _: &OpenHistory, cx| {
514 this.active_view = ActiveView::History;
515 this.history.focus_handle(cx).focus(cx);
516 cx.notify();
517 }))
518 .child(self.render_toolbar(cx))
519 .map(|parent| match self.active_view {
520 ActiveView::Thread => parent
521 .child(self.render_active_thread_or_empty_state(cx))
522 .child(
523 h_flex()
524 .border_t_1()
525 .border_color(cx.theme().colors().border_variant)
526 .child(self.message_editor.clone()),
527 )
528 .children(self.render_last_error(cx)),
529 ActiveView::History => parent.child(self.history.clone()),
530 })
531 }
532}