agent_panel.rs

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