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