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