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 let (active_thread, message_editor) = match &self.active_view {
1979 ActiveView::Thread {
1980 thread,
1981 message_editor,
1982 ..
1983 } => (thread.read(cx), message_editor.read(cx)),
1984 ActiveView::AcpThread { .. } => {
1985 return None;
1986 }
1987 ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
1988 return None;
1989 }
1990 };
1991
1992 let editor_empty = message_editor.is_editor_fully_empty(cx);
1993
1994 if active_thread.is_empty() && editor_empty {
1995 return None;
1996 }
1997
1998 let thread = active_thread.thread().read(cx);
1999 let is_generating = thread.is_generating();
2000 let conversation_token_usage = thread.total_token_usage()?;
2001
2002 let (total_token_usage, is_estimating) =
2003 if let Some((editing_message_id, unsent_tokens)) = active_thread.editing_message_id() {
2004 let combined = thread
2005 .token_usage_up_to_message(editing_message_id)
2006 .add(unsent_tokens);
2007
2008 (combined, unsent_tokens > 0)
2009 } else {
2010 let unsent_tokens = message_editor.last_estimated_token_count().unwrap_or(0);
2011 let combined = conversation_token_usage.add(unsent_tokens);
2012
2013 (combined, unsent_tokens > 0)
2014 };
2015
2016 let is_waiting_to_update_token_count = message_editor.is_waiting_to_update_token_count();
2017
2018 match &self.active_view {
2019 ActiveView::Thread { .. } => {
2020 if total_token_usage.total == 0 {
2021 return None;
2022 }
2023
2024 let token_color = match total_token_usage.ratio() {
2025 TokenUsageRatio::Normal if is_estimating => Color::Default,
2026 TokenUsageRatio::Normal => Color::Muted,
2027 TokenUsageRatio::Warning => Color::Warning,
2028 TokenUsageRatio::Exceeded => Color::Error,
2029 };
2030
2031 let token_count = h_flex()
2032 .id("token-count")
2033 .flex_shrink_0()
2034 .gap_0p5()
2035 .when(!is_generating && is_estimating, |parent| {
2036 parent
2037 .child(
2038 h_flex()
2039 .mr_1()
2040 .size_2p5()
2041 .justify_center()
2042 .rounded_full()
2043 .bg(cx.theme().colors().text.opacity(0.1))
2044 .child(
2045 div().size_1().rounded_full().bg(cx.theme().colors().text),
2046 ),
2047 )
2048 .tooltip(move |window, cx| {
2049 Tooltip::with_meta(
2050 "Estimated New Token Count",
2051 None,
2052 format!(
2053 "Current Conversation Tokens: {}",
2054 humanize_token_count(conversation_token_usage.total)
2055 ),
2056 window,
2057 cx,
2058 )
2059 })
2060 })
2061 .child(
2062 Label::new(humanize_token_count(total_token_usage.total))
2063 .size(LabelSize::Small)
2064 .color(token_color)
2065 .map(|label| {
2066 if is_generating || is_waiting_to_update_token_count {
2067 label
2068 .with_animation(
2069 "used-tokens-label",
2070 Animation::new(Duration::from_secs(2))
2071 .repeat()
2072 .with_easing(pulsating_between(0.6, 1.)),
2073 |label, delta| label.alpha(delta),
2074 )
2075 .into_any()
2076 } else {
2077 label.into_any_element()
2078 }
2079 }),
2080 )
2081 .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
2082 .child(
2083 Label::new(humanize_token_count(total_token_usage.max))
2084 .size(LabelSize::Small)
2085 .color(Color::Muted),
2086 )
2087 .into_any();
2088
2089 Some(token_count)
2090 }
2091 ActiveView::TextThread { context_editor, .. } => {
2092 let element = render_remaining_tokens(context_editor, cx)?;
2093
2094 Some(element.into_any_element())
2095 }
2096 _ => None,
2097 }
2098 }
2099
2100 fn should_render_trial_end_upsell(&self, cx: &mut Context<Self>) -> bool {
2101 if TrialEndUpsell::dismissed() {
2102 return false;
2103 }
2104
2105 let plan = self.user_store.read(cx).current_plan();
2106 let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
2107
2108 matches!(plan, Some(Plan::Free)) && has_previous_trial
2109 }
2110
2111 fn should_render_upsell(&self, cx: &mut Context<Self>) -> bool {
2112 match &self.active_view {
2113 ActiveView::Thread { thread, .. } => {
2114 let is_using_zed_provider = thread
2115 .read(cx)
2116 .thread()
2117 .read(cx)
2118 .configured_model()
2119 .map_or(false, |model| model.provider.id() == ZED_CLOUD_PROVIDER_ID);
2120
2121 if !is_using_zed_provider {
2122 return false;
2123 }
2124 }
2125 ActiveView::AcpThread { .. } => {
2126 return false;
2127 }
2128 ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
2129 return false;
2130 }
2131 };
2132
2133 if self.hide_upsell || Upsell::dismissed() {
2134 return false;
2135 }
2136
2137 let plan = self.user_store.read(cx).current_plan();
2138 if matches!(plan, Some(Plan::ZedPro | Plan::ZedProTrial)) {
2139 return false;
2140 }
2141
2142 let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
2143 if has_previous_trial {
2144 return false;
2145 }
2146
2147 true
2148 }
2149
2150 fn render_upsell(
2151 &self,
2152 _window: &mut Window,
2153 cx: &mut Context<Self>,
2154 ) -> Option<impl IntoElement> {
2155 if !self.should_render_upsell(cx) {
2156 return None;
2157 }
2158
2159 if self.user_store.read(cx).account_too_young() {
2160 Some(self.render_young_account_upsell(cx).into_any_element())
2161 } else {
2162 Some(self.render_trial_upsell(cx).into_any_element())
2163 }
2164 }
2165
2166 fn render_young_account_upsell(&self, cx: &mut Context<Self>) -> impl IntoElement {
2167 let checkbox = CheckboxWithLabel::new(
2168 "dont-show-again",
2169 Label::new("Don't show again").color(Color::Muted),
2170 ToggleState::Unselected,
2171 move |toggle_state, _window, cx| {
2172 let toggle_state_bool = toggle_state.selected();
2173
2174 Upsell::set_dismissed(toggle_state_bool, cx);
2175 },
2176 );
2177
2178 let contents = div()
2179 .size_full()
2180 .gap_2()
2181 .flex()
2182 .flex_col()
2183 .child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small))
2184 .child(
2185 Label::new("Your GitHub account was created less than 30 days ago, so we can't offer you a free trial.")
2186 .size(LabelSize::Small),
2187 )
2188 .child(
2189 Label::new(
2190 "Use your own API keys, upgrade to Zed Pro or send an email to billing-support@zed.dev.",
2191 )
2192 .color(Color::Muted),
2193 )
2194 .child(
2195 h_flex()
2196 .w_full()
2197 .px_neg_1()
2198 .justify_between()
2199 .items_center()
2200 .child(h_flex().items_center().gap_1().child(checkbox))
2201 .child(
2202 h_flex()
2203 .gap_2()
2204 .child(
2205 Button::new("dismiss-button", "Not Now")
2206 .style(ButtonStyle::Transparent)
2207 .color(Color::Muted)
2208 .on_click({
2209 let agent_panel = cx.entity();
2210 move |_, _, cx| {
2211 agent_panel.update(cx, |this, cx| {
2212 this.hide_upsell = true;
2213 cx.notify();
2214 });
2215 }
2216 }),
2217 )
2218 .child(
2219 Button::new("cta-button", "Upgrade to Zed Pro")
2220 .style(ButtonStyle::Transparent)
2221 .on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))),
2222 ),
2223 ),
2224 );
2225
2226 self.render_upsell_container(cx, contents)
2227 }
2228
2229 fn render_trial_upsell(&self, cx: &mut Context<Self>) -> impl IntoElement {
2230 let checkbox = CheckboxWithLabel::new(
2231 "dont-show-again",
2232 Label::new("Don't show again").color(Color::Muted),
2233 ToggleState::Unselected,
2234 move |toggle_state, _window, cx| {
2235 let toggle_state_bool = toggle_state.selected();
2236
2237 Upsell::set_dismissed(toggle_state_bool, cx);
2238 },
2239 );
2240
2241 let contents = div()
2242 .size_full()
2243 .gap_2()
2244 .flex()
2245 .flex_col()
2246 .child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small))
2247 .child(
2248 Label::new("Try Zed Pro for free for 14 days - no credit card required.")
2249 .size(LabelSize::Small),
2250 )
2251 .child(
2252 Label::new(
2253 "Use your own API keys or enable usage-based billing once you hit the cap.",
2254 )
2255 .color(Color::Muted),
2256 )
2257 .child(
2258 h_flex()
2259 .w_full()
2260 .px_neg_1()
2261 .justify_between()
2262 .items_center()
2263 .child(h_flex().items_center().gap_1().child(checkbox))
2264 .child(
2265 h_flex()
2266 .gap_2()
2267 .child(
2268 Button::new("dismiss-button", "Not Now")
2269 .style(ButtonStyle::Transparent)
2270 .color(Color::Muted)
2271 .on_click({
2272 let agent_panel = cx.entity();
2273 move |_, _, cx| {
2274 agent_panel.update(cx, |this, cx| {
2275 this.hide_upsell = true;
2276 cx.notify();
2277 });
2278 }
2279 }),
2280 )
2281 .child(
2282 Button::new("cta-button", "Start Trial")
2283 .style(ButtonStyle::Transparent)
2284 .on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))),
2285 ),
2286 ),
2287 );
2288
2289 self.render_upsell_container(cx, contents)
2290 }
2291
2292 fn render_trial_end_upsell(
2293 &self,
2294 _window: &mut Window,
2295 cx: &mut Context<Self>,
2296 ) -> Option<impl IntoElement> {
2297 if !self.should_render_trial_end_upsell(cx) {
2298 return None;
2299 }
2300
2301 Some(
2302 self.render_upsell_container(
2303 cx,
2304 div()
2305 .size_full()
2306 .gap_2()
2307 .flex()
2308 .flex_col()
2309 .child(
2310 Headline::new("Your Zed Pro trial has expired.").size(HeadlineSize::Small),
2311 )
2312 .child(
2313 Label::new("You've been automatically reset to the free plan.")
2314 .size(LabelSize::Small),
2315 )
2316 .child(
2317 h_flex()
2318 .w_full()
2319 .px_neg_1()
2320 .justify_between()
2321 .items_center()
2322 .child(div())
2323 .child(
2324 h_flex()
2325 .gap_2()
2326 .child(
2327 Button::new("dismiss-button", "Stay on Free")
2328 .style(ButtonStyle::Transparent)
2329 .color(Color::Muted)
2330 .on_click({
2331 let agent_panel = cx.entity();
2332 move |_, _, cx| {
2333 agent_panel.update(cx, |_this, cx| {
2334 TrialEndUpsell::set_dismissed(true, cx);
2335 cx.notify();
2336 });
2337 }
2338 }),
2339 )
2340 .child(
2341 Button::new("cta-button", "Upgrade to Zed Pro")
2342 .style(ButtonStyle::Transparent)
2343 .on_click(|_, _, cx| {
2344 cx.open_url(&zed_urls::account_url(cx))
2345 }),
2346 ),
2347 ),
2348 ),
2349 ),
2350 )
2351 }
2352
2353 fn render_upsell_container(&self, cx: &mut Context<Self>, content: Div) -> Div {
2354 div().p_2().child(
2355 v_flex()
2356 .w_full()
2357 .elevation_2(cx)
2358 .rounded(px(8.))
2359 .bg(cx.theme().colors().background.alpha(0.5))
2360 .p(px(3.))
2361 .child(
2362 div()
2363 .gap_2()
2364 .flex()
2365 .flex_col()
2366 .size_full()
2367 .border_1()
2368 .rounded(px(5.))
2369 .border_color(cx.theme().colors().text.alpha(0.1))
2370 .overflow_hidden()
2371 .relative()
2372 .bg(cx.theme().colors().panel_background)
2373 .px_4()
2374 .py_3()
2375 .child(
2376 div()
2377 .absolute()
2378 .top_0()
2379 .right(px(-1.0))
2380 .w(px(441.))
2381 .h(px(167.))
2382 .child(
2383 Vector::new(
2384 VectorName::Grid,
2385 rems_from_px(441.),
2386 rems_from_px(167.),
2387 )
2388 .color(ui::Color::Custom(cx.theme().colors().text.alpha(0.1))),
2389 ),
2390 )
2391 .child(
2392 div()
2393 .absolute()
2394 .top(px(-8.0))
2395 .right_0()
2396 .w(px(400.))
2397 .h(px(92.))
2398 .child(
2399 Vector::new(
2400 VectorName::AiGrid,
2401 rems_from_px(400.),
2402 rems_from_px(92.),
2403 )
2404 .color(ui::Color::Custom(cx.theme().colors().text.alpha(0.32))),
2405 ),
2406 )
2407 // .child(
2408 // div()
2409 // .absolute()
2410 // .top_0()
2411 // .right(px(360.))
2412 // .size(px(401.))
2413 // .overflow_hidden()
2414 // .bg(cx.theme().colors().panel_background)
2415 // )
2416 .child(
2417 div()
2418 .absolute()
2419 .top_0()
2420 .right_0()
2421 .w(px(660.))
2422 .h(px(401.))
2423 .overflow_hidden()
2424 .bg(linear_gradient(
2425 75.,
2426 linear_color_stop(
2427 cx.theme().colors().panel_background.alpha(0.01),
2428 1.0,
2429 ),
2430 linear_color_stop(cx.theme().colors().panel_background, 0.45),
2431 )),
2432 )
2433 .child(content),
2434 ),
2435 )
2436 }
2437
2438 fn render_thread_empty_state(
2439 &self,
2440 window: &mut Window,
2441 cx: &mut Context<Self>,
2442 ) -> impl IntoElement {
2443 let recent_history = self
2444 .history_store
2445 .update(cx, |this, cx| this.recent_entries(6, cx));
2446
2447 let model_registry = LanguageModelRegistry::read_global(cx);
2448 let configuration_error =
2449 model_registry.configuration_error(model_registry.default_model(), cx);
2450 let no_error = configuration_error.is_none();
2451 let focus_handle = self.focus_handle(cx);
2452
2453 v_flex()
2454 .size_full()
2455 .bg(cx.theme().colors().panel_background)
2456 .when(recent_history.is_empty(), |this| {
2457 let configuration_error_ref = &configuration_error;
2458 this.child(
2459 v_flex()
2460 .size_full()
2461 .max_w_80()
2462 .mx_auto()
2463 .justify_center()
2464 .items_center()
2465 .gap_1()
2466 .child(h_flex().child(Headline::new("Welcome to the Agent Panel")))
2467 .when(no_error, |parent| {
2468 parent
2469 .child(
2470 h_flex().child(
2471 Label::new("Ask and build anything.")
2472 .color(Color::Muted)
2473 .mb_2p5(),
2474 ),
2475 )
2476 .child(
2477 Button::new("new-thread", "Start New Thread")
2478 .icon(IconName::Plus)
2479 .icon_position(IconPosition::Start)
2480 .icon_size(IconSize::Small)
2481 .icon_color(Color::Muted)
2482 .full_width()
2483 .key_binding(KeyBinding::for_action_in(
2484 &NewThread::default(),
2485 &focus_handle,
2486 window,
2487 cx,
2488 ))
2489 .on_click(|_event, window, cx| {
2490 window.dispatch_action(
2491 NewThread::default().boxed_clone(),
2492 cx,
2493 )
2494 }),
2495 )
2496 .child(
2497 Button::new("context", "Add Context")
2498 .icon(IconName::FileCode)
2499 .icon_position(IconPosition::Start)
2500 .icon_size(IconSize::Small)
2501 .icon_color(Color::Muted)
2502 .full_width()
2503 .key_binding(KeyBinding::for_action_in(
2504 &ToggleContextPicker,
2505 &focus_handle,
2506 window,
2507 cx,
2508 ))
2509 .on_click(|_event, window, cx| {
2510 window.dispatch_action(
2511 ToggleContextPicker.boxed_clone(),
2512 cx,
2513 )
2514 }),
2515 )
2516 .child(
2517 Button::new("mode", "Switch Model")
2518 .icon(IconName::DatabaseZap)
2519 .icon_position(IconPosition::Start)
2520 .icon_size(IconSize::Small)
2521 .icon_color(Color::Muted)
2522 .full_width()
2523 .key_binding(KeyBinding::for_action_in(
2524 &ToggleModelSelector,
2525 &focus_handle,
2526 window,
2527 cx,
2528 ))
2529 .on_click(|_event, window, cx| {
2530 window.dispatch_action(
2531 ToggleModelSelector.boxed_clone(),
2532 cx,
2533 )
2534 }),
2535 )
2536 .child(
2537 Button::new("settings", "View Settings")
2538 .icon(IconName::Settings)
2539 .icon_position(IconPosition::Start)
2540 .icon_size(IconSize::Small)
2541 .icon_color(Color::Muted)
2542 .full_width()
2543 .key_binding(KeyBinding::for_action_in(
2544 &OpenConfiguration,
2545 &focus_handle,
2546 window,
2547 cx,
2548 ))
2549 .on_click(|_event, window, cx| {
2550 window.dispatch_action(
2551 OpenConfiguration.boxed_clone(),
2552 cx,
2553 )
2554 }),
2555 )
2556 })
2557 .map(|parent| match configuration_error_ref {
2558 Some(
2559 err @ (ConfigurationError::ModelNotFound
2560 | ConfigurationError::ProviderNotAuthenticated(_)
2561 | ConfigurationError::NoProvider),
2562 ) => parent
2563 .child(h_flex().child(
2564 Label::new(err.to_string()).color(Color::Muted).mb_2p5(),
2565 ))
2566 .child(
2567 Button::new("settings", "Configure a Provider")
2568 .icon(IconName::Settings)
2569 .icon_position(IconPosition::Start)
2570 .icon_size(IconSize::Small)
2571 .icon_color(Color::Muted)
2572 .full_width()
2573 .key_binding(KeyBinding::for_action_in(
2574 &OpenConfiguration,
2575 &focus_handle,
2576 window,
2577 cx,
2578 ))
2579 .on_click(|_event, window, cx| {
2580 window.dispatch_action(
2581 OpenConfiguration.boxed_clone(),
2582 cx,
2583 )
2584 }),
2585 ),
2586 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
2587 parent.children(provider.render_accept_terms(
2588 LanguageModelProviderTosView::ThreadFreshStart,
2589 cx,
2590 ))
2591 }
2592 None => parent,
2593 }),
2594 )
2595 })
2596 .when(!recent_history.is_empty(), |parent| {
2597 let focus_handle = focus_handle.clone();
2598 let configuration_error_ref = &configuration_error;
2599
2600 parent
2601 .overflow_hidden()
2602 .p_1p5()
2603 .justify_end()
2604 .gap_1()
2605 .child(
2606 h_flex()
2607 .pl_1p5()
2608 .pb_1()
2609 .w_full()
2610 .justify_between()
2611 .border_b_1()
2612 .border_color(cx.theme().colors().border_variant)
2613 .child(
2614 Label::new("Recent")
2615 .size(LabelSize::Small)
2616 .color(Color::Muted),
2617 )
2618 .child(
2619 Button::new("view-history", "View All")
2620 .style(ButtonStyle::Subtle)
2621 .label_size(LabelSize::Small)
2622 .key_binding(
2623 KeyBinding::for_action_in(
2624 &OpenHistory,
2625 &self.focus_handle(cx),
2626 window,
2627 cx,
2628 )
2629 .map(|kb| kb.size(rems_from_px(12.))),
2630 )
2631 .on_click(move |_event, window, cx| {
2632 window.dispatch_action(OpenHistory.boxed_clone(), cx);
2633 }),
2634 ),
2635 )
2636 .child(
2637 v_flex()
2638 .gap_1()
2639 .children(recent_history.into_iter().enumerate().map(
2640 |(index, entry)| {
2641 // TODO: Add keyboard navigation.
2642 let is_hovered =
2643 self.hovered_recent_history_item == Some(index);
2644 HistoryEntryElement::new(entry.clone(), cx.entity().downgrade())
2645 .hovered(is_hovered)
2646 .on_hover(cx.listener(
2647 move |this, is_hovered, _window, cx| {
2648 if *is_hovered {
2649 this.hovered_recent_history_item = Some(index);
2650 } else if this.hovered_recent_history_item
2651 == Some(index)
2652 {
2653 this.hovered_recent_history_item = None;
2654 }
2655 cx.notify();
2656 },
2657 ))
2658 .into_any_element()
2659 },
2660 )),
2661 )
2662 .map(|parent| match configuration_error_ref {
2663 Some(
2664 err @ (ConfigurationError::ModelNotFound
2665 | ConfigurationError::ProviderNotAuthenticated(_)
2666 | ConfigurationError::NoProvider),
2667 ) => parent.child(
2668 Banner::new()
2669 .severity(ui::Severity::Warning)
2670 .child(Label::new(err.to_string()).size(LabelSize::Small))
2671 .action_slot(
2672 Button::new("settings", "Configure Provider")
2673 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
2674 .label_size(LabelSize::Small)
2675 .key_binding(
2676 KeyBinding::for_action_in(
2677 &OpenConfiguration,
2678 &focus_handle,
2679 window,
2680 cx,
2681 )
2682 .map(|kb| kb.size(rems_from_px(12.))),
2683 )
2684 .on_click(|_event, window, cx| {
2685 window.dispatch_action(
2686 OpenConfiguration.boxed_clone(),
2687 cx,
2688 )
2689 }),
2690 ),
2691 ),
2692 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
2693 parent.child(Banner::new().severity(ui::Severity::Warning).child(
2694 h_flex().w_full().children(provider.render_accept_terms(
2695 LanguageModelProviderTosView::ThreadEmptyState,
2696 cx,
2697 )),
2698 ))
2699 }
2700 None => parent,
2701 })
2702 })
2703 }
2704
2705 fn render_tool_use_limit_reached(
2706 &self,
2707 window: &mut Window,
2708 cx: &mut Context<Self>,
2709 ) -> Option<AnyElement> {
2710 let active_thread = match &self.active_view {
2711 ActiveView::Thread { thread, .. } => thread,
2712 ActiveView::AcpThread { .. } => {
2713 return None;
2714 }
2715 ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
2716 return None;
2717 }
2718 };
2719
2720 let thread = active_thread.read(cx).thread().read(cx);
2721
2722 let tool_use_limit_reached = thread.tool_use_limit_reached();
2723 if !tool_use_limit_reached {
2724 return None;
2725 }
2726
2727 let model = thread.configured_model()?.model;
2728
2729 let focus_handle = self.focus_handle(cx);
2730
2731 let banner = Banner::new()
2732 .severity(ui::Severity::Info)
2733 .child(Label::new("Consecutive tool use limit reached.").size(LabelSize::Small))
2734 .action_slot(
2735 h_flex()
2736 .gap_1()
2737 .child(
2738 Button::new("continue-conversation", "Continue")
2739 .layer(ElevationIndex::ModalSurface)
2740 .label_size(LabelSize::Small)
2741 .key_binding(
2742 KeyBinding::for_action_in(
2743 &ContinueThread,
2744 &focus_handle,
2745 window,
2746 cx,
2747 )
2748 .map(|kb| kb.size(rems_from_px(10.))),
2749 )
2750 .on_click(cx.listener(|this, _, window, cx| {
2751 this.continue_conversation(window, cx);
2752 })),
2753 )
2754 .when(model.supports_burn_mode(), |this| {
2755 this.child(
2756 Button::new("continue-burn-mode", "Continue with Burn Mode")
2757 .style(ButtonStyle::Filled)
2758 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
2759 .layer(ElevationIndex::ModalSurface)
2760 .label_size(LabelSize::Small)
2761 .key_binding(
2762 KeyBinding::for_action_in(
2763 &ContinueWithBurnMode,
2764 &focus_handle,
2765 window,
2766 cx,
2767 )
2768 .map(|kb| kb.size(rems_from_px(10.))),
2769 )
2770 .tooltip(Tooltip::text("Enable Burn Mode for unlimited tool use."))
2771 .on_click({
2772 let active_thread = active_thread.clone();
2773 cx.listener(move |this, _, window, cx| {
2774 active_thread.update(cx, |active_thread, cx| {
2775 active_thread.thread().update(cx, |thread, _cx| {
2776 thread.set_completion_mode(CompletionMode::Burn);
2777 });
2778 });
2779 this.continue_conversation(window, cx);
2780 })
2781 }),
2782 )
2783 }),
2784 );
2785
2786 Some(div().px_2().pb_2().child(banner).into_any_element())
2787 }
2788
2789 fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
2790 let message = message.into();
2791
2792 IconButton::new("copy", IconName::Copy)
2793 .icon_size(IconSize::Small)
2794 .icon_color(Color::Muted)
2795 .tooltip(Tooltip::text("Copy Error Message"))
2796 .on_click(move |_, _, cx| {
2797 cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
2798 })
2799 }
2800
2801 fn dismiss_error_button(
2802 &self,
2803 thread: &Entity<ActiveThread>,
2804 cx: &mut Context<Self>,
2805 ) -> impl IntoElement {
2806 IconButton::new("dismiss", IconName::Close)
2807 .icon_size(IconSize::Small)
2808 .icon_color(Color::Muted)
2809 .tooltip(Tooltip::text("Dismiss Error"))
2810 .on_click(cx.listener({
2811 let thread = thread.clone();
2812 move |_, _, _, cx| {
2813 thread.update(cx, |this, _cx| {
2814 this.clear_last_error();
2815 });
2816
2817 cx.notify();
2818 }
2819 }))
2820 }
2821
2822 fn upgrade_button(
2823 &self,
2824 thread: &Entity<ActiveThread>,
2825 cx: &mut Context<Self>,
2826 ) -> impl IntoElement {
2827 Button::new("upgrade", "Upgrade")
2828 .label_size(LabelSize::Small)
2829 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
2830 .on_click(cx.listener({
2831 let thread = thread.clone();
2832 move |_, _, _, cx| {
2833 thread.update(cx, |this, _cx| {
2834 this.clear_last_error();
2835 });
2836
2837 cx.open_url(&zed_urls::account_url(cx));
2838 cx.notify();
2839 }
2840 }))
2841 }
2842
2843 fn error_callout_bg(&self, cx: &Context<Self>) -> Hsla {
2844 cx.theme().status().error.opacity(0.08)
2845 }
2846
2847 fn render_payment_required_error(
2848 &self,
2849 thread: &Entity<ActiveThread>,
2850 cx: &mut Context<Self>,
2851 ) -> AnyElement {
2852 const ERROR_MESSAGE: &str =
2853 "You reached your free usage limit. Upgrade to Zed Pro for more prompts.";
2854
2855 let icon = Icon::new(IconName::XCircle)
2856 .size(IconSize::Small)
2857 .color(Color::Error);
2858
2859 div()
2860 .border_t_1()
2861 .border_color(cx.theme().colors().border)
2862 .child(
2863 Callout::new()
2864 .icon(icon)
2865 .title("Free Usage Exceeded")
2866 .description(ERROR_MESSAGE)
2867 .tertiary_action(self.upgrade_button(thread, cx))
2868 .secondary_action(self.create_copy_button(ERROR_MESSAGE))
2869 .primary_action(self.dismiss_error_button(thread, cx))
2870 .bg_color(self.error_callout_bg(cx)),
2871 )
2872 .into_any_element()
2873 }
2874
2875 fn render_model_request_limit_reached_error(
2876 &self,
2877 plan: Plan,
2878 thread: &Entity<ActiveThread>,
2879 cx: &mut Context<Self>,
2880 ) -> AnyElement {
2881 let error_message = match plan {
2882 Plan::ZedPro => "Upgrade to usage-based billing for more prompts.",
2883 Plan::ZedProTrial | Plan::Free => "Upgrade to Zed Pro for more prompts.",
2884 };
2885
2886 let icon = Icon::new(IconName::XCircle)
2887 .size(IconSize::Small)
2888 .color(Color::Error);
2889
2890 div()
2891 .border_t_1()
2892 .border_color(cx.theme().colors().border)
2893 .child(
2894 Callout::new()
2895 .icon(icon)
2896 .title("Model Prompt Limit Reached")
2897 .description(error_message)
2898 .tertiary_action(self.upgrade_button(thread, cx))
2899 .secondary_action(self.create_copy_button(error_message))
2900 .primary_action(self.dismiss_error_button(thread, cx))
2901 .bg_color(self.error_callout_bg(cx)),
2902 )
2903 .into_any_element()
2904 }
2905
2906 fn render_error_message(
2907 &self,
2908 header: SharedString,
2909 message: SharedString,
2910 thread: &Entity<ActiveThread>,
2911 cx: &mut Context<Self>,
2912 ) -> AnyElement {
2913 let message_with_header = format!("{}\n{}", header, message);
2914
2915 let icon = Icon::new(IconName::XCircle)
2916 .size(IconSize::Small)
2917 .color(Color::Error);
2918
2919 div()
2920 .border_t_1()
2921 .border_color(cx.theme().colors().border)
2922 .child(
2923 Callout::new()
2924 .icon(icon)
2925 .title(header)
2926 .description(message.clone())
2927 .primary_action(self.dismiss_error_button(thread, cx))
2928 .secondary_action(self.create_copy_button(message_with_header))
2929 .bg_color(self.error_callout_bg(cx)),
2930 )
2931 .into_any_element()
2932 }
2933
2934 fn render_prompt_editor(
2935 &self,
2936 context_editor: &Entity<TextThreadEditor>,
2937 buffer_search_bar: &Entity<BufferSearchBar>,
2938 window: &mut Window,
2939 cx: &mut Context<Self>,
2940 ) -> Div {
2941 let mut registrar = buffer_search::DivRegistrar::new(
2942 |this, _, _cx| match &this.active_view {
2943 ActiveView::TextThread {
2944 buffer_search_bar, ..
2945 } => Some(buffer_search_bar.clone()),
2946 _ => None,
2947 },
2948 cx,
2949 );
2950 BufferSearchBar::register(&mut registrar);
2951 registrar
2952 .into_div()
2953 .size_full()
2954 .relative()
2955 .map(|parent| {
2956 buffer_search_bar.update(cx, |buffer_search_bar, cx| {
2957 if buffer_search_bar.is_dismissed() {
2958 return parent;
2959 }
2960 parent.child(
2961 div()
2962 .p(DynamicSpacing::Base08.rems(cx))
2963 .border_b_1()
2964 .border_color(cx.theme().colors().border_variant)
2965 .bg(cx.theme().colors().editor_background)
2966 .child(buffer_search_bar.render(window, cx)),
2967 )
2968 })
2969 })
2970 .child(context_editor.clone())
2971 .child(self.render_drag_target(cx))
2972 }
2973
2974 fn render_drag_target(&self, cx: &Context<Self>) -> Div {
2975 let is_local = self.project.read(cx).is_local();
2976 div()
2977 .invisible()
2978 .absolute()
2979 .top_0()
2980 .right_0()
2981 .bottom_0()
2982 .left_0()
2983 .bg(cx.theme().colors().drop_target_background)
2984 .drag_over::<DraggedTab>(|this, _, _, _| this.visible())
2985 .drag_over::<DraggedSelection>(|this, _, _, _| this.visible())
2986 .when(is_local, |this| {
2987 this.drag_over::<ExternalPaths>(|this, _, _, _| this.visible())
2988 })
2989 .on_drop(cx.listener(move |this, tab: &DraggedTab, window, cx| {
2990 let item = tab.pane.read(cx).item_for_index(tab.ix);
2991 let project_paths = item
2992 .and_then(|item| item.project_path(cx))
2993 .into_iter()
2994 .collect::<Vec<_>>();
2995 this.handle_drop(project_paths, vec![], window, cx);
2996 }))
2997 .on_drop(
2998 cx.listener(move |this, selection: &DraggedSelection, window, cx| {
2999 let project_paths = selection
3000 .items()
3001 .filter_map(|item| this.project.read(cx).path_for_entry(item.entry_id, cx))
3002 .collect::<Vec<_>>();
3003 this.handle_drop(project_paths, vec![], window, cx);
3004 }),
3005 )
3006 .on_drop(cx.listener(move |this, paths: &ExternalPaths, window, cx| {
3007 let tasks = paths
3008 .paths()
3009 .into_iter()
3010 .map(|path| {
3011 Workspace::project_path_for_path(this.project.clone(), &path, false, cx)
3012 })
3013 .collect::<Vec<_>>();
3014 cx.spawn_in(window, async move |this, cx| {
3015 let mut paths = vec![];
3016 let mut added_worktrees = vec![];
3017 let opened_paths = futures::future::join_all(tasks).await;
3018 for entry in opened_paths {
3019 if let Some((worktree, project_path)) = entry.log_err() {
3020 added_worktrees.push(worktree);
3021 paths.push(project_path);
3022 }
3023 }
3024 this.update_in(cx, |this, window, cx| {
3025 this.handle_drop(paths, added_worktrees, window, cx);
3026 })
3027 .ok();
3028 })
3029 .detach();
3030 }))
3031 }
3032
3033 fn handle_drop(
3034 &mut self,
3035 paths: Vec<ProjectPath>,
3036 added_worktrees: Vec<Entity<Worktree>>,
3037 window: &mut Window,
3038 cx: &mut Context<Self>,
3039 ) {
3040 match &self.active_view {
3041 ActiveView::Thread { thread, .. } => {
3042 let context_store = thread.read(cx).context_store().clone();
3043 context_store.update(cx, move |context_store, cx| {
3044 let mut tasks = Vec::new();
3045 for project_path in &paths {
3046 tasks.push(context_store.add_file_from_path(
3047 project_path.clone(),
3048 false,
3049 cx,
3050 ));
3051 }
3052 cx.background_spawn(async move {
3053 futures::future::join_all(tasks).await;
3054 // Need to hold onto the worktrees until they have already been used when
3055 // opening the buffers.
3056 drop(added_worktrees);
3057 })
3058 .detach();
3059 });
3060 }
3061 ActiveView::AcpThread { .. } => {
3062 unimplemented!()
3063 }
3064 ActiveView::TextThread { context_editor, .. } => {
3065 context_editor.update(cx, |context_editor, cx| {
3066 TextThreadEditor::insert_dragged_files(
3067 context_editor,
3068 paths,
3069 added_worktrees,
3070 window,
3071 cx,
3072 );
3073 });
3074 }
3075 ActiveView::History | ActiveView::Configuration => {}
3076 }
3077 }
3078
3079 fn key_context(&self) -> KeyContext {
3080 let mut key_context = KeyContext::new_with_defaults();
3081 key_context.add("AgentPanel");
3082 match &self.active_view {
3083 ActiveView::AcpThread { .. } => key_context.add("acp_thread"),
3084 ActiveView::TextThread { .. } => key_context.add("prompt_editor"),
3085 ActiveView::Thread { .. } | ActiveView::History | ActiveView::Configuration => {}
3086 }
3087 key_context
3088 }
3089}
3090
3091impl Render for AgentPanel {
3092 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3093 // WARNING: Changes to this element hierarchy can have
3094 // non-obvious implications to the layout of children.
3095 //
3096 // If you need to change it, please confirm:
3097 // - The message editor expands (cmd-option-esc) correctly
3098 // - When expanded, the buttons at the bottom of the panel are displayed correctly
3099 // - Font size works as expected and can be changed with cmd-+/cmd-
3100 // - Scrolling in all views works as expected
3101 // - Files can be dropped into the panel
3102 let content = v_flex()
3103 .key_context(self.key_context())
3104 .justify_between()
3105 .size_full()
3106 .on_action(cx.listener(Self::cancel))
3107 .on_action(cx.listener(|this, action: &NewThread, window, cx| {
3108 this.new_thread(action, window, cx);
3109 }))
3110 .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
3111 this.open_history(window, cx);
3112 }))
3113 .on_action(cx.listener(|this, _: &OpenConfiguration, window, cx| {
3114 this.open_configuration(window, cx);
3115 }))
3116 .on_action(cx.listener(Self::open_active_thread_as_markdown))
3117 .on_action(cx.listener(Self::deploy_rules_library))
3118 .on_action(cx.listener(Self::open_agent_diff))
3119 .on_action(cx.listener(Self::go_back))
3120 .on_action(cx.listener(Self::toggle_navigation_menu))
3121 .on_action(cx.listener(Self::toggle_options_menu))
3122 .on_action(cx.listener(Self::increase_font_size))
3123 .on_action(cx.listener(Self::decrease_font_size))
3124 .on_action(cx.listener(Self::reset_font_size))
3125 .on_action(cx.listener(Self::toggle_zoom))
3126 .on_action(cx.listener(|this, _: &ContinueThread, window, cx| {
3127 this.continue_conversation(window, cx);
3128 }))
3129 .on_action(cx.listener(|this, _: &ContinueWithBurnMode, window, cx| {
3130 match &this.active_view {
3131 ActiveView::Thread { thread, .. } => {
3132 thread.update(cx, |active_thread, cx| {
3133 active_thread.thread().update(cx, |thread, _cx| {
3134 thread.set_completion_mode(CompletionMode::Burn);
3135 });
3136 });
3137 this.continue_conversation(window, cx);
3138 }
3139 ActiveView::AcpThread { .. } => {}
3140 ActiveView::TextThread { .. }
3141 | ActiveView::History
3142 | ActiveView::Configuration => {}
3143 }
3144 }))
3145 .on_action(cx.listener(Self::toggle_burn_mode))
3146 .child(self.render_toolbar(window, cx))
3147 .children(self.render_upsell(window, cx))
3148 .children(self.render_trial_end_upsell(window, cx))
3149 .map(|parent| match &self.active_view {
3150 ActiveView::Thread {
3151 thread,
3152 message_editor,
3153 ..
3154 } => parent
3155 .relative()
3156 .child(if thread.read(cx).is_empty() {
3157 self.render_thread_empty_state(window, cx)
3158 .into_any_element()
3159 } else {
3160 thread.clone().into_any_element()
3161 })
3162 .children(self.render_tool_use_limit_reached(window, cx))
3163 .when_some(thread.read(cx).last_error(), |this, last_error| {
3164 this.child(
3165 div()
3166 .child(match last_error {
3167 ThreadError::PaymentRequired => {
3168 self.render_payment_required_error(thread, cx)
3169 }
3170 ThreadError::ModelRequestLimitReached { plan } => self
3171 .render_model_request_limit_reached_error(plan, thread, cx),
3172 ThreadError::Message { header, message } => {
3173 self.render_error_message(header, message, thread, cx)
3174 }
3175 })
3176 .into_any(),
3177 )
3178 })
3179 .child(h_flex().child(message_editor.clone()))
3180 .child(self.render_drag_target(cx)),
3181 ActiveView::AcpThread { thread_view, .. } => parent
3182 .relative()
3183 .child(thread_view.clone())
3184 .child(self.render_drag_target(cx)),
3185 ActiveView::History => parent.child(self.history.clone()),
3186 ActiveView::TextThread {
3187 context_editor,
3188 buffer_search_bar,
3189 ..
3190 } => parent.child(self.render_prompt_editor(
3191 context_editor,
3192 buffer_search_bar,
3193 window,
3194 cx,
3195 )),
3196 ActiveView::Configuration => parent.children(self.configuration.clone()),
3197 });
3198
3199 match self.active_view.which_font_size_used() {
3200 WhichFontSize::AgentFont => {
3201 WithRemSize::new(ThemeSettings::get_global(cx).agent_font_size(cx))
3202 .size_full()
3203 .child(content)
3204 .into_any()
3205 }
3206 _ => content.into_any(),
3207 }
3208 }
3209}
3210
3211struct PromptLibraryInlineAssist {
3212 workspace: WeakEntity<Workspace>,
3213}
3214
3215impl PromptLibraryInlineAssist {
3216 pub fn new(workspace: WeakEntity<Workspace>) -> Self {
3217 Self { workspace }
3218 }
3219}
3220
3221impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
3222 fn assist(
3223 &self,
3224 prompt_editor: &Entity<Editor>,
3225 initial_prompt: Option<String>,
3226 window: &mut Window,
3227 cx: &mut Context<RulesLibrary>,
3228 ) {
3229 InlineAssistant::update_global(cx, |assistant, cx| {
3230 let Some(project) = self
3231 .workspace
3232 .upgrade()
3233 .map(|workspace| workspace.read(cx).project().downgrade())
3234 else {
3235 return;
3236 };
3237 let prompt_store = None;
3238 let thread_store = None;
3239 let text_thread_store = None;
3240 let context_store = cx.new(|_| ContextStore::new(project.clone(), None));
3241 assistant.assist(
3242 &prompt_editor,
3243 self.workspace.clone(),
3244 context_store,
3245 project,
3246 prompt_store,
3247 thread_store,
3248 text_thread_store,
3249 initial_prompt,
3250 window,
3251 cx,
3252 )
3253 })
3254 }
3255
3256 fn focus_agent_panel(
3257 &self,
3258 workspace: &mut Workspace,
3259 window: &mut Window,
3260 cx: &mut Context<Workspace>,
3261 ) -> bool {
3262 workspace.focus_panel::<AgentPanel>(window, cx).is_some()
3263 }
3264}
3265
3266pub struct ConcreteAssistantPanelDelegate;
3267
3268impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
3269 fn active_context_editor(
3270 &self,
3271 workspace: &mut Workspace,
3272 _window: &mut Window,
3273 cx: &mut Context<Workspace>,
3274 ) -> Option<Entity<TextThreadEditor>> {
3275 let panel = workspace.panel::<AgentPanel>(cx)?;
3276 panel.read(cx).active_context_editor()
3277 }
3278
3279 fn open_saved_context(
3280 &self,
3281 workspace: &mut Workspace,
3282 path: Arc<Path>,
3283 window: &mut Window,
3284 cx: &mut Context<Workspace>,
3285 ) -> Task<Result<()>> {
3286 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3287 return Task::ready(Err(anyhow!("Agent panel not found")));
3288 };
3289
3290 panel.update(cx, |panel, cx| {
3291 panel.open_saved_prompt_editor(path, window, cx)
3292 })
3293 }
3294
3295 fn open_remote_context(
3296 &self,
3297 _workspace: &mut Workspace,
3298 _context_id: assistant_context::ContextId,
3299 _window: &mut Window,
3300 _cx: &mut Context<Workspace>,
3301 ) -> Task<Result<Entity<TextThreadEditor>>> {
3302 Task::ready(Err(anyhow!("opening remote context not implemented")))
3303 }
3304
3305 fn quote_selection(
3306 &self,
3307 workspace: &mut Workspace,
3308 selection_ranges: Vec<Range<Anchor>>,
3309 buffer: Entity<MultiBuffer>,
3310 window: &mut Window,
3311 cx: &mut Context<Workspace>,
3312 ) {
3313 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3314 return;
3315 };
3316
3317 if !panel.focus_handle(cx).contains_focused(window, cx) {
3318 workspace.toggle_panel_focus::<AgentPanel>(window, cx);
3319 }
3320
3321 panel.update(cx, |_, cx| {
3322 // Wait to create a new context until the workspace is no longer
3323 // being updated.
3324 cx.defer_in(window, move |panel, window, cx| {
3325 if let Some(message_editor) = panel.active_message_editor() {
3326 message_editor.update(cx, |message_editor, cx| {
3327 message_editor.context_store().update(cx, |store, cx| {
3328 let buffer = buffer.read(cx);
3329 let selection_ranges = selection_ranges
3330 .into_iter()
3331 .flat_map(|range| {
3332 let (start_buffer, start) =
3333 buffer.text_anchor_for_position(range.start, cx)?;
3334 let (end_buffer, end) =
3335 buffer.text_anchor_for_position(range.end, cx)?;
3336 if start_buffer != end_buffer {
3337 return None;
3338 }
3339 Some((start_buffer, start..end))
3340 })
3341 .collect::<Vec<_>>();
3342
3343 for (buffer, range) in selection_ranges {
3344 store.add_selection(buffer, range, cx);
3345 }
3346 })
3347 })
3348 } else if let Some(context_editor) = panel.active_context_editor() {
3349 let snapshot = buffer.read(cx).snapshot(cx);
3350 let selection_ranges = selection_ranges
3351 .into_iter()
3352 .map(|range| range.to_point(&snapshot))
3353 .collect::<Vec<_>>();
3354
3355 context_editor.update(cx, |context_editor, cx| {
3356 context_editor.quote_ranges(selection_ranges, snapshot, window, cx)
3357 });
3358 }
3359 });
3360 });
3361 }
3362}
3363
3364struct Upsell;
3365
3366impl Dismissable for Upsell {
3367 const KEY: &'static str = "dismissed-trial-upsell";
3368}
3369
3370struct TrialEndUpsell;
3371
3372impl Dismissable for TrialEndUpsell {
3373 const KEY: &'static str = "dismissed-trial-end-upsell";
3374}