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