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