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