agent_panel.rs

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