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