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