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