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