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