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