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