agent_panel.rs

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