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