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