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