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