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,
92 language_registry,
93 tools.clone(),
94 cx,
95 )
96 }),
97 message_editor: cx.new_view(|cx| MessageEditor::new(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 = cx.new_view(|cx| MessageEditor::new(thread, cx));
127 self.message_editor.focus_handle(cx).focus(cx);
128 }
129
130 pub(crate) fn open_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext<Self>) {
131 let Some(thread) = self
132 .thread_store
133 .update(cx, |this, cx| this.open_thread(thread_id, cx))
134 else {
135 return;
136 };
137
138 self.active_view = ActiveView::Thread;
139 self.thread = cx.new_view(|cx| {
140 ActiveThread::new(
141 thread.clone(),
142 self.workspace.clone(),
143 self.language_registry.clone(),
144 self.tools.clone(),
145 cx,
146 )
147 });
148 self.message_editor = cx.new_view(|cx| MessageEditor::new(thread, cx));
149 self.message_editor.focus_handle(cx).focus(cx);
150 }
151
152 pub(crate) fn delete_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext<Self>) {
153 self.thread_store
154 .update(cx, |this, cx| this.delete_thread(thread_id, cx));
155 }
156}
157
158impl FocusableView for AssistantPanel {
159 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
160 match self.active_view {
161 ActiveView::Thread => self.message_editor.focus_handle(cx),
162 ActiveView::History => self.history.focus_handle(cx),
163 }
164 }
165}
166
167impl EventEmitter<PanelEvent> for AssistantPanel {}
168
169impl Panel for AssistantPanel {
170 fn persistent_name() -> &'static str {
171 "AssistantPanel2"
172 }
173
174 fn position(&self, _cx: &WindowContext) -> DockPosition {
175 DockPosition::Right
176 }
177
178 fn position_is_valid(&self, _: DockPosition) -> bool {
179 true
180 }
181
182 fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext<Self>) {}
183
184 fn size(&self, _cx: &WindowContext) -> Pixels {
185 px(640.)
186 }
187
188 fn set_size(&mut self, _size: Option<Pixels>, _cx: &mut ViewContext<Self>) {}
189
190 fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
191
192 fn remote_id() -> Option<proto::PanelId> {
193 Some(proto::PanelId::AssistantPanel)
194 }
195
196 fn icon(&self, _cx: &WindowContext) -> Option<IconName> {
197 Some(IconName::ZedAssistant)
198 }
199
200 fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
201 Some("Assistant Panel")
202 }
203
204 fn toggle_action(&self) -> Box<dyn Action> {
205 Box::new(ToggleFocus)
206 }
207}
208
209impl AssistantPanel {
210 fn render_toolbar(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
211 let focus_handle = self.focus_handle(cx);
212
213 h_flex()
214 .id("assistant-toolbar")
215 .justify_between()
216 .gap(DynamicSpacing::Base08.rems(cx))
217 .h(Tab::container_height(cx))
218 .px(DynamicSpacing::Base08.rems(cx))
219 .bg(cx.theme().colors().tab_bar_background)
220 .border_b_1()
221 .border_color(cx.theme().colors().border_variant)
222 .child(h_flex().children(self.thread.read(cx).summary(cx).map(Label::new)))
223 .child(
224 h_flex()
225 .gap(DynamicSpacing::Base08.rems(cx))
226 .child(Divider::vertical())
227 .child(
228 IconButton::new("new-thread", IconName::Plus)
229 .shape(IconButtonShape::Square)
230 .icon_size(IconSize::Small)
231 .style(ButtonStyle::Subtle)
232 .tooltip({
233 let focus_handle = focus_handle.clone();
234 move |cx| {
235 Tooltip::for_action_in(
236 "New Thread",
237 &NewThread,
238 &focus_handle,
239 cx,
240 )
241 }
242 })
243 .on_click(move |_event, cx| {
244 cx.dispatch_action(NewThread.boxed_clone());
245 }),
246 )
247 .child(
248 IconButton::new("open-history", IconName::HistoryRerun)
249 .shape(IconButtonShape::Square)
250 .icon_size(IconSize::Small)
251 .style(ButtonStyle::Subtle)
252 .tooltip({
253 let focus_handle = focus_handle.clone();
254 move |cx| {
255 Tooltip::for_action_in(
256 "Open History",
257 &OpenHistory,
258 &focus_handle,
259 cx,
260 )
261 }
262 })
263 .on_click(move |_event, cx| {
264 cx.dispatch_action(OpenHistory.boxed_clone());
265 }),
266 )
267 .child(
268 IconButton::new("configure-assistant", IconName::Settings)
269 .shape(IconButtonShape::Square)
270 .icon_size(IconSize::Small)
271 .style(ButtonStyle::Subtle)
272 .tooltip(move |cx| Tooltip::text("Configure Assistant", cx))
273 .on_click(move |_event, _cx| {
274 println!("Configure Assistant");
275 }),
276 ),
277 )
278 }
279
280 fn render_active_thread_or_empty_state(&self, cx: &mut ViewContext<Self>) -> AnyElement {
281 if self.thread.read(cx).is_empty() {
282 return self.render_thread_empty_state(cx).into_any_element();
283 }
284
285 self.thread.clone().into_any()
286 }
287
288 fn render_thread_empty_state(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
289 let recent_threads = self
290 .thread_store
291 .update(cx, |this, cx| this.recent_threads(3, cx));
292
293 v_flex()
294 .gap_2()
295 .mx_auto()
296 .child(
297 v_flex().w_full().child(
298 svg()
299 .path("icons/logo_96.svg")
300 .text_color(cx.theme().colors().text)
301 .w(px(40.))
302 .h(px(40.))
303 .mx_auto()
304 .mb_4(),
305 ),
306 )
307 .when(!recent_threads.is_empty(), |parent| {
308 parent
309 .child(
310 h_flex()
311 .w_full()
312 .justify_center()
313 .child(Label::new("Recent Threads:").size(LabelSize::Small)),
314 )
315 .child(
316 v_flex().gap_2().children(
317 recent_threads
318 .into_iter()
319 .map(|thread| PastThread::new(thread, cx.view().downgrade())),
320 ),
321 )
322 .child(
323 h_flex().w_full().justify_center().child(
324 Button::new("view-all-past-threads", "View All Past Threads")
325 .style(ButtonStyle::Subtle)
326 .label_size(LabelSize::Small)
327 .key_binding(KeyBinding::for_action_in(
328 &OpenHistory,
329 &self.focus_handle(cx),
330 cx,
331 ))
332 .on_click(move |_event, cx| {
333 cx.dispatch_action(OpenHistory.boxed_clone());
334 }),
335 ),
336 )
337 })
338 }
339
340 fn render_last_error(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
341 let last_error = self.thread.read(cx).last_error()?;
342
343 Some(
344 div()
345 .absolute()
346 .right_3()
347 .bottom_12()
348 .max_w_96()
349 .py_2()
350 .px_3()
351 .elevation_2(cx)
352 .occlude()
353 .child(match last_error {
354 ThreadError::PaymentRequired => self.render_payment_required_error(cx),
355 ThreadError::MaxMonthlySpendReached => {
356 self.render_max_monthly_spend_reached_error(cx)
357 }
358 ThreadError::Message(error_message) => {
359 self.render_error_message(&error_message, cx)
360 }
361 })
362 .into_any(),
363 )
364 }
365
366 fn render_payment_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
367 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.";
368
369 v_flex()
370 .gap_0p5()
371 .child(
372 h_flex()
373 .gap_1p5()
374 .items_center()
375 .child(Icon::new(IconName::XCircle).color(Color::Error))
376 .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
377 )
378 .child(
379 div()
380 .id("error-message")
381 .max_h_24()
382 .overflow_y_scroll()
383 .child(Label::new(ERROR_MESSAGE)),
384 )
385 .child(
386 h_flex()
387 .justify_end()
388 .mt_1()
389 .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
390 |this, _, cx| {
391 this.thread.update(cx, |this, _cx| {
392 this.clear_last_error();
393 });
394
395 cx.open_url(&zed_urls::account_url(cx));
396 cx.notify();
397 },
398 )))
399 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
400 |this, _, cx| {
401 this.thread.update(cx, |this, _cx| {
402 this.clear_last_error();
403 });
404
405 cx.notify();
406 },
407 ))),
408 )
409 .into_any()
410 }
411
412 fn render_max_monthly_spend_reached_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
413 const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
414
415 v_flex()
416 .gap_0p5()
417 .child(
418 h_flex()
419 .gap_1p5()
420 .items_center()
421 .child(Icon::new(IconName::XCircle).color(Color::Error))
422 .child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
423 )
424 .child(
425 div()
426 .id("error-message")
427 .max_h_24()
428 .overflow_y_scroll()
429 .child(Label::new(ERROR_MESSAGE)),
430 )
431 .child(
432 h_flex()
433 .justify_end()
434 .mt_1()
435 .child(
436 Button::new("subscribe", "Update Monthly Spend Limit").on_click(
437 cx.listener(|this, _, cx| {
438 this.thread.update(cx, |this, _cx| {
439 this.clear_last_error();
440 });
441
442 cx.open_url(&zed_urls::account_url(cx));
443 cx.notify();
444 }),
445 ),
446 )
447 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
448 |this, _, cx| {
449 this.thread.update(cx, |this, _cx| {
450 this.clear_last_error();
451 });
452
453 cx.notify();
454 },
455 ))),
456 )
457 .into_any()
458 }
459
460 fn render_error_message(
461 &self,
462 error_message: &SharedString,
463 cx: &mut ViewContext<Self>,
464 ) -> AnyElement {
465 v_flex()
466 .gap_0p5()
467 .child(
468 h_flex()
469 .gap_1p5()
470 .items_center()
471 .child(Icon::new(IconName::XCircle).color(Color::Error))
472 .child(
473 Label::new("Error interacting with language model")
474 .weight(FontWeight::MEDIUM),
475 ),
476 )
477 .child(
478 div()
479 .id("error-message")
480 .max_h_32()
481 .overflow_y_scroll()
482 .child(Label::new(error_message.clone())),
483 )
484 .child(
485 h_flex()
486 .justify_end()
487 .mt_1()
488 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
489 |this, _, cx| {
490 this.thread.update(cx, |this, _cx| {
491 this.clear_last_error();
492 });
493
494 cx.notify();
495 },
496 ))),
497 )
498 .into_any()
499 }
500}
501
502impl Render for AssistantPanel {
503 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
504 v_flex()
505 .key_context("AssistantPanel2")
506 .justify_between()
507 .size_full()
508 .on_action(cx.listener(|this, _: &NewThread, cx| {
509 this.new_thread(cx);
510 }))
511 .on_action(cx.listener(|this, _: &OpenHistory, cx| {
512 this.active_view = ActiveView::History;
513 this.history.focus_handle(cx).focus(cx);
514 cx.notify();
515 }))
516 .child(self.render_toolbar(cx))
517 .map(|parent| match self.active_view {
518 ActiveView::Thread => parent
519 .child(self.render_active_thread_or_empty_state(cx))
520 .child(
521 h_flex()
522 .border_t_1()
523 .border_color(cx.theme().colors().border_variant)
524 .child(self.message_editor.clone()),
525 )
526 .children(self.render_last_error(cx)),
527 ActiveView::History => parent.child(self.history.clone()),
528 })
529 }
530}