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