1use std::path::PathBuf;
2use std::sync::Arc;
3use std::time::Duration;
4
5use anyhow::{Result, anyhow};
6use assistant_context_editor::{
7 AssistantPanelDelegate, ConfigurationError, ContextEditor, SlashCommandCompletionProvider,
8 make_lsp_adapter_delegate, render_remaining_tokens,
9};
10use assistant_settings::{AssistantDockPosition, AssistantSettings};
11use assistant_slash_command::SlashCommandWorkingSet;
12use assistant_tool::ToolWorkingSet;
13
14use client::zed_urls;
15use editor::{Editor, EditorEvent, MultiBuffer};
16use fs::Fs;
17use gpui::{
18 Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, Corner, Entity,
19 EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext, Pixels, Subscription, Task,
20 UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
21};
22use language::LanguageRegistry;
23use language_model::{LanguageModelProviderTosView, LanguageModelRegistry};
24use language_model_selector::ToggleModelSelector;
25use project::Project;
26use prompt_library::{PromptLibrary, open_prompt_library};
27use prompt_store::PromptBuilder;
28use settings::{Settings, update_settings_file};
29use time::UtcOffset;
30use ui::{
31 Banner, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, Tab, Tooltip, prelude::*,
32};
33use util::ResultExt as _;
34use workspace::Workspace;
35use workspace::dock::{DockPosition, Panel, PanelEvent};
36use zed_actions::agent::OpenConfiguration;
37use zed_actions::assistant::{OpenPromptLibrary, ToggleFocus};
38
39use crate::active_thread::ActiveThread;
40use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent};
41use crate::history_store::{HistoryEntry, HistoryStore};
42use crate::message_editor::MessageEditor;
43use crate::thread::{Thread, ThreadError, ThreadId, TokenUsageRatio};
44use crate::thread_history::{PastContext, PastThread, ThreadHistory};
45use crate::thread_store::ThreadStore;
46use crate::{
47 AgentDiff, InlineAssistant, NewPromptEditor, NewThread, OpenActiveThreadAsMarkdown,
48 OpenAgentDiff, OpenHistory, ThreadEvent, ToggleContextPicker,
49};
50
51pub fn init(cx: &mut App) {
52 cx.observe_new(
53 |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
54 workspace
55 .register_action(|workspace, action: &NewThread, window, cx| {
56 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
57 panel.update(cx, |panel, cx| panel.new_thread(action, window, cx));
58 workspace.focus_panel::<AssistantPanel>(window, cx);
59 }
60 })
61 .register_action(|workspace, _: &OpenHistory, window, cx| {
62 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
63 workspace.focus_panel::<AssistantPanel>(window, cx);
64 panel.update(cx, |panel, cx| panel.open_history(window, cx));
65 }
66 })
67 .register_action(|workspace, _: &OpenConfiguration, window, cx| {
68 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
69 workspace.focus_panel::<AssistantPanel>(window, cx);
70 panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
71 }
72 })
73 .register_action(|workspace, _: &NewPromptEditor, window, cx| {
74 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
75 workspace.focus_panel::<AssistantPanel>(window, cx);
76 panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx));
77 }
78 })
79 .register_action(|workspace, _: &OpenPromptLibrary, window, cx| {
80 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
81 workspace.focus_panel::<AssistantPanel>(window, cx);
82 panel.update(cx, |panel, cx| {
83 panel.deploy_prompt_library(&OpenPromptLibrary, window, cx)
84 });
85 }
86 })
87 .register_action(|workspace, _: &OpenAgentDiff, window, cx| {
88 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
89 workspace.focus_panel::<AssistantPanel>(window, cx);
90 let thread = panel.read(cx).thread.read(cx).thread().clone();
91 AgentDiff::deploy_in_workspace(thread, workspace, window, cx);
92 }
93 });
94 },
95 )
96 .detach();
97}
98
99enum ActiveView {
100 Thread {
101 change_title_editor: Entity<Editor>,
102 _subscriptions: Vec<gpui::Subscription>,
103 },
104 PromptEditor,
105 History,
106 Configuration,
107}
108
109impl ActiveView {
110 pub fn thread(thread: Entity<Thread>, window: &mut Window, cx: &mut App) -> Self {
111 let summary = thread.read(cx).summary_or_default();
112
113 let editor = cx.new(|cx| {
114 let mut editor = Editor::single_line(window, cx);
115 editor.set_text(summary, window, cx);
116 editor
117 });
118
119 let subscriptions = vec![
120 window.subscribe(&editor, cx, {
121 {
122 let thread = thread.clone();
123 move |editor, event, window, cx| match event {
124 EditorEvent::BufferEdited => {
125 let new_summary = editor.read(cx).text(cx);
126
127 thread.update(cx, |thread, cx| {
128 thread.set_summary(new_summary, cx);
129 })
130 }
131 EditorEvent::Blurred => {
132 if editor.read(cx).text(cx).is_empty() {
133 let summary = thread.read(cx).summary_or_default();
134
135 editor.update(cx, |editor, cx| {
136 editor.set_text(summary, window, cx);
137 });
138 }
139 }
140 _ => {}
141 }
142 }
143 }),
144 window.subscribe(&thread, cx, {
145 let editor = editor.clone();
146 move |thread, event, window, cx| match event {
147 ThreadEvent::SummaryGenerated => {
148 let summary = thread.read(cx).summary_or_default();
149
150 editor.update(cx, |editor, cx| {
151 editor.set_text(summary, window, cx);
152 })
153 }
154 _ => {}
155 }
156 }),
157 ];
158
159 Self::Thread {
160 change_title_editor: editor,
161 _subscriptions: subscriptions,
162 }
163 }
164}
165
166pub struct AssistantPanel {
167 workspace: WeakEntity<Workspace>,
168 project: Entity<Project>,
169 fs: Arc<dyn Fs>,
170 language_registry: Arc<LanguageRegistry>,
171 thread_store: Entity<ThreadStore>,
172 thread: Entity<ActiveThread>,
173 _thread_subscription: Subscription,
174 message_editor: Entity<MessageEditor>,
175 context_store: Entity<assistant_context_editor::ContextStore>,
176 context_editor: Option<Entity<ContextEditor>>,
177 configuration: Option<Entity<AssistantConfiguration>>,
178 configuration_subscription: Option<Subscription>,
179 local_timezone: UtcOffset,
180 active_view: ActiveView,
181 history_store: Entity<HistoryStore>,
182 history: Entity<ThreadHistory>,
183 assistant_dropdown_menu_handle: PopoverMenuHandle<ContextMenu>,
184 width: Option<Pixels>,
185 height: Option<Pixels>,
186}
187
188impl AssistantPanel {
189 pub fn load(
190 workspace: WeakEntity<Workspace>,
191 prompt_builder: Arc<PromptBuilder>,
192 cx: AsyncWindowContext,
193 ) -> Task<Result<Entity<Self>>> {
194 cx.spawn(async move |cx| {
195 let tools = Arc::new(ToolWorkingSet::default());
196 let thread_store = workspace.update(cx, |workspace, cx| {
197 let project = workspace.project().clone();
198 ThreadStore::new(project, tools.clone(), prompt_builder.clone(), cx)
199 })??;
200
201 let slash_commands = Arc::new(SlashCommandWorkingSet::default());
202 let context_store = workspace
203 .update(cx, |workspace, cx| {
204 let project = workspace.project().clone();
205 assistant_context_editor::ContextStore::new(
206 project,
207 prompt_builder.clone(),
208 slash_commands,
209 cx,
210 )
211 })?
212 .await?;
213
214 workspace.update_in(cx, |workspace, window, cx| {
215 cx.new(|cx| Self::new(workspace, thread_store, context_store, window, cx))
216 })
217 })
218 }
219
220 fn new(
221 workspace: &Workspace,
222 thread_store: Entity<ThreadStore>,
223 context_store: Entity<assistant_context_editor::ContextStore>,
224 window: &mut Window,
225 cx: &mut Context<Self>,
226 ) -> Self {
227 let thread = thread_store.update(cx, |this, cx| this.create_thread(cx));
228 let fs = workspace.app_state().fs.clone();
229 let project = workspace.project().clone();
230 let language_registry = project.read(cx).languages().clone();
231 let workspace = workspace.weak_handle();
232 let weak_self = cx.entity().downgrade();
233
234 let message_editor_context_store = cx.new(|_cx| {
235 crate::context_store::ContextStore::new(
236 workspace.clone(),
237 Some(thread_store.downgrade()),
238 )
239 });
240
241 let message_editor = cx.new(|cx| {
242 MessageEditor::new(
243 fs.clone(),
244 workspace.clone(),
245 message_editor_context_store.clone(),
246 thread_store.downgrade(),
247 thread.clone(),
248 window,
249 cx,
250 )
251 });
252
253 let history_store =
254 cx.new(|cx| HistoryStore::new(thread_store.clone(), context_store.clone(), cx));
255
256 cx.observe(&history_store, |_, _, cx| cx.notify()).detach();
257
258 let active_view = ActiveView::thread(thread.clone(), window, cx);
259 let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
260 if let ThreadEvent::MessageAdded(_) = &event {
261 // needed to leave empty state
262 cx.notify();
263 }
264 });
265 let thread = cx.new(|cx| {
266 ActiveThread::new(
267 thread.clone(),
268 thread_store.clone(),
269 language_registry.clone(),
270 message_editor_context_store.clone(),
271 workspace.clone(),
272 window,
273 cx,
274 )
275 });
276
277 Self {
278 active_view,
279 workspace,
280 project: project.clone(),
281 fs: fs.clone(),
282 language_registry,
283 thread_store: thread_store.clone(),
284 thread,
285 _thread_subscription: thread_subscription,
286 message_editor,
287 context_store,
288 context_editor: None,
289 configuration: None,
290 configuration_subscription: None,
291 local_timezone: UtcOffset::from_whole_seconds(
292 chrono::Local::now().offset().local_minus_utc(),
293 )
294 .unwrap(),
295 history_store: history_store.clone(),
296 history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)),
297 assistant_dropdown_menu_handle: PopoverMenuHandle::default(),
298 width: None,
299 height: None,
300 }
301 }
302
303 pub fn toggle_focus(
304 workspace: &mut Workspace,
305 _: &ToggleFocus,
306 window: &mut Window,
307 cx: &mut Context<Workspace>,
308 ) {
309 if workspace
310 .panel::<Self>(cx)
311 .is_some_and(|panel| panel.read(cx).enabled(cx))
312 {
313 workspace.toggle_panel_focus::<Self>(window, cx);
314 }
315 }
316
317 pub(crate) fn local_timezone(&self) -> UtcOffset {
318 self.local_timezone
319 }
320
321 pub(crate) fn thread_store(&self) -> &Entity<ThreadStore> {
322 &self.thread_store
323 }
324
325 fn cancel(
326 &mut self,
327 _: &editor::actions::Cancel,
328 _window: &mut Window,
329 cx: &mut Context<Self>,
330 ) {
331 self.thread
332 .update(cx, |thread, cx| thread.cancel_last_completion(cx));
333 }
334
335 fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {
336 let thread = self
337 .thread_store
338 .update(cx, |this, cx| this.create_thread(cx));
339
340 self.active_view = ActiveView::thread(thread.clone(), window, cx);
341
342 let message_editor_context_store = cx.new(|_cx| {
343 crate::context_store::ContextStore::new(
344 self.workspace.clone(),
345 Some(self.thread_store.downgrade()),
346 )
347 });
348
349 if let Some(other_thread_id) = action.from_thread_id.clone() {
350 let other_thread_task = self
351 .thread_store
352 .update(cx, |this, cx| this.open_thread(&other_thread_id, cx));
353
354 cx.spawn({
355 let context_store = message_editor_context_store.clone();
356
357 async move |_panel, cx| {
358 let other_thread = other_thread_task.await?;
359
360 context_store.update(cx, |this, cx| {
361 this.add_thread(other_thread, false, cx);
362 })?;
363 anyhow::Ok(())
364 }
365 })
366 .detach_and_log_err(cx);
367 }
368
369 self.thread = cx.new(|cx| {
370 ActiveThread::new(
371 thread.clone(),
372 self.thread_store.clone(),
373 self.language_registry.clone(),
374 message_editor_context_store.clone(),
375 self.workspace.clone(),
376 window,
377 cx,
378 )
379 });
380
381 self._thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
382 if let ThreadEvent::MessageAdded(_) = &event {
383 // needed to leave empty state
384 cx.notify();
385 }
386 });
387
388 self.message_editor = cx.new(|cx| {
389 MessageEditor::new(
390 self.fs.clone(),
391 self.workspace.clone(),
392 message_editor_context_store,
393 self.thread_store.downgrade(),
394 thread,
395 window,
396 cx,
397 )
398 });
399 self.message_editor.focus_handle(cx).focus(window);
400 }
401
402 fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
403 self.active_view = ActiveView::PromptEditor;
404
405 let context = self
406 .context_store
407 .update(cx, |context_store, cx| context_store.create(cx));
408 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
409 .log_err()
410 .flatten();
411
412 self.context_editor = Some(cx.new(|cx| {
413 let mut editor = ContextEditor::for_context(
414 context,
415 self.fs.clone(),
416 self.workspace.clone(),
417 self.project.clone(),
418 lsp_adapter_delegate,
419 window,
420 cx,
421 );
422 editor.insert_default_prompt(window, cx);
423 editor
424 }));
425
426 if let Some(context_editor) = self.context_editor.as_ref() {
427 context_editor.focus_handle(cx).focus(window);
428 }
429 }
430
431 fn deploy_prompt_library(
432 &mut self,
433 _: &OpenPromptLibrary,
434 _window: &mut Window,
435 cx: &mut Context<Self>,
436 ) {
437 open_prompt_library(
438 self.language_registry.clone(),
439 Box::new(PromptLibraryInlineAssist::new(self.workspace.clone())),
440 Arc::new(|| {
441 Box::new(SlashCommandCompletionProvider::new(
442 Arc::new(SlashCommandWorkingSet::default()),
443 None,
444 None,
445 ))
446 }),
447 cx,
448 )
449 .detach_and_log_err(cx);
450 }
451
452 fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
453 self.thread_store
454 .update(cx, |thread_store, cx| thread_store.reload(cx))
455 .detach_and_log_err(cx);
456 self.active_view = ActiveView::History;
457 self.history.focus_handle(cx).focus(window);
458 cx.notify();
459 }
460
461 pub(crate) fn open_saved_prompt_editor(
462 &mut self,
463 path: PathBuf,
464 window: &mut Window,
465 cx: &mut Context<Self>,
466 ) -> Task<Result<()>> {
467 let context = self
468 .context_store
469 .update(cx, |store, cx| store.open_local_context(path.clone(), cx));
470 let fs = self.fs.clone();
471 let project = self.project.clone();
472 let workspace = self.workspace.clone();
473
474 let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err().flatten();
475
476 cx.spawn_in(window, async move |this, cx| {
477 let context = context.await?;
478 this.update_in(cx, |this, window, cx| {
479 let editor = cx.new(|cx| {
480 ContextEditor::for_context(
481 context,
482 fs,
483 workspace,
484 project,
485 lsp_adapter_delegate,
486 window,
487 cx,
488 )
489 });
490 this.active_view = ActiveView::PromptEditor;
491 this.context_editor = Some(editor);
492
493 anyhow::Ok(())
494 })??;
495 Ok(())
496 })
497 }
498
499 pub(crate) fn open_thread(
500 &mut self,
501 thread_id: &ThreadId,
502 window: &mut Window,
503 cx: &mut Context<Self>,
504 ) -> Task<Result<()>> {
505 let open_thread_task = self
506 .thread_store
507 .update(cx, |this, cx| this.open_thread(thread_id, cx));
508
509 cx.spawn_in(window, async move |this, cx| {
510 let thread = open_thread_task.await?;
511 this.update_in(cx, |this, window, cx| {
512 this.active_view = ActiveView::thread(thread.clone(), window, cx);
513 let message_editor_context_store = cx.new(|_cx| {
514 crate::context_store::ContextStore::new(
515 this.workspace.clone(),
516 Some(this.thread_store.downgrade()),
517 )
518 });
519 this.thread = cx.new(|cx| {
520 ActiveThread::new(
521 thread.clone(),
522 this.thread_store.clone(),
523 this.language_registry.clone(),
524 message_editor_context_store.clone(),
525 this.workspace.clone(),
526 window,
527 cx,
528 )
529 });
530 this.message_editor = cx.new(|cx| {
531 MessageEditor::new(
532 this.fs.clone(),
533 this.workspace.clone(),
534 message_editor_context_store,
535 this.thread_store.downgrade(),
536 thread,
537 window,
538 cx,
539 )
540 });
541 this.message_editor.focus_handle(cx).focus(window);
542 })
543 })
544 }
545
546 pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
547 match self.active_view {
548 ActiveView::Configuration | ActiveView::History => {
549 self.active_view =
550 ActiveView::thread(self.thread.read(cx).thread().clone(), window, cx);
551 cx.notify();
552 }
553 _ => {}
554 }
555 }
556
557 pub fn open_agent_diff(
558 &mut self,
559 _: &OpenAgentDiff,
560 window: &mut Window,
561 cx: &mut Context<Self>,
562 ) {
563 let thread = self.thread.read(cx).thread().clone();
564 self.workspace
565 .update(cx, |workspace, cx| {
566 AgentDiff::deploy_in_workspace(thread, workspace, window, cx)
567 })
568 .log_err();
569 }
570
571 pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
572 let context_server_manager = self.thread_store.read(cx).context_server_manager();
573 let tools = self.thread_store.read(cx).tools();
574 let fs = self.fs.clone();
575
576 self.active_view = ActiveView::Configuration;
577 self.configuration =
578 Some(cx.new(|cx| {
579 AssistantConfiguration::new(fs, context_server_manager, tools, window, cx)
580 }));
581
582 if let Some(configuration) = self.configuration.as_ref() {
583 self.configuration_subscription = Some(cx.subscribe_in(
584 configuration,
585 window,
586 Self::handle_assistant_configuration_event,
587 ));
588
589 configuration.focus_handle(cx).focus(window);
590 }
591 }
592
593 pub(crate) fn open_active_thread_as_markdown(
594 &mut self,
595 _: &OpenActiveThreadAsMarkdown,
596 window: &mut Window,
597 cx: &mut Context<Self>,
598 ) {
599 let Some(workspace) = self
600 .workspace
601 .upgrade()
602 .ok_or_else(|| anyhow!("workspace dropped"))
603 .log_err()
604 else {
605 return;
606 };
607
608 let markdown_language_task = workspace
609 .read(cx)
610 .app_state()
611 .languages
612 .language_for_name("Markdown");
613 let thread = self.active_thread(cx);
614 cx.spawn_in(window, async move |_this, cx| {
615 let markdown_language = markdown_language_task.await?;
616
617 workspace.update_in(cx, |workspace, window, cx| {
618 let thread = thread.read(cx);
619 let markdown = thread.to_markdown(cx)?;
620 let thread_summary = thread
621 .summary()
622 .map(|summary| summary.to_string())
623 .unwrap_or_else(|| "Thread".to_string());
624
625 let project = workspace.project().clone();
626 let buffer = project.update(cx, |project, cx| {
627 project.create_local_buffer(&markdown, Some(markdown_language), cx)
628 });
629 let buffer = cx.new(|cx| {
630 MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone())
631 });
632
633 workspace.add_item_to_active_pane(
634 Box::new(cx.new(|cx| {
635 let mut editor =
636 Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
637 editor.set_breadcrumb_header(thread_summary);
638 editor
639 })),
640 None,
641 true,
642 window,
643 cx,
644 );
645
646 anyhow::Ok(())
647 })
648 })
649 .detach_and_log_err(cx);
650 }
651
652 fn handle_assistant_configuration_event(
653 &mut self,
654 _entity: &Entity<AssistantConfiguration>,
655 event: &AssistantConfigurationEvent,
656 window: &mut Window,
657 cx: &mut Context<Self>,
658 ) {
659 match event {
660 AssistantConfigurationEvent::NewThread(provider) => {
661 if LanguageModelRegistry::read_global(cx)
662 .default_model()
663 .map_or(true, |model| model.provider.id() != provider.id())
664 {
665 if let Some(model) = provider.default_model(cx) {
666 update_settings_file::<AssistantSettings>(
667 self.fs.clone(),
668 cx,
669 move |settings, _| settings.set_model(model),
670 );
671 }
672 }
673
674 self.new_thread(&NewThread::default(), window, cx);
675 }
676 }
677 }
678
679 pub(crate) fn active_thread(&self, cx: &App) -> Entity<Thread> {
680 self.thread.read(cx).thread().clone()
681 }
682
683 pub(crate) fn delete_thread(
684 &mut self,
685 thread_id: &ThreadId,
686 cx: &mut Context<Self>,
687 ) -> Task<Result<()>> {
688 self.thread_store
689 .update(cx, |this, cx| this.delete_thread(thread_id, cx))
690 }
691
692 pub(crate) fn active_context_editor(&self) -> Option<Entity<ContextEditor>> {
693 self.context_editor.clone()
694 }
695
696 pub(crate) fn delete_context(
697 &mut self,
698 path: PathBuf,
699 cx: &mut Context<Self>,
700 ) -> Task<Result<()>> {
701 self.context_store
702 .update(cx, |this, cx| this.delete_local_context(path, cx))
703 }
704}
705
706impl Focusable for AssistantPanel {
707 fn focus_handle(&self, cx: &App) -> FocusHandle {
708 match self.active_view {
709 ActiveView::Thread { .. } => self.message_editor.focus_handle(cx),
710 ActiveView::History => self.history.focus_handle(cx),
711 ActiveView::PromptEditor => {
712 if let Some(context_editor) = self.context_editor.as_ref() {
713 context_editor.focus_handle(cx)
714 } else {
715 cx.focus_handle()
716 }
717 }
718 ActiveView::Configuration => {
719 if let Some(configuration) = self.configuration.as_ref() {
720 configuration.focus_handle(cx)
721 } else {
722 cx.focus_handle()
723 }
724 }
725 }
726 }
727}
728
729impl EventEmitter<PanelEvent> for AssistantPanel {}
730
731impl Panel for AssistantPanel {
732 fn persistent_name() -> &'static str {
733 "AgentPanel"
734 }
735
736 fn position(&self, _window: &Window, cx: &App) -> DockPosition {
737 match AssistantSettings::get_global(cx).dock {
738 AssistantDockPosition::Left => DockPosition::Left,
739 AssistantDockPosition::Bottom => DockPosition::Bottom,
740 AssistantDockPosition::Right => DockPosition::Right,
741 }
742 }
743
744 fn position_is_valid(&self, _: DockPosition) -> bool {
745 true
746 }
747
748 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
749 settings::update_settings_file::<AssistantSettings>(
750 self.fs.clone(),
751 cx,
752 move |settings, _| {
753 let dock = match position {
754 DockPosition::Left => AssistantDockPosition::Left,
755 DockPosition::Bottom => AssistantDockPosition::Bottom,
756 DockPosition::Right => AssistantDockPosition::Right,
757 };
758 settings.set_dock(dock);
759 },
760 );
761 }
762
763 fn size(&self, window: &Window, cx: &App) -> Pixels {
764 let settings = AssistantSettings::get_global(cx);
765 match self.position(window, cx) {
766 DockPosition::Left | DockPosition::Right => {
767 self.width.unwrap_or(settings.default_width)
768 }
769 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
770 }
771 }
772
773 fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
774 match self.position(window, cx) {
775 DockPosition::Left | DockPosition::Right => self.width = size,
776 DockPosition::Bottom => self.height = size,
777 }
778 cx.notify();
779 }
780
781 fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
782
783 fn remote_id() -> Option<proto::PanelId> {
784 Some(proto::PanelId::AssistantPanel)
785 }
786
787 fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
788 (self.enabled(cx) && AssistantSettings::get_global(cx).button)
789 .then_some(IconName::ZedAssistant)
790 }
791
792 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
793 Some("Agent Panel")
794 }
795
796 fn toggle_action(&self) -> Box<dyn Action> {
797 Box::new(ToggleFocus)
798 }
799
800 fn activation_priority(&self) -> u32 {
801 3
802 }
803
804 fn enabled(&self, cx: &App) -> bool {
805 AssistantSettings::get_global(cx).enabled
806 }
807}
808
809impl AssistantPanel {
810 fn render_title_view(&self, _window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
811 const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
812
813 let content = match &self.active_view {
814 ActiveView::Thread {
815 change_title_editor,
816 ..
817 } => {
818 let active_thread = self.thread.read(cx);
819 let is_empty = active_thread.is_empty();
820
821 let summary = active_thread.summary(cx);
822
823 if is_empty {
824 Label::new(Thread::DEFAULT_SUMMARY.clone())
825 .truncate()
826 .into_any_element()
827 } else if summary.is_none() {
828 Label::new(LOADING_SUMMARY_PLACEHOLDER)
829 .truncate()
830 .into_any_element()
831 } else {
832 change_title_editor.clone().into_any_element()
833 }
834 }
835 ActiveView::PromptEditor => {
836 let title = self
837 .context_editor
838 .as_ref()
839 .map(|context_editor| {
840 SharedString::from(context_editor.read(cx).title(cx).to_string())
841 })
842 .unwrap_or_else(|| SharedString::from(LOADING_SUMMARY_PLACEHOLDER));
843
844 Label::new(title).truncate().into_any_element()
845 }
846 ActiveView::History => Label::new("History").truncate().into_any_element(),
847 ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
848 };
849
850 h_flex()
851 .key_context("TitleEditor")
852 .id("TitleEditor")
853 .flex_grow()
854 .w_full()
855 .max_w_full()
856 .overflow_x_scroll()
857 .child(content)
858 .into_any()
859 }
860
861 fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
862 let active_thread = self.thread.read(cx);
863 let thread = active_thread.thread().read(cx);
864 let token_usage = thread.total_token_usage(cx);
865 let thread_id = thread.id().clone();
866
867 let is_generating = thread.is_generating();
868 let is_empty = active_thread.is_empty();
869 let focus_handle = self.focus_handle(cx);
870
871 let show_token_count = match &self.active_view {
872 ActiveView::Thread { .. } => !is_empty,
873 ActiveView::PromptEditor => self.context_editor.is_some(),
874 _ => false,
875 };
876
877 let go_back_button = match &self.active_view {
878 ActiveView::History | ActiveView::Configuration => Some(
879 IconButton::new("go-back", IconName::ArrowLeft)
880 .icon_size(IconSize::Small)
881 .on_click(cx.listener(|this, _, window, cx| {
882 this.go_back(&workspace::GoBack, window, cx);
883 }))
884 .tooltip({
885 let focus_handle = focus_handle.clone();
886 move |window, cx| {
887 Tooltip::for_action_in(
888 "Go Back",
889 &workspace::GoBack,
890 &focus_handle,
891 window,
892 cx,
893 )
894 }
895 }),
896 ),
897 _ => None,
898 };
899
900 h_flex()
901 .id("assistant-toolbar")
902 .h(Tab::container_height(cx))
903 .max_w_full()
904 .flex_none()
905 .justify_between()
906 .gap_2()
907 .bg(cx.theme().colors().tab_bar_background)
908 .border_b_1()
909 .border_color(cx.theme().colors().border)
910 .child(
911 h_flex()
912 .w_full()
913 .pl_2()
914 .gap_2()
915 .children(go_back_button)
916 .child(self.render_title_view(window, cx)),
917 )
918 .child(
919 h_flex()
920 .h_full()
921 .gap_2()
922 .when(show_token_count, |parent| match self.active_view {
923 ActiveView::Thread { .. } => {
924 if token_usage.total == 0 {
925 return parent;
926 }
927
928 let token_color = match token_usage.ratio {
929 TokenUsageRatio::Normal => Color::Muted,
930 TokenUsageRatio::Warning => Color::Warning,
931 TokenUsageRatio::Exceeded => Color::Error,
932 };
933
934 parent.child(
935 h_flex()
936 .flex_shrink_0()
937 .gap_0p5()
938 .child(
939 Label::new(assistant_context_editor::humanize_token_count(
940 token_usage.total,
941 ))
942 .size(LabelSize::Small)
943 .color(token_color)
944 .map(|label| {
945 if is_generating {
946 label
947 .with_animation(
948 "used-tokens-label",
949 Animation::new(Duration::from_secs(2))
950 .repeat()
951 .with_easing(pulsating_between(
952 0.6, 1.,
953 )),
954 |label, delta| label.alpha(delta),
955 )
956 .into_any()
957 } else {
958 label.into_any_element()
959 }
960 }),
961 )
962 .child(
963 Label::new("/").size(LabelSize::Small).color(Color::Muted),
964 )
965 .child(
966 Label::new(assistant_context_editor::humanize_token_count(
967 token_usage.max,
968 ))
969 .size(LabelSize::Small)
970 .color(Color::Muted),
971 ),
972 )
973 }
974 ActiveView::PromptEditor => {
975 let Some(editor) = self.context_editor.as_ref() else {
976 return parent;
977 };
978 let Some(element) = render_remaining_tokens(editor, cx) else {
979 return parent;
980 };
981 parent.child(element)
982 }
983 _ => parent,
984 })
985 .child(
986 h_flex()
987 .h_full()
988 .gap(DynamicSpacing::Base02.rems(cx))
989 .px(DynamicSpacing::Base08.rems(cx))
990 .border_l_1()
991 .border_color(cx.theme().colors().border)
992 .child(
993 IconButton::new("new", IconName::Plus)
994 .icon_size(IconSize::Small)
995 .style(ButtonStyle::Subtle)
996 .tooltip(move |window, cx| {
997 Tooltip::for_action_in(
998 "New Thread",
999 &NewThread::default(),
1000 &focus_handle,
1001 window,
1002 cx,
1003 )
1004 })
1005 .on_click(move |_event, window, cx| {
1006 window.dispatch_action(
1007 NewThread::default().boxed_clone(),
1008 cx,
1009 );
1010 }),
1011 )
1012 .child(
1013 PopoverMenu::new("assistant-menu")
1014 .trigger_with_tooltip(
1015 IconButton::new("new", IconName::Ellipsis)
1016 .icon_size(IconSize::Small)
1017 .style(ButtonStyle::Subtle),
1018 Tooltip::text("Toggle Agent Menu"),
1019 )
1020 .anchor(Corner::TopRight)
1021 .with_handle(self.assistant_dropdown_menu_handle.clone())
1022 .menu(move |window, cx| {
1023 Some(ContextMenu::build(
1024 window,
1025 cx,
1026 |menu, _window, _cx| {
1027 menu.action(
1028 "New Thread",
1029 Box::new(NewThread {
1030 from_thread_id: None,
1031 }),
1032 )
1033 .action(
1034 "New Prompt Editor",
1035 NewPromptEditor.boxed_clone(),
1036 )
1037 .when(!is_empty, |menu| {
1038 menu.action(
1039 "Continue in New Thread",
1040 Box::new(NewThread {
1041 from_thread_id: Some(thread_id.clone()),
1042 }),
1043 )
1044 })
1045 .separator()
1046 .action("History", OpenHistory.boxed_clone())
1047 .action("Settings", OpenConfiguration.boxed_clone())
1048 },
1049 ))
1050 }),
1051 ),
1052 ),
1053 )
1054 }
1055
1056 fn render_active_thread_or_empty_state(
1057 &self,
1058 window: &mut Window,
1059 cx: &mut Context<Self>,
1060 ) -> AnyElement {
1061 if self.thread.read(cx).is_empty() {
1062 return self
1063 .render_thread_empty_state(window, cx)
1064 .into_any_element();
1065 }
1066
1067 self.thread.clone().into_any_element()
1068 }
1069
1070 fn configuration_error(&self, cx: &App) -> Option<ConfigurationError> {
1071 let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
1072 return Some(ConfigurationError::NoProvider);
1073 };
1074
1075 if !model.provider.is_authenticated(cx) {
1076 return Some(ConfigurationError::ProviderNotAuthenticated);
1077 }
1078
1079 if model.provider.must_accept_terms(cx) {
1080 return Some(ConfigurationError::ProviderPendingTermsAcceptance(
1081 model.provider,
1082 ));
1083 }
1084
1085 None
1086 }
1087
1088 fn render_thread_empty_state(
1089 &self,
1090 window: &mut Window,
1091 cx: &mut Context<Self>,
1092 ) -> impl IntoElement {
1093 let recent_history = self
1094 .history_store
1095 .update(cx, |this, cx| this.recent_entries(6, cx));
1096
1097 let configuration_error = self.configuration_error(cx);
1098 let no_error = configuration_error.is_none();
1099 let focus_handle = self.focus_handle(cx);
1100
1101 v_flex()
1102 .size_full()
1103 .when(recent_history.is_empty(), |this| {
1104 let configuration_error_ref = &configuration_error;
1105 this.child(
1106 v_flex()
1107 .size_full()
1108 .max_w_80()
1109 .mx_auto()
1110 .justify_center()
1111 .items_center()
1112 .gap_1()
1113 .child(
1114 h_flex().child(
1115 Headline::new("Welcome to the Agent Panel")
1116 ),
1117 )
1118 .when(no_error, |parent| {
1119 parent
1120 .child(
1121 h_flex().child(
1122 Label::new("Ask and build anything.")
1123 .color(Color::Muted)
1124 .mb_2p5(),
1125 ),
1126 )
1127 .child(
1128 Button::new("new-thread", "Start New Thread")
1129 .icon(IconName::Plus)
1130 .icon_position(IconPosition::Start)
1131 .icon_size(IconSize::Small)
1132 .icon_color(Color::Muted)
1133 .full_width()
1134 .key_binding(KeyBinding::for_action_in(
1135 &NewThread::default(),
1136 &focus_handle,
1137 window,
1138 cx,
1139 ))
1140 .on_click(|_event, window, cx| {
1141 window.dispatch_action(NewThread::default().boxed_clone(), cx)
1142 }),
1143 )
1144 .child(
1145 Button::new("context", "Add Context")
1146 .icon(IconName::FileCode)
1147 .icon_position(IconPosition::Start)
1148 .icon_size(IconSize::Small)
1149 .icon_color(Color::Muted)
1150 .full_width()
1151 .key_binding(KeyBinding::for_action_in(
1152 &ToggleContextPicker,
1153 &focus_handle,
1154 window,
1155 cx,
1156 ))
1157 .on_click(|_event, window, cx| {
1158 window.dispatch_action(ToggleContextPicker.boxed_clone(), cx)
1159 }),
1160 )
1161 .child(
1162 Button::new("mode", "Switch Model")
1163 .icon(IconName::DatabaseZap)
1164 .icon_position(IconPosition::Start)
1165 .icon_size(IconSize::Small)
1166 .icon_color(Color::Muted)
1167 .full_width()
1168 .key_binding(KeyBinding::for_action_in(
1169 &ToggleModelSelector,
1170 &focus_handle,
1171 window,
1172 cx,
1173 ))
1174 .on_click(|_event, window, cx| {
1175 window.dispatch_action(ToggleModelSelector.boxed_clone(), cx)
1176 }),
1177 )
1178 .child(
1179 Button::new("settings", "View Settings")
1180 .icon(IconName::Settings)
1181 .icon_position(IconPosition::Start)
1182 .icon_size(IconSize::Small)
1183 .icon_color(Color::Muted)
1184 .full_width()
1185 .key_binding(KeyBinding::for_action_in(
1186 &OpenConfiguration,
1187 &focus_handle,
1188 window,
1189 cx,
1190 ))
1191 .on_click(|_event, window, cx| {
1192 window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
1193 }),
1194 )
1195 })
1196 .map(|parent| {
1197 match configuration_error_ref {
1198 Some(ConfigurationError::ProviderNotAuthenticated)
1199 | Some(ConfigurationError::NoProvider) => {
1200 parent
1201 .child(
1202 h_flex().child(
1203 Label::new("To start using the agent, configure at least one LLM provider.")
1204 .color(Color::Muted)
1205 .mb_2p5()
1206 )
1207 )
1208 .child(
1209 Button::new("settings", "Configure a Provider")
1210 .icon(IconName::Settings)
1211 .icon_position(IconPosition::Start)
1212 .icon_size(IconSize::Small)
1213 .icon_color(Color::Muted)
1214 .full_width()
1215 .key_binding(KeyBinding::for_action_in(
1216 &OpenConfiguration,
1217 &focus_handle,
1218 window,
1219 cx,
1220 ))
1221 .on_click(|_event, window, cx| {
1222 window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
1223 }),
1224 )
1225 }
1226 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
1227 parent.children(
1228 provider.render_accept_terms(
1229 LanguageModelProviderTosView::ThreadFreshStart,
1230 cx,
1231 ),
1232 )
1233 }
1234 None => parent,
1235 }
1236 })
1237 )
1238 })
1239 .when(!recent_history.is_empty(), |parent| {
1240 let focus_handle = focus_handle.clone();
1241 let configuration_error_ref = &configuration_error;
1242
1243 parent
1244 .p_1p5()
1245 .justify_end()
1246 .gap_1()
1247 .child(
1248 h_flex()
1249 .pl_1p5()
1250 .pb_1()
1251 .w_full()
1252 .justify_between()
1253 .border_b_1()
1254 .border_color(cx.theme().colors().border_variant)
1255 .child(
1256 Label::new("Past Interactions")
1257 .size(LabelSize::Small)
1258 .color(Color::Muted),
1259 )
1260 .child(
1261 Button::new("view-history", "View All")
1262 .style(ButtonStyle::Subtle)
1263 .label_size(LabelSize::Small)
1264 .key_binding(
1265 KeyBinding::for_action_in(
1266 &OpenHistory,
1267 &self.focus_handle(cx),
1268 window,
1269 cx,
1270 ).map(|kb| kb.size(rems_from_px(12.))),
1271 )
1272 .on_click(move |_event, window, cx| {
1273 window.dispatch_action(OpenHistory.boxed_clone(), cx);
1274 }),
1275 ),
1276 )
1277 .child(
1278 v_flex()
1279 .gap_1()
1280 .children(
1281 recent_history.into_iter().map(|entry| {
1282 // TODO: Add keyboard navigation.
1283 match entry {
1284 HistoryEntry::Thread(thread) => {
1285 PastThread::new(thread, cx.entity().downgrade(), false, vec![])
1286 .into_any_element()
1287 }
1288 HistoryEntry::Context(context) => {
1289 PastContext::new(context, cx.entity().downgrade(), false, vec![])
1290 .into_any_element()
1291 }
1292 }
1293 }),
1294 )
1295 )
1296 .map(|parent| {
1297 match configuration_error_ref {
1298 Some(ConfigurationError::ProviderNotAuthenticated)
1299 | Some(ConfigurationError::NoProvider) => {
1300 parent
1301 .child(
1302 Banner::new()
1303 .severity(ui::Severity::Warning)
1304 .children(
1305 Label::new(
1306 "Configure at least one LLM provider to start using the panel.",
1307 )
1308 .size(LabelSize::Small),
1309 )
1310 .action_slot(
1311 Button::new("settings", "Configure Provider")
1312 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
1313 .label_size(LabelSize::Small)
1314 .key_binding(
1315 KeyBinding::for_action_in(
1316 &OpenConfiguration,
1317 &focus_handle,
1318 window,
1319 cx,
1320 )
1321 .map(|kb| kb.size(rems_from_px(12.))),
1322 )
1323 .on_click(|_event, window, cx| {
1324 window.dispatch_action(
1325 OpenConfiguration.boxed_clone(),
1326 cx,
1327 )
1328 }),
1329 ),
1330 )
1331 }
1332 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
1333 parent
1334 .child(
1335 Banner::new()
1336 .severity(ui::Severity::Warning)
1337 .children(
1338 h_flex()
1339 .w_full()
1340 .children(
1341 provider.render_accept_terms(
1342 LanguageModelProviderTosView::ThreadtEmptyState,
1343 cx,
1344 ),
1345 ),
1346 ),
1347 )
1348 }
1349 None => parent,
1350 }
1351 })
1352 })
1353 }
1354
1355 fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
1356 let last_error = self.thread.read(cx).last_error()?;
1357
1358 Some(
1359 div()
1360 .absolute()
1361 .right_3()
1362 .bottom_12()
1363 .max_w_96()
1364 .py_2()
1365 .px_3()
1366 .elevation_2(cx)
1367 .occlude()
1368 .child(match last_error {
1369 ThreadError::PaymentRequired => self.render_payment_required_error(cx),
1370 ThreadError::MaxMonthlySpendReached => {
1371 self.render_max_monthly_spend_reached_error(cx)
1372 }
1373 ThreadError::Message { header, message } => {
1374 self.render_error_message(header, message, cx)
1375 }
1376 })
1377 .into_any(),
1378 )
1379 }
1380
1381 fn render_payment_required_error(&self, cx: &mut Context<Self>) -> AnyElement {
1382 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.";
1383
1384 v_flex()
1385 .gap_0p5()
1386 .child(
1387 h_flex()
1388 .gap_1p5()
1389 .items_center()
1390 .child(Icon::new(IconName::XCircle).color(Color::Error))
1391 .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
1392 )
1393 .child(
1394 div()
1395 .id("error-message")
1396 .max_h_24()
1397 .overflow_y_scroll()
1398 .child(Label::new(ERROR_MESSAGE)),
1399 )
1400 .child(
1401 h_flex()
1402 .justify_end()
1403 .mt_1()
1404 .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
1405 |this, _, _, cx| {
1406 this.thread.update(cx, |this, _cx| {
1407 this.clear_last_error();
1408 });
1409
1410 cx.open_url(&zed_urls::account_url(cx));
1411 cx.notify();
1412 },
1413 )))
1414 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1415 |this, _, _, cx| {
1416 this.thread.update(cx, |this, _cx| {
1417 this.clear_last_error();
1418 });
1419
1420 cx.notify();
1421 },
1422 ))),
1423 )
1424 .into_any()
1425 }
1426
1427 fn render_max_monthly_spend_reached_error(&self, cx: &mut Context<Self>) -> AnyElement {
1428 const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
1429
1430 v_flex()
1431 .gap_0p5()
1432 .child(
1433 h_flex()
1434 .gap_1p5()
1435 .items_center()
1436 .child(Icon::new(IconName::XCircle).color(Color::Error))
1437 .child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
1438 )
1439 .child(
1440 div()
1441 .id("error-message")
1442 .max_h_24()
1443 .overflow_y_scroll()
1444 .child(Label::new(ERROR_MESSAGE)),
1445 )
1446 .child(
1447 h_flex()
1448 .justify_end()
1449 .mt_1()
1450 .child(
1451 Button::new("subscribe", "Update Monthly Spend Limit").on_click(
1452 cx.listener(|this, _, _, cx| {
1453 this.thread.update(cx, |this, _cx| {
1454 this.clear_last_error();
1455 });
1456
1457 cx.open_url(&zed_urls::account_url(cx));
1458 cx.notify();
1459 }),
1460 ),
1461 )
1462 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1463 |this, _, _, cx| {
1464 this.thread.update(cx, |this, _cx| {
1465 this.clear_last_error();
1466 });
1467
1468 cx.notify();
1469 },
1470 ))),
1471 )
1472 .into_any()
1473 }
1474
1475 fn render_error_message(
1476 &self,
1477 header: SharedString,
1478 message: SharedString,
1479 cx: &mut Context<Self>,
1480 ) -> AnyElement {
1481 v_flex()
1482 .gap_0p5()
1483 .child(
1484 h_flex()
1485 .gap_1p5()
1486 .items_center()
1487 .child(Icon::new(IconName::XCircle).color(Color::Error))
1488 .child(Label::new(header).weight(FontWeight::MEDIUM)),
1489 )
1490 .child(
1491 div()
1492 .id("error-message")
1493 .max_h_32()
1494 .overflow_y_scroll()
1495 .child(Label::new(message)),
1496 )
1497 .child(
1498 h_flex()
1499 .justify_end()
1500 .mt_1()
1501 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1502 |this, _, _, cx| {
1503 this.thread.update(cx, |this, _cx| {
1504 this.clear_last_error();
1505 });
1506
1507 cx.notify();
1508 },
1509 ))),
1510 )
1511 .into_any()
1512 }
1513
1514 fn key_context(&self) -> KeyContext {
1515 let mut key_context = KeyContext::new_with_defaults();
1516 key_context.add("AgentPanel");
1517 if matches!(self.active_view, ActiveView::PromptEditor) {
1518 key_context.add("prompt_editor");
1519 }
1520 key_context
1521 }
1522}
1523
1524impl Render for AssistantPanel {
1525 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1526 v_flex()
1527 .key_context(self.key_context())
1528 .justify_between()
1529 .size_full()
1530 .on_action(cx.listener(Self::cancel))
1531 .on_action(cx.listener(|this, action: &NewThread, window, cx| {
1532 this.new_thread(action, window, cx);
1533 }))
1534 .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
1535 this.open_history(window, cx);
1536 }))
1537 .on_action(cx.listener(|this, _: &OpenConfiguration, window, cx| {
1538 this.open_configuration(window, cx);
1539 }))
1540 .on_action(cx.listener(Self::open_active_thread_as_markdown))
1541 .on_action(cx.listener(Self::deploy_prompt_library))
1542 .on_action(cx.listener(Self::open_agent_diff))
1543 .on_action(cx.listener(Self::go_back))
1544 .child(self.render_toolbar(window, cx))
1545 .map(|parent| match self.active_view {
1546 ActiveView::Thread { .. } => parent
1547 .child(self.render_active_thread_or_empty_state(window, cx))
1548 .child(h_flex().child(self.message_editor.clone()))
1549 .children(self.render_last_error(cx)),
1550 ActiveView::History => parent.child(self.history.clone()),
1551 ActiveView::PromptEditor => parent.children(self.context_editor.clone()),
1552 ActiveView::Configuration => parent.children(self.configuration.clone()),
1553 })
1554 }
1555}
1556
1557struct PromptLibraryInlineAssist {
1558 workspace: WeakEntity<Workspace>,
1559}
1560
1561impl PromptLibraryInlineAssist {
1562 pub fn new(workspace: WeakEntity<Workspace>) -> Self {
1563 Self { workspace }
1564 }
1565}
1566
1567impl prompt_library::InlineAssistDelegate for PromptLibraryInlineAssist {
1568 fn assist(
1569 &self,
1570 prompt_editor: &Entity<Editor>,
1571 _initial_prompt: Option<String>,
1572 window: &mut Window,
1573 cx: &mut Context<PromptLibrary>,
1574 ) {
1575 InlineAssistant::update_global(cx, |assistant, cx| {
1576 assistant.assist(&prompt_editor, self.workspace.clone(), None, window, cx)
1577 })
1578 }
1579
1580 fn focus_assistant_panel(
1581 &self,
1582 workspace: &mut Workspace,
1583 window: &mut Window,
1584 cx: &mut Context<Workspace>,
1585 ) -> bool {
1586 workspace
1587 .focus_panel::<AssistantPanel>(window, cx)
1588 .is_some()
1589 }
1590}
1591
1592pub struct ConcreteAssistantPanelDelegate;
1593
1594impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
1595 fn active_context_editor(
1596 &self,
1597 workspace: &mut Workspace,
1598 _window: &mut Window,
1599 cx: &mut Context<Workspace>,
1600 ) -> Option<Entity<ContextEditor>> {
1601 let panel = workspace.panel::<AssistantPanel>(cx)?;
1602 panel.update(cx, |panel, _cx| panel.context_editor.clone())
1603 }
1604
1605 fn open_saved_context(
1606 &self,
1607 workspace: &mut Workspace,
1608 path: std::path::PathBuf,
1609 window: &mut Window,
1610 cx: &mut Context<Workspace>,
1611 ) -> Task<Result<()>> {
1612 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
1613 return Task::ready(Err(anyhow!("Agent panel not found")));
1614 };
1615
1616 panel.update(cx, |panel, cx| {
1617 panel.open_saved_prompt_editor(path, window, cx)
1618 })
1619 }
1620
1621 fn open_remote_context(
1622 &self,
1623 _workspace: &mut Workspace,
1624 _context_id: assistant_context_editor::ContextId,
1625 _window: &mut Window,
1626 _cx: &mut Context<Workspace>,
1627 ) -> Task<Result<Entity<ContextEditor>>> {
1628 Task::ready(Err(anyhow!("opening remote context not implemented")))
1629 }
1630
1631 fn quote_selection(
1632 &self,
1633 _workspace: &mut Workspace,
1634 _creases: Vec<(String, String)>,
1635 _window: &mut Window,
1636 _cx: &mut Context<Workspace>,
1637 ) {
1638 }
1639}