agent_panel.rs

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