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