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