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