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