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