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