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