agent_panel.rs

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