agent_panel.rs

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