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