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