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