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