1use std::ops::Range;
2use std::path::Path;
3use std::rc::Rc;
4use std::sync::Arc;
5use std::time::Duration;
6
7use db::kvp::{Dismissable, KEY_VALUE_STORE};
8use serde::{Deserialize, Serialize};
9
10use agent_settings::{AgentDockPosition, AgentSettings, CompletionMode, DefaultView};
11use anyhow::{Result, anyhow};
12use assistant_context_editor::{
13 AgentPanelDelegate, AssistantContext, ConfigurationError, ContextEditor, ContextEvent,
14 ContextSummary, SlashCommandCompletionProvider, humanize_token_count,
15 make_lsp_adapter_delegate, render_remaining_tokens,
16};
17use assistant_slash_command::SlashCommandWorkingSet;
18use assistant_tool::ToolWorkingSet;
19
20use assistant_context_editor::language_model_selector::ToggleModelSelector;
21use client::{UserStore, zed_urls};
22use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
23use fs::Fs;
24use gpui::{
25 Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem,
26 Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, FontWeight,
27 KeyContext, Pixels, Subscription, Task, UpdateGlobal, WeakEntity, linear_color_stop,
28 linear_gradient, prelude::*, pulsating_between,
29};
30use language::LanguageRegistry;
31use language_model::{
32 LanguageModelProviderTosView, LanguageModelRegistry, RequestUsage, ZED_CLOUD_PROVIDER_ID,
33};
34use project::{Project, ProjectPath, Worktree};
35use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
36use proto::Plan;
37use rules_library::{RulesLibrary, open_rules_library};
38use search::{BufferSearchBar, buffer_search};
39use settings::{Settings, update_settings_file};
40use theme::ThemeSettings;
41use time::UtcOffset;
42use ui::utils::WithRemSize;
43use ui::{
44 Banner, CheckboxWithLabel, ContextMenu, ElevationIndex, KeyBinding, PopoverMenu,
45 PopoverMenuHandle, ProgressBar, Tab, Tooltip, Vector, VectorName, prelude::*,
46};
47use util::{ResultExt as _, maybe};
48use workspace::dock::{DockPosition, Panel, PanelEvent};
49use workspace::{
50 CollaboratorId, DraggedSelection, DraggedTab, ToggleZoom, ToolbarItemView, Workspace,
51};
52use zed_actions::agent::{OpenConfiguration, OpenOnboardingModal, ResetOnboarding};
53use zed_actions::assistant::{OpenRulesLibrary, ToggleFocus};
54use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize};
55use zed_llm_client::{CompletionIntent, UsageLimit};
56
57use crate::active_thread::{self, ActiveThread, ActiveThreadEvent};
58use crate::agent_configuration::{AgentConfiguration, AssistantConfigurationEvent};
59use crate::agent_diff::AgentDiff;
60use crate::history_store::{HistoryStore, RecentEntry};
61use crate::message_editor::{MessageEditor, MessageEditorEvent};
62use crate::thread::{Thread, ThreadError, ThreadId, ThreadSummary, TokenUsageRatio};
63use crate::thread_history::{HistoryEntryElement, ThreadHistory};
64use crate::thread_store::ThreadStore;
65use crate::ui::AgentOnboardingModal;
66use crate::{
67 AddContextServer, AgentDiffPane, ContextStore, ContinueThread, ContinueWithBurnMode,
68 DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
69 NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ResetTrialEndUpsell,
70 ResetTrialUpsell, TextThreadStore, ThreadEvent, ToggleBurnMode, ToggleContextPicker,
71 ToggleNavigationMenu, ToggleOptionsMenu,
72};
73
74const AGENT_PANEL_KEY: &str = "agent_panel";
75
76#[derive(Serialize, Deserialize)]
77struct SerializedAgentPanel {
78 width: Option<Pixels>,
79}
80
81pub fn init(cx: &mut App) {
82 cx.observe_new(
83 |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
84 workspace
85 .register_action(|workspace, action: &NewThread, window, cx| {
86 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
87 panel.update(cx, |panel, cx| panel.new_thread(action, window, cx));
88 workspace.focus_panel::<AgentPanel>(window, cx);
89 }
90 })
91 .register_action(|workspace, _: &OpenHistory, window, cx| {
92 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
93 workspace.focus_panel::<AgentPanel>(window, cx);
94 panel.update(cx, |panel, cx| panel.open_history(window, cx));
95 }
96 })
97 .register_action(|workspace, _: &OpenConfiguration, window, cx| {
98 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
99 workspace.focus_panel::<AgentPanel>(window, cx);
100 panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
101 }
102 })
103 .register_action(|workspace, _: &NewTextThread, window, cx| {
104 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
105 workspace.focus_panel::<AgentPanel>(window, cx);
106 panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx));
107 }
108 })
109 .register_action(|workspace, action: &OpenRulesLibrary, window, cx| {
110 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
111 workspace.focus_panel::<AgentPanel>(window, cx);
112 panel.update(cx, |panel, cx| {
113 panel.deploy_rules_library(action, window, cx)
114 });
115 }
116 })
117 .register_action(|workspace, _: &OpenAgentDiff, window, cx| {
118 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
119 workspace.focus_panel::<AgentPanel>(window, cx);
120 let thread = panel.read(cx).thread.read(cx).thread().clone();
121 AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx);
122 }
123 })
124 .register_action(|workspace, _: &Follow, window, cx| {
125 workspace.follow(CollaboratorId::Agent, window, cx);
126 })
127 .register_action(|workspace, _: &ExpandMessageEditor, window, cx| {
128 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
129 workspace.focus_panel::<AgentPanel>(window, cx);
130 panel.update(cx, |panel, cx| {
131 panel.message_editor.update(cx, |editor, cx| {
132 editor.expand_message_editor(&ExpandMessageEditor, window, cx);
133 });
134 });
135 }
136 })
137 .register_action(|workspace, _: &ToggleNavigationMenu, window, cx| {
138 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
139 workspace.focus_panel::<AgentPanel>(window, cx);
140 panel.update(cx, |panel, cx| {
141 panel.toggle_navigation_menu(&ToggleNavigationMenu, window, cx);
142 });
143 }
144 })
145 .register_action(|workspace, _: &ToggleOptionsMenu, window, cx| {
146 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
147 workspace.focus_panel::<AgentPanel>(window, cx);
148 panel.update(cx, |panel, cx| {
149 panel.toggle_options_menu(&ToggleOptionsMenu, window, cx);
150 });
151 }
152 })
153 .register_action(|workspace, _: &OpenOnboardingModal, window, cx| {
154 AgentOnboardingModal::toggle(workspace, window, cx)
155 })
156 .register_action(|_workspace, _: &ResetOnboarding, window, cx| {
157 window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
158 window.refresh();
159 })
160 .register_action(|_workspace, _: &ResetTrialUpsell, _window, cx| {
161 Upsell::set_dismissed(false, cx);
162 })
163 .register_action(|_workspace, _: &ResetTrialEndUpsell, _window, cx| {
164 TrialEndUpsell::set_dismissed(false, cx);
165 });
166 },
167 )
168 .detach();
169}
170
171enum ActiveView {
172 Thread {
173 change_title_editor: Entity<Editor>,
174 thread: WeakEntity<Thread>,
175 _subscriptions: Vec<gpui::Subscription>,
176 },
177 TextThread {
178 context_editor: Entity<ContextEditor>,
179 title_editor: Entity<Editor>,
180 buffer_search_bar: Entity<BufferSearchBar>,
181 _subscriptions: Vec<gpui::Subscription>,
182 },
183 History,
184 Configuration,
185}
186
187enum WhichFontSize {
188 AgentFont,
189 BufferFont,
190 None,
191}
192
193impl ActiveView {
194 pub fn which_font_size_used(&self) -> WhichFontSize {
195 match self {
196 ActiveView::Thread { .. } | ActiveView::History => WhichFontSize::AgentFont,
197 ActiveView::TextThread { .. } => WhichFontSize::BufferFont,
198 ActiveView::Configuration => WhichFontSize::None,
199 }
200 }
201
202 pub fn thread(thread: Entity<Thread>, window: &mut Window, cx: &mut App) -> Self {
203 let summary = thread.read(cx).summary().or_default();
204
205 let editor = cx.new(|cx| {
206 let mut editor = Editor::single_line(window, cx);
207 editor.set_text(summary.clone(), window, cx);
208 editor
209 });
210
211 let subscriptions = vec![
212 window.subscribe(&editor, cx, {
213 {
214 let thread = thread.clone();
215 move |editor, event, window, cx| match event {
216 EditorEvent::BufferEdited => {
217 let new_summary = editor.read(cx).text(cx);
218
219 thread.update(cx, |thread, cx| {
220 thread.set_summary(new_summary, cx);
221 })
222 }
223 EditorEvent::Blurred => {
224 if editor.read(cx).text(cx).is_empty() {
225 let summary = thread.read(cx).summary().or_default();
226
227 editor.update(cx, |editor, cx| {
228 editor.set_text(summary, window, cx);
229 });
230 }
231 }
232 _ => {}
233 }
234 }
235 }),
236 window.subscribe(&thread, cx, {
237 let editor = editor.clone();
238 move |thread, event, window, cx| match event {
239 ThreadEvent::SummaryGenerated => {
240 let summary = thread.read(cx).summary().or_default();
241
242 editor.update(cx, |editor, cx| {
243 editor.set_text(summary, window, cx);
244 })
245 }
246 _ => {}
247 }
248 }),
249 ];
250
251 Self::Thread {
252 change_title_editor: editor,
253 thread: thread.downgrade(),
254 _subscriptions: subscriptions,
255 }
256 }
257
258 pub fn prompt_editor(
259 context_editor: Entity<ContextEditor>,
260 language_registry: Arc<LanguageRegistry>,
261 window: &mut Window,
262 cx: &mut App,
263 ) -> Self {
264 let title = context_editor.read(cx).title(cx).to_string();
265
266 let editor = cx.new(|cx| {
267 let mut editor = Editor::single_line(window, cx);
268 editor.set_text(title, window, cx);
269 editor
270 });
271
272 // This is a workaround for `editor.set_text` emitting a `BufferEdited` event, which would
273 // cause a custom summary to be set. The presence of this custom summary would cause
274 // summarization to not happen.
275 let mut suppress_first_edit = true;
276
277 let subscriptions = vec![
278 window.subscribe(&editor, cx, {
279 {
280 let context_editor = context_editor.clone();
281 move |editor, event, window, cx| match event {
282 EditorEvent::BufferEdited => {
283 if suppress_first_edit {
284 suppress_first_edit = false;
285 return;
286 }
287 let new_summary = editor.read(cx).text(cx);
288
289 context_editor.update(cx, |context_editor, cx| {
290 context_editor
291 .context()
292 .update(cx, |assistant_context, cx| {
293 assistant_context.set_custom_summary(new_summary, cx);
294 })
295 })
296 }
297 EditorEvent::Blurred => {
298 if editor.read(cx).text(cx).is_empty() {
299 let summary = context_editor
300 .read(cx)
301 .context()
302 .read(cx)
303 .summary()
304 .or_default();
305
306 editor.update(cx, |editor, cx| {
307 editor.set_text(summary, window, cx);
308 });
309 }
310 }
311 _ => {}
312 }
313 }
314 }),
315 window.subscribe(&context_editor.read(cx).context().clone(), cx, {
316 let editor = editor.clone();
317 move |assistant_context, event, window, cx| match event {
318 ContextEvent::SummaryGenerated => {
319 let summary = assistant_context.read(cx).summary().or_default();
320
321 editor.update(cx, |editor, cx| {
322 editor.set_text(summary, window, cx);
323 })
324 }
325 _ => {}
326 }
327 }),
328 ];
329
330 let buffer_search_bar =
331 cx.new(|cx| BufferSearchBar::new(Some(language_registry), window, cx));
332 buffer_search_bar.update(cx, |buffer_search_bar, cx| {
333 buffer_search_bar.set_active_pane_item(Some(&context_editor), window, cx)
334 });
335
336 Self::TextThread {
337 context_editor,
338 title_editor: editor,
339 buffer_search_bar,
340 _subscriptions: subscriptions,
341 }
342 }
343}
344
345pub struct AgentPanel {
346 workspace: WeakEntity<Workspace>,
347 user_store: Entity<UserStore>,
348 project: Entity<Project>,
349 fs: Arc<dyn Fs>,
350 language_registry: Arc<LanguageRegistry>,
351 thread_store: Entity<ThreadStore>,
352 thread: Entity<ActiveThread>,
353 message_editor: Entity<MessageEditor>,
354 _active_thread_subscriptions: Vec<Subscription>,
355 _default_model_subscription: Subscription,
356 context_store: Entity<TextThreadStore>,
357 prompt_store: Option<Entity<PromptStore>>,
358 inline_assist_context_store: Entity<crate::context_store::ContextStore>,
359 configuration: Option<Entity<AgentConfiguration>>,
360 configuration_subscription: Option<Subscription>,
361 local_timezone: UtcOffset,
362 active_view: ActiveView,
363 previous_view: Option<ActiveView>,
364 history_store: Entity<HistoryStore>,
365 history: Entity<ThreadHistory>,
366 hovered_recent_history_item: Option<usize>,
367 assistant_dropdown_menu_handle: PopoverMenuHandle<ContextMenu>,
368 assistant_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
369 assistant_navigation_menu: Option<Entity<ContextMenu>>,
370 width: Option<Pixels>,
371 height: Option<Pixels>,
372 zoomed: bool,
373 pending_serialization: Option<Task<Result<()>>>,
374 hide_upsell: bool,
375}
376
377impl AgentPanel {
378 fn serialize(&mut self, cx: &mut Context<Self>) {
379 let width = self.width;
380 self.pending_serialization = Some(cx.background_spawn(async move {
381 KEY_VALUE_STORE
382 .write_kvp(
383 AGENT_PANEL_KEY.into(),
384 serde_json::to_string(&SerializedAgentPanel { width })?,
385 )
386 .await?;
387 anyhow::Ok(())
388 }));
389 }
390 pub fn load(
391 workspace: WeakEntity<Workspace>,
392 prompt_builder: Arc<PromptBuilder>,
393 mut cx: AsyncWindowContext,
394 ) -> Task<Result<Entity<Self>>> {
395 let prompt_store = cx.update(|_window, cx| PromptStore::global(cx));
396 cx.spawn(async move |cx| {
397 let prompt_store = match prompt_store {
398 Ok(prompt_store) => prompt_store.await.ok(),
399 Err(_) => None,
400 };
401 let tools = cx.new(|_| ToolWorkingSet::default())?;
402 let thread_store = workspace
403 .update(cx, |workspace, cx| {
404 let project = workspace.project().clone();
405 ThreadStore::load(
406 project,
407 tools.clone(),
408 prompt_store.clone(),
409 prompt_builder.clone(),
410 cx,
411 )
412 })?
413 .await?;
414
415 let slash_commands = Arc::new(SlashCommandWorkingSet::default());
416 let context_store = workspace
417 .update(cx, |workspace, cx| {
418 let project = workspace.project().clone();
419 assistant_context_editor::ContextStore::new(
420 project,
421 prompt_builder.clone(),
422 slash_commands,
423 cx,
424 )
425 })?
426 .await?;
427
428 let serialized_panel = if let Some(panel) = cx
429 .background_spawn(async move { KEY_VALUE_STORE.read_kvp(AGENT_PANEL_KEY) })
430 .await
431 .log_err()
432 .flatten()
433 {
434 Some(serde_json::from_str::<SerializedAgentPanel>(&panel)?)
435 } else {
436 None
437 };
438
439 let panel = workspace.update_in(cx, |workspace, window, cx| {
440 let panel = cx.new(|cx| {
441 Self::new(
442 workspace,
443 thread_store,
444 context_store,
445 prompt_store,
446 window,
447 cx,
448 )
449 });
450 if let Some(serialized_panel) = serialized_panel {
451 panel.update(cx, |panel, cx| {
452 panel.width = serialized_panel.width.map(|w| w.round());
453 cx.notify();
454 });
455 }
456 panel
457 })?;
458
459 Ok(panel)
460 })
461 }
462
463 fn new(
464 workspace: &Workspace,
465 thread_store: Entity<ThreadStore>,
466 context_store: Entity<TextThreadStore>,
467 prompt_store: Option<Entity<PromptStore>>,
468 window: &mut Window,
469 cx: &mut Context<Self>,
470 ) -> Self {
471 let thread = thread_store.update(cx, |this, cx| this.create_thread(cx));
472 let fs = workspace.app_state().fs.clone();
473 let user_store = workspace.app_state().user_store.clone();
474 let project = workspace.project();
475 let language_registry = project.read(cx).languages().clone();
476 let workspace = workspace.weak_handle();
477 let weak_self = cx.entity().downgrade();
478
479 let message_editor_context_store = cx.new(|_cx| {
480 crate::context_store::ContextStore::new(
481 project.downgrade(),
482 Some(thread_store.downgrade()),
483 )
484 });
485 let inline_assist_context_store = cx.new(|_cx| {
486 crate::context_store::ContextStore::new(
487 project.downgrade(),
488 Some(thread_store.downgrade()),
489 )
490 });
491
492 let message_editor = cx.new(|cx| {
493 MessageEditor::new(
494 fs.clone(),
495 workspace.clone(),
496 user_store.clone(),
497 message_editor_context_store.clone(),
498 prompt_store.clone(),
499 thread_store.downgrade(),
500 context_store.downgrade(),
501 thread.clone(),
502 window,
503 cx,
504 )
505 });
506
507 let message_editor_subscription =
508 cx.subscribe(&message_editor, |_, _, event, cx| match event {
509 MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
510 cx.notify();
511 }
512 });
513
514 let thread_id = thread.read(cx).id().clone();
515 let history_store = cx.new(|cx| {
516 HistoryStore::new(
517 thread_store.clone(),
518 context_store.clone(),
519 [RecentEntry::Thread(thread_id, thread.clone())],
520 window,
521 cx,
522 )
523 });
524
525 cx.observe(&history_store, |_, _, cx| cx.notify()).detach();
526
527 let panel_type = AgentSettings::get_global(cx).default_view;
528 let active_view = match panel_type {
529 DefaultView::Thread => ActiveView::thread(thread.clone(), window, cx),
530 DefaultView::TextThread => {
531 let context =
532 context_store.update(cx, |context_store, cx| context_store.create(cx));
533 let lsp_adapter_delegate = make_lsp_adapter_delegate(&project.clone(), cx).unwrap();
534 let context_editor = cx.new(|cx| {
535 let mut editor = ContextEditor::for_context(
536 context,
537 fs.clone(),
538 workspace.clone(),
539 project.clone(),
540 lsp_adapter_delegate,
541 window,
542 cx,
543 );
544 editor.insert_default_prompt(window, cx);
545 editor
546 });
547 ActiveView::prompt_editor(context_editor, language_registry.clone(), window, cx)
548 }
549 };
550
551 let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
552 if let ThreadEvent::MessageAdded(_) = &event {
553 // needed to leave empty state
554 cx.notify();
555 }
556 });
557 let active_thread = cx.new(|cx| {
558 ActiveThread::new(
559 thread.clone(),
560 thread_store.clone(),
561 context_store.clone(),
562 message_editor_context_store.clone(),
563 language_registry.clone(),
564 workspace.clone(),
565 window,
566 cx,
567 )
568 });
569 AgentDiff::set_active_thread(&workspace, &thread, window, cx);
570
571 let active_thread_subscription =
572 cx.subscribe(&active_thread, |_, _, event, cx| match &event {
573 ActiveThreadEvent::EditingMessageTokenCountChanged => {
574 cx.notify();
575 }
576 });
577
578 let weak_panel = weak_self.clone();
579
580 window.defer(cx, move |window, cx| {
581 let panel = weak_panel.clone();
582 let assistant_navigation_menu =
583 ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| {
584 let recently_opened = panel
585 .update(cx, |this, cx| {
586 this.history_store.update(cx, |history_store, cx| {
587 history_store.recently_opened_entries(cx)
588 })
589 })
590 .unwrap_or_default();
591
592 if !recently_opened.is_empty() {
593 menu = menu.header("Recently Opened");
594
595 for entry in recently_opened.iter() {
596 if let RecentEntry::Context(context) = entry {
597 if context.read(cx).path().is_none() {
598 log::error!(
599 "bug: text thread in recent history list was never saved"
600 );
601 continue;
602 }
603 }
604
605 let summary = entry.summary(cx);
606
607 menu = menu.entry_with_end_slot_on_hover(
608 summary,
609 None,
610 {
611 let panel = panel.clone();
612 let entry = entry.clone();
613 move |window, cx| {
614 panel
615 .update(cx, {
616 let entry = entry.clone();
617 move |this, cx| match entry {
618 RecentEntry::Thread(_, thread) => {
619 this.open_thread(thread, window, cx)
620 }
621 RecentEntry::Context(context) => {
622 let Some(path) = context.read(cx).path()
623 else {
624 return;
625 };
626 this.open_saved_prompt_editor(
627 path.clone(),
628 window,
629 cx,
630 )
631 .detach_and_log_err(cx)
632 }
633 }
634 })
635 .ok();
636 }
637 },
638 IconName::Close,
639 "Close Entry".into(),
640 {
641 let panel = panel.clone();
642 let entry = entry.clone();
643 move |_window, cx| {
644 panel
645 .update(cx, |this, cx| {
646 this.history_store.update(
647 cx,
648 |history_store, cx| {
649 history_store.remove_recently_opened_entry(
650 &entry, cx,
651 );
652 },
653 );
654 })
655 .ok();
656 }
657 },
658 );
659 }
660
661 menu = menu.separator();
662 }
663
664 menu.action("View All", Box::new(OpenHistory))
665 .end_slot_action(DeleteRecentlyOpenThread.boxed_clone())
666 .fixed_width(px(320.).into())
667 .keep_open_on_confirm(false)
668 .key_context("NavigationMenu")
669 });
670 weak_panel
671 .update(cx, |panel, cx| {
672 cx.subscribe_in(
673 &assistant_navigation_menu,
674 window,
675 |_, menu, _: &DismissEvent, window, cx| {
676 menu.update(cx, |menu, _| {
677 menu.clear_selected();
678 });
679 cx.focus_self(window);
680 },
681 )
682 .detach();
683 panel.assistant_navigation_menu = Some(assistant_navigation_menu);
684 })
685 .ok();
686 });
687
688 let _default_model_subscription = cx.subscribe(
689 &LanguageModelRegistry::global(cx),
690 |this, _, event: &language_model::Event, cx| match event {
691 language_model::Event::DefaultModelChanged => {
692 this.thread
693 .read(cx)
694 .thread()
695 .clone()
696 .update(cx, |thread, cx| thread.get_or_init_configured_model(cx));
697 }
698 _ => {}
699 },
700 );
701
702 Self {
703 active_view,
704 workspace,
705 user_store,
706 project: project.clone(),
707 fs: fs.clone(),
708 language_registry,
709 thread_store: thread_store.clone(),
710 thread: active_thread,
711 message_editor,
712 _active_thread_subscriptions: vec![
713 thread_subscription,
714 active_thread_subscription,
715 message_editor_subscription,
716 ],
717 _default_model_subscription,
718 context_store,
719 prompt_store,
720 configuration: None,
721 configuration_subscription: None,
722 local_timezone: UtcOffset::from_whole_seconds(
723 chrono::Local::now().offset().local_minus_utc(),
724 )
725 .unwrap(),
726 inline_assist_context_store,
727 previous_view: None,
728 history_store: history_store.clone(),
729 history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)),
730 hovered_recent_history_item: None,
731 assistant_dropdown_menu_handle: PopoverMenuHandle::default(),
732 assistant_navigation_menu_handle: PopoverMenuHandle::default(),
733 assistant_navigation_menu: None,
734 width: None,
735 height: None,
736 zoomed: false,
737 pending_serialization: None,
738 hide_upsell: false,
739 }
740 }
741
742 pub fn toggle_focus(
743 workspace: &mut Workspace,
744 _: &ToggleFocus,
745 window: &mut Window,
746 cx: &mut Context<Workspace>,
747 ) {
748 if workspace
749 .panel::<Self>(cx)
750 .is_some_and(|panel| panel.read(cx).enabled(cx))
751 {
752 workspace.toggle_panel_focus::<Self>(window, cx);
753 }
754 }
755
756 pub(crate) fn local_timezone(&self) -> UtcOffset {
757 self.local_timezone
758 }
759
760 pub(crate) fn prompt_store(&self) -> &Option<Entity<PromptStore>> {
761 &self.prompt_store
762 }
763
764 pub(crate) fn inline_assist_context_store(
765 &self,
766 ) -> &Entity<crate::context_store::ContextStore> {
767 &self.inline_assist_context_store
768 }
769
770 pub(crate) fn thread_store(&self) -> &Entity<ThreadStore> {
771 &self.thread_store
772 }
773
774 pub(crate) fn text_thread_store(&self) -> &Entity<TextThreadStore> {
775 &self.context_store
776 }
777
778 fn cancel(&mut self, _: &editor::actions::Cancel, window: &mut Window, cx: &mut Context<Self>) {
779 self.thread
780 .update(cx, |thread, cx| thread.cancel_last_completion(window, cx));
781 }
782
783 fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {
784 let thread = self
785 .thread_store
786 .update(cx, |this, cx| this.create_thread(cx));
787
788 let thread_view = ActiveView::thread(thread.clone(), window, cx);
789 self.set_active_view(thread_view, window, cx);
790
791 let context_store = cx.new(|_cx| {
792 crate::context_store::ContextStore::new(
793 self.project.downgrade(),
794 Some(self.thread_store.downgrade()),
795 )
796 });
797
798 if let Some(other_thread_id) = action.from_thread_id.clone() {
799 let other_thread_task = self.thread_store.update(cx, |this, cx| {
800 this.open_thread(&other_thread_id, window, cx)
801 });
802
803 cx.spawn({
804 let context_store = context_store.clone();
805
806 async move |_panel, cx| {
807 let other_thread = other_thread_task.await?;
808
809 context_store.update(cx, |this, cx| {
810 this.add_thread(other_thread, false, cx);
811 })?;
812 anyhow::Ok(())
813 }
814 })
815 .detach_and_log_err(cx);
816 }
817
818 let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
819 if let ThreadEvent::MessageAdded(_) = &event {
820 // needed to leave empty state
821 cx.notify();
822 }
823 });
824
825 self.thread = cx.new(|cx| {
826 ActiveThread::new(
827 thread.clone(),
828 self.thread_store.clone(),
829 self.context_store.clone(),
830 context_store.clone(),
831 self.language_registry.clone(),
832 self.workspace.clone(),
833 window,
834 cx,
835 )
836 });
837 AgentDiff::set_active_thread(&self.workspace, &thread, window, cx);
838
839 let active_thread_subscription =
840 cx.subscribe(&self.thread, |_, _, event, cx| match &event {
841 ActiveThreadEvent::EditingMessageTokenCountChanged => {
842 cx.notify();
843 }
844 });
845
846 self.message_editor = cx.new(|cx| {
847 MessageEditor::new(
848 self.fs.clone(),
849 self.workspace.clone(),
850 self.user_store.clone(),
851 context_store,
852 self.prompt_store.clone(),
853 self.thread_store.downgrade(),
854 self.context_store.downgrade(),
855 thread,
856 window,
857 cx,
858 )
859 });
860 self.message_editor.focus_handle(cx).focus(window);
861
862 let message_editor_subscription =
863 cx.subscribe(&self.message_editor, |_, _, event, cx| match event {
864 MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
865 cx.notify();
866 }
867 });
868
869 self._active_thread_subscriptions = vec![
870 thread_subscription,
871 active_thread_subscription,
872 message_editor_subscription,
873 ];
874 }
875
876 fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
877 let context = self
878 .context_store
879 .update(cx, |context_store, cx| context_store.create(cx));
880 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
881 .log_err()
882 .flatten();
883
884 let context_editor = cx.new(|cx| {
885 let mut editor = ContextEditor::for_context(
886 context,
887 self.fs.clone(),
888 self.workspace.clone(),
889 self.project.clone(),
890 lsp_adapter_delegate,
891 window,
892 cx,
893 );
894 editor.insert_default_prompt(window, cx);
895 editor
896 });
897
898 self.set_active_view(
899 ActiveView::prompt_editor(
900 context_editor.clone(),
901 self.language_registry.clone(),
902 window,
903 cx,
904 ),
905 window,
906 cx,
907 );
908 context_editor.focus_handle(cx).focus(window);
909 }
910
911 fn deploy_rules_library(
912 &mut self,
913 action: &OpenRulesLibrary,
914 _window: &mut Window,
915 cx: &mut Context<Self>,
916 ) {
917 open_rules_library(
918 self.language_registry.clone(),
919 Box::new(PromptLibraryInlineAssist::new(self.workspace.clone())),
920 Rc::new(|| {
921 Rc::new(SlashCommandCompletionProvider::new(
922 Arc::new(SlashCommandWorkingSet::default()),
923 None,
924 None,
925 ))
926 }),
927 action
928 .prompt_to_select
929 .map(|uuid| UserPromptId(uuid).into()),
930 cx,
931 )
932 .detach_and_log_err(cx);
933 }
934
935 fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
936 if matches!(self.active_view, ActiveView::History) {
937 if let Some(previous_view) = self.previous_view.take() {
938 self.set_active_view(previous_view, window, cx);
939 }
940 } else {
941 self.thread_store
942 .update(cx, |thread_store, cx| thread_store.reload(cx))
943 .detach_and_log_err(cx);
944 self.set_active_view(ActiveView::History, window, cx);
945 }
946 cx.notify();
947 }
948
949 pub(crate) fn open_saved_prompt_editor(
950 &mut self,
951 path: Arc<Path>,
952 window: &mut Window,
953 cx: &mut Context<Self>,
954 ) -> Task<Result<()>> {
955 let context = self
956 .context_store
957 .update(cx, |store, cx| store.open_local_context(path, cx));
958 cx.spawn_in(window, async move |this, cx| {
959 let context = context.await?;
960 this.update_in(cx, |this, window, cx| {
961 this.open_prompt_editor(context, window, cx);
962 })
963 })
964 }
965
966 pub(crate) fn open_prompt_editor(
967 &mut self,
968 context: Entity<AssistantContext>,
969 window: &mut Window,
970 cx: &mut Context<Self>,
971 ) {
972 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project.clone(), cx)
973 .log_err()
974 .flatten();
975 let editor = cx.new(|cx| {
976 ContextEditor::for_context(
977 context,
978 self.fs.clone(),
979 self.workspace.clone(),
980 self.project.clone(),
981 lsp_adapter_delegate,
982 window,
983 cx,
984 )
985 });
986 self.set_active_view(
987 ActiveView::prompt_editor(editor.clone(), self.language_registry.clone(), window, cx),
988 window,
989 cx,
990 );
991 }
992
993 pub(crate) fn open_thread_by_id(
994 &mut self,
995 thread_id: &ThreadId,
996 window: &mut Window,
997 cx: &mut Context<Self>,
998 ) -> Task<Result<()>> {
999 let open_thread_task = self
1000 .thread_store
1001 .update(cx, |this, cx| this.open_thread(thread_id, window, cx));
1002 cx.spawn_in(window, async move |this, cx| {
1003 let thread = open_thread_task.await?;
1004 this.update_in(cx, |this, window, cx| {
1005 this.open_thread(thread, window, cx);
1006 anyhow::Ok(())
1007 })??;
1008 Ok(())
1009 })
1010 }
1011
1012 pub(crate) fn open_thread(
1013 &mut self,
1014 thread: Entity<Thread>,
1015 window: &mut Window,
1016 cx: &mut Context<Self>,
1017 ) {
1018 let thread_view = ActiveView::thread(thread.clone(), window, cx);
1019 self.set_active_view(thread_view, window, cx);
1020 let context_store = cx.new(|_cx| {
1021 crate::context_store::ContextStore::new(
1022 self.project.downgrade(),
1023 Some(self.thread_store.downgrade()),
1024 )
1025 });
1026 let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
1027 if let ThreadEvent::MessageAdded(_) = &event {
1028 // needed to leave empty state
1029 cx.notify();
1030 }
1031 });
1032
1033 self.thread = cx.new(|cx| {
1034 ActiveThread::new(
1035 thread.clone(),
1036 self.thread_store.clone(),
1037 self.context_store.clone(),
1038 context_store.clone(),
1039 self.language_registry.clone(),
1040 self.workspace.clone(),
1041 window,
1042 cx,
1043 )
1044 });
1045 AgentDiff::set_active_thread(&self.workspace, &thread, window, cx);
1046
1047 let active_thread_subscription =
1048 cx.subscribe(&self.thread, |_, _, event, cx| match &event {
1049 ActiveThreadEvent::EditingMessageTokenCountChanged => {
1050 cx.notify();
1051 }
1052 });
1053
1054 self.message_editor = cx.new(|cx| {
1055 MessageEditor::new(
1056 self.fs.clone(),
1057 self.workspace.clone(),
1058 self.user_store.clone(),
1059 context_store,
1060 self.prompt_store.clone(),
1061 self.thread_store.downgrade(),
1062 self.context_store.downgrade(),
1063 thread,
1064 window,
1065 cx,
1066 )
1067 });
1068 self.message_editor.focus_handle(cx).focus(window);
1069
1070 let message_editor_subscription =
1071 cx.subscribe(&self.message_editor, |_, _, event, cx| match event {
1072 MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
1073 cx.notify();
1074 }
1075 });
1076
1077 self._active_thread_subscriptions = vec![
1078 thread_subscription,
1079 active_thread_subscription,
1080 message_editor_subscription,
1081 ];
1082 }
1083
1084 pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
1085 match self.active_view {
1086 ActiveView::Configuration | ActiveView::History => {
1087 if let Some(previous_view) = self.previous_view.take() {
1088 self.active_view = previous_view;
1089
1090 match &self.active_view {
1091 ActiveView::Thread { .. } => {
1092 self.message_editor.focus_handle(cx).focus(window);
1093 }
1094 ActiveView::TextThread { context_editor, .. } => {
1095 context_editor.focus_handle(cx).focus(window);
1096 }
1097 _ => {}
1098 }
1099 } else {
1100 self.active_view =
1101 ActiveView::thread(self.thread.read(cx).thread().clone(), window, cx);
1102 self.message_editor.focus_handle(cx).focus(window);
1103 }
1104 cx.notify();
1105 }
1106 _ => {}
1107 }
1108 }
1109
1110 pub fn toggle_navigation_menu(
1111 &mut self,
1112 _: &ToggleNavigationMenu,
1113 window: &mut Window,
1114 cx: &mut Context<Self>,
1115 ) {
1116 self.assistant_navigation_menu_handle.toggle(window, cx);
1117 }
1118
1119 pub fn toggle_options_menu(
1120 &mut self,
1121 _: &ToggleOptionsMenu,
1122 window: &mut Window,
1123 cx: &mut Context<Self>,
1124 ) {
1125 self.assistant_dropdown_menu_handle.toggle(window, cx);
1126 }
1127
1128 pub fn increase_font_size(
1129 &mut self,
1130 action: &IncreaseBufferFontSize,
1131 _: &mut Window,
1132 cx: &mut Context<Self>,
1133 ) {
1134 self.handle_font_size_action(action.persist, px(1.0), cx);
1135 }
1136
1137 pub fn decrease_font_size(
1138 &mut self,
1139 action: &DecreaseBufferFontSize,
1140 _: &mut Window,
1141 cx: &mut Context<Self>,
1142 ) {
1143 self.handle_font_size_action(action.persist, px(-1.0), cx);
1144 }
1145
1146 fn handle_font_size_action(&mut self, persist: bool, delta: Pixels, cx: &mut Context<Self>) {
1147 match self.active_view.which_font_size_used() {
1148 WhichFontSize::AgentFont => {
1149 if persist {
1150 update_settings_file::<ThemeSettings>(
1151 self.fs.clone(),
1152 cx,
1153 move |settings, cx| {
1154 let agent_font_size =
1155 ThemeSettings::get_global(cx).agent_font_size(cx) + delta;
1156 let _ = settings
1157 .agent_font_size
1158 .insert(theme::clamp_font_size(agent_font_size).0);
1159 },
1160 );
1161 } else {
1162 theme::adjust_agent_font_size(cx, |size| {
1163 *size += delta;
1164 });
1165 }
1166 }
1167 WhichFontSize::BufferFont => {
1168 // Prompt editor uses the buffer font size, so allow the action to propagate to the
1169 // default handler that changes that font size.
1170 cx.propagate();
1171 }
1172 WhichFontSize::None => {}
1173 }
1174 }
1175
1176 pub fn reset_font_size(
1177 &mut self,
1178 action: &ResetBufferFontSize,
1179 _: &mut Window,
1180 cx: &mut Context<Self>,
1181 ) {
1182 if action.persist {
1183 update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings, _| {
1184 settings.agent_font_size = None;
1185 });
1186 } else {
1187 theme::reset_agent_font_size(cx);
1188 }
1189 }
1190
1191 pub fn toggle_zoom(&mut self, _: &ToggleZoom, window: &mut Window, cx: &mut Context<Self>) {
1192 if self.zoomed {
1193 cx.emit(PanelEvent::ZoomOut);
1194 } else {
1195 if !self.focus_handle(cx).contains_focused(window, cx) {
1196 cx.focus_self(window);
1197 }
1198 cx.emit(PanelEvent::ZoomIn);
1199 }
1200 }
1201
1202 pub fn open_agent_diff(
1203 &mut self,
1204 _: &OpenAgentDiff,
1205 window: &mut Window,
1206 cx: &mut Context<Self>,
1207 ) {
1208 let thread = self.thread.read(cx).thread().clone();
1209 self.workspace
1210 .update(cx, |workspace, cx| {
1211 AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx)
1212 })
1213 .log_err();
1214 }
1215
1216 pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1217 let context_server_store = self.project.read(cx).context_server_store();
1218 let tools = self.thread_store.read(cx).tools();
1219 let fs = self.fs.clone();
1220
1221 self.set_active_view(ActiveView::Configuration, window, cx);
1222 self.configuration =
1223 Some(cx.new(|cx| AgentConfiguration::new(fs, context_server_store, tools, window, cx)));
1224
1225 if let Some(configuration) = self.configuration.as_ref() {
1226 self.configuration_subscription = Some(cx.subscribe_in(
1227 configuration,
1228 window,
1229 Self::handle_agent_configuration_event,
1230 ));
1231
1232 configuration.focus_handle(cx).focus(window);
1233 }
1234 }
1235
1236 pub(crate) fn open_active_thread_as_markdown(
1237 &mut self,
1238 _: &OpenActiveThreadAsMarkdown,
1239 window: &mut Window,
1240 cx: &mut Context<Self>,
1241 ) {
1242 let Some(workspace) = self.workspace.upgrade() else {
1243 return;
1244 };
1245
1246 let Some(thread) = self.active_thread() else {
1247 return;
1248 };
1249
1250 active_thread::open_active_thread_as_markdown(thread, workspace, window, cx)
1251 .detach_and_log_err(cx);
1252 }
1253
1254 fn handle_agent_configuration_event(
1255 &mut self,
1256 _entity: &Entity<AgentConfiguration>,
1257 event: &AssistantConfigurationEvent,
1258 window: &mut Window,
1259 cx: &mut Context<Self>,
1260 ) {
1261 match event {
1262 AssistantConfigurationEvent::NewThread(provider) => {
1263 if LanguageModelRegistry::read_global(cx)
1264 .default_model()
1265 .map_or(true, |model| model.provider.id() != provider.id())
1266 {
1267 if let Some(model) = provider.default_model(cx) {
1268 update_settings_file::<AgentSettings>(
1269 self.fs.clone(),
1270 cx,
1271 move |settings, _| settings.set_model(model),
1272 );
1273 }
1274 }
1275
1276 self.new_thread(&NewThread::default(), window, cx);
1277 }
1278 }
1279 }
1280
1281 pub(crate) fn active_thread(&self) -> Option<Entity<Thread>> {
1282 match &self.active_view {
1283 ActiveView::Thread { thread, .. } => thread.upgrade(),
1284 _ => None,
1285 }
1286 }
1287
1288 pub(crate) fn delete_thread(
1289 &mut self,
1290 thread_id: &ThreadId,
1291 cx: &mut Context<Self>,
1292 ) -> Task<Result<()>> {
1293 self.thread_store
1294 .update(cx, |this, cx| this.delete_thread(thread_id, cx))
1295 }
1296
1297 pub(crate) fn has_active_thread(&self) -> bool {
1298 matches!(self.active_view, ActiveView::Thread { .. })
1299 }
1300
1301 fn continue_conversation(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1302 let thread_state = self.thread.read(cx).thread().read(cx);
1303 if !thread_state.tool_use_limit_reached() {
1304 return;
1305 }
1306
1307 let model = thread_state.configured_model().map(|cm| cm.model.clone());
1308 if let Some(model) = model {
1309 self.thread.update(cx, |active_thread, cx| {
1310 active_thread.thread().update(cx, |thread, cx| {
1311 thread.insert_invisible_continue_message(cx);
1312 thread.advance_prompt_id();
1313 thread.send_to_model(
1314 model,
1315 CompletionIntent::UserPrompt,
1316 Some(window.window_handle()),
1317 cx,
1318 );
1319 });
1320 });
1321 } else {
1322 log::warn!("No configured model available for continuation");
1323 }
1324 }
1325
1326 fn toggle_burn_mode(
1327 &mut self,
1328 _: &ToggleBurnMode,
1329 _window: &mut Window,
1330 cx: &mut Context<Self>,
1331 ) {
1332 self.thread.update(cx, |active_thread, cx| {
1333 active_thread.thread().update(cx, |thread, _cx| {
1334 let current_mode = thread.completion_mode();
1335
1336 thread.set_completion_mode(match current_mode {
1337 CompletionMode::Burn => CompletionMode::Normal,
1338 CompletionMode::Normal => CompletionMode::Burn,
1339 });
1340 });
1341 });
1342 }
1343
1344 pub(crate) fn active_context_editor(&self) -> Option<Entity<ContextEditor>> {
1345 match &self.active_view {
1346 ActiveView::TextThread { context_editor, .. } => Some(context_editor.clone()),
1347 _ => None,
1348 }
1349 }
1350
1351 pub(crate) fn delete_context(
1352 &mut self,
1353 path: Arc<Path>,
1354 cx: &mut Context<Self>,
1355 ) -> Task<Result<()>> {
1356 self.context_store
1357 .update(cx, |this, cx| this.delete_local_context(path, cx))
1358 }
1359
1360 fn set_active_view(
1361 &mut self,
1362 new_view: ActiveView,
1363 window: &mut Window,
1364 cx: &mut Context<Self>,
1365 ) {
1366 let current_is_history = matches!(self.active_view, ActiveView::History);
1367 let new_is_history = matches!(new_view, ActiveView::History);
1368
1369 let current_is_config = matches!(self.active_view, ActiveView::Configuration);
1370 let new_is_config = matches!(new_view, ActiveView::Configuration);
1371
1372 let current_is_special = current_is_history || current_is_config;
1373 let new_is_special = new_is_history || new_is_config;
1374
1375 match &self.active_view {
1376 ActiveView::Thread { thread, .. } => {
1377 if let Some(thread) = thread.upgrade() {
1378 if thread.read(cx).is_empty() {
1379 let id = thread.read(cx).id().clone();
1380 self.history_store.update(cx, |store, cx| {
1381 store.remove_recently_opened_thread(id, cx);
1382 });
1383 }
1384 }
1385 }
1386 ActiveView::TextThread { context_editor, .. } => {
1387 let context = context_editor.read(cx).context();
1388 // When switching away from an unsaved text thread, delete its entry.
1389 if context.read(cx).path().is_none() {
1390 let context = context.clone();
1391 self.history_store.update(cx, |store, cx| {
1392 store.remove_recently_opened_entry(&RecentEntry::Context(context), cx);
1393 });
1394 }
1395 }
1396 _ => {}
1397 }
1398
1399 match &new_view {
1400 ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
1401 if let Some(thread) = thread.upgrade() {
1402 let id = thread.read(cx).id().clone();
1403 store.push_recently_opened_entry(RecentEntry::Thread(id, thread), cx);
1404 }
1405 }),
1406 ActiveView::TextThread { context_editor, .. } => {
1407 self.history_store.update(cx, |store, cx| {
1408 let context = context_editor.read(cx).context().clone();
1409 store.push_recently_opened_entry(RecentEntry::Context(context), cx)
1410 })
1411 }
1412 _ => {}
1413 }
1414
1415 if current_is_special && !new_is_special {
1416 self.active_view = new_view;
1417 } else if !current_is_special && new_is_special {
1418 self.previous_view = Some(std::mem::replace(&mut self.active_view, new_view));
1419 } else {
1420 if !new_is_special {
1421 self.previous_view = None;
1422 }
1423 self.active_view = new_view;
1424 }
1425
1426 self.focus_handle(cx).focus(window);
1427 }
1428}
1429
1430impl Focusable for AgentPanel {
1431 fn focus_handle(&self, cx: &App) -> FocusHandle {
1432 match &self.active_view {
1433 ActiveView::Thread { .. } => self.message_editor.focus_handle(cx),
1434 ActiveView::History => self.history.focus_handle(cx),
1435 ActiveView::TextThread { context_editor, .. } => context_editor.focus_handle(cx),
1436 ActiveView::Configuration => {
1437 if let Some(configuration) = self.configuration.as_ref() {
1438 configuration.focus_handle(cx)
1439 } else {
1440 cx.focus_handle()
1441 }
1442 }
1443 }
1444 }
1445}
1446
1447fn agent_panel_dock_position(cx: &App) -> DockPosition {
1448 match AgentSettings::get_global(cx).dock {
1449 AgentDockPosition::Left => DockPosition::Left,
1450 AgentDockPosition::Bottom => DockPosition::Bottom,
1451 AgentDockPosition::Right => DockPosition::Right,
1452 }
1453}
1454
1455impl EventEmitter<PanelEvent> for AgentPanel {}
1456
1457impl Panel for AgentPanel {
1458 fn persistent_name() -> &'static str {
1459 "AgentPanel"
1460 }
1461
1462 fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1463 agent_panel_dock_position(cx)
1464 }
1465
1466 fn position_is_valid(&self, position: DockPosition) -> bool {
1467 position != DockPosition::Bottom
1468 }
1469
1470 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
1471 settings::update_settings_file::<AgentSettings>(self.fs.clone(), cx, move |settings, _| {
1472 let dock = match position {
1473 DockPosition::Left => AgentDockPosition::Left,
1474 DockPosition::Bottom => AgentDockPosition::Bottom,
1475 DockPosition::Right => AgentDockPosition::Right,
1476 };
1477 settings.set_dock(dock);
1478 });
1479 }
1480
1481 fn size(&self, window: &Window, cx: &App) -> Pixels {
1482 let settings = AgentSettings::get_global(cx);
1483 match self.position(window, cx) {
1484 DockPosition::Left | DockPosition::Right => {
1485 self.width.unwrap_or(settings.default_width)
1486 }
1487 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1488 }
1489 }
1490
1491 fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
1492 match self.position(window, cx) {
1493 DockPosition::Left | DockPosition::Right => self.width = size,
1494 DockPosition::Bottom => self.height = size,
1495 }
1496 self.serialize(cx);
1497 cx.notify();
1498 }
1499
1500 fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
1501
1502 fn remote_id() -> Option<proto::PanelId> {
1503 Some(proto::PanelId::AssistantPanel)
1504 }
1505
1506 fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
1507 (self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAssistant)
1508 }
1509
1510 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
1511 Some("Agent Panel")
1512 }
1513
1514 fn toggle_action(&self) -> Box<dyn Action> {
1515 Box::new(ToggleFocus)
1516 }
1517
1518 fn activation_priority(&self) -> u32 {
1519 3
1520 }
1521
1522 fn enabled(&self, cx: &App) -> bool {
1523 AgentSettings::get_global(cx).enabled
1524 }
1525
1526 fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
1527 self.zoomed
1528 }
1529
1530 fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
1531 self.zoomed = zoomed;
1532 cx.notify();
1533 }
1534}
1535
1536impl AgentPanel {
1537 fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
1538 const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
1539
1540 let content = match &self.active_view {
1541 ActiveView::Thread {
1542 change_title_editor,
1543 ..
1544 } => {
1545 let active_thread = self.thread.read(cx);
1546 let state = if active_thread.is_empty() {
1547 &ThreadSummary::Pending
1548 } else {
1549 active_thread.summary(cx)
1550 };
1551
1552 match state {
1553 ThreadSummary::Pending => Label::new(ThreadSummary::DEFAULT.clone())
1554 .truncate()
1555 .into_any_element(),
1556 ThreadSummary::Generating => Label::new(LOADING_SUMMARY_PLACEHOLDER)
1557 .truncate()
1558 .into_any_element(),
1559 ThreadSummary::Ready(_) => div()
1560 .w_full()
1561 .child(change_title_editor.clone())
1562 .into_any_element(),
1563 ThreadSummary::Error => h_flex()
1564 .w_full()
1565 .child(change_title_editor.clone())
1566 .child(
1567 ui::IconButton::new("retry-summary-generation", IconName::RotateCcw)
1568 .on_click({
1569 let active_thread = self.thread.clone();
1570 move |_, _window, cx| {
1571 active_thread.update(cx, |thread, cx| {
1572 thread.regenerate_summary(cx);
1573 });
1574 }
1575 })
1576 .tooltip(move |_window, cx| {
1577 cx.new(|_| {
1578 Tooltip::new("Failed to generate title")
1579 .meta("Click to try again")
1580 })
1581 .into()
1582 }),
1583 )
1584 .into_any_element(),
1585 }
1586 }
1587 ActiveView::TextThread {
1588 title_editor,
1589 context_editor,
1590 ..
1591 } => {
1592 let summary = context_editor.read(cx).context().read(cx).summary();
1593
1594 match summary {
1595 ContextSummary::Pending => Label::new(ContextSummary::DEFAULT)
1596 .truncate()
1597 .into_any_element(),
1598 ContextSummary::Content(summary) => {
1599 if summary.done {
1600 div()
1601 .w_full()
1602 .child(title_editor.clone())
1603 .into_any_element()
1604 } else {
1605 Label::new(LOADING_SUMMARY_PLACEHOLDER)
1606 .truncate()
1607 .into_any_element()
1608 }
1609 }
1610 ContextSummary::Error => h_flex()
1611 .w_full()
1612 .child(title_editor.clone())
1613 .child(
1614 ui::IconButton::new("retry-summary-generation", IconName::RotateCcw)
1615 .on_click({
1616 let context_editor = context_editor.clone();
1617 move |_, _window, cx| {
1618 context_editor.update(cx, |context_editor, cx| {
1619 context_editor.regenerate_summary(cx);
1620 });
1621 }
1622 })
1623 .tooltip(move |_window, cx| {
1624 cx.new(|_| {
1625 Tooltip::new("Failed to generate title")
1626 .meta("Click to try again")
1627 })
1628 .into()
1629 }),
1630 )
1631 .into_any_element(),
1632 }
1633 }
1634 ActiveView::History => Label::new("History").truncate().into_any_element(),
1635 ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
1636 };
1637
1638 h_flex()
1639 .key_context("TitleEditor")
1640 .id("TitleEditor")
1641 .flex_grow()
1642 .w_full()
1643 .max_w_full()
1644 .overflow_x_scroll()
1645 .child(content)
1646 .into_any()
1647 }
1648
1649 fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1650 let active_thread = self.thread.read(cx);
1651 let user_store = self.user_store.read(cx);
1652 let thread = active_thread.thread().read(cx);
1653 let thread_id = thread.id().clone();
1654 let is_empty = active_thread.is_empty();
1655 let editor_empty = self.message_editor.read(cx).is_editor_fully_empty(cx);
1656 let last_usage = active_thread.thread().read(cx).last_usage().or_else(|| {
1657 maybe!({
1658 let amount = user_store.model_request_usage_amount()?;
1659 let limit = user_store.model_request_usage_limit()?.variant?;
1660
1661 Some(RequestUsage {
1662 amount: amount as i32,
1663 limit: match limit {
1664 proto::usage_limit::Variant::Limited(limited) => {
1665 zed_llm_client::UsageLimit::Limited(limited.limit as i32)
1666 }
1667 proto::usage_limit::Variant::Unlimited(_) => {
1668 zed_llm_client::UsageLimit::Unlimited
1669 }
1670 },
1671 })
1672 })
1673 });
1674
1675 let account_url = zed_urls::account_url(cx);
1676
1677 let show_token_count = match &self.active_view {
1678 ActiveView::Thread { .. } => !is_empty || !editor_empty,
1679 ActiveView::TextThread { .. } => true,
1680 _ => false,
1681 };
1682
1683 let focus_handle = self.focus_handle(cx);
1684
1685 let go_back_button = div().child(
1686 IconButton::new("go-back", IconName::ArrowLeft)
1687 .icon_size(IconSize::Small)
1688 .on_click(cx.listener(|this, _, window, cx| {
1689 this.go_back(&workspace::GoBack, window, cx);
1690 }))
1691 .tooltip({
1692 let focus_handle = focus_handle.clone();
1693 move |window, cx| {
1694 Tooltip::for_action_in(
1695 "Go Back",
1696 &workspace::GoBack,
1697 &focus_handle,
1698 window,
1699 cx,
1700 )
1701 }
1702 }),
1703 );
1704
1705 let recent_entries_menu = div().child(
1706 PopoverMenu::new("agent-nav-menu")
1707 .trigger_with_tooltip(
1708 IconButton::new("agent-nav-menu", IconName::MenuAlt)
1709 .icon_size(IconSize::Small)
1710 .style(ui::ButtonStyle::Subtle),
1711 {
1712 let focus_handle = focus_handle.clone();
1713 move |window, cx| {
1714 Tooltip::for_action_in(
1715 "Toggle Panel Menu",
1716 &ToggleNavigationMenu,
1717 &focus_handle,
1718 window,
1719 cx,
1720 )
1721 }
1722 },
1723 )
1724 .anchor(Corner::TopLeft)
1725 .with_handle(self.assistant_navigation_menu_handle.clone())
1726 .menu({
1727 let menu = self.assistant_navigation_menu.clone();
1728 move |window, cx| {
1729 if let Some(menu) = menu.as_ref() {
1730 menu.update(cx, |_, cx| {
1731 cx.defer_in(window, |menu, window, cx| {
1732 menu.rebuild(window, cx);
1733 });
1734 })
1735 }
1736 menu.clone()
1737 }
1738 }),
1739 );
1740
1741 let zoom_in_label = if self.is_zoomed(window, cx) {
1742 "Zoom Out"
1743 } else {
1744 "Zoom In"
1745 };
1746
1747 let agent_extra_menu = PopoverMenu::new("agent-options-menu")
1748 .trigger_with_tooltip(
1749 IconButton::new("agent-options-menu", IconName::Ellipsis)
1750 .icon_size(IconSize::Small),
1751 {
1752 let focus_handle = focus_handle.clone();
1753 move |window, cx| {
1754 Tooltip::for_action_in(
1755 "Toggle Agent Menu",
1756 &ToggleOptionsMenu,
1757 &focus_handle,
1758 window,
1759 cx,
1760 )
1761 }
1762 },
1763 )
1764 .anchor(Corner::TopRight)
1765 .with_handle(self.assistant_dropdown_menu_handle.clone())
1766 .menu(move |window, cx| {
1767 Some(ContextMenu::build(window, cx, |mut menu, _window, _cx| {
1768 menu = menu
1769 .action("New Thread", NewThread::default().boxed_clone())
1770 .action("New Text Thread", NewTextThread.boxed_clone())
1771 .when(!is_empty, |menu| {
1772 menu.action(
1773 "New From Summary",
1774 Box::new(NewThread {
1775 from_thread_id: Some(thread_id.clone()),
1776 }),
1777 )
1778 })
1779 .separator();
1780
1781 menu = menu
1782 .header("MCP Servers")
1783 .action(
1784 "View Server Extensions",
1785 Box::new(zed_actions::Extensions {
1786 category_filter: Some(
1787 zed_actions::ExtensionCategoryFilter::ContextServers,
1788 ),
1789 }),
1790 )
1791 .action("Add Custom Server…", Box::new(AddContextServer))
1792 .separator();
1793
1794 if let Some(usage) = last_usage {
1795 menu = menu
1796 .header_with_link("Prompt Usage", "Manage", account_url.clone())
1797 .custom_entry(
1798 move |_window, cx| {
1799 let used_percentage = match usage.limit {
1800 UsageLimit::Limited(limit) => {
1801 Some((usage.amount as f32 / limit as f32) * 100.)
1802 }
1803 UsageLimit::Unlimited => None,
1804 };
1805
1806 h_flex()
1807 .flex_1()
1808 .gap_1p5()
1809 .children(used_percentage.map(|percent| {
1810 ProgressBar::new("usage", percent, 100., cx)
1811 }))
1812 .child(
1813 Label::new(match usage.limit {
1814 UsageLimit::Limited(limit) => {
1815 format!("{} / {limit}", usage.amount)
1816 }
1817 UsageLimit::Unlimited => {
1818 format!("{} / ∞", usage.amount)
1819 }
1820 })
1821 .size(LabelSize::Small)
1822 .color(Color::Muted),
1823 )
1824 .into_any_element()
1825 },
1826 move |_, cx| cx.open_url(&zed_urls::account_url(cx)),
1827 )
1828 .separator()
1829 }
1830
1831 menu = menu
1832 .action("Rules…", Box::new(OpenRulesLibrary::default()))
1833 .action("Settings", Box::new(OpenConfiguration))
1834 .action(zoom_in_label, Box::new(ToggleZoom));
1835 menu
1836 }))
1837 });
1838
1839 h_flex()
1840 .id("assistant-toolbar")
1841 .h(Tab::container_height(cx))
1842 .max_w_full()
1843 .flex_none()
1844 .justify_between()
1845 .gap_2()
1846 .bg(cx.theme().colors().tab_bar_background)
1847 .border_b_1()
1848 .border_color(cx.theme().colors().border)
1849 .child(
1850 h_flex()
1851 .size_full()
1852 .pl_1()
1853 .gap_1()
1854 .child(match &self.active_view {
1855 ActiveView::History | ActiveView::Configuration => go_back_button,
1856 _ => recent_entries_menu,
1857 })
1858 .child(self.render_title_view(window, cx)),
1859 )
1860 .child(
1861 h_flex()
1862 .h_full()
1863 .gap_2()
1864 .when(show_token_count, |parent| {
1865 parent.children(self.render_token_count(&thread, cx))
1866 })
1867 .child(
1868 h_flex()
1869 .h_full()
1870 .gap(DynamicSpacing::Base02.rems(cx))
1871 .px(DynamicSpacing::Base08.rems(cx))
1872 .border_l_1()
1873 .border_color(cx.theme().colors().border)
1874 .child(
1875 IconButton::new("new", IconName::Plus)
1876 .icon_size(IconSize::Small)
1877 .style(ButtonStyle::Subtle)
1878 .tooltip(move |window, cx| {
1879 Tooltip::for_action_in(
1880 "New Thread",
1881 &NewThread::default(),
1882 &focus_handle,
1883 window,
1884 cx,
1885 )
1886 })
1887 .on_click(move |_event, window, cx| {
1888 window.dispatch_action(
1889 NewThread::default().boxed_clone(),
1890 cx,
1891 );
1892 }),
1893 )
1894 .child(agent_extra_menu),
1895 ),
1896 )
1897 }
1898
1899 fn render_token_count(&self, thread: &Thread, cx: &App) -> Option<AnyElement> {
1900 let is_generating = thread.is_generating();
1901 let message_editor = self.message_editor.read(cx);
1902
1903 let conversation_token_usage = thread.total_token_usage()?;
1904
1905 let (total_token_usage, is_estimating) = if let Some((editing_message_id, unsent_tokens)) =
1906 self.thread.read(cx).editing_message_id()
1907 {
1908 let combined = thread
1909 .token_usage_up_to_message(editing_message_id)
1910 .add(unsent_tokens);
1911
1912 (combined, unsent_tokens > 0)
1913 } else {
1914 let unsent_tokens = message_editor.last_estimated_token_count().unwrap_or(0);
1915 let combined = conversation_token_usage.add(unsent_tokens);
1916
1917 (combined, unsent_tokens > 0)
1918 };
1919
1920 let is_waiting_to_update_token_count = message_editor.is_waiting_to_update_token_count();
1921
1922 match &self.active_view {
1923 ActiveView::Thread { .. } => {
1924 if total_token_usage.total == 0 {
1925 return None;
1926 }
1927
1928 let token_color = match total_token_usage.ratio() {
1929 TokenUsageRatio::Normal if is_estimating => Color::Default,
1930 TokenUsageRatio::Normal => Color::Muted,
1931 TokenUsageRatio::Warning => Color::Warning,
1932 TokenUsageRatio::Exceeded => Color::Error,
1933 };
1934
1935 let token_count = h_flex()
1936 .id("token-count")
1937 .flex_shrink_0()
1938 .gap_0p5()
1939 .when(!is_generating && is_estimating, |parent| {
1940 parent
1941 .child(
1942 h_flex()
1943 .mr_1()
1944 .size_2p5()
1945 .justify_center()
1946 .rounded_full()
1947 .bg(cx.theme().colors().text.opacity(0.1))
1948 .child(
1949 div().size_1().rounded_full().bg(cx.theme().colors().text),
1950 ),
1951 )
1952 .tooltip(move |window, cx| {
1953 Tooltip::with_meta(
1954 "Estimated New Token Count",
1955 None,
1956 format!(
1957 "Current Conversation Tokens: {}",
1958 humanize_token_count(conversation_token_usage.total)
1959 ),
1960 window,
1961 cx,
1962 )
1963 })
1964 })
1965 .child(
1966 Label::new(humanize_token_count(total_token_usage.total))
1967 .size(LabelSize::Small)
1968 .color(token_color)
1969 .map(|label| {
1970 if is_generating || is_waiting_to_update_token_count {
1971 label
1972 .with_animation(
1973 "used-tokens-label",
1974 Animation::new(Duration::from_secs(2))
1975 .repeat()
1976 .with_easing(pulsating_between(0.6, 1.)),
1977 |label, delta| label.alpha(delta),
1978 )
1979 .into_any()
1980 } else {
1981 label.into_any_element()
1982 }
1983 }),
1984 )
1985 .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
1986 .child(
1987 Label::new(humanize_token_count(total_token_usage.max))
1988 .size(LabelSize::Small)
1989 .color(Color::Muted),
1990 )
1991 .into_any();
1992
1993 Some(token_count)
1994 }
1995 ActiveView::TextThread { context_editor, .. } => {
1996 let element = render_remaining_tokens(context_editor, cx)?;
1997
1998 Some(element.into_any_element())
1999 }
2000 _ => None,
2001 }
2002 }
2003
2004 fn should_render_trial_end_upsell(&self, cx: &mut Context<Self>) -> bool {
2005 if TrialEndUpsell::dismissed() {
2006 return false;
2007 }
2008
2009 let plan = self.user_store.read(cx).current_plan();
2010 let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
2011
2012 matches!(plan, Some(Plan::Free)) && has_previous_trial
2013 }
2014
2015 fn should_render_upsell(&self, cx: &mut Context<Self>) -> bool {
2016 if !matches!(self.active_view, ActiveView::Thread { .. }) {
2017 return false;
2018 }
2019
2020 if self.hide_upsell || Upsell::dismissed() {
2021 return false;
2022 }
2023
2024 let is_using_zed_provider = self
2025 .thread
2026 .read(cx)
2027 .thread()
2028 .read(cx)
2029 .configured_model()
2030 .map_or(false, |model| {
2031 model.provider.id().0 == ZED_CLOUD_PROVIDER_ID
2032 });
2033 if !is_using_zed_provider {
2034 return false;
2035 }
2036
2037 let plan = self.user_store.read(cx).current_plan();
2038 if matches!(plan, Some(Plan::ZedPro | Plan::ZedProTrial)) {
2039 return false;
2040 }
2041
2042 let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
2043 if has_previous_trial {
2044 return false;
2045 }
2046
2047 true
2048 }
2049
2050 fn render_upsell(
2051 &self,
2052 _window: &mut Window,
2053 cx: &mut Context<Self>,
2054 ) -> Option<impl IntoElement> {
2055 if !self.should_render_upsell(cx) {
2056 return None;
2057 }
2058
2059 if self.user_store.read(cx).account_too_young() {
2060 Some(self.render_young_account_upsell(cx).into_any_element())
2061 } else {
2062 Some(self.render_trial_upsell(cx).into_any_element())
2063 }
2064 }
2065
2066 fn render_young_account_upsell(&self, cx: &mut Context<Self>) -> impl IntoElement {
2067 let checkbox = CheckboxWithLabel::new(
2068 "dont-show-again",
2069 Label::new("Don't show again").color(Color::Muted),
2070 ToggleState::Unselected,
2071 move |toggle_state, _window, cx| {
2072 let toggle_state_bool = toggle_state.selected();
2073
2074 Upsell::set_dismissed(toggle_state_bool, cx);
2075 },
2076 );
2077
2078 let contents = div()
2079 .size_full()
2080 .gap_2()
2081 .flex()
2082 .flex_col()
2083 .child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small))
2084 .child(
2085 Label::new("Your GitHub account was created less than 30 days ago, so we can't offer you a free trial.")
2086 .size(LabelSize::Small),
2087 )
2088 .child(
2089 Label::new(
2090 "Use your own API keys, upgrade to Zed Pro or send an email to billing-support@zed.dev.",
2091 )
2092 .color(Color::Muted),
2093 )
2094 .child(
2095 h_flex()
2096 .w_full()
2097 .px_neg_1()
2098 .justify_between()
2099 .items_center()
2100 .child(h_flex().items_center().gap_1().child(checkbox))
2101 .child(
2102 h_flex()
2103 .gap_2()
2104 .child(
2105 Button::new("dismiss-button", "Not Now")
2106 .style(ButtonStyle::Transparent)
2107 .color(Color::Muted)
2108 .on_click({
2109 let agent_panel = cx.entity();
2110 move |_, _, cx| {
2111 agent_panel.update(cx, |this, cx| {
2112 this.hide_upsell = true;
2113 cx.notify();
2114 });
2115 }
2116 }),
2117 )
2118 .child(
2119 Button::new("cta-button", "Upgrade to Zed Pro")
2120 .style(ButtonStyle::Transparent)
2121 .on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))),
2122 ),
2123 ),
2124 );
2125
2126 self.render_upsell_container(cx, contents)
2127 }
2128
2129 fn render_trial_upsell(&self, cx: &mut Context<Self>) -> impl IntoElement {
2130 let checkbox = CheckboxWithLabel::new(
2131 "dont-show-again",
2132 Label::new("Don't show again").color(Color::Muted),
2133 ToggleState::Unselected,
2134 move |toggle_state, _window, cx| {
2135 let toggle_state_bool = toggle_state.selected();
2136
2137 Upsell::set_dismissed(toggle_state_bool, cx);
2138 },
2139 );
2140
2141 let contents = div()
2142 .size_full()
2143 .gap_2()
2144 .flex()
2145 .flex_col()
2146 .child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small))
2147 .child(
2148 Label::new("Try Zed Pro for free for 14 days - no credit card required.")
2149 .size(LabelSize::Small),
2150 )
2151 .child(
2152 Label::new(
2153 "Use your own API keys or enable usage-based billing once you hit the cap.",
2154 )
2155 .color(Color::Muted),
2156 )
2157 .child(
2158 h_flex()
2159 .w_full()
2160 .px_neg_1()
2161 .justify_between()
2162 .items_center()
2163 .child(h_flex().items_center().gap_1().child(checkbox))
2164 .child(
2165 h_flex()
2166 .gap_2()
2167 .child(
2168 Button::new("dismiss-button", "Not Now")
2169 .style(ButtonStyle::Transparent)
2170 .color(Color::Muted)
2171 .on_click({
2172 let agent_panel = cx.entity();
2173 move |_, _, cx| {
2174 agent_panel.update(cx, |this, cx| {
2175 this.hide_upsell = true;
2176 cx.notify();
2177 });
2178 }
2179 }),
2180 )
2181 .child(
2182 Button::new("cta-button", "Start Trial")
2183 .style(ButtonStyle::Transparent)
2184 .on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))),
2185 ),
2186 ),
2187 );
2188
2189 self.render_upsell_container(cx, contents)
2190 }
2191
2192 fn render_trial_end_upsell(
2193 &self,
2194 _window: &mut Window,
2195 cx: &mut Context<Self>,
2196 ) -> Option<impl IntoElement> {
2197 if !self.should_render_trial_end_upsell(cx) {
2198 return None;
2199 }
2200
2201 Some(
2202 self.render_upsell_container(
2203 cx,
2204 div()
2205 .size_full()
2206 .gap_2()
2207 .flex()
2208 .flex_col()
2209 .child(
2210 Headline::new("Your Zed Pro trial has expired.").size(HeadlineSize::Small),
2211 )
2212 .child(
2213 Label::new("You've been automatically reset to the free plan.")
2214 .size(LabelSize::Small),
2215 )
2216 .child(
2217 h_flex()
2218 .w_full()
2219 .px_neg_1()
2220 .justify_between()
2221 .items_center()
2222 .child(div())
2223 .child(
2224 h_flex()
2225 .gap_2()
2226 .child(
2227 Button::new("dismiss-button", "Stay on Free")
2228 .style(ButtonStyle::Transparent)
2229 .color(Color::Muted)
2230 .on_click({
2231 let agent_panel = cx.entity();
2232 move |_, _, cx| {
2233 agent_panel.update(cx, |_this, cx| {
2234 TrialEndUpsell::set_dismissed(true, cx);
2235 cx.notify();
2236 });
2237 }
2238 }),
2239 )
2240 .child(
2241 Button::new("cta-button", "Upgrade to Zed Pro")
2242 .style(ButtonStyle::Transparent)
2243 .on_click(|_, _, cx| {
2244 cx.open_url(&zed_urls::account_url(cx))
2245 }),
2246 ),
2247 ),
2248 ),
2249 ),
2250 )
2251 }
2252
2253 fn render_upsell_container(&self, cx: &mut Context<Self>, content: Div) -> Div {
2254 div().p_2().child(
2255 v_flex()
2256 .w_full()
2257 .elevation_2(cx)
2258 .rounded(px(8.))
2259 .bg(cx.theme().colors().background.alpha(0.5))
2260 .p(px(3.))
2261 .child(
2262 div()
2263 .gap_2()
2264 .flex()
2265 .flex_col()
2266 .size_full()
2267 .border_1()
2268 .rounded(px(5.))
2269 .border_color(cx.theme().colors().text.alpha(0.1))
2270 .overflow_hidden()
2271 .relative()
2272 .bg(cx.theme().colors().panel_background)
2273 .px_4()
2274 .py_3()
2275 .child(
2276 div()
2277 .absolute()
2278 .top_0()
2279 .right(px(-1.0))
2280 .w(px(441.))
2281 .h(px(167.))
2282 .child(
2283 Vector::new(
2284 VectorName::Grid,
2285 rems_from_px(441.),
2286 rems_from_px(167.),
2287 )
2288 .color(ui::Color::Custom(cx.theme().colors().text.alpha(0.1))),
2289 ),
2290 )
2291 .child(
2292 div()
2293 .absolute()
2294 .top(px(-8.0))
2295 .right_0()
2296 .w(px(400.))
2297 .h(px(92.))
2298 .child(
2299 Vector::new(
2300 VectorName::AiGrid,
2301 rems_from_px(400.),
2302 rems_from_px(92.),
2303 )
2304 .color(ui::Color::Custom(cx.theme().colors().text.alpha(0.32))),
2305 ),
2306 )
2307 // .child(
2308 // div()
2309 // .absolute()
2310 // .top_0()
2311 // .right(px(360.))
2312 // .size(px(401.))
2313 // .overflow_hidden()
2314 // .bg(cx.theme().colors().panel_background)
2315 // )
2316 .child(
2317 div()
2318 .absolute()
2319 .top_0()
2320 .right_0()
2321 .w(px(660.))
2322 .h(px(401.))
2323 .overflow_hidden()
2324 .bg(linear_gradient(
2325 75.,
2326 linear_color_stop(
2327 cx.theme().colors().panel_background.alpha(0.01),
2328 1.0,
2329 ),
2330 linear_color_stop(cx.theme().colors().panel_background, 0.45),
2331 )),
2332 )
2333 .child(content),
2334 ),
2335 )
2336 }
2337
2338 fn render_active_thread_or_empty_state(
2339 &self,
2340 window: &mut Window,
2341 cx: &mut Context<Self>,
2342 ) -> AnyElement {
2343 if self.thread.read(cx).is_empty() {
2344 return self
2345 .render_thread_empty_state(window, cx)
2346 .into_any_element();
2347 }
2348
2349 self.thread.clone().into_any_element()
2350 }
2351
2352 fn configuration_error(&self, cx: &App) -> Option<ConfigurationError> {
2353 let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
2354 return Some(ConfigurationError::NoProvider);
2355 };
2356
2357 if !model.provider.is_authenticated(cx) {
2358 return Some(ConfigurationError::ProviderNotAuthenticated);
2359 }
2360
2361 if model.provider.must_accept_terms(cx) {
2362 return Some(ConfigurationError::ProviderPendingTermsAcceptance(
2363 model.provider,
2364 ));
2365 }
2366
2367 None
2368 }
2369
2370 fn render_thread_empty_state(
2371 &self,
2372 window: &mut Window,
2373 cx: &mut Context<Self>,
2374 ) -> impl IntoElement {
2375 let recent_history = self
2376 .history_store
2377 .update(cx, |this, cx| this.recent_entries(6, cx));
2378
2379 let configuration_error = self.configuration_error(cx);
2380 let no_error = configuration_error.is_none();
2381 let focus_handle = self.focus_handle(cx);
2382
2383 v_flex()
2384 .size_full()
2385 .bg(cx.theme().colors().panel_background)
2386 .when(recent_history.is_empty(), |this| {
2387 let configuration_error_ref = &configuration_error;
2388 this.child(
2389 v_flex()
2390 .size_full()
2391 .max_w_80()
2392 .mx_auto()
2393 .justify_center()
2394 .items_center()
2395 .gap_1()
2396 .child(
2397 h_flex().child(
2398 Headline::new("Welcome to the Agent Panel")
2399 ),
2400 )
2401 .when(no_error, |parent| {
2402 parent
2403 .child(
2404 h_flex().child(
2405 Label::new("Ask and build anything.")
2406 .color(Color::Muted)
2407 .mb_2p5(),
2408 ),
2409 )
2410 .child(
2411 Button::new("new-thread", "Start New Thread")
2412 .icon(IconName::Plus)
2413 .icon_position(IconPosition::Start)
2414 .icon_size(IconSize::Small)
2415 .icon_color(Color::Muted)
2416 .full_width()
2417 .key_binding(KeyBinding::for_action_in(
2418 &NewThread::default(),
2419 &focus_handle,
2420 window,
2421 cx,
2422 ))
2423 .on_click(|_event, window, cx| {
2424 window.dispatch_action(NewThread::default().boxed_clone(), cx)
2425 }),
2426 )
2427 .child(
2428 Button::new("context", "Add Context")
2429 .icon(IconName::FileCode)
2430 .icon_position(IconPosition::Start)
2431 .icon_size(IconSize::Small)
2432 .icon_color(Color::Muted)
2433 .full_width()
2434 .key_binding(KeyBinding::for_action_in(
2435 &ToggleContextPicker,
2436 &focus_handle,
2437 window,
2438 cx,
2439 ))
2440 .on_click(|_event, window, cx| {
2441 window.dispatch_action(ToggleContextPicker.boxed_clone(), cx)
2442 }),
2443 )
2444 .child(
2445 Button::new("mode", "Switch Model")
2446 .icon(IconName::DatabaseZap)
2447 .icon_position(IconPosition::Start)
2448 .icon_size(IconSize::Small)
2449 .icon_color(Color::Muted)
2450 .full_width()
2451 .key_binding(KeyBinding::for_action_in(
2452 &ToggleModelSelector,
2453 &focus_handle,
2454 window,
2455 cx,
2456 ))
2457 .on_click(|_event, window, cx| {
2458 window.dispatch_action(ToggleModelSelector.boxed_clone(), cx)
2459 }),
2460 )
2461 .child(
2462 Button::new("settings", "View Settings")
2463 .icon(IconName::Settings)
2464 .icon_position(IconPosition::Start)
2465 .icon_size(IconSize::Small)
2466 .icon_color(Color::Muted)
2467 .full_width()
2468 .key_binding(KeyBinding::for_action_in(
2469 &OpenConfiguration,
2470 &focus_handle,
2471 window,
2472 cx,
2473 ))
2474 .on_click(|_event, window, cx| {
2475 window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
2476 }),
2477 )
2478 })
2479 .map(|parent| {
2480 match configuration_error_ref {
2481 Some(ConfigurationError::ProviderNotAuthenticated)
2482 | Some(ConfigurationError::NoProvider) => {
2483 parent
2484 .child(
2485 h_flex().child(
2486 Label::new("To start using the agent, configure at least one LLM provider.")
2487 .color(Color::Muted)
2488 .mb_2p5()
2489 )
2490 )
2491 .child(
2492 Button::new("settings", "Configure a Provider")
2493 .icon(IconName::Settings)
2494 .icon_position(IconPosition::Start)
2495 .icon_size(IconSize::Small)
2496 .icon_color(Color::Muted)
2497 .full_width()
2498 .key_binding(KeyBinding::for_action_in(
2499 &OpenConfiguration,
2500 &focus_handle,
2501 window,
2502 cx,
2503 ))
2504 .on_click(|_event, window, cx| {
2505 window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
2506 }),
2507 )
2508 }
2509 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
2510 parent.children(
2511 provider.render_accept_terms(
2512 LanguageModelProviderTosView::ThreadFreshStart,
2513 cx,
2514 ),
2515 )
2516 }
2517 None => parent,
2518 }
2519 })
2520 )
2521 })
2522 .when(!recent_history.is_empty(), |parent| {
2523 let focus_handle = focus_handle.clone();
2524 let configuration_error_ref = &configuration_error;
2525
2526 parent
2527 .overflow_hidden()
2528 .p_1p5()
2529 .justify_end()
2530 .gap_1()
2531 .child(
2532 h_flex()
2533 .pl_1p5()
2534 .pb_1()
2535 .w_full()
2536 .justify_between()
2537 .border_b_1()
2538 .border_color(cx.theme().colors().border_variant)
2539 .child(
2540 Label::new("Recent")
2541 .size(LabelSize::Small)
2542 .color(Color::Muted),
2543 )
2544 .child(
2545 Button::new("view-history", "View All")
2546 .style(ButtonStyle::Subtle)
2547 .label_size(LabelSize::Small)
2548 .key_binding(
2549 KeyBinding::for_action_in(
2550 &OpenHistory,
2551 &self.focus_handle(cx),
2552 window,
2553 cx,
2554 ).map(|kb| kb.size(rems_from_px(12.))),
2555 )
2556 .on_click(move |_event, window, cx| {
2557 window.dispatch_action(OpenHistory.boxed_clone(), cx);
2558 }),
2559 ),
2560 )
2561 .child(
2562 v_flex()
2563 .gap_1()
2564 .children(
2565 recent_history.into_iter().enumerate().map(|(index, entry)| {
2566 // TODO: Add keyboard navigation.
2567 let is_hovered = self.hovered_recent_history_item == Some(index);
2568 HistoryEntryElement::new(entry.clone(), cx.entity().downgrade())
2569 .hovered(is_hovered)
2570 .on_hover(cx.listener(move |this, is_hovered, _window, cx| {
2571 if *is_hovered {
2572 this.hovered_recent_history_item = Some(index);
2573 } else if this.hovered_recent_history_item == Some(index) {
2574 this.hovered_recent_history_item = None;
2575 }
2576 cx.notify();
2577 }))
2578 .into_any_element()
2579 }),
2580 )
2581 )
2582 .map(|parent| {
2583 match configuration_error_ref {
2584 Some(ConfigurationError::ProviderNotAuthenticated)
2585 | Some(ConfigurationError::NoProvider) => {
2586 parent
2587 .child(
2588 Banner::new()
2589 .severity(ui::Severity::Warning)
2590 .child(
2591 Label::new(
2592 "Configure at least one LLM provider to start using the panel.",
2593 )
2594 .size(LabelSize::Small),
2595 )
2596 .action_slot(
2597 Button::new("settings", "Configure Provider")
2598 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
2599 .label_size(LabelSize::Small)
2600 .key_binding(
2601 KeyBinding::for_action_in(
2602 &OpenConfiguration,
2603 &focus_handle,
2604 window,
2605 cx,
2606 )
2607 .map(|kb| kb.size(rems_from_px(12.))),
2608 )
2609 .on_click(|_event, window, cx| {
2610 window.dispatch_action(
2611 OpenConfiguration.boxed_clone(),
2612 cx,
2613 )
2614 }),
2615 ),
2616 )
2617 }
2618 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
2619 parent
2620 .child(
2621 Banner::new()
2622 .severity(ui::Severity::Warning)
2623 .child(
2624 h_flex()
2625 .w_full()
2626 .children(
2627 provider.render_accept_terms(
2628 LanguageModelProviderTosView::ThreadtEmptyState,
2629 cx,
2630 ),
2631 ),
2632 ),
2633 )
2634 }
2635 None => parent,
2636 }
2637 })
2638 })
2639 }
2640
2641 fn render_tool_use_limit_reached(
2642 &self,
2643 window: &mut Window,
2644 cx: &mut Context<Self>,
2645 ) -> Option<AnyElement> {
2646 let tool_use_limit_reached = self
2647 .thread
2648 .read(cx)
2649 .thread()
2650 .read(cx)
2651 .tool_use_limit_reached();
2652 if !tool_use_limit_reached {
2653 return None;
2654 }
2655
2656 let model = self
2657 .thread
2658 .read(cx)
2659 .thread()
2660 .read(cx)
2661 .configured_model()?
2662 .model;
2663
2664 let focus_handle = self.focus_handle(cx);
2665
2666 let banner = Banner::new()
2667 .severity(ui::Severity::Info)
2668 .child(Label::new("Consecutive tool use limit reached.").size(LabelSize::Small))
2669 .action_slot(
2670 h_flex()
2671 .gap_1()
2672 .child(
2673 Button::new("continue-conversation", "Continue")
2674 .layer(ElevationIndex::ModalSurface)
2675 .label_size(LabelSize::Small)
2676 .key_binding(
2677 KeyBinding::for_action_in(
2678 &ContinueThread,
2679 &focus_handle,
2680 window,
2681 cx,
2682 )
2683 .map(|kb| kb.size(rems_from_px(10.))),
2684 )
2685 .on_click(cx.listener(|this, _, window, cx| {
2686 this.continue_conversation(window, cx);
2687 })),
2688 )
2689 .when(model.supports_max_mode(), |this| {
2690 this.child(
2691 Button::new("continue-burn-mode", "Continue with Burn Mode")
2692 .style(ButtonStyle::Filled)
2693 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
2694 .layer(ElevationIndex::ModalSurface)
2695 .label_size(LabelSize::Small)
2696 .key_binding(
2697 KeyBinding::for_action_in(
2698 &ContinueWithBurnMode,
2699 &focus_handle,
2700 window,
2701 cx,
2702 )
2703 .map(|kb| kb.size(rems_from_px(10.))),
2704 )
2705 .tooltip(Tooltip::text("Enable Burn Mode for unlimited tool use."))
2706 .on_click(cx.listener(|this, _, window, cx| {
2707 this.thread.update(cx, |active_thread, cx| {
2708 active_thread.thread().update(cx, |thread, _cx| {
2709 thread.set_completion_mode(CompletionMode::Burn);
2710 });
2711 });
2712 this.continue_conversation(window, cx);
2713 })),
2714 )
2715 }),
2716 );
2717
2718 Some(div().px_2().pb_2().child(banner).into_any_element())
2719 }
2720
2721 fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
2722 let last_error = self.thread.read(cx).last_error()?;
2723
2724 Some(
2725 div()
2726 .absolute()
2727 .right_3()
2728 .bottom_12()
2729 .max_w_96()
2730 .py_2()
2731 .px_3()
2732 .elevation_2(cx)
2733 .occlude()
2734 .child(match last_error {
2735 ThreadError::PaymentRequired => self.render_payment_required_error(cx),
2736 ThreadError::ModelRequestLimitReached { plan } => {
2737 self.render_model_request_limit_reached_error(plan, cx)
2738 }
2739 ThreadError::Message { header, message } => {
2740 self.render_error_message(header, message, cx)
2741 }
2742 })
2743 .into_any(),
2744 )
2745 }
2746
2747 fn render_payment_required_error(&self, cx: &mut Context<Self>) -> AnyElement {
2748 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.";
2749
2750 v_flex()
2751 .gap_0p5()
2752 .child(
2753 h_flex()
2754 .gap_1p5()
2755 .items_center()
2756 .child(Icon::new(IconName::XCircle).color(Color::Error))
2757 .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
2758 )
2759 .child(
2760 div()
2761 .id("error-message")
2762 .max_h_24()
2763 .overflow_y_scroll()
2764 .child(Label::new(ERROR_MESSAGE)),
2765 )
2766 .child(
2767 h_flex()
2768 .justify_end()
2769 .mt_1()
2770 .gap_1()
2771 .child(self.create_copy_button(ERROR_MESSAGE))
2772 .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
2773 |this, _, _, cx| {
2774 this.thread.update(cx, |this, _cx| {
2775 this.clear_last_error();
2776 });
2777
2778 cx.open_url(&zed_urls::account_url(cx));
2779 cx.notify();
2780 },
2781 )))
2782 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2783 |this, _, _, cx| {
2784 this.thread.update(cx, |this, _cx| {
2785 this.clear_last_error();
2786 });
2787
2788 cx.notify();
2789 },
2790 ))),
2791 )
2792 .into_any()
2793 }
2794
2795 fn render_model_request_limit_reached_error(
2796 &self,
2797 plan: Plan,
2798 cx: &mut Context<Self>,
2799 ) -> AnyElement {
2800 let error_message = match plan {
2801 Plan::ZedPro => {
2802 "Model request limit reached. Upgrade to usage-based billing for more requests."
2803 }
2804 Plan::ZedProTrial => {
2805 "Model request limit reached. Upgrade to Zed Pro for more requests."
2806 }
2807 Plan::Free => "Model request limit reached. Upgrade to Zed Pro for more requests.",
2808 };
2809 let call_to_action = match plan {
2810 Plan::ZedPro => "Upgrade to usage-based billing",
2811 Plan::ZedProTrial => "Upgrade to Zed Pro",
2812 Plan::Free => "Upgrade to Zed Pro",
2813 };
2814
2815 v_flex()
2816 .gap_0p5()
2817 .child(
2818 h_flex()
2819 .gap_1p5()
2820 .items_center()
2821 .child(Icon::new(IconName::XCircle).color(Color::Error))
2822 .child(Label::new("Model Request Limit Reached").weight(FontWeight::MEDIUM)),
2823 )
2824 .child(
2825 div()
2826 .id("error-message")
2827 .max_h_24()
2828 .overflow_y_scroll()
2829 .child(Label::new(error_message)),
2830 )
2831 .child(
2832 h_flex()
2833 .justify_end()
2834 .mt_1()
2835 .gap_1()
2836 .child(self.create_copy_button(error_message))
2837 .child(
2838 Button::new("subscribe", call_to_action).on_click(cx.listener(
2839 |this, _, _, cx| {
2840 this.thread.update(cx, |this, _cx| {
2841 this.clear_last_error();
2842 });
2843
2844 cx.open_url(&zed_urls::account_url(cx));
2845 cx.notify();
2846 },
2847 )),
2848 )
2849 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2850 |this, _, _, cx| {
2851 this.thread.update(cx, |this, _cx| {
2852 this.clear_last_error();
2853 });
2854
2855 cx.notify();
2856 },
2857 ))),
2858 )
2859 .into_any()
2860 }
2861
2862 fn render_error_message(
2863 &self,
2864 header: SharedString,
2865 message: SharedString,
2866 cx: &mut Context<Self>,
2867 ) -> AnyElement {
2868 let message_with_header = format!("{}\n{}", header, message);
2869 v_flex()
2870 .gap_0p5()
2871 .child(
2872 h_flex()
2873 .gap_1p5()
2874 .items_center()
2875 .child(Icon::new(IconName::XCircle).color(Color::Error))
2876 .child(Label::new(header).weight(FontWeight::MEDIUM)),
2877 )
2878 .child(
2879 div()
2880 .id("error-message")
2881 .max_h_32()
2882 .overflow_y_scroll()
2883 .child(Label::new(message.clone())),
2884 )
2885 .child(
2886 h_flex()
2887 .justify_end()
2888 .mt_1()
2889 .gap_1()
2890 .child(self.create_copy_button(message_with_header))
2891 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2892 |this, _, _, cx| {
2893 this.thread.update(cx, |this, _cx| {
2894 this.clear_last_error();
2895 });
2896
2897 cx.notify();
2898 },
2899 ))),
2900 )
2901 .into_any()
2902 }
2903
2904 fn render_prompt_editor(
2905 &self,
2906 context_editor: &Entity<ContextEditor>,
2907 buffer_search_bar: &Entity<BufferSearchBar>,
2908 window: &mut Window,
2909 cx: &mut Context<Self>,
2910 ) -> Div {
2911 let mut registrar = buffer_search::DivRegistrar::new(
2912 |this, _, _cx| match &this.active_view {
2913 ActiveView::TextThread {
2914 buffer_search_bar, ..
2915 } => Some(buffer_search_bar.clone()),
2916 _ => None,
2917 },
2918 cx,
2919 );
2920 BufferSearchBar::register(&mut registrar);
2921 registrar
2922 .into_div()
2923 .size_full()
2924 .relative()
2925 .map(|parent| {
2926 buffer_search_bar.update(cx, |buffer_search_bar, cx| {
2927 if buffer_search_bar.is_dismissed() {
2928 return parent;
2929 }
2930 parent.child(
2931 div()
2932 .p(DynamicSpacing::Base08.rems(cx))
2933 .border_b_1()
2934 .border_color(cx.theme().colors().border_variant)
2935 .bg(cx.theme().colors().editor_background)
2936 .child(buffer_search_bar.render(window, cx)),
2937 )
2938 })
2939 })
2940 .child(context_editor.clone())
2941 .child(self.render_drag_target(cx))
2942 }
2943
2944 fn render_drag_target(&self, cx: &Context<Self>) -> Div {
2945 let is_local = self.project.read(cx).is_local();
2946 div()
2947 .invisible()
2948 .absolute()
2949 .top_0()
2950 .right_0()
2951 .bottom_0()
2952 .left_0()
2953 .bg(cx.theme().colors().drop_target_background)
2954 .drag_over::<DraggedTab>(|this, _, _, _| this.visible())
2955 .drag_over::<DraggedSelection>(|this, _, _, _| this.visible())
2956 .when(is_local, |this| {
2957 this.drag_over::<ExternalPaths>(|this, _, _, _| this.visible())
2958 })
2959 .on_drop(cx.listener(move |this, tab: &DraggedTab, window, cx| {
2960 let item = tab.pane.read(cx).item_for_index(tab.ix);
2961 let project_paths = item
2962 .and_then(|item| item.project_path(cx))
2963 .into_iter()
2964 .collect::<Vec<_>>();
2965 this.handle_drop(project_paths, vec![], window, cx);
2966 }))
2967 .on_drop(
2968 cx.listener(move |this, selection: &DraggedSelection, window, cx| {
2969 let project_paths = selection
2970 .items()
2971 .filter_map(|item| this.project.read(cx).path_for_entry(item.entry_id, cx))
2972 .collect::<Vec<_>>();
2973 this.handle_drop(project_paths, vec![], window, cx);
2974 }),
2975 )
2976 .on_drop(cx.listener(move |this, paths: &ExternalPaths, window, cx| {
2977 let tasks = paths
2978 .paths()
2979 .into_iter()
2980 .map(|path| {
2981 Workspace::project_path_for_path(this.project.clone(), &path, false, cx)
2982 })
2983 .collect::<Vec<_>>();
2984 cx.spawn_in(window, async move |this, cx| {
2985 let mut paths = vec![];
2986 let mut added_worktrees = vec![];
2987 let opened_paths = futures::future::join_all(tasks).await;
2988 for entry in opened_paths {
2989 if let Some((worktree, project_path)) = entry.log_err() {
2990 added_worktrees.push(worktree);
2991 paths.push(project_path);
2992 }
2993 }
2994 this.update_in(cx, |this, window, cx| {
2995 this.handle_drop(paths, added_worktrees, window, cx);
2996 })
2997 .ok();
2998 })
2999 .detach();
3000 }))
3001 }
3002
3003 fn handle_drop(
3004 &mut self,
3005 paths: Vec<ProjectPath>,
3006 added_worktrees: Vec<Entity<Worktree>>,
3007 window: &mut Window,
3008 cx: &mut Context<Self>,
3009 ) {
3010 match &self.active_view {
3011 ActiveView::Thread { .. } => {
3012 let context_store = self.thread.read(cx).context_store().clone();
3013 context_store.update(cx, move |context_store, cx| {
3014 let mut tasks = Vec::new();
3015 for project_path in &paths {
3016 tasks.push(context_store.add_file_from_path(
3017 project_path.clone(),
3018 false,
3019 cx,
3020 ));
3021 }
3022 cx.background_spawn(async move {
3023 futures::future::join_all(tasks).await;
3024 // Need to hold onto the worktrees until they have already been used when
3025 // opening the buffers.
3026 drop(added_worktrees);
3027 })
3028 .detach();
3029 });
3030 }
3031 ActiveView::TextThread { context_editor, .. } => {
3032 context_editor.update(cx, |context_editor, cx| {
3033 ContextEditor::insert_dragged_files(
3034 context_editor,
3035 paths,
3036 added_worktrees,
3037 window,
3038 cx,
3039 );
3040 });
3041 }
3042 ActiveView::History | ActiveView::Configuration => {}
3043 }
3044 }
3045
3046 fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
3047 let message = message.into();
3048 IconButton::new("copy", IconName::Copy)
3049 .on_click(move |_, _, cx| {
3050 cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
3051 })
3052 .tooltip(Tooltip::text("Copy Error Message"))
3053 }
3054
3055 fn key_context(&self) -> KeyContext {
3056 let mut key_context = KeyContext::new_with_defaults();
3057 key_context.add("AgentPanel");
3058 if matches!(self.active_view, ActiveView::TextThread { .. }) {
3059 key_context.add("prompt_editor");
3060 }
3061 key_context
3062 }
3063}
3064
3065impl Render for AgentPanel {
3066 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3067 // WARNING: Changes to this element hierarchy can have
3068 // non-obvious implications to the layout of children.
3069 //
3070 // If you need to change it, please confirm:
3071 // - The message editor expands (cmd-option-esc) correctly
3072 // - When expanded, the buttons at the bottom of the panel are displayed correctly
3073 // - Font size works as expected and can be changed with cmd-+/cmd-
3074 // - Scrolling in all views works as expected
3075 // - Files can be dropped into the panel
3076 let content = v_flex()
3077 .key_context(self.key_context())
3078 .justify_between()
3079 .size_full()
3080 .on_action(cx.listener(Self::cancel))
3081 .on_action(cx.listener(|this, action: &NewThread, window, cx| {
3082 this.new_thread(action, window, cx);
3083 }))
3084 .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
3085 this.open_history(window, cx);
3086 }))
3087 .on_action(cx.listener(|this, _: &OpenConfiguration, window, cx| {
3088 this.open_configuration(window, cx);
3089 }))
3090 .on_action(cx.listener(Self::open_active_thread_as_markdown))
3091 .on_action(cx.listener(Self::deploy_rules_library))
3092 .on_action(cx.listener(Self::open_agent_diff))
3093 .on_action(cx.listener(Self::go_back))
3094 .on_action(cx.listener(Self::toggle_navigation_menu))
3095 .on_action(cx.listener(Self::toggle_options_menu))
3096 .on_action(cx.listener(Self::increase_font_size))
3097 .on_action(cx.listener(Self::decrease_font_size))
3098 .on_action(cx.listener(Self::reset_font_size))
3099 .on_action(cx.listener(Self::toggle_zoom))
3100 .on_action(cx.listener(|this, _: &ContinueThread, window, cx| {
3101 this.continue_conversation(window, cx);
3102 }))
3103 .on_action(cx.listener(|this, _: &ContinueWithBurnMode, window, cx| {
3104 this.thread.update(cx, |active_thread, cx| {
3105 active_thread.thread().update(cx, |thread, _cx| {
3106 thread.set_completion_mode(CompletionMode::Burn);
3107 });
3108 });
3109 this.continue_conversation(window, cx);
3110 }))
3111 .on_action(cx.listener(Self::toggle_burn_mode))
3112 .child(self.render_toolbar(window, cx))
3113 .children(self.render_upsell(window, cx))
3114 .children(self.render_trial_end_upsell(window, cx))
3115 .map(|parent| match &self.active_view {
3116 ActiveView::Thread { .. } => parent
3117 .relative()
3118 .child(self.render_active_thread_or_empty_state(window, cx))
3119 .children(self.render_tool_use_limit_reached(window, cx))
3120 .child(h_flex().child(self.message_editor.clone()))
3121 .children(self.render_last_error(cx))
3122 .child(self.render_drag_target(cx)),
3123 ActiveView::History => parent.child(self.history.clone()),
3124 ActiveView::TextThread {
3125 context_editor,
3126 buffer_search_bar,
3127 ..
3128 } => parent.child(self.render_prompt_editor(
3129 context_editor,
3130 buffer_search_bar,
3131 window,
3132 cx,
3133 )),
3134 ActiveView::Configuration => parent.children(self.configuration.clone()),
3135 });
3136
3137 match self.active_view.which_font_size_used() {
3138 WhichFontSize::AgentFont => {
3139 WithRemSize::new(ThemeSettings::get_global(cx).agent_font_size(cx))
3140 .size_full()
3141 .child(content)
3142 .into_any()
3143 }
3144 _ => content.into_any(),
3145 }
3146 }
3147}
3148
3149struct PromptLibraryInlineAssist {
3150 workspace: WeakEntity<Workspace>,
3151}
3152
3153impl PromptLibraryInlineAssist {
3154 pub fn new(workspace: WeakEntity<Workspace>) -> Self {
3155 Self { workspace }
3156 }
3157}
3158
3159impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
3160 fn assist(
3161 &self,
3162 prompt_editor: &Entity<Editor>,
3163 initial_prompt: Option<String>,
3164 window: &mut Window,
3165 cx: &mut Context<RulesLibrary>,
3166 ) {
3167 InlineAssistant::update_global(cx, |assistant, cx| {
3168 let Some(project) = self
3169 .workspace
3170 .upgrade()
3171 .map(|workspace| workspace.read(cx).project().downgrade())
3172 else {
3173 return;
3174 };
3175 let prompt_store = None;
3176 let thread_store = None;
3177 let text_thread_store = None;
3178 let context_store = cx.new(|_| ContextStore::new(project.clone(), None));
3179 assistant.assist(
3180 &prompt_editor,
3181 self.workspace.clone(),
3182 context_store,
3183 project,
3184 prompt_store,
3185 thread_store,
3186 text_thread_store,
3187 initial_prompt,
3188 window,
3189 cx,
3190 )
3191 })
3192 }
3193
3194 fn focus_agent_panel(
3195 &self,
3196 workspace: &mut Workspace,
3197 window: &mut Window,
3198 cx: &mut Context<Workspace>,
3199 ) -> bool {
3200 workspace.focus_panel::<AgentPanel>(window, cx).is_some()
3201 }
3202}
3203
3204pub struct ConcreteAssistantPanelDelegate;
3205
3206impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
3207 fn active_context_editor(
3208 &self,
3209 workspace: &mut Workspace,
3210 _window: &mut Window,
3211 cx: &mut Context<Workspace>,
3212 ) -> Option<Entity<ContextEditor>> {
3213 let panel = workspace.panel::<AgentPanel>(cx)?;
3214 panel.read(cx).active_context_editor()
3215 }
3216
3217 fn open_saved_context(
3218 &self,
3219 workspace: &mut Workspace,
3220 path: Arc<Path>,
3221 window: &mut Window,
3222 cx: &mut Context<Workspace>,
3223 ) -> Task<Result<()>> {
3224 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3225 return Task::ready(Err(anyhow!("Agent panel not found")));
3226 };
3227
3228 panel.update(cx, |panel, cx| {
3229 panel.open_saved_prompt_editor(path, window, cx)
3230 })
3231 }
3232
3233 fn open_remote_context(
3234 &self,
3235 _workspace: &mut Workspace,
3236 _context_id: assistant_context_editor::ContextId,
3237 _window: &mut Window,
3238 _cx: &mut Context<Workspace>,
3239 ) -> Task<Result<Entity<ContextEditor>>> {
3240 Task::ready(Err(anyhow!("opening remote context not implemented")))
3241 }
3242
3243 fn quote_selection(
3244 &self,
3245 workspace: &mut Workspace,
3246 selection_ranges: Vec<Range<Anchor>>,
3247 buffer: Entity<MultiBuffer>,
3248 window: &mut Window,
3249 cx: &mut Context<Workspace>,
3250 ) {
3251 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3252 return;
3253 };
3254
3255 if !panel.focus_handle(cx).contains_focused(window, cx) {
3256 workspace.toggle_panel_focus::<AgentPanel>(window, cx);
3257 }
3258
3259 panel.update(cx, |_, cx| {
3260 // Wait to create a new context until the workspace is no longer
3261 // being updated.
3262 cx.defer_in(window, move |panel, window, cx| {
3263 if panel.has_active_thread() {
3264 panel.message_editor.update(cx, |message_editor, cx| {
3265 message_editor.context_store().update(cx, |store, cx| {
3266 let buffer = buffer.read(cx);
3267 let selection_ranges = selection_ranges
3268 .into_iter()
3269 .flat_map(|range| {
3270 let (start_buffer, start) =
3271 buffer.text_anchor_for_position(range.start, cx)?;
3272 let (end_buffer, end) =
3273 buffer.text_anchor_for_position(range.end, cx)?;
3274 if start_buffer != end_buffer {
3275 return None;
3276 }
3277 Some((start_buffer, start..end))
3278 })
3279 .collect::<Vec<_>>();
3280
3281 for (buffer, range) in selection_ranges {
3282 store.add_selection(buffer, range, cx);
3283 }
3284 })
3285 })
3286 } else if let Some(context_editor) = panel.active_context_editor() {
3287 let snapshot = buffer.read(cx).snapshot(cx);
3288 let selection_ranges = selection_ranges
3289 .into_iter()
3290 .map(|range| range.to_point(&snapshot))
3291 .collect::<Vec<_>>();
3292
3293 context_editor.update(cx, |context_editor, cx| {
3294 context_editor.quote_ranges(selection_ranges, snapshot, window, cx)
3295 });
3296 }
3297 });
3298 });
3299 }
3300}
3301
3302struct Upsell;
3303
3304impl Dismissable for Upsell {
3305 const KEY: &'static str = "dismissed-trial-upsell";
3306}
3307
3308struct TrialEndUpsell;
3309
3310impl Dismissable for TrialEndUpsell {
3311 const KEY: &'static str = "dismissed-trial-end-upsell";
3312}