agent_panel.rs

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