assistant_panel.rs

   1use std::ops::Range;
   2use std::path::Path;
   3use std::sync::Arc;
   4use std::time::Duration;
   5
   6use db::kvp::KEY_VALUE_STORE;
   7use markdown::Markdown;
   8use serde::{Deserialize, Serialize};
   9
  10use anyhow::{Result, anyhow};
  11use assistant_context_editor::{
  12    AssistantContext, AssistantPanelDelegate, ConfigurationError, ContextEditor, ContextEvent,
  13    SlashCommandCompletionProvider, humanize_token_count, make_lsp_adapter_delegate,
  14    render_remaining_tokens,
  15};
  16use assistant_settings::{AssistantDockPosition, AssistantSettings};
  17use assistant_slash_command::SlashCommandWorkingSet;
  18use assistant_tool::ToolWorkingSet;
  19
  20use client::{UserStore, zed_urls};
  21use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
  22use fs::Fs;
  23use gpui::{
  24    Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem,
  25    Corner, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext,
  26    Pixels, Subscription, Task, UpdateGlobal, WeakEntity, linear_color_stop, linear_gradient,
  27    prelude::*, pulsating_between,
  28};
  29use language::LanguageRegistry;
  30use language_model::{LanguageModelProviderTosView, LanguageModelRegistry, RequestUsage};
  31use language_model_selector::ToggleModelSelector;
  32use project::Project;
  33use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
  34use proto::Plan;
  35use rules_library::{RulesLibrary, open_rules_library};
  36use search::{BufferSearchBar, buffer_search::DivRegistrar};
  37use settings::{Settings, update_settings_file};
  38use theme::ThemeSettings;
  39use time::UtcOffset;
  40use ui::{
  41    Banner, CheckboxWithLabel, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle,
  42    ProgressBar, Tab, Tooltip, Vector, VectorName, prelude::*,
  43};
  44use util::{ResultExt as _, maybe};
  45use workspace::dock::{DockPosition, Panel, PanelEvent};
  46use workspace::{CollaboratorId, ToolbarItemView, Workspace};
  47use zed_actions::agent::OpenConfiguration;
  48use zed_actions::assistant::{OpenRulesLibrary, ToggleFocus};
  49use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize};
  50use zed_llm_client::UsageLimit;
  51
  52use crate::active_thread::{ActiveThread, ActiveThreadEvent};
  53use crate::agent_diff::AgentDiff;
  54use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent};
  55use crate::history_store::{HistoryEntry, HistoryStore, RecentEntry};
  56use crate::message_editor::{MessageEditor, MessageEditorEvent};
  57use crate::thread::{Thread, ThreadError, ThreadId, TokenUsageRatio};
  58use crate::thread_history::{PastContext, PastThread, ThreadHistory};
  59use crate::thread_store::{TextThreadStore, ThreadStore};
  60use crate::{
  61    AddContextServer, AgentDiffPane, ContextStore, DeleteRecentlyOpenThread, ExpandMessageEditor,
  62    Follow, InlineAssistant, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff,
  63    OpenHistory, ResetTrialUpsell, ThreadEvent, ToggleContextPicker, ToggleNavigationMenu,
  64    ToggleOptionsMenu,
  65};
  66
  67const AGENT_PANEL_KEY: &str = "agent_panel";
  68
  69#[derive(Serialize, Deserialize)]
  70struct SerializedAssistantPanel {
  71    width: Option<Pixels>,
  72}
  73
  74pub fn init(cx: &mut App) {
  75    cx.observe_new(
  76        |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
  77            workspace
  78                .register_action(|workspace, action: &NewThread, window, cx| {
  79                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
  80                        panel.update(cx, |panel, cx| panel.new_thread(action, window, cx));
  81                        workspace.focus_panel::<AssistantPanel>(window, cx);
  82                    }
  83                })
  84                .register_action(|workspace, _: &OpenHistory, window, cx| {
  85                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
  86                        workspace.focus_panel::<AssistantPanel>(window, cx);
  87                        panel.update(cx, |panel, cx| panel.open_history(window, cx));
  88                    }
  89                })
  90                .register_action(|workspace, _: &OpenConfiguration, window, cx| {
  91                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
  92                        workspace.focus_panel::<AssistantPanel>(window, cx);
  93                        panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
  94                    }
  95                })
  96                .register_action(|workspace, _: &NewTextThread, window, cx| {
  97                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
  98                        workspace.focus_panel::<AssistantPanel>(window, cx);
  99                        panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx));
 100                    }
 101                })
 102                .register_action(|workspace, action: &OpenRulesLibrary, window, cx| {
 103                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
 104                        workspace.focus_panel::<AssistantPanel>(window, cx);
 105                        panel.update(cx, |panel, cx| {
 106                            panel.deploy_rules_library(action, window, cx)
 107                        });
 108                    }
 109                })
 110                .register_action(|workspace, _: &OpenAgentDiff, window, cx| {
 111                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
 112                        workspace.focus_panel::<AssistantPanel>(window, cx);
 113                        let thread = panel.read(cx).thread.read(cx).thread().clone();
 114                        AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx);
 115                    }
 116                })
 117                .register_action(|workspace, _: &Follow, window, cx| {
 118                    workspace.follow(CollaboratorId::Agent, window, cx);
 119                })
 120                .register_action(|workspace, _: &ExpandMessageEditor, window, cx| {
 121                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
 122                        workspace.focus_panel::<AssistantPanel>(window, cx);
 123                        panel.update(cx, |panel, cx| {
 124                            panel.message_editor.update(cx, |editor, cx| {
 125                                editor.expand_message_editor(&ExpandMessageEditor, window, cx);
 126                            });
 127                        });
 128                    }
 129                })
 130                .register_action(|workspace, _: &ToggleNavigationMenu, window, cx| {
 131                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
 132                        workspace.focus_panel::<AssistantPanel>(window, cx);
 133                        panel.update(cx, |panel, cx| {
 134                            panel.toggle_navigation_menu(&ToggleNavigationMenu, window, cx);
 135                        });
 136                    }
 137                })
 138                .register_action(|workspace, _: &ToggleOptionsMenu, window, cx| {
 139                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
 140                        workspace.focus_panel::<AssistantPanel>(window, cx);
 141                        panel.update(cx, |panel, cx| {
 142                            panel.toggle_options_menu(&ToggleOptionsMenu, window, cx);
 143                        });
 144                    }
 145                })
 146                .register_action(|_workspace, _: &ResetTrialUpsell, _window, cx| {
 147                    set_trial_upsell_dismissed(false, cx);
 148                });
 149        },
 150    )
 151    .detach();
 152}
 153
 154enum ActiveView {
 155    Thread {
 156        change_title_editor: Entity<Editor>,
 157        thread: WeakEntity<Thread>,
 158        _subscriptions: Vec<gpui::Subscription>,
 159    },
 160    PromptEditor {
 161        context_editor: Entity<ContextEditor>,
 162        title_editor: Entity<Editor>,
 163        buffer_search_bar: Entity<BufferSearchBar>,
 164        _subscriptions: Vec<gpui::Subscription>,
 165    },
 166    History,
 167    Configuration,
 168}
 169
 170impl ActiveView {
 171    pub fn thread(thread: Entity<Thread>, window: &mut Window, cx: &mut App) -> Self {
 172        let summary = thread.read(cx).summary_or_default();
 173
 174        let editor = cx.new(|cx| {
 175            let mut editor = Editor::single_line(window, cx);
 176            editor.set_text(summary.clone(), window, cx);
 177            editor
 178        });
 179
 180        let subscriptions = vec![
 181            window.subscribe(&editor, cx, {
 182                {
 183                    let thread = thread.clone();
 184                    move |editor, event, window, cx| match event {
 185                        EditorEvent::BufferEdited => {
 186                            let new_summary = editor.read(cx).text(cx);
 187
 188                            thread.update(cx, |thread, cx| {
 189                                thread.set_summary(new_summary, cx);
 190                            })
 191                        }
 192                        EditorEvent::Blurred => {
 193                            if editor.read(cx).text(cx).is_empty() {
 194                                let summary = thread.read(cx).summary_or_default();
 195
 196                                editor.update(cx, |editor, cx| {
 197                                    editor.set_text(summary, window, cx);
 198                                });
 199                            }
 200                        }
 201                        _ => {}
 202                    }
 203                }
 204            }),
 205            window.subscribe(&thread, cx, {
 206                let editor = editor.clone();
 207                move |thread, event, window, cx| match event {
 208                    ThreadEvent::SummaryGenerated => {
 209                        let summary = thread.read(cx).summary_or_default();
 210
 211                        editor.update(cx, |editor, cx| {
 212                            editor.set_text(summary, window, cx);
 213                        })
 214                    }
 215                    _ => {}
 216                }
 217            }),
 218        ];
 219
 220        Self::Thread {
 221            change_title_editor: editor,
 222            thread: thread.downgrade(),
 223            _subscriptions: subscriptions,
 224        }
 225    }
 226
 227    pub fn prompt_editor(
 228        context_editor: Entity<ContextEditor>,
 229        language_registry: Arc<LanguageRegistry>,
 230        window: &mut Window,
 231        cx: &mut App,
 232    ) -> Self {
 233        let title = context_editor.read(cx).title(cx).to_string();
 234
 235        let editor = cx.new(|cx| {
 236            let mut editor = Editor::single_line(window, cx);
 237            editor.set_text(title, window, cx);
 238            editor
 239        });
 240
 241        // This is a workaround for `editor.set_text` emitting a `BufferEdited` event, which would
 242        // cause a custom summary to be set. The presence of this custom summary would cause
 243        // summarization to not happen.
 244        let mut suppress_first_edit = true;
 245
 246        let subscriptions = vec![
 247            window.subscribe(&editor, cx, {
 248                {
 249                    let context_editor = context_editor.clone();
 250                    move |editor, event, window, cx| match event {
 251                        EditorEvent::BufferEdited => {
 252                            if suppress_first_edit {
 253                                suppress_first_edit = false;
 254                                return;
 255                            }
 256                            let new_summary = editor.read(cx).text(cx);
 257
 258                            context_editor.update(cx, |context_editor, cx| {
 259                                context_editor
 260                                    .context()
 261                                    .update(cx, |assistant_context, cx| {
 262                                        assistant_context.set_custom_summary(new_summary, cx);
 263                                    })
 264                            })
 265                        }
 266                        EditorEvent::Blurred => {
 267                            if editor.read(cx).text(cx).is_empty() {
 268                                let summary = context_editor
 269                                    .read(cx)
 270                                    .context()
 271                                    .read(cx)
 272                                    .summary_or_default();
 273
 274                                editor.update(cx, |editor, cx| {
 275                                    editor.set_text(summary, window, cx);
 276                                });
 277                            }
 278                        }
 279                        _ => {}
 280                    }
 281                }
 282            }),
 283            window.subscribe(&context_editor.read(cx).context().clone(), cx, {
 284                let editor = editor.clone();
 285                move |assistant_context, event, window, cx| match event {
 286                    ContextEvent::SummaryGenerated => {
 287                        let summary = assistant_context.read(cx).summary_or_default();
 288
 289                        editor.update(cx, |editor, cx| {
 290                            editor.set_text(summary, window, cx);
 291                        })
 292                    }
 293                    _ => {}
 294                }
 295            }),
 296        ];
 297
 298        let buffer_search_bar =
 299            cx.new(|cx| BufferSearchBar::new(Some(language_registry), window, cx));
 300        buffer_search_bar.update(cx, |buffer_search_bar, cx| {
 301            buffer_search_bar.set_active_pane_item(Some(&context_editor), window, cx)
 302        });
 303
 304        Self::PromptEditor {
 305            context_editor,
 306            title_editor: editor,
 307            buffer_search_bar,
 308            _subscriptions: subscriptions,
 309        }
 310    }
 311}
 312
 313pub struct AssistantPanel {
 314    workspace: WeakEntity<Workspace>,
 315    user_store: Entity<UserStore>,
 316    project: Entity<Project>,
 317    fs: Arc<dyn Fs>,
 318    language_registry: Arc<LanguageRegistry>,
 319    thread_store: Entity<ThreadStore>,
 320    thread: Entity<ActiveThread>,
 321    message_editor: Entity<MessageEditor>,
 322    _active_thread_subscriptions: Vec<Subscription>,
 323    _default_model_subscription: Subscription,
 324    context_store: Entity<TextThreadStore>,
 325    prompt_store: Option<Entity<PromptStore>>,
 326    inline_assist_context_store: Entity<crate::context_store::ContextStore>,
 327    configuration: Option<Entity<AssistantConfiguration>>,
 328    configuration_subscription: Option<Subscription>,
 329    local_timezone: UtcOffset,
 330    active_view: ActiveView,
 331    previous_view: Option<ActiveView>,
 332    history_store: Entity<HistoryStore>,
 333    history: Entity<ThreadHistory>,
 334    assistant_dropdown_menu_handle: PopoverMenuHandle<ContextMenu>,
 335    assistant_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
 336    assistant_navigation_menu: Option<Entity<ContextMenu>>,
 337    width: Option<Pixels>,
 338    height: Option<Pixels>,
 339    pending_serialization: Option<Task<Result<()>>>,
 340    hide_trial_upsell: bool,
 341    _trial_markdown: Entity<Markdown>,
 342}
 343
 344impl AssistantPanel {
 345    fn serialize(&mut self, cx: &mut Context<Self>) {
 346        let width = self.width;
 347        self.pending_serialization = Some(cx.background_spawn(async move {
 348            KEY_VALUE_STORE
 349                .write_kvp(
 350                    AGENT_PANEL_KEY.into(),
 351                    serde_json::to_string(&SerializedAssistantPanel { width })?,
 352                )
 353                .await?;
 354            anyhow::Ok(())
 355        }));
 356    }
 357    pub fn load(
 358        workspace: WeakEntity<Workspace>,
 359        prompt_builder: Arc<PromptBuilder>,
 360        mut cx: AsyncWindowContext,
 361    ) -> Task<Result<Entity<Self>>> {
 362        let prompt_store = cx.update(|_window, cx| PromptStore::global(cx));
 363        cx.spawn(async move |cx| {
 364            let prompt_store = match prompt_store {
 365                Ok(prompt_store) => prompt_store.await.ok(),
 366                Err(_) => None,
 367            };
 368            let tools = cx.new(|_| ToolWorkingSet::default())?;
 369            let thread_store = workspace
 370                .update(cx, |workspace, cx| {
 371                    let project = workspace.project().clone();
 372                    ThreadStore::load(
 373                        project,
 374                        tools.clone(),
 375                        prompt_store.clone(),
 376                        prompt_builder.clone(),
 377                        cx,
 378                    )
 379                })?
 380                .await?;
 381
 382            let slash_commands = Arc::new(SlashCommandWorkingSet::default());
 383            let context_store = workspace
 384                .update(cx, |workspace, cx| {
 385                    let project = workspace.project().clone();
 386                    assistant_context_editor::ContextStore::new(
 387                        project,
 388                        prompt_builder.clone(),
 389                        slash_commands,
 390                        cx,
 391                    )
 392                })?
 393                .await?;
 394
 395            let serialized_panel = if let Some(panel) = cx
 396                .background_spawn(async move { KEY_VALUE_STORE.read_kvp(AGENT_PANEL_KEY) })
 397                .await
 398                .log_err()
 399                .flatten()
 400            {
 401                Some(serde_json::from_str::<SerializedAssistantPanel>(&panel)?)
 402            } else {
 403                None
 404            };
 405
 406            let panel = workspace.update_in(cx, |workspace, window, cx| {
 407                let panel = cx.new(|cx| {
 408                    Self::new(
 409                        workspace,
 410                        thread_store,
 411                        context_store,
 412                        prompt_store,
 413                        window,
 414                        cx,
 415                    )
 416                });
 417                if let Some(serialized_panel) = serialized_panel {
 418                    panel.update(cx, |panel, cx| {
 419                        panel.width = serialized_panel.width.map(|w| w.round());
 420                        cx.notify();
 421                    });
 422                }
 423                panel
 424            })?;
 425
 426            Ok(panel)
 427        })
 428    }
 429
 430    fn new(
 431        workspace: &Workspace,
 432        thread_store: Entity<ThreadStore>,
 433        context_store: Entity<TextThreadStore>,
 434        prompt_store: Option<Entity<PromptStore>>,
 435        window: &mut Window,
 436        cx: &mut Context<Self>,
 437    ) -> Self {
 438        let thread = thread_store.update(cx, |this, cx| this.create_thread(cx));
 439        let fs = workspace.app_state().fs.clone();
 440        let user_store = workspace.app_state().user_store.clone();
 441        let project = workspace.project();
 442        let language_registry = project.read(cx).languages().clone();
 443        let workspace = workspace.weak_handle();
 444        let weak_self = cx.entity().downgrade();
 445
 446        let message_editor_context_store = cx.new(|_cx| {
 447            crate::context_store::ContextStore::new(
 448                project.downgrade(),
 449                Some(thread_store.downgrade()),
 450            )
 451        });
 452        let inline_assist_context_store = cx.new(|_cx| {
 453            crate::context_store::ContextStore::new(
 454                project.downgrade(),
 455                Some(thread_store.downgrade()),
 456            )
 457        });
 458
 459        let message_editor = cx.new(|cx| {
 460            MessageEditor::new(
 461                fs.clone(),
 462                workspace.clone(),
 463                user_store.clone(),
 464                message_editor_context_store.clone(),
 465                prompt_store.clone(),
 466                thread_store.downgrade(),
 467                context_store.downgrade(),
 468                thread.clone(),
 469                window,
 470                cx,
 471            )
 472        });
 473
 474        let message_editor_subscription =
 475            cx.subscribe(&message_editor, |_, _, event, cx| match event {
 476                MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
 477                    cx.notify();
 478                }
 479            });
 480
 481        let thread_id = thread.read(cx).id().clone();
 482        let history_store = cx.new(|cx| {
 483            HistoryStore::new(
 484                thread_store.clone(),
 485                context_store.clone(),
 486                [RecentEntry::Thread(thread_id, thread.clone())],
 487                cx,
 488            )
 489        });
 490
 491        cx.observe(&history_store, |_, _, cx| cx.notify()).detach();
 492
 493        let active_view = ActiveView::thread(thread.clone(), window, cx);
 494        let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
 495            if let ThreadEvent::MessageAdded(_) = &event {
 496                // needed to leave empty state
 497                cx.notify();
 498            }
 499        });
 500        let active_thread = cx.new(|cx| {
 501            ActiveThread::new(
 502                thread.clone(),
 503                thread_store.clone(),
 504                context_store.clone(),
 505                message_editor_context_store.clone(),
 506                language_registry.clone(),
 507                workspace.clone(),
 508                window,
 509                cx,
 510            )
 511        });
 512        AgentDiff::set_active_thread(&workspace, &thread, window, cx);
 513
 514        let active_thread_subscription =
 515            cx.subscribe(&active_thread, |_, _, event, cx| match &event {
 516                ActiveThreadEvent::EditingMessageTokenCountChanged => {
 517                    cx.notify();
 518                }
 519            });
 520
 521        let weak_panel = weak_self.clone();
 522
 523        window.defer(cx, move |window, cx| {
 524            let panel = weak_panel.clone();
 525            let assistant_navigation_menu =
 526                ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| {
 527                    let recently_opened = panel
 528                        .update(cx, |this, cx| {
 529                            this.history_store.update(cx, |history_store, cx| {
 530                                history_store.recently_opened_entries(cx)
 531                            })
 532                        })
 533                        .unwrap_or_default();
 534
 535                    if !recently_opened.is_empty() {
 536                        menu = menu.header("Recently Opened");
 537
 538                        for entry in recently_opened.iter() {
 539                            let summary = entry.summary(cx);
 540
 541                            menu = menu.entry_with_end_slot_on_hover(
 542                                summary,
 543                                None,
 544                                {
 545                                    let panel = panel.clone();
 546                                    let entry = entry.clone();
 547                                    move |window, cx| {
 548                                        panel
 549                                            .update(cx, {
 550                                                let entry = entry.clone();
 551                                                move |this, cx| match entry {
 552                                                    RecentEntry::Thread(_, thread) => {
 553                                                        this.open_thread(thread, window, cx)
 554                                                    }
 555                                                    RecentEntry::Context(context) => {
 556                                                        let Some(path) = context.read(cx).path()
 557                                                        else {
 558                                                            return;
 559                                                        };
 560                                                        this.open_saved_prompt_editor(
 561                                                            path.clone(),
 562                                                            window,
 563                                                            cx,
 564                                                        )
 565                                                        .detach_and_log_err(cx)
 566                                                    }
 567                                                }
 568                                            })
 569                                            .ok();
 570                                    }
 571                                },
 572                                IconName::Close,
 573                                "Close Entry".into(),
 574                                {
 575                                    let panel = panel.clone();
 576                                    let entry = entry.clone();
 577                                    move |_window, cx| {
 578                                        panel
 579                                            .update(cx, |this, cx| {
 580                                                this.history_store.update(
 581                                                    cx,
 582                                                    |history_store, cx| {
 583                                                        history_store.remove_recently_opened_entry(
 584                                                            &entry, cx,
 585                                                        );
 586                                                    },
 587                                                );
 588                                            })
 589                                            .ok();
 590                                    }
 591                                },
 592                            );
 593                        }
 594
 595                        menu = menu.separator();
 596                    }
 597
 598                    menu.action("View All", Box::new(OpenHistory))
 599                        .end_slot_action(DeleteRecentlyOpenThread.boxed_clone())
 600                        .fixed_width(px(320.).into())
 601                        .keep_open_on_confirm(false)
 602                        .key_context("NavigationMenu")
 603                });
 604            weak_panel
 605                .update(cx, |panel, cx| {
 606                    cx.subscribe_in(
 607                        &assistant_navigation_menu,
 608                        window,
 609                        |_, menu, _: &DismissEvent, window, cx| {
 610                            menu.update(cx, |menu, _| {
 611                                menu.clear_selected();
 612                            });
 613                            cx.focus_self(window);
 614                        },
 615                    )
 616                    .detach();
 617                    panel.assistant_navigation_menu = Some(assistant_navigation_menu);
 618                })
 619                .ok();
 620        });
 621
 622        let _default_model_subscription = cx.subscribe(
 623            &LanguageModelRegistry::global(cx),
 624            |this, _, event: &language_model::Event, cx| match event {
 625                language_model::Event::DefaultModelChanged => {
 626                    this.thread
 627                        .read(cx)
 628                        .thread()
 629                        .clone()
 630                        .update(cx, |thread, cx| thread.get_or_init_configured_model(cx));
 631                }
 632                _ => {}
 633            },
 634        );
 635
 636        let trial_markdown = cx.new(|cx| {
 637            Markdown::new(
 638                include_str!("trial_markdown.md").into(),
 639                Some(language_registry.clone()),
 640                None,
 641                cx,
 642            )
 643        });
 644
 645        Self {
 646            active_view,
 647            workspace,
 648            user_store,
 649            project: project.clone(),
 650            fs: fs.clone(),
 651            language_registry,
 652            thread_store: thread_store.clone(),
 653            thread: active_thread,
 654            message_editor,
 655            _active_thread_subscriptions: vec![
 656                thread_subscription,
 657                active_thread_subscription,
 658                message_editor_subscription,
 659            ],
 660            _default_model_subscription,
 661            context_store,
 662            prompt_store,
 663            configuration: None,
 664            configuration_subscription: None,
 665            local_timezone: UtcOffset::from_whole_seconds(
 666                chrono::Local::now().offset().local_minus_utc(),
 667            )
 668            .unwrap(),
 669            inline_assist_context_store,
 670            previous_view: None,
 671            history_store: history_store.clone(),
 672            history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)),
 673            assistant_dropdown_menu_handle: PopoverMenuHandle::default(),
 674            assistant_navigation_menu_handle: PopoverMenuHandle::default(),
 675            assistant_navigation_menu: None,
 676            width: None,
 677            height: None,
 678            pending_serialization: None,
 679            hide_trial_upsell: false,
 680            _trial_markdown: trial_markdown,
 681        }
 682    }
 683
 684    pub fn toggle_focus(
 685        workspace: &mut Workspace,
 686        _: &ToggleFocus,
 687        window: &mut Window,
 688        cx: &mut Context<Workspace>,
 689    ) {
 690        if workspace
 691            .panel::<Self>(cx)
 692            .is_some_and(|panel| panel.read(cx).enabled(cx))
 693        {
 694            workspace.toggle_panel_focus::<Self>(window, cx);
 695        }
 696    }
 697
 698    pub(crate) fn local_timezone(&self) -> UtcOffset {
 699        self.local_timezone
 700    }
 701
 702    pub(crate) fn prompt_store(&self) -> &Option<Entity<PromptStore>> {
 703        &self.prompt_store
 704    }
 705
 706    pub(crate) fn inline_assist_context_store(
 707        &self,
 708    ) -> &Entity<crate::context_store::ContextStore> {
 709        &self.inline_assist_context_store
 710    }
 711
 712    pub(crate) fn thread_store(&self) -> &Entity<ThreadStore> {
 713        &self.thread_store
 714    }
 715
 716    pub(crate) fn text_thread_store(&self) -> &Entity<TextThreadStore> {
 717        &self.context_store
 718    }
 719
 720    fn cancel(&mut self, _: &editor::actions::Cancel, window: &mut Window, cx: &mut Context<Self>) {
 721        self.thread
 722            .update(cx, |thread, cx| thread.cancel_last_completion(window, cx));
 723    }
 724
 725    fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {
 726        let thread = self
 727            .thread_store
 728            .update(cx, |this, cx| this.create_thread(cx));
 729
 730        let thread_view = ActiveView::thread(thread.clone(), window, cx);
 731        self.set_active_view(thread_view, window, cx);
 732
 733        let context_store = cx.new(|_cx| {
 734            crate::context_store::ContextStore::new(
 735                self.project.downgrade(),
 736                Some(self.thread_store.downgrade()),
 737            )
 738        });
 739
 740        if let Some(other_thread_id) = action.from_thread_id.clone() {
 741            let other_thread_task = self
 742                .thread_store
 743                .update(cx, |this, cx| this.open_thread(&other_thread_id, cx));
 744
 745            cx.spawn({
 746                let context_store = context_store.clone();
 747
 748                async move |_panel, cx| {
 749                    let other_thread = other_thread_task.await?;
 750
 751                    context_store.update(cx, |this, cx| {
 752                        this.add_thread(other_thread, false, cx);
 753                    })?;
 754                    anyhow::Ok(())
 755                }
 756            })
 757            .detach_and_log_err(cx);
 758        }
 759
 760        let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
 761            if let ThreadEvent::MessageAdded(_) = &event {
 762                // needed to leave empty state
 763                cx.notify();
 764            }
 765        });
 766
 767        self.thread = cx.new(|cx| {
 768            ActiveThread::new(
 769                thread.clone(),
 770                self.thread_store.clone(),
 771                self.context_store.clone(),
 772                context_store.clone(),
 773                self.language_registry.clone(),
 774                self.workspace.clone(),
 775                window,
 776                cx,
 777            )
 778        });
 779        AgentDiff::set_active_thread(&self.workspace, &thread, window, cx);
 780
 781        let active_thread_subscription =
 782            cx.subscribe(&self.thread, |_, _, event, cx| match &event {
 783                ActiveThreadEvent::EditingMessageTokenCountChanged => {
 784                    cx.notify();
 785                }
 786            });
 787
 788        self.message_editor = cx.new(|cx| {
 789            MessageEditor::new(
 790                self.fs.clone(),
 791                self.workspace.clone(),
 792                self.user_store.clone(),
 793                context_store,
 794                self.prompt_store.clone(),
 795                self.thread_store.downgrade(),
 796                self.context_store.downgrade(),
 797                thread,
 798                window,
 799                cx,
 800            )
 801        });
 802        self.message_editor.focus_handle(cx).focus(window);
 803
 804        let message_editor_subscription =
 805            cx.subscribe(&self.message_editor, |_, _, event, cx| match event {
 806                MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
 807                    cx.notify();
 808                }
 809            });
 810
 811        self._active_thread_subscriptions = vec![
 812            thread_subscription,
 813            active_thread_subscription,
 814            message_editor_subscription,
 815        ];
 816    }
 817
 818    fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 819        let context = self
 820            .context_store
 821            .update(cx, |context_store, cx| context_store.create(cx));
 822        let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
 823            .log_err()
 824            .flatten();
 825
 826        let context_editor = cx.new(|cx| {
 827            let mut editor = ContextEditor::for_context(
 828                context,
 829                self.fs.clone(),
 830                self.workspace.clone(),
 831                self.project.clone(),
 832                lsp_adapter_delegate,
 833                window,
 834                cx,
 835            );
 836            editor.insert_default_prompt(window, cx);
 837            editor
 838        });
 839
 840        self.set_active_view(
 841            ActiveView::prompt_editor(
 842                context_editor.clone(),
 843                self.language_registry.clone(),
 844                window,
 845                cx,
 846            ),
 847            window,
 848            cx,
 849        );
 850        context_editor.focus_handle(cx).focus(window);
 851    }
 852
 853    fn deploy_rules_library(
 854        &mut self,
 855        action: &OpenRulesLibrary,
 856        _window: &mut Window,
 857        cx: &mut Context<Self>,
 858    ) {
 859        open_rules_library(
 860            self.language_registry.clone(),
 861            Box::new(PromptLibraryInlineAssist::new(self.workspace.clone())),
 862            Arc::new(|| {
 863                Box::new(SlashCommandCompletionProvider::new(
 864                    Arc::new(SlashCommandWorkingSet::default()),
 865                    None,
 866                    None,
 867                ))
 868            }),
 869            action
 870                .prompt_to_select
 871                .map(|uuid| UserPromptId(uuid).into()),
 872            cx,
 873        )
 874        .detach_and_log_err(cx);
 875    }
 876
 877    fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 878        if matches!(self.active_view, ActiveView::History) {
 879            if let Some(previous_view) = self.previous_view.take() {
 880                self.set_active_view(previous_view, window, cx);
 881            }
 882        } else {
 883            self.thread_store
 884                .update(cx, |thread_store, cx| thread_store.reload(cx))
 885                .detach_and_log_err(cx);
 886            self.set_active_view(ActiveView::History, window, cx);
 887        }
 888        cx.notify();
 889    }
 890
 891    pub(crate) fn open_saved_prompt_editor(
 892        &mut self,
 893        path: Arc<Path>,
 894        window: &mut Window,
 895        cx: &mut Context<Self>,
 896    ) -> Task<Result<()>> {
 897        let context = self
 898            .context_store
 899            .update(cx, |store, cx| store.open_local_context(path, cx));
 900        cx.spawn_in(window, async move |this, cx| {
 901            let context = context.await?;
 902            this.update_in(cx, |this, window, cx| {
 903                this.open_prompt_editor(context, window, cx);
 904            })
 905        })
 906    }
 907
 908    pub(crate) fn open_prompt_editor(
 909        &mut self,
 910        context: Entity<AssistantContext>,
 911        window: &mut Window,
 912        cx: &mut Context<Self>,
 913    ) {
 914        let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project.clone(), cx)
 915            .log_err()
 916            .flatten();
 917        let editor = cx.new(|cx| {
 918            ContextEditor::for_context(
 919                context,
 920                self.fs.clone(),
 921                self.workspace.clone(),
 922                self.project.clone(),
 923                lsp_adapter_delegate,
 924                window,
 925                cx,
 926            )
 927        });
 928        self.set_active_view(
 929            ActiveView::prompt_editor(editor.clone(), self.language_registry.clone(), window, cx),
 930            window,
 931            cx,
 932        );
 933    }
 934
 935    pub(crate) fn open_thread_by_id(
 936        &mut self,
 937        thread_id: &ThreadId,
 938        window: &mut Window,
 939        cx: &mut Context<Self>,
 940    ) -> Task<Result<()>> {
 941        let open_thread_task = self
 942            .thread_store
 943            .update(cx, |this, cx| this.open_thread(thread_id, cx));
 944        cx.spawn_in(window, async move |this, cx| {
 945            let thread = open_thread_task.await?;
 946            this.update_in(cx, |this, window, cx| {
 947                this.open_thread(thread, window, cx);
 948                anyhow::Ok(())
 949            })??;
 950            Ok(())
 951        })
 952    }
 953
 954    pub(crate) fn open_thread(
 955        &mut self,
 956        thread: Entity<Thread>,
 957        window: &mut Window,
 958        cx: &mut Context<Self>,
 959    ) {
 960        let thread_view = ActiveView::thread(thread.clone(), window, cx);
 961        self.set_active_view(thread_view, window, cx);
 962        let context_store = cx.new(|_cx| {
 963            crate::context_store::ContextStore::new(
 964                self.project.downgrade(),
 965                Some(self.thread_store.downgrade()),
 966            )
 967        });
 968        let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
 969            if let ThreadEvent::MessageAdded(_) = &event {
 970                // needed to leave empty state
 971                cx.notify();
 972            }
 973        });
 974
 975        self.thread = cx.new(|cx| {
 976            ActiveThread::new(
 977                thread.clone(),
 978                self.thread_store.clone(),
 979                self.context_store.clone(),
 980                context_store.clone(),
 981                self.language_registry.clone(),
 982                self.workspace.clone(),
 983                window,
 984                cx,
 985            )
 986        });
 987        AgentDiff::set_active_thread(&self.workspace, &thread, window, cx);
 988
 989        let active_thread_subscription =
 990            cx.subscribe(&self.thread, |_, _, event, cx| match &event {
 991                ActiveThreadEvent::EditingMessageTokenCountChanged => {
 992                    cx.notify();
 993                }
 994            });
 995
 996        self.message_editor = cx.new(|cx| {
 997            MessageEditor::new(
 998                self.fs.clone(),
 999                self.workspace.clone(),
1000                self.user_store.clone(),
1001                context_store,
1002                self.prompt_store.clone(),
1003                self.thread_store.downgrade(),
1004                self.context_store.downgrade(),
1005                thread,
1006                window,
1007                cx,
1008            )
1009        });
1010        self.message_editor.focus_handle(cx).focus(window);
1011
1012        let message_editor_subscription =
1013            cx.subscribe(&self.message_editor, |_, _, event, cx| match event {
1014                MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
1015                    cx.notify();
1016                }
1017            });
1018
1019        self._active_thread_subscriptions = vec![
1020            thread_subscription,
1021            active_thread_subscription,
1022            message_editor_subscription,
1023        ];
1024    }
1025
1026    pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
1027        match self.active_view {
1028            ActiveView::Configuration | ActiveView::History => {
1029                self.active_view =
1030                    ActiveView::thread(self.thread.read(cx).thread().clone(), window, cx);
1031                self.message_editor.focus_handle(cx).focus(window);
1032                cx.notify();
1033            }
1034            _ => {}
1035        }
1036    }
1037
1038    pub fn toggle_navigation_menu(
1039        &mut self,
1040        _: &ToggleNavigationMenu,
1041        window: &mut Window,
1042        cx: &mut Context<Self>,
1043    ) {
1044        self.assistant_navigation_menu_handle.toggle(window, cx);
1045    }
1046
1047    pub fn toggle_options_menu(
1048        &mut self,
1049        _: &ToggleOptionsMenu,
1050        window: &mut Window,
1051        cx: &mut Context<Self>,
1052    ) {
1053        self.assistant_dropdown_menu_handle.toggle(window, cx);
1054    }
1055
1056    pub fn increase_font_size(
1057        &mut self,
1058        action: &IncreaseBufferFontSize,
1059        _: &mut Window,
1060        cx: &mut Context<Self>,
1061    ) {
1062        self.adjust_font_size(action.persist, px(1.0), cx);
1063    }
1064
1065    pub fn decrease_font_size(
1066        &mut self,
1067        action: &DecreaseBufferFontSize,
1068        _: &mut Window,
1069        cx: &mut Context<Self>,
1070    ) {
1071        self.adjust_font_size(action.persist, px(-1.0), cx);
1072    }
1073
1074    fn adjust_font_size(&mut self, persist: bool, delta: Pixels, cx: &mut Context<Self>) {
1075        if persist {
1076            update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings, cx| {
1077                let agent_font_size = ThemeSettings::get_global(cx).agent_font_size(cx) + delta;
1078                let _ = settings
1079                    .agent_font_size
1080                    .insert(theme::clamp_font_size(agent_font_size).0);
1081            });
1082        } else {
1083            theme::adjust_agent_font_size(cx, |size| {
1084                *size += delta;
1085            });
1086        }
1087    }
1088
1089    pub fn reset_font_size(
1090        &mut self,
1091        action: &ResetBufferFontSize,
1092        _: &mut Window,
1093        cx: &mut Context<Self>,
1094    ) {
1095        if action.persist {
1096            update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings, _| {
1097                settings.agent_font_size = None;
1098            });
1099        } else {
1100            theme::reset_agent_font_size(cx);
1101        }
1102    }
1103
1104    pub fn open_agent_diff(
1105        &mut self,
1106        _: &OpenAgentDiff,
1107        window: &mut Window,
1108        cx: &mut Context<Self>,
1109    ) {
1110        let thread = self.thread.read(cx).thread().clone();
1111        self.workspace
1112            .update(cx, |workspace, cx| {
1113                AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx)
1114            })
1115            .log_err();
1116    }
1117
1118    pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1119        let context_server_store = self.project.read(cx).context_server_store();
1120        let tools = self.thread_store.read(cx).tools();
1121        let fs = self.fs.clone();
1122
1123        self.set_active_view(ActiveView::Configuration, window, cx);
1124        self.configuration =
1125            Some(cx.new(|cx| {
1126                AssistantConfiguration::new(fs, context_server_store, tools, window, cx)
1127            }));
1128
1129        if let Some(configuration) = self.configuration.as_ref() {
1130            self.configuration_subscription = Some(cx.subscribe_in(
1131                configuration,
1132                window,
1133                Self::handle_assistant_configuration_event,
1134            ));
1135
1136            configuration.focus_handle(cx).focus(window);
1137        }
1138    }
1139
1140    pub(crate) fn open_active_thread_as_markdown(
1141        &mut self,
1142        _: &OpenActiveThreadAsMarkdown,
1143        window: &mut Window,
1144        cx: &mut Context<Self>,
1145    ) {
1146        let Some(workspace) = self
1147            .workspace
1148            .upgrade()
1149            .ok_or_else(|| anyhow!("workspace dropped"))
1150            .log_err()
1151        else {
1152            return;
1153        };
1154
1155        let markdown_language_task = workspace
1156            .read(cx)
1157            .app_state()
1158            .languages
1159            .language_for_name("Markdown");
1160        let Some(thread) = self.active_thread() else {
1161            return;
1162        };
1163        cx.spawn_in(window, async move |_this, cx| {
1164            let markdown_language = markdown_language_task.await?;
1165
1166            workspace.update_in(cx, |workspace, window, cx| {
1167                let thread = thread.read(cx);
1168                let markdown = thread.to_markdown(cx)?;
1169                let thread_summary = thread
1170                    .summary()
1171                    .map(|summary| summary.to_string())
1172                    .unwrap_or_else(|| "Thread".to_string());
1173
1174                let project = workspace.project().clone();
1175                let buffer = project.update(cx, |project, cx| {
1176                    project.create_local_buffer(&markdown, Some(markdown_language), cx)
1177                });
1178                let buffer = cx.new(|cx| {
1179                    MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone())
1180                });
1181
1182                workspace.add_item_to_active_pane(
1183                    Box::new(cx.new(|cx| {
1184                        let mut editor =
1185                            Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
1186                        editor.set_breadcrumb_header(thread_summary);
1187                        editor
1188                    })),
1189                    None,
1190                    true,
1191                    window,
1192                    cx,
1193                );
1194
1195                anyhow::Ok(())
1196            })
1197        })
1198        .detach_and_log_err(cx);
1199    }
1200
1201    fn handle_assistant_configuration_event(
1202        &mut self,
1203        _entity: &Entity<AssistantConfiguration>,
1204        event: &AssistantConfigurationEvent,
1205        window: &mut Window,
1206        cx: &mut Context<Self>,
1207    ) {
1208        match event {
1209            AssistantConfigurationEvent::NewThread(provider) => {
1210                if LanguageModelRegistry::read_global(cx)
1211                    .default_model()
1212                    .map_or(true, |model| model.provider.id() != provider.id())
1213                {
1214                    if let Some(model) = provider.default_model(cx) {
1215                        update_settings_file::<AssistantSettings>(
1216                            self.fs.clone(),
1217                            cx,
1218                            move |settings, _| settings.set_model(model),
1219                        );
1220                    }
1221                }
1222
1223                self.new_thread(&NewThread::default(), window, cx);
1224            }
1225        }
1226    }
1227
1228    pub(crate) fn active_thread(&self) -> Option<Entity<Thread>> {
1229        match &self.active_view {
1230            ActiveView::Thread { thread, .. } => thread.upgrade(),
1231            _ => None,
1232        }
1233    }
1234
1235    pub(crate) fn delete_thread(
1236        &mut self,
1237        thread_id: &ThreadId,
1238        cx: &mut Context<Self>,
1239    ) -> Task<Result<()>> {
1240        self.thread_store
1241            .update(cx, |this, cx| this.delete_thread(thread_id, cx))
1242    }
1243
1244    pub(crate) fn has_active_thread(&self) -> bool {
1245        matches!(self.active_view, ActiveView::Thread { .. })
1246    }
1247
1248    pub(crate) fn active_context_editor(&self) -> Option<Entity<ContextEditor>> {
1249        match &self.active_view {
1250            ActiveView::PromptEditor { context_editor, .. } => Some(context_editor.clone()),
1251            _ => None,
1252        }
1253    }
1254
1255    pub(crate) fn delete_context(
1256        &mut self,
1257        path: Arc<Path>,
1258        cx: &mut Context<Self>,
1259    ) -> Task<Result<()>> {
1260        self.context_store
1261            .update(cx, |this, cx| this.delete_local_context(path, cx))
1262    }
1263
1264    fn set_active_view(
1265        &mut self,
1266        new_view: ActiveView,
1267        window: &mut Window,
1268        cx: &mut Context<Self>,
1269    ) {
1270        let current_is_history = matches!(self.active_view, ActiveView::History);
1271        let new_is_history = matches!(new_view, ActiveView::History);
1272
1273        match &self.active_view {
1274            ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
1275                if let Some(thread) = thread.upgrade() {
1276                    if thread.read(cx).is_empty() {
1277                        let id = thread.read(cx).id().clone();
1278                        store.remove_recently_opened_thread(id, cx);
1279                    }
1280                }
1281            }),
1282            _ => {}
1283        }
1284
1285        match &new_view {
1286            ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
1287                if let Some(thread) = thread.upgrade() {
1288                    let id = thread.read(cx).id().clone();
1289                    store.push_recently_opened_entry(RecentEntry::Thread(id, thread), cx);
1290                }
1291            }),
1292            ActiveView::PromptEditor { context_editor, .. } => {
1293                self.history_store.update(cx, |store, cx| {
1294                    let context = context_editor.read(cx).context().clone();
1295                    store.push_recently_opened_entry(RecentEntry::Context(context), cx)
1296                })
1297            }
1298            _ => {}
1299        }
1300
1301        if current_is_history && !new_is_history {
1302            self.active_view = new_view;
1303        } else if !current_is_history && new_is_history {
1304            self.previous_view = Some(std::mem::replace(&mut self.active_view, new_view));
1305        } else {
1306            if !new_is_history {
1307                self.previous_view = None;
1308            }
1309            self.active_view = new_view;
1310        }
1311
1312        self.focus_handle(cx).focus(window);
1313    }
1314}
1315
1316impl Focusable for AssistantPanel {
1317    fn focus_handle(&self, cx: &App) -> FocusHandle {
1318        match &self.active_view {
1319            ActiveView::Thread { .. } => self.message_editor.focus_handle(cx),
1320            ActiveView::History => self.history.focus_handle(cx),
1321            ActiveView::PromptEditor { context_editor, .. } => context_editor.focus_handle(cx),
1322            ActiveView::Configuration => {
1323                if let Some(configuration) = self.configuration.as_ref() {
1324                    configuration.focus_handle(cx)
1325                } else {
1326                    cx.focus_handle()
1327                }
1328            }
1329        }
1330    }
1331}
1332
1333impl EventEmitter<PanelEvent> for AssistantPanel {}
1334
1335impl Panel for AssistantPanel {
1336    fn persistent_name() -> &'static str {
1337        "AgentPanel"
1338    }
1339
1340    fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1341        match AssistantSettings::get_global(cx).dock {
1342            AssistantDockPosition::Left => DockPosition::Left,
1343            AssistantDockPosition::Bottom => DockPosition::Bottom,
1344            AssistantDockPosition::Right => DockPosition::Right,
1345        }
1346    }
1347
1348    fn position_is_valid(&self, _: DockPosition) -> bool {
1349        true
1350    }
1351
1352    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
1353        settings::update_settings_file::<AssistantSettings>(
1354            self.fs.clone(),
1355            cx,
1356            move |settings, _| {
1357                let dock = match position {
1358                    DockPosition::Left => AssistantDockPosition::Left,
1359                    DockPosition::Bottom => AssistantDockPosition::Bottom,
1360                    DockPosition::Right => AssistantDockPosition::Right,
1361                };
1362                settings.set_dock(dock);
1363            },
1364        );
1365    }
1366
1367    fn size(&self, window: &Window, cx: &App) -> Pixels {
1368        let settings = AssistantSettings::get_global(cx);
1369        match self.position(window, cx) {
1370            DockPosition::Left | DockPosition::Right => {
1371                self.width.unwrap_or(settings.default_width)
1372            }
1373            DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1374        }
1375    }
1376
1377    fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
1378        match self.position(window, cx) {
1379            DockPosition::Left | DockPosition::Right => self.width = size,
1380            DockPosition::Bottom => self.height = size,
1381        }
1382        self.serialize(cx);
1383        cx.notify();
1384    }
1385
1386    fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
1387
1388    fn remote_id() -> Option<proto::PanelId> {
1389        Some(proto::PanelId::AssistantPanel)
1390    }
1391
1392    fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
1393        (self.enabled(cx) && AssistantSettings::get_global(cx).button)
1394            .then_some(IconName::ZedAssistant)
1395    }
1396
1397    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
1398        Some("Agent Panel")
1399    }
1400
1401    fn toggle_action(&self) -> Box<dyn Action> {
1402        Box::new(ToggleFocus)
1403    }
1404
1405    fn activation_priority(&self) -> u32 {
1406        3
1407    }
1408
1409    fn enabled(&self, cx: &App) -> bool {
1410        AssistantSettings::get_global(cx).enabled
1411    }
1412}
1413
1414impl AssistantPanel {
1415    fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
1416        const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
1417
1418        let content = match &self.active_view {
1419            ActiveView::Thread {
1420                change_title_editor,
1421                ..
1422            } => {
1423                let active_thread = self.thread.read(cx);
1424                let is_empty = active_thread.is_empty();
1425
1426                let summary = active_thread.summary(cx);
1427
1428                if is_empty {
1429                    Label::new(Thread::DEFAULT_SUMMARY.clone())
1430                        .truncate()
1431                        .into_any_element()
1432                } else if summary.is_none() {
1433                    Label::new(LOADING_SUMMARY_PLACEHOLDER)
1434                        .truncate()
1435                        .into_any_element()
1436                } else {
1437                    div()
1438                        .w_full()
1439                        .child(change_title_editor.clone())
1440                        .into_any_element()
1441                }
1442            }
1443            ActiveView::PromptEditor {
1444                title_editor,
1445                context_editor,
1446                ..
1447            } => {
1448                let context_editor = context_editor.read(cx);
1449                let summary = context_editor.context().read(cx).summary();
1450
1451                match summary {
1452                    None => Label::new(AssistantContext::DEFAULT_SUMMARY.clone())
1453                        .truncate()
1454                        .into_any_element(),
1455                    Some(summary) => {
1456                        if summary.done {
1457                            div()
1458                                .w_full()
1459                                .child(title_editor.clone())
1460                                .into_any_element()
1461                        } else {
1462                            Label::new(LOADING_SUMMARY_PLACEHOLDER)
1463                                .truncate()
1464                                .into_any_element()
1465                        }
1466                    }
1467                }
1468            }
1469            ActiveView::History => Label::new("History").truncate().into_any_element(),
1470            ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
1471        };
1472
1473        h_flex()
1474            .key_context("TitleEditor")
1475            .id("TitleEditor")
1476            .flex_grow()
1477            .w_full()
1478            .max_w_full()
1479            .overflow_x_scroll()
1480            .child(content)
1481            .into_any()
1482    }
1483
1484    fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1485        let active_thread = self.thread.read(cx);
1486        let user_store = self.user_store.read(cx);
1487        let thread = active_thread.thread().read(cx);
1488        let thread_id = thread.id().clone();
1489        let is_empty = active_thread.is_empty();
1490        let last_usage = active_thread.thread().read(cx).last_usage().or_else(|| {
1491            maybe!({
1492                let amount = user_store.model_request_usage_amount()?;
1493                let limit = user_store.model_request_usage_limit()?.variant?;
1494
1495                Some(RequestUsage {
1496                    amount: amount as i32,
1497                    limit: match limit {
1498                        proto::usage_limit::Variant::Limited(limited) => {
1499                            zed_llm_client::UsageLimit::Limited(limited.limit as i32)
1500                        }
1501                        proto::usage_limit::Variant::Unlimited(_) => {
1502                            zed_llm_client::UsageLimit::Unlimited
1503                        }
1504                    },
1505                })
1506            })
1507        });
1508
1509        let account_url = zed_urls::account_url(cx);
1510
1511        let show_token_count = match &self.active_view {
1512            ActiveView::Thread { .. } => !is_empty,
1513            ActiveView::PromptEditor { .. } => true,
1514            _ => false,
1515        };
1516
1517        let focus_handle = self.focus_handle(cx);
1518
1519        let go_back_button = div().child(
1520            IconButton::new("go-back", IconName::ArrowLeft)
1521                .icon_size(IconSize::Small)
1522                .on_click(cx.listener(|this, _, window, cx| {
1523                    this.go_back(&workspace::GoBack, window, cx);
1524                }))
1525                .tooltip({
1526                    let focus_handle = focus_handle.clone();
1527                    move |window, cx| {
1528                        Tooltip::for_action_in(
1529                            "Go Back",
1530                            &workspace::GoBack,
1531                            &focus_handle,
1532                            window,
1533                            cx,
1534                        )
1535                    }
1536                }),
1537        );
1538
1539        let recent_entries_menu = div().child(
1540            PopoverMenu::new("agent-nav-menu")
1541                .trigger_with_tooltip(
1542                    IconButton::new("agent-nav-menu", IconName::MenuAlt)
1543                        .icon_size(IconSize::Small)
1544                        .style(ui::ButtonStyle::Subtle),
1545                    {
1546                        let focus_handle = focus_handle.clone();
1547                        move |window, cx| {
1548                            Tooltip::for_action_in(
1549                                "Toggle Panel Menu",
1550                                &ToggleNavigationMenu,
1551                                &focus_handle,
1552                                window,
1553                                cx,
1554                            )
1555                        }
1556                    },
1557                )
1558                .anchor(Corner::TopLeft)
1559                .with_handle(self.assistant_navigation_menu_handle.clone())
1560                .menu({
1561                    let menu = self.assistant_navigation_menu.clone();
1562                    move |window, cx| {
1563                        if let Some(menu) = menu.as_ref() {
1564                            menu.update(cx, |_, cx| {
1565                                cx.defer_in(window, |menu, window, cx| {
1566                                    menu.rebuild(window, cx);
1567                                });
1568                            })
1569                        }
1570                        menu.clone()
1571                    }
1572                }),
1573        );
1574
1575        let agent_extra_menu = PopoverMenu::new("agent-options-menu")
1576            .trigger_with_tooltip(
1577                IconButton::new("agent-options-menu", IconName::Ellipsis)
1578                    .icon_size(IconSize::Small),
1579                {
1580                    let focus_handle = focus_handle.clone();
1581                    move |window, cx| {
1582                        Tooltip::for_action_in(
1583                            "Toggle Agent Menu",
1584                            &ToggleOptionsMenu,
1585                            &focus_handle,
1586                            window,
1587                            cx,
1588                        )
1589                    }
1590                },
1591            )
1592            .anchor(Corner::TopRight)
1593            .with_handle(self.assistant_dropdown_menu_handle.clone())
1594            .menu(move |window, cx| {
1595                Some(ContextMenu::build(window, cx, |mut menu, _window, _cx| {
1596                    menu = menu
1597                        .action("New Thread", NewThread::default().boxed_clone())
1598                        .action("New Text Thread", NewTextThread.boxed_clone())
1599                        .when(!is_empty, |menu| {
1600                            menu.action(
1601                                "New From Summary",
1602                                Box::new(NewThread {
1603                                    from_thread_id: Some(thread_id.clone()),
1604                                }),
1605                            )
1606                        })
1607                        .separator();
1608
1609                    menu = menu
1610                        .header("MCP Servers")
1611                        .action(
1612                            "View Server Extensions",
1613                            Box::new(zed_actions::Extensions {
1614                                category_filter: Some(
1615                                    zed_actions::ExtensionCategoryFilter::ContextServers,
1616                                ),
1617                            }),
1618                        )
1619                        .action("Add Custom Server…", Box::new(AddContextServer))
1620                        .separator();
1621
1622                    if let Some(usage) = last_usage {
1623                        menu = menu
1624                            .header_with_link("Prompt Usage", "Manage", account_url.clone())
1625                            .custom_entry(
1626                                move |_window, cx| {
1627                                    let used_percentage = match usage.limit {
1628                                        UsageLimit::Limited(limit) => {
1629                                            Some((usage.amount as f32 / limit as f32) * 100.)
1630                                        }
1631                                        UsageLimit::Unlimited => None,
1632                                    };
1633
1634                                    h_flex()
1635                                        .flex_1()
1636                                        .gap_1p5()
1637                                        .children(used_percentage.map(|percent| {
1638                                            ProgressBar::new("usage", percent, 100., cx)
1639                                        }))
1640                                        .child(
1641                                            Label::new(match usage.limit {
1642                                                UsageLimit::Limited(limit) => {
1643                                                    format!("{} / {limit}", usage.amount)
1644                                                }
1645                                                UsageLimit::Unlimited => {
1646                                                    format!("{} / ∞", usage.amount)
1647                                                }
1648                                            })
1649                                            .size(LabelSize::Small)
1650                                            .color(Color::Muted),
1651                                        )
1652                                        .into_any_element()
1653                                },
1654                                move |_, cx| cx.open_url(&zed_urls::account_url(cx)),
1655                            )
1656                            .separator()
1657                    }
1658
1659                    menu = menu
1660                        .action("Rules…", Box::new(OpenRulesLibrary::default()))
1661                        .action("Settings", Box::new(OpenConfiguration));
1662                    menu
1663                }))
1664            });
1665
1666        h_flex()
1667            .id("assistant-toolbar")
1668            .h(Tab::container_height(cx))
1669            .max_w_full()
1670            .flex_none()
1671            .justify_between()
1672            .gap_2()
1673            .bg(cx.theme().colors().tab_bar_background)
1674            .border_b_1()
1675            .border_color(cx.theme().colors().border)
1676            .child(
1677                h_flex()
1678                    .size_full()
1679                    .pl_1()
1680                    .gap_1()
1681                    .child(match &self.active_view {
1682                        ActiveView::History | ActiveView::Configuration => go_back_button,
1683                        _ => recent_entries_menu,
1684                    })
1685                    .child(self.render_title_view(window, cx)),
1686            )
1687            .child(
1688                h_flex()
1689                    .h_full()
1690                    .gap_2()
1691                    .when(show_token_count, |parent| {
1692                        parent.children(self.render_token_count(&thread, cx))
1693                    })
1694                    .child(
1695                        h_flex()
1696                            .h_full()
1697                            .gap(DynamicSpacing::Base02.rems(cx))
1698                            .px(DynamicSpacing::Base08.rems(cx))
1699                            .border_l_1()
1700                            .border_color(cx.theme().colors().border)
1701                            .child(
1702                                IconButton::new("new", IconName::Plus)
1703                                    .icon_size(IconSize::Small)
1704                                    .style(ButtonStyle::Subtle)
1705                                    .tooltip(move |window, cx| {
1706                                        Tooltip::for_action_in(
1707                                            "New Thread",
1708                                            &NewThread::default(),
1709                                            &focus_handle,
1710                                            window,
1711                                            cx,
1712                                        )
1713                                    })
1714                                    .on_click(move |_event, window, cx| {
1715                                        window.dispatch_action(
1716                                            NewThread::default().boxed_clone(),
1717                                            cx,
1718                                        );
1719                                    }),
1720                            )
1721                            .child(agent_extra_menu),
1722                    ),
1723            )
1724    }
1725
1726    fn render_token_count(&self, thread: &Thread, cx: &App) -> Option<AnyElement> {
1727        let is_generating = thread.is_generating();
1728        let message_editor = self.message_editor.read(cx);
1729
1730        let conversation_token_usage = thread.total_token_usage()?;
1731
1732        let (total_token_usage, is_estimating) = if let Some((editing_message_id, unsent_tokens)) =
1733            self.thread.read(cx).editing_message_id()
1734        {
1735            let combined = thread
1736                .token_usage_up_to_message(editing_message_id)
1737                .add(unsent_tokens);
1738
1739            (combined, unsent_tokens > 0)
1740        } else {
1741            let unsent_tokens = message_editor.last_estimated_token_count().unwrap_or(0);
1742            let combined = conversation_token_usage.add(unsent_tokens);
1743
1744            (combined, unsent_tokens > 0)
1745        };
1746
1747        let is_waiting_to_update_token_count = message_editor.is_waiting_to_update_token_count();
1748
1749        match &self.active_view {
1750            ActiveView::Thread { .. } => {
1751                if total_token_usage.total == 0 {
1752                    return None;
1753                }
1754
1755                let token_color = match total_token_usage.ratio() {
1756                    TokenUsageRatio::Normal if is_estimating => Color::Default,
1757                    TokenUsageRatio::Normal => Color::Muted,
1758                    TokenUsageRatio::Warning => Color::Warning,
1759                    TokenUsageRatio::Exceeded => Color::Error,
1760                };
1761
1762                let token_count = h_flex()
1763                    .id("token-count")
1764                    .flex_shrink_0()
1765                    .gap_0p5()
1766                    .when(!is_generating && is_estimating, |parent| {
1767                        parent
1768                            .child(
1769                                h_flex()
1770                                    .mr_1()
1771                                    .size_2p5()
1772                                    .justify_center()
1773                                    .rounded_full()
1774                                    .bg(cx.theme().colors().text.opacity(0.1))
1775                                    .child(
1776                                        div().size_1().rounded_full().bg(cx.theme().colors().text),
1777                                    ),
1778                            )
1779                            .tooltip(move |window, cx| {
1780                                Tooltip::with_meta(
1781                                    "Estimated New Token Count",
1782                                    None,
1783                                    format!(
1784                                        "Current Conversation Tokens: {}",
1785                                        humanize_token_count(conversation_token_usage.total)
1786                                    ),
1787                                    window,
1788                                    cx,
1789                                )
1790                            })
1791                    })
1792                    .child(
1793                        Label::new(humanize_token_count(total_token_usage.total))
1794                            .size(LabelSize::Small)
1795                            .color(token_color)
1796                            .map(|label| {
1797                                if is_generating || is_waiting_to_update_token_count {
1798                                    label
1799                                        .with_animation(
1800                                            "used-tokens-label",
1801                                            Animation::new(Duration::from_secs(2))
1802                                                .repeat()
1803                                                .with_easing(pulsating_between(0.6, 1.)),
1804                                            |label, delta| label.alpha(delta),
1805                                        )
1806                                        .into_any()
1807                                } else {
1808                                    label.into_any_element()
1809                                }
1810                            }),
1811                    )
1812                    .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
1813                    .child(
1814                        Label::new(humanize_token_count(total_token_usage.max))
1815                            .size(LabelSize::Small)
1816                            .color(Color::Muted),
1817                    )
1818                    .into_any();
1819
1820                Some(token_count)
1821            }
1822            ActiveView::PromptEditor { context_editor, .. } => {
1823                let element = render_remaining_tokens(context_editor, cx)?;
1824
1825                Some(element.into_any_element())
1826            }
1827            _ => None,
1828        }
1829    }
1830
1831    fn should_render_upsell(&self, cx: &mut Context<Self>) -> bool {
1832        if self.hide_trial_upsell || dismissed_trial_upsell() {
1833            return false;
1834        }
1835
1836        let plan = self.user_store.read(cx).current_plan();
1837        if matches!(plan, Some(Plan::ZedPro | Plan::ZedProTrial)) {
1838            return false;
1839        }
1840
1841        let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
1842        if has_previous_trial {
1843            return false;
1844        }
1845
1846        true
1847    }
1848
1849    fn render_trial_upsell(
1850        &self,
1851        _window: &mut Window,
1852        cx: &mut Context<Self>,
1853    ) -> Option<impl IntoElement> {
1854        if !self.should_render_upsell(cx) {
1855            return None;
1856        }
1857
1858        let checkbox = CheckboxWithLabel::new(
1859            "dont-show-again",
1860            Label::new("Don't show again").color(Color::Muted),
1861            ToggleState::Unselected,
1862            move |toggle_state, _window, cx| {
1863                let toggle_state_bool = toggle_state.selected();
1864
1865                set_trial_upsell_dismissed(toggle_state_bool, cx);
1866            },
1867        );
1868
1869        Some(
1870            div().p_2().child(
1871                v_flex()
1872                    .w_full()
1873                    .elevation_2(cx)
1874                    .rounded(px(8.))
1875                    .bg(cx.theme().colors().background.alpha(0.5))
1876                    .p(px(3.))
1877
1878                    .child(
1879                        div()
1880                            .gap_2()
1881                            .flex()
1882                            .flex_col()
1883                            .size_full()
1884                            .border_1()
1885                            .rounded(px(5.))
1886                            .border_color(cx.theme().colors().text.alpha(0.1))
1887                            .overflow_hidden()
1888                            .relative()
1889                            .bg(cx.theme().colors().panel_background)
1890                            .px_4()
1891                            .py_3()
1892                            .child(
1893                                div()
1894                                    .absolute()
1895                                    .top_0()
1896                                    .right(px(-1.0))
1897                                    .w(px(441.))
1898                                    .h(px(167.))
1899                                    .child(
1900                                    Vector::new(VectorName::Grid, rems_from_px(441.), rems_from_px(167.)).color(ui::Color::Custom(cx.theme().colors().text.alpha(0.1)))
1901                                )
1902                            )
1903                            .child(
1904                                div()
1905                                    .absolute()
1906                                    .top(px(-8.0))
1907                                    .right_0()
1908                                    .w(px(400.))
1909                                    .h(px(92.))
1910                                    .child(
1911                                    Vector::new(VectorName::AiGrid, rems_from_px(400.), rems_from_px(92.)).color(ui::Color::Custom(cx.theme().colors().text.alpha(0.32)))
1912                                )
1913                            )
1914                            // .child(
1915                            //     div()
1916                            //         .absolute()
1917                            //         .top_0()
1918                            //         .right(px(360.))
1919                            //         .size(px(401.))
1920                            //         .overflow_hidden()
1921                            //         .bg(cx.theme().colors().panel_background)
1922                            // )
1923                            .child(
1924                                div()
1925                                    .absolute()
1926                                    .top_0()
1927                                    .right_0()
1928                                    .w(px(660.))
1929                                    .h(px(401.))
1930                                    .overflow_hidden()
1931                                    .bg(linear_gradient(
1932                                        75.,
1933                                        linear_color_stop(cx.theme().colors().panel_background.alpha(0.01), 1.0),
1934                                        linear_color_stop(cx.theme().colors().panel_background, 0.45),
1935                                    ))
1936                            )
1937                            .child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small))
1938                            .child(Label::new("Try Zed Pro for free for 14 days - no credit card required.").size(LabelSize::Small))
1939                            .child(Label::new("Use your own API keys or enable usage-based billing once you hit the cap.").color(Color::Muted))
1940                            .child(
1941                                h_flex()
1942                                    .w_full()
1943                                    .px_neg_1()
1944                                    .justify_between()
1945                                    .items_center()
1946                                    .child(h_flex().items_center().gap_1().child(checkbox))
1947                                    .child(
1948                                        h_flex()
1949                                            .gap_2()
1950                                            .child(
1951                                                Button::new("dismiss-button", "Not Now")
1952                                                    .style(ButtonStyle::Transparent)
1953                                                    .color(Color::Muted)
1954                                                    .on_click({
1955                                                        let assistant_panel = cx.entity();
1956                                                        move |_, _, cx| {
1957                                                            assistant_panel.update(
1958                                                                cx,
1959                                                                |this, cx| {
1960                                                                    let hidden =
1961                                                                        this.hide_trial_upsell;
1962                                                                    println!("hidden: {}", hidden);
1963                                                                    this.hide_trial_upsell = true;
1964                                                                    let new_hidden =
1965                                                                        this.hide_trial_upsell;
1966                                                                    println!(
1967                                                                        "new_hidden: {}",
1968                                                                        new_hidden
1969                                                                    );
1970
1971                                                                    cx.notify();
1972                                                                },
1973                                                            );
1974                                                        }
1975                                                    }),
1976                                            )
1977                                            .child(
1978                                                Button::new("cta-button", "Start Trial")
1979                                                    .style(ButtonStyle::Transparent)
1980                                                    .on_click(|_, _, cx| {
1981                                                        cx.open_url(&zed_urls::account_url(cx))
1982                                                    }),
1983                                            ),
1984                                    ),
1985                            ),
1986                    ),
1987            ),
1988        )
1989    }
1990
1991    fn render_active_thread_or_empty_state(
1992        &self,
1993        window: &mut Window,
1994        cx: &mut Context<Self>,
1995    ) -> AnyElement {
1996        if self.thread.read(cx).is_empty() {
1997            return self
1998                .render_thread_empty_state(window, cx)
1999                .into_any_element();
2000        }
2001
2002        self.thread.clone().into_any_element()
2003    }
2004
2005    fn configuration_error(&self, cx: &App) -> Option<ConfigurationError> {
2006        let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
2007            return Some(ConfigurationError::NoProvider);
2008        };
2009
2010        if !model.provider.is_authenticated(cx) {
2011            return Some(ConfigurationError::ProviderNotAuthenticated);
2012        }
2013
2014        if model.provider.must_accept_terms(cx) {
2015            return Some(ConfigurationError::ProviderPendingTermsAcceptance(
2016                model.provider,
2017            ));
2018        }
2019
2020        None
2021    }
2022
2023    fn render_thread_empty_state(
2024        &self,
2025        window: &mut Window,
2026        cx: &mut Context<Self>,
2027    ) -> impl IntoElement {
2028        let recent_history = self
2029            .history_store
2030            .update(cx, |this, cx| this.recent_entries(6, cx));
2031
2032        let configuration_error = self.configuration_error(cx);
2033        let no_error = configuration_error.is_none();
2034        let focus_handle = self.focus_handle(cx);
2035
2036        v_flex()
2037            .size_full()
2038            .when(recent_history.is_empty(), |this| {
2039                let configuration_error_ref = &configuration_error;
2040                this.child(
2041                    v_flex()
2042                        .size_full()
2043                        .max_w_80()
2044                        .mx_auto()
2045                        .justify_center()
2046                        .items_center()
2047                        .gap_1()
2048                        .child(
2049                            h_flex().child(
2050                                Headline::new("Welcome to the Agent Panel")
2051                            ),
2052                        )
2053                        .when(no_error, |parent| {
2054                            parent
2055                                .child(
2056                                    h_flex().child(
2057                                        Label::new("Ask and build anything.")
2058                                            .color(Color::Muted)
2059                                            .mb_2p5(),
2060                                    ),
2061                                )
2062                                .child(
2063                                    Button::new("new-thread", "Start New Thread")
2064                                        .icon(IconName::Plus)
2065                                        .icon_position(IconPosition::Start)
2066                                        .icon_size(IconSize::Small)
2067                                        .icon_color(Color::Muted)
2068                                        .full_width()
2069                                        .key_binding(KeyBinding::for_action_in(
2070                                            &NewThread::default(),
2071                                            &focus_handle,
2072                                            window,
2073                                            cx,
2074                                        ))
2075                                        .on_click(|_event, window, cx| {
2076                                            window.dispatch_action(NewThread::default().boxed_clone(), cx)
2077                                        }),
2078                                )
2079                                .child(
2080                                    Button::new("context", "Add Context")
2081                                        .icon(IconName::FileCode)
2082                                        .icon_position(IconPosition::Start)
2083                                        .icon_size(IconSize::Small)
2084                                        .icon_color(Color::Muted)
2085                                        .full_width()
2086                                        .key_binding(KeyBinding::for_action_in(
2087                                            &ToggleContextPicker,
2088                                            &focus_handle,
2089                                            window,
2090                                            cx,
2091                                        ))
2092                                        .on_click(|_event, window, cx| {
2093                                            window.dispatch_action(ToggleContextPicker.boxed_clone(), cx)
2094                                        }),
2095                                )
2096                                .child(
2097                                    Button::new("mode", "Switch Model")
2098                                        .icon(IconName::DatabaseZap)
2099                                        .icon_position(IconPosition::Start)
2100                                        .icon_size(IconSize::Small)
2101                                        .icon_color(Color::Muted)
2102                                        .full_width()
2103                                        .key_binding(KeyBinding::for_action_in(
2104                                            &ToggleModelSelector,
2105                                            &focus_handle,
2106                                            window,
2107                                            cx,
2108                                        ))
2109                                        .on_click(|_event, window, cx| {
2110                                            window.dispatch_action(ToggleModelSelector.boxed_clone(), cx)
2111                                        }),
2112                                )
2113                                .child(
2114                                    Button::new("settings", "View Settings")
2115                                        .icon(IconName::Settings)
2116                                        .icon_position(IconPosition::Start)
2117                                        .icon_size(IconSize::Small)
2118                                        .icon_color(Color::Muted)
2119                                        .full_width()
2120                                        .key_binding(KeyBinding::for_action_in(
2121                                            &OpenConfiguration,
2122                                            &focus_handle,
2123                                            window,
2124                                            cx,
2125                                        ))
2126                                        .on_click(|_event, window, cx| {
2127                                            window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
2128                                        }),
2129                                )
2130                        })
2131                        .map(|parent| {
2132                            match configuration_error_ref {
2133                                Some(ConfigurationError::ProviderNotAuthenticated)
2134                                | Some(ConfigurationError::NoProvider) => {
2135                                    parent
2136                                        .child(
2137                                            h_flex().child(
2138                                                Label::new("To start using the agent, configure at least one LLM provider.")
2139                                                    .color(Color::Muted)
2140                                                    .mb_2p5()
2141                                            )
2142                                        )
2143                                        .child(
2144                                            Button::new("settings", "Configure a Provider")
2145                                                .icon(IconName::Settings)
2146                                                .icon_position(IconPosition::Start)
2147                                                .icon_size(IconSize::Small)
2148                                                .icon_color(Color::Muted)
2149                                                .full_width()
2150                                                .key_binding(KeyBinding::for_action_in(
2151                                                    &OpenConfiguration,
2152                                                    &focus_handle,
2153                                                    window,
2154                                                    cx,
2155                                                ))
2156                                                .on_click(|_event, window, cx| {
2157                                                    window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
2158                                                }),
2159                                        )
2160                                }
2161                                Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
2162                                    parent.children(
2163                                        provider.render_accept_terms(
2164                                            LanguageModelProviderTosView::ThreadFreshStart,
2165                                            cx,
2166                                        ),
2167                                    )
2168                                }
2169                                None => parent,
2170                            }
2171                        })
2172                )
2173            })
2174            .when(!recent_history.is_empty(), |parent| {
2175                let focus_handle = focus_handle.clone();
2176                let configuration_error_ref = &configuration_error;
2177
2178                parent
2179                    .overflow_hidden()
2180                    .p_1p5()
2181                    .justify_end()
2182                    .gap_1()
2183                    .child(
2184                        h_flex()
2185                            .pl_1p5()
2186                            .pb_1()
2187                            .w_full()
2188                            .justify_between()
2189                            .border_b_1()
2190                            .border_color(cx.theme().colors().border_variant)
2191                            .child(
2192                                Label::new("Past Interactions")
2193                                    .size(LabelSize::Small)
2194                                    .color(Color::Muted),
2195                            )
2196                            .child(
2197                                Button::new("view-history", "View All")
2198                                    .style(ButtonStyle::Subtle)
2199                                    .label_size(LabelSize::Small)
2200                                    .key_binding(
2201                                        KeyBinding::for_action_in(
2202                                            &OpenHistory,
2203                                            &self.focus_handle(cx),
2204                                            window,
2205                                            cx,
2206                                        ).map(|kb| kb.size(rems_from_px(12.))),
2207                                    )
2208                                    .on_click(move |_event, window, cx| {
2209                                        window.dispatch_action(OpenHistory.boxed_clone(), cx);
2210                                    }),
2211                            ),
2212                    )
2213                    .child(
2214                        v_flex()
2215                            .gap_1()
2216                            .children(
2217                                recent_history.into_iter().map(|entry| {
2218                                    // TODO: Add keyboard navigation.
2219                                    match entry {
2220                                        HistoryEntry::Thread(thread) => {
2221                                            PastThread::new(thread, cx.entity().downgrade(), false, vec![])
2222                                                .into_any_element()
2223                                        }
2224                                        HistoryEntry::Context(context) => {
2225                                            PastContext::new(context, cx.entity().downgrade(), false, vec![])
2226                                                .into_any_element()
2227                                        }
2228                                    }
2229                                }),
2230                            )
2231                    )
2232                    .map(|parent| {
2233                        match configuration_error_ref {
2234                            Some(ConfigurationError::ProviderNotAuthenticated)
2235                            | Some(ConfigurationError::NoProvider) => {
2236                                parent
2237                                    .child(
2238                                        Banner::new()
2239                                            .severity(ui::Severity::Warning)
2240                                            .child(
2241                                                Label::new(
2242                                                    "Configure at least one LLM provider to start using the panel.",
2243                                                )
2244                                                .size(LabelSize::Small),
2245                                            )
2246                                            .action_slot(
2247                                                Button::new("settings", "Configure Provider")
2248                                                    .style(ButtonStyle::Tinted(ui::TintColor::Warning))
2249                                                    .label_size(LabelSize::Small)
2250                                                    .key_binding(
2251                                                        KeyBinding::for_action_in(
2252                                                            &OpenConfiguration,
2253                                                            &focus_handle,
2254                                                            window,
2255                                                            cx,
2256                                                        )
2257                                                        .map(|kb| kb.size(rems_from_px(12.))),
2258                                                    )
2259                                                    .on_click(|_event, window, cx| {
2260                                                        window.dispatch_action(
2261                                                            OpenConfiguration.boxed_clone(),
2262                                                            cx,
2263                                                        )
2264                                                    }),
2265                                            ),
2266                                    )
2267                            }
2268                            Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
2269                                parent
2270                                    .child(
2271                                        Banner::new()
2272                                            .severity(ui::Severity::Warning)
2273                                            .child(
2274                                                h_flex()
2275                                                    .w_full()
2276                                                    .children(
2277                                                        provider.render_accept_terms(
2278                                                            LanguageModelProviderTosView::ThreadtEmptyState,
2279                                                            cx,
2280                                                        ),
2281                                                    ),
2282                                            ),
2283                                    )
2284                            }
2285                            None => parent,
2286                        }
2287                    })
2288            })
2289    }
2290
2291    fn render_tool_use_limit_reached(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
2292        let tool_use_limit_reached = self
2293            .thread
2294            .read(cx)
2295            .thread()
2296            .read(cx)
2297            .tool_use_limit_reached();
2298        if !tool_use_limit_reached {
2299            return None;
2300        }
2301
2302        let model = self
2303            .thread
2304            .read(cx)
2305            .thread()
2306            .read(cx)
2307            .configured_model()?
2308            .model;
2309
2310        let max_mode_upsell = if model.supports_max_mode() {
2311            " Enable max mode for unlimited tool use."
2312        } else {
2313            ""
2314        };
2315
2316        Some(
2317            Banner::new()
2318                .severity(ui::Severity::Info)
2319                .child(h_flex().child(Label::new(format!(
2320                    "Consecutive tool use limit reached.{max_mode_upsell}"
2321                ))))
2322                .into_any_element(),
2323        )
2324    }
2325
2326    fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
2327        let last_error = self.thread.read(cx).last_error()?;
2328
2329        Some(
2330            div()
2331                .absolute()
2332                .right_3()
2333                .bottom_12()
2334                .max_w_96()
2335                .py_2()
2336                .px_3()
2337                .elevation_2(cx)
2338                .occlude()
2339                .child(match last_error {
2340                    ThreadError::PaymentRequired => self.render_payment_required_error(cx),
2341                    ThreadError::MaxMonthlySpendReached => {
2342                        self.render_max_monthly_spend_reached_error(cx)
2343                    }
2344                    ThreadError::ModelRequestLimitReached { plan } => {
2345                        self.render_model_request_limit_reached_error(plan, cx)
2346                    }
2347                    ThreadError::Message { header, message } => {
2348                        self.render_error_message(header, message, cx)
2349                    }
2350                })
2351                .into_any(),
2352        )
2353    }
2354
2355    fn render_payment_required_error(&self, cx: &mut Context<Self>) -> AnyElement {
2356        const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
2357
2358        v_flex()
2359            .gap_0p5()
2360            .child(
2361                h_flex()
2362                    .gap_1p5()
2363                    .items_center()
2364                    .child(Icon::new(IconName::XCircle).color(Color::Error))
2365                    .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
2366            )
2367            .child(
2368                div()
2369                    .id("error-message")
2370                    .max_h_24()
2371                    .overflow_y_scroll()
2372                    .child(Label::new(ERROR_MESSAGE)),
2373            )
2374            .child(
2375                h_flex()
2376                    .justify_end()
2377                    .mt_1()
2378                    .gap_1()
2379                    .child(self.create_copy_button(ERROR_MESSAGE))
2380                    .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
2381                        |this, _, _, cx| {
2382                            this.thread.update(cx, |this, _cx| {
2383                                this.clear_last_error();
2384                            });
2385
2386                            cx.open_url(&zed_urls::account_url(cx));
2387                            cx.notify();
2388                        },
2389                    )))
2390                    .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2391                        |this, _, _, cx| {
2392                            this.thread.update(cx, |this, _cx| {
2393                                this.clear_last_error();
2394                            });
2395
2396                            cx.notify();
2397                        },
2398                    ))),
2399            )
2400            .into_any()
2401    }
2402
2403    fn render_max_monthly_spend_reached_error(&self, cx: &mut Context<Self>) -> AnyElement {
2404        const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
2405
2406        v_flex()
2407            .gap_0p5()
2408            .child(
2409                h_flex()
2410                    .gap_1p5()
2411                    .items_center()
2412                    .child(Icon::new(IconName::XCircle).color(Color::Error))
2413                    .child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
2414            )
2415            .child(
2416                div()
2417                    .id("error-message")
2418                    .max_h_24()
2419                    .overflow_y_scroll()
2420                    .child(Label::new(ERROR_MESSAGE)),
2421            )
2422            .child(
2423                h_flex()
2424                    .justify_end()
2425                    .mt_1()
2426                    .gap_1()
2427                    .child(self.create_copy_button(ERROR_MESSAGE))
2428                    .child(
2429                        Button::new("subscribe", "Update Monthly Spend Limit").on_click(
2430                            cx.listener(|this, _, _, cx| {
2431                                this.thread.update(cx, |this, _cx| {
2432                                    this.clear_last_error();
2433                                });
2434
2435                                cx.open_url(&zed_urls::account_url(cx));
2436                                cx.notify();
2437                            }),
2438                        ),
2439                    )
2440                    .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2441                        |this, _, _, cx| {
2442                            this.thread.update(cx, |this, _cx| {
2443                                this.clear_last_error();
2444                            });
2445
2446                            cx.notify();
2447                        },
2448                    ))),
2449            )
2450            .into_any()
2451    }
2452
2453    fn render_model_request_limit_reached_error(
2454        &self,
2455        plan: Plan,
2456        cx: &mut Context<Self>,
2457    ) -> AnyElement {
2458        let error_message = match plan {
2459            Plan::ZedPro => {
2460                "Model request limit reached. Upgrade to usage-based billing for more requests."
2461            }
2462            Plan::ZedProTrial => {
2463                "Model request limit reached. Upgrade to Zed Pro for more requests."
2464            }
2465            Plan::Free => "Model request limit reached. Upgrade to Zed Pro for more requests.",
2466        };
2467        let call_to_action = match plan {
2468            Plan::ZedPro => "Upgrade to usage-based billing",
2469            Plan::ZedProTrial => "Upgrade to Zed Pro",
2470            Plan::Free => "Upgrade to Zed Pro",
2471        };
2472
2473        v_flex()
2474            .gap_0p5()
2475            .child(
2476                h_flex()
2477                    .gap_1p5()
2478                    .items_center()
2479                    .child(Icon::new(IconName::XCircle).color(Color::Error))
2480                    .child(Label::new("Model Request Limit Reached").weight(FontWeight::MEDIUM)),
2481            )
2482            .child(
2483                div()
2484                    .id("error-message")
2485                    .max_h_24()
2486                    .overflow_y_scroll()
2487                    .child(Label::new(error_message)),
2488            )
2489            .child(
2490                h_flex()
2491                    .justify_end()
2492                    .mt_1()
2493                    .gap_1()
2494                    .child(self.create_copy_button(error_message))
2495                    .child(
2496                        Button::new("subscribe", call_to_action).on_click(cx.listener(
2497                            |this, _, _, cx| {
2498                                this.thread.update(cx, |this, _cx| {
2499                                    this.clear_last_error();
2500                                });
2501
2502                                cx.open_url(&zed_urls::account_url(cx));
2503                                cx.notify();
2504                            },
2505                        )),
2506                    )
2507                    .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2508                        |this, _, _, cx| {
2509                            this.thread.update(cx, |this, _cx| {
2510                                this.clear_last_error();
2511                            });
2512
2513                            cx.notify();
2514                        },
2515                    ))),
2516            )
2517            .into_any()
2518    }
2519
2520    fn render_error_message(
2521        &self,
2522        header: SharedString,
2523        message: SharedString,
2524        cx: &mut Context<Self>,
2525    ) -> AnyElement {
2526        let message_with_header = format!("{}\n{}", header, message);
2527        v_flex()
2528            .gap_0p5()
2529            .child(
2530                h_flex()
2531                    .gap_1p5()
2532                    .items_center()
2533                    .child(Icon::new(IconName::XCircle).color(Color::Error))
2534                    .child(Label::new(header).weight(FontWeight::MEDIUM)),
2535            )
2536            .child(
2537                div()
2538                    .id("error-message")
2539                    .max_h_32()
2540                    .overflow_y_scroll()
2541                    .child(Label::new(message.clone())),
2542            )
2543            .child(
2544                h_flex()
2545                    .justify_end()
2546                    .mt_1()
2547                    .gap_1()
2548                    .child(self.create_copy_button(message_with_header))
2549                    .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2550                        |this, _, _, cx| {
2551                            this.thread.update(cx, |this, _cx| {
2552                                this.clear_last_error();
2553                            });
2554
2555                            cx.notify();
2556                        },
2557                    ))),
2558            )
2559            .into_any()
2560    }
2561
2562    fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
2563        let message = message.into();
2564        IconButton::new("copy", IconName::Copy)
2565            .on_click(move |_, _, cx| {
2566                cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
2567            })
2568            .tooltip(Tooltip::text("Copy Error Message"))
2569    }
2570
2571    fn key_context(&self) -> KeyContext {
2572        let mut key_context = KeyContext::new_with_defaults();
2573        key_context.add("AgentPanel");
2574        if matches!(self.active_view, ActiveView::PromptEditor { .. }) {
2575            key_context.add("prompt_editor");
2576        }
2577        key_context
2578    }
2579}
2580
2581impl Render for AssistantPanel {
2582    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2583        v_flex()
2584            .key_context(self.key_context())
2585            .justify_between()
2586            .size_full()
2587            .on_action(cx.listener(Self::cancel))
2588            .on_action(cx.listener(|this, action: &NewThread, window, cx| {
2589                this.new_thread(action, window, cx);
2590            }))
2591            .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
2592                this.open_history(window, cx);
2593            }))
2594            .on_action(cx.listener(|this, _: &OpenConfiguration, window, cx| {
2595                this.open_configuration(window, cx);
2596            }))
2597            .on_action(cx.listener(Self::open_active_thread_as_markdown))
2598            .on_action(cx.listener(Self::deploy_rules_library))
2599            .on_action(cx.listener(Self::open_agent_diff))
2600            .on_action(cx.listener(Self::go_back))
2601            .on_action(cx.listener(Self::toggle_navigation_menu))
2602            .on_action(cx.listener(Self::toggle_options_menu))
2603            .on_action(cx.listener(Self::increase_font_size))
2604            .on_action(cx.listener(Self::decrease_font_size))
2605            .on_action(cx.listener(Self::reset_font_size))
2606            .child(self.render_toolbar(window, cx))
2607            .children(self.render_trial_upsell(window, cx))
2608            .map(|parent| match &self.active_view {
2609                ActiveView::Thread { .. } => parent
2610                    .child(self.render_active_thread_or_empty_state(window, cx))
2611                    .children(self.render_tool_use_limit_reached(cx))
2612                    .child(h_flex().child(self.message_editor.clone()))
2613                    .children(self.render_last_error(cx)),
2614                ActiveView::History => parent.child(self.history.clone()),
2615                ActiveView::PromptEditor {
2616                    context_editor,
2617                    buffer_search_bar,
2618                    ..
2619                } => {
2620                    let mut registrar = DivRegistrar::new(
2621                        |this, _, _cx| match &this.active_view {
2622                            ActiveView::PromptEditor {
2623                                buffer_search_bar, ..
2624                            } => Some(buffer_search_bar.clone()),
2625                            _ => None,
2626                        },
2627                        cx,
2628                    );
2629                    BufferSearchBar::register(&mut registrar);
2630                    parent.child(
2631                        registrar
2632                            .into_div()
2633                            .size_full()
2634                            .map(|parent| {
2635                                buffer_search_bar.update(cx, |buffer_search_bar, cx| {
2636                                    if buffer_search_bar.is_dismissed() {
2637                                        return parent;
2638                                    }
2639                                    parent.child(
2640                                        div()
2641                                            .p(DynamicSpacing::Base08.rems(cx))
2642                                            .border_b_1()
2643                                            .border_color(cx.theme().colors().border_variant)
2644                                            .bg(cx.theme().colors().editor_background)
2645                                            .child(buffer_search_bar.render(window, cx)),
2646                                    )
2647                                })
2648                            })
2649                            .child(context_editor.clone()),
2650                    )
2651                }
2652                ActiveView::Configuration => parent.children(self.configuration.clone()),
2653            })
2654    }
2655}
2656
2657struct PromptLibraryInlineAssist {
2658    workspace: WeakEntity<Workspace>,
2659}
2660
2661impl PromptLibraryInlineAssist {
2662    pub fn new(workspace: WeakEntity<Workspace>) -> Self {
2663        Self { workspace }
2664    }
2665}
2666
2667impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
2668    fn assist(
2669        &self,
2670        prompt_editor: &Entity<Editor>,
2671        _initial_prompt: Option<String>,
2672        window: &mut Window,
2673        cx: &mut Context<RulesLibrary>,
2674    ) {
2675        InlineAssistant::update_global(cx, |assistant, cx| {
2676            let Some(project) = self
2677                .workspace
2678                .upgrade()
2679                .map(|workspace| workspace.read(cx).project().downgrade())
2680            else {
2681                return;
2682            };
2683            let prompt_store = None;
2684            let thread_store = None;
2685            let text_thread_store = None;
2686            let context_store = cx.new(|_| ContextStore::new(project.clone(), None));
2687            assistant.assist(
2688                &prompt_editor,
2689                self.workspace.clone(),
2690                context_store,
2691                project,
2692                prompt_store,
2693                thread_store,
2694                text_thread_store,
2695                window,
2696                cx,
2697            )
2698        })
2699    }
2700
2701    fn focus_assistant_panel(
2702        &self,
2703        workspace: &mut Workspace,
2704        window: &mut Window,
2705        cx: &mut Context<Workspace>,
2706    ) -> bool {
2707        workspace
2708            .focus_panel::<AssistantPanel>(window, cx)
2709            .is_some()
2710    }
2711}
2712
2713pub struct ConcreteAssistantPanelDelegate;
2714
2715impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
2716    fn active_context_editor(
2717        &self,
2718        workspace: &mut Workspace,
2719        _window: &mut Window,
2720        cx: &mut Context<Workspace>,
2721    ) -> Option<Entity<ContextEditor>> {
2722        let panel = workspace.panel::<AssistantPanel>(cx)?;
2723        panel.read(cx).active_context_editor()
2724    }
2725
2726    fn open_saved_context(
2727        &self,
2728        workspace: &mut Workspace,
2729        path: Arc<Path>,
2730        window: &mut Window,
2731        cx: &mut Context<Workspace>,
2732    ) -> Task<Result<()>> {
2733        let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2734            return Task::ready(Err(anyhow!("Agent panel not found")));
2735        };
2736
2737        panel.update(cx, |panel, cx| {
2738            panel.open_saved_prompt_editor(path, window, cx)
2739        })
2740    }
2741
2742    fn open_remote_context(
2743        &self,
2744        _workspace: &mut Workspace,
2745        _context_id: assistant_context_editor::ContextId,
2746        _window: &mut Window,
2747        _cx: &mut Context<Workspace>,
2748    ) -> Task<Result<Entity<ContextEditor>>> {
2749        Task::ready(Err(anyhow!("opening remote context not implemented")))
2750    }
2751
2752    fn quote_selection(
2753        &self,
2754        workspace: &mut Workspace,
2755        selection_ranges: Vec<Range<Anchor>>,
2756        buffer: Entity<MultiBuffer>,
2757        window: &mut Window,
2758        cx: &mut Context<Workspace>,
2759    ) {
2760        let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2761            return;
2762        };
2763
2764        if !panel.focus_handle(cx).contains_focused(window, cx) {
2765            workspace.toggle_panel_focus::<AssistantPanel>(window, cx);
2766        }
2767
2768        panel.update(cx, |_, cx| {
2769            // Wait to create a new context until the workspace is no longer
2770            // being updated.
2771            cx.defer_in(window, move |panel, window, cx| {
2772                if panel.has_active_thread() {
2773                    panel.message_editor.update(cx, |message_editor, cx| {
2774                        message_editor.context_store().update(cx, |store, cx| {
2775                            let buffer = buffer.read(cx);
2776                            let selection_ranges = selection_ranges
2777                                .into_iter()
2778                                .flat_map(|range| {
2779                                    let (start_buffer, start) =
2780                                        buffer.text_anchor_for_position(range.start, cx)?;
2781                                    let (end_buffer, end) =
2782                                        buffer.text_anchor_for_position(range.end, cx)?;
2783                                    if start_buffer != end_buffer {
2784                                        return None;
2785                                    }
2786                                    Some((start_buffer, start..end))
2787                                })
2788                                .collect::<Vec<_>>();
2789
2790                            for (buffer, range) in selection_ranges {
2791                                store.add_selection(buffer, range, cx);
2792                            }
2793                        })
2794                    })
2795                } else if let Some(context_editor) = panel.active_context_editor() {
2796                    let snapshot = buffer.read(cx).snapshot(cx);
2797                    let selection_ranges = selection_ranges
2798                        .into_iter()
2799                        .map(|range| range.to_point(&snapshot))
2800                        .collect::<Vec<_>>();
2801
2802                    context_editor.update(cx, |context_editor, cx| {
2803                        context_editor.quote_ranges(selection_ranges, snapshot, window, cx)
2804                    });
2805                }
2806            });
2807        });
2808    }
2809}
2810
2811const DISMISSED_TRIAL_UPSELL_KEY: &str = "dismissed-trial-upsell";
2812
2813fn dismissed_trial_upsell() -> bool {
2814    db::kvp::KEY_VALUE_STORE
2815        .read_kvp(DISMISSED_TRIAL_UPSELL_KEY)
2816        .log_err()
2817        .map_or(false, |s| s.is_some())
2818}
2819
2820fn set_trial_upsell_dismissed(is_dismissed: bool, cx: &mut App) {
2821    db::write_and_log(cx, move || async move {
2822        if is_dismissed {
2823            db::kvp::KEY_VALUE_STORE
2824                .write_kvp(DISMISSED_TRIAL_UPSELL_KEY.into(), "1".into())
2825                .await
2826        } else {
2827            db::kvp::KEY_VALUE_STORE
2828                .delete_kvp(DISMISSED_TRIAL_UPSELL_KEY.into())
2829                .await
2830        }
2831    })
2832}