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