assistant_panel.rs

   1use std::path::PathBuf;
   2use std::sync::Arc;
   3use std::time::Duration;
   4
   5use anyhow::{Result, anyhow};
   6use assistant_context_editor::{
   7    AssistantPanelDelegate, ConfigurationError, ContextEditor, SlashCommandCompletionProvider,
   8    make_lsp_adapter_delegate, render_remaining_tokens,
   9};
  10use assistant_settings::{AssistantDockPosition, AssistantSettings};
  11use assistant_slash_command::SlashCommandWorkingSet;
  12use assistant_tool::ToolWorkingSet;
  13
  14use client::zed_urls;
  15use editor::{Editor, EditorEvent, MultiBuffer};
  16use fs::Fs;
  17use gpui::{
  18    Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, Corner, Entity,
  19    EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext, Pixels, Subscription, Task,
  20    UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
  21};
  22use language::LanguageRegistry;
  23use language_model::{LanguageModelProviderTosView, LanguageModelRegistry};
  24use language_model_selector::ToggleModelSelector;
  25use project::Project;
  26use prompt_library::{PromptLibrary, open_prompt_library};
  27use prompt_store::PromptBuilder;
  28use settings::{Settings, update_settings_file};
  29use time::UtcOffset;
  30use ui::{
  31    Banner, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, Tab, Tooltip, prelude::*,
  32};
  33use util::ResultExt as _;
  34use workspace::Workspace;
  35use workspace::dock::{DockPosition, Panel, PanelEvent};
  36use zed_actions::agent::OpenConfiguration;
  37use zed_actions::assistant::{OpenPromptLibrary, ToggleFocus};
  38
  39use crate::active_thread::ActiveThread;
  40use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent};
  41use crate::history_store::{HistoryEntry, HistoryStore};
  42use crate::message_editor::MessageEditor;
  43use crate::thread::{Thread, ThreadError, ThreadId, TokenUsageRatio};
  44use crate::thread_history::{PastContext, PastThread, ThreadHistory};
  45use crate::thread_store::ThreadStore;
  46use crate::{
  47    AgentDiff, ExpandMessageEditor, InlineAssistant, NewTextThread, NewThread,
  48    OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ThreadEvent, ToggleContextPicker,
  49};
  50
  51pub fn init(cx: &mut App) {
  52    cx.observe_new(
  53        |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
  54            workspace
  55                .register_action(|workspace, action: &NewThread, window, cx| {
  56                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
  57                        panel.update(cx, |panel, cx| panel.new_thread(action, window, cx));
  58                        workspace.focus_panel::<AssistantPanel>(window, cx);
  59                    }
  60                })
  61                .register_action(|workspace, _: &OpenHistory, window, cx| {
  62                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
  63                        workspace.focus_panel::<AssistantPanel>(window, cx);
  64                        panel.update(cx, |panel, cx| panel.open_history(window, cx));
  65                    }
  66                })
  67                .register_action(|workspace, _: &OpenConfiguration, window, cx| {
  68                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
  69                        workspace.focus_panel::<AssistantPanel>(window, cx);
  70                        panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
  71                    }
  72                })
  73                .register_action(|workspace, _: &NewTextThread, window, cx| {
  74                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
  75                        workspace.focus_panel::<AssistantPanel>(window, cx);
  76                        panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx));
  77                    }
  78                })
  79                .register_action(|workspace, _: &OpenPromptLibrary, window, cx| {
  80                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
  81                        workspace.focus_panel::<AssistantPanel>(window, cx);
  82                        panel.update(cx, |panel, cx| {
  83                            panel.deploy_prompt_library(&OpenPromptLibrary, window, cx)
  84                        });
  85                    }
  86                })
  87                .register_action(|workspace, _: &OpenAgentDiff, window, cx| {
  88                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
  89                        workspace.focus_panel::<AssistantPanel>(window, cx);
  90                        let thread = panel.read(cx).thread.read(cx).thread().clone();
  91                        AgentDiff::deploy_in_workspace(thread, workspace, window, cx);
  92                    }
  93                })
  94                .register_action(|workspace, _: &ExpandMessageEditor, window, cx| {
  95                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
  96                        workspace.focus_panel::<AssistantPanel>(window, cx);
  97                        panel.update(cx, |panel, cx| {
  98                            panel.message_editor.update(cx, |editor, cx| {
  99                                editor.expand_message_editor(&ExpandMessageEditor, window, cx);
 100                            });
 101                        });
 102                    }
 103                });
 104        },
 105    )
 106    .detach();
 107}
 108
 109enum ActiveView {
 110    Thread {
 111        change_title_editor: Entity<Editor>,
 112        _subscriptions: Vec<gpui::Subscription>,
 113    },
 114    PromptEditor,
 115    History,
 116    Configuration,
 117}
 118
 119impl ActiveView {
 120    pub fn thread(thread: Entity<Thread>, window: &mut Window, cx: &mut App) -> Self {
 121        let summary = thread.read(cx).summary_or_default();
 122
 123        let editor = cx.new(|cx| {
 124            let mut editor = Editor::single_line(window, cx);
 125            editor.set_text(summary, window, cx);
 126            editor
 127        });
 128
 129        let subscriptions = vec![
 130            window.subscribe(&editor, cx, {
 131                {
 132                    let thread = thread.clone();
 133                    move |editor, event, window, cx| match event {
 134                        EditorEvent::BufferEdited => {
 135                            let new_summary = editor.read(cx).text(cx);
 136
 137                            thread.update(cx, |thread, cx| {
 138                                thread.set_summary(new_summary, cx);
 139                            })
 140                        }
 141                        EditorEvent::Blurred => {
 142                            if editor.read(cx).text(cx).is_empty() {
 143                                let summary = thread.read(cx).summary_or_default();
 144
 145                                editor.update(cx, |editor, cx| {
 146                                    editor.set_text(summary, window, cx);
 147                                });
 148                            }
 149                        }
 150                        _ => {}
 151                    }
 152                }
 153            }),
 154            window.subscribe(&thread, cx, {
 155                let editor = editor.clone();
 156                move |thread, event, window, cx| match event {
 157                    ThreadEvent::SummaryGenerated => {
 158                        let summary = thread.read(cx).summary_or_default();
 159
 160                        editor.update(cx, |editor, cx| {
 161                            editor.set_text(summary, window, cx);
 162                        })
 163                    }
 164                    _ => {}
 165                }
 166            }),
 167        ];
 168
 169        Self::Thread {
 170            change_title_editor: editor,
 171            _subscriptions: subscriptions,
 172        }
 173    }
 174}
 175
 176pub struct AssistantPanel {
 177    workspace: WeakEntity<Workspace>,
 178    project: Entity<Project>,
 179    fs: Arc<dyn Fs>,
 180    language_registry: Arc<LanguageRegistry>,
 181    thread_store: Entity<ThreadStore>,
 182    thread: Entity<ActiveThread>,
 183    _thread_subscription: Subscription,
 184    message_editor: Entity<MessageEditor>,
 185    context_store: Entity<assistant_context_editor::ContextStore>,
 186    context_editor: Option<Entity<ContextEditor>>,
 187    configuration: Option<Entity<AssistantConfiguration>>,
 188    configuration_subscription: Option<Subscription>,
 189    local_timezone: UtcOffset,
 190    active_view: ActiveView,
 191    previous_view: Option<ActiveView>,
 192    history_store: Entity<HistoryStore>,
 193    history: Entity<ThreadHistory>,
 194    assistant_dropdown_menu_handle: PopoverMenuHandle<ContextMenu>,
 195    width: Option<Pixels>,
 196    height: Option<Pixels>,
 197}
 198
 199impl AssistantPanel {
 200    pub fn load(
 201        workspace: WeakEntity<Workspace>,
 202        prompt_builder: Arc<PromptBuilder>,
 203        cx: AsyncWindowContext,
 204    ) -> Task<Result<Entity<Self>>> {
 205        cx.spawn(async move |cx| {
 206            let tools = Arc::new(ToolWorkingSet::default());
 207            let thread_store = workspace
 208                .update(cx, |workspace, cx| {
 209                    let project = workspace.project().clone();
 210                    ThreadStore::load(project, tools.clone(), prompt_builder.clone(), cx)
 211                })?
 212                .await;
 213
 214            let slash_commands = Arc::new(SlashCommandWorkingSet::default());
 215            let context_store = workspace
 216                .update(cx, |workspace, cx| {
 217                    let project = workspace.project().clone();
 218                    assistant_context_editor::ContextStore::new(
 219                        project,
 220                        prompt_builder.clone(),
 221                        slash_commands,
 222                        cx,
 223                    )
 224                })?
 225                .await?;
 226
 227            workspace.update_in(cx, |workspace, window, cx| {
 228                cx.new(|cx| Self::new(workspace, thread_store, context_store, window, cx))
 229            })
 230        })
 231    }
 232
 233    fn new(
 234        workspace: &Workspace,
 235        thread_store: Entity<ThreadStore>,
 236        context_store: Entity<assistant_context_editor::ContextStore>,
 237        window: &mut Window,
 238        cx: &mut Context<Self>,
 239    ) -> Self {
 240        let thread = thread_store.update(cx, |this, cx| this.create_thread(cx));
 241        let fs = workspace.app_state().fs.clone();
 242        let project = workspace.project();
 243        let language_registry = project.read(cx).languages().clone();
 244        let workspace = workspace.weak_handle();
 245        let weak_self = cx.entity().downgrade();
 246
 247        let message_editor_context_store = cx.new(|_cx| {
 248            crate::context_store::ContextStore::new(
 249                project.downgrade(),
 250                Some(thread_store.downgrade()),
 251            )
 252        });
 253
 254        let message_editor = cx.new(|cx| {
 255            MessageEditor::new(
 256                fs.clone(),
 257                workspace.clone(),
 258                message_editor_context_store.clone(),
 259                thread_store.downgrade(),
 260                thread.clone(),
 261                window,
 262                cx,
 263            )
 264        });
 265
 266        let history_store =
 267            cx.new(|cx| HistoryStore::new(thread_store.clone(), context_store.clone(), cx));
 268
 269        cx.observe(&history_store, |_, _, cx| cx.notify()).detach();
 270
 271        let active_view = ActiveView::thread(thread.clone(), window, cx);
 272        let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
 273            if let ThreadEvent::MessageAdded(_) = &event {
 274                // needed to leave empty state
 275                cx.notify();
 276            }
 277        });
 278        let thread = cx.new(|cx| {
 279            ActiveThread::new(
 280                thread.clone(),
 281                thread_store.clone(),
 282                language_registry.clone(),
 283                message_editor_context_store.clone(),
 284                workspace.clone(),
 285                window,
 286                cx,
 287            )
 288        });
 289
 290        Self {
 291            active_view,
 292            workspace,
 293            project: project.clone(),
 294            fs: fs.clone(),
 295            language_registry,
 296            thread_store: thread_store.clone(),
 297            thread,
 298            _thread_subscription: thread_subscription,
 299            message_editor,
 300            context_store,
 301            context_editor: None,
 302            configuration: None,
 303            configuration_subscription: None,
 304            local_timezone: UtcOffset::from_whole_seconds(
 305                chrono::Local::now().offset().local_minus_utc(),
 306            )
 307            .unwrap(),
 308            previous_view: None,
 309            history_store: history_store.clone(),
 310            history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)),
 311            assistant_dropdown_menu_handle: PopoverMenuHandle::default(),
 312            width: None,
 313            height: None,
 314        }
 315    }
 316
 317    pub fn toggle_focus(
 318        workspace: &mut Workspace,
 319        _: &ToggleFocus,
 320        window: &mut Window,
 321        cx: &mut Context<Workspace>,
 322    ) {
 323        if workspace
 324            .panel::<Self>(cx)
 325            .is_some_and(|panel| panel.read(cx).enabled(cx))
 326        {
 327            workspace.toggle_panel_focus::<Self>(window, cx);
 328        }
 329    }
 330
 331    pub(crate) fn local_timezone(&self) -> UtcOffset {
 332        self.local_timezone
 333    }
 334
 335    pub(crate) fn thread_store(&self) -> &Entity<ThreadStore> {
 336        &self.thread_store
 337    }
 338
 339    fn cancel(
 340        &mut self,
 341        _: &editor::actions::Cancel,
 342        _window: &mut Window,
 343        cx: &mut Context<Self>,
 344    ) {
 345        self.thread
 346            .update(cx, |thread, cx| thread.cancel_last_completion(cx));
 347    }
 348
 349    fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {
 350        let thread = self
 351            .thread_store
 352            .update(cx, |this, cx| this.create_thread(cx));
 353
 354        let thread_view = ActiveView::thread(thread.clone(), window, cx);
 355        self.set_active_view(thread_view, window, cx);
 356
 357        let message_editor_context_store = cx.new(|_cx| {
 358            crate::context_store::ContextStore::new(
 359                self.project.downgrade(),
 360                Some(self.thread_store.downgrade()),
 361            )
 362        });
 363
 364        if let Some(other_thread_id) = action.from_thread_id.clone() {
 365            let other_thread_task = self
 366                .thread_store
 367                .update(cx, |this, cx| this.open_thread(&other_thread_id, cx));
 368
 369            cx.spawn({
 370                let context_store = message_editor_context_store.clone();
 371
 372                async move |_panel, cx| {
 373                    let other_thread = other_thread_task.await?;
 374
 375                    context_store.update(cx, |this, cx| {
 376                        this.add_thread(other_thread, false, cx);
 377                    })?;
 378                    anyhow::Ok(())
 379                }
 380            })
 381            .detach_and_log_err(cx);
 382        }
 383
 384        self.thread = cx.new(|cx| {
 385            ActiveThread::new(
 386                thread.clone(),
 387                self.thread_store.clone(),
 388                self.language_registry.clone(),
 389                message_editor_context_store.clone(),
 390                self.workspace.clone(),
 391                window,
 392                cx,
 393            )
 394        });
 395
 396        self._thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
 397            if let ThreadEvent::MessageAdded(_) = &event {
 398                // needed to leave empty state
 399                cx.notify();
 400            }
 401        });
 402
 403        self.message_editor = cx.new(|cx| {
 404            MessageEditor::new(
 405                self.fs.clone(),
 406                self.workspace.clone(),
 407                message_editor_context_store,
 408                self.thread_store.downgrade(),
 409                thread,
 410                window,
 411                cx,
 412            )
 413        });
 414        self.message_editor.focus_handle(cx).focus(window);
 415    }
 416
 417    fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 418        self.set_active_view(ActiveView::PromptEditor, window, cx);
 419
 420        let context = self
 421            .context_store
 422            .update(cx, |context_store, cx| context_store.create(cx));
 423        let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
 424            .log_err()
 425            .flatten();
 426
 427        self.context_editor = Some(cx.new(|cx| {
 428            let mut editor = ContextEditor::for_context(
 429                context,
 430                self.fs.clone(),
 431                self.workspace.clone(),
 432                self.project.clone(),
 433                lsp_adapter_delegate,
 434                window,
 435                cx,
 436            );
 437            editor.insert_default_prompt(window, cx);
 438            editor
 439        }));
 440
 441        if let Some(context_editor) = self.context_editor.as_ref() {
 442            context_editor.focus_handle(cx).focus(window);
 443        }
 444    }
 445
 446    fn deploy_prompt_library(
 447        &mut self,
 448        _: &OpenPromptLibrary,
 449        _window: &mut Window,
 450        cx: &mut Context<Self>,
 451    ) {
 452        open_prompt_library(
 453            self.language_registry.clone(),
 454            Box::new(PromptLibraryInlineAssist::new(self.workspace.clone())),
 455            Arc::new(|| {
 456                Box::new(SlashCommandCompletionProvider::new(
 457                    Arc::new(SlashCommandWorkingSet::default()),
 458                    None,
 459                    None,
 460                ))
 461            }),
 462            cx,
 463        )
 464        .detach_and_log_err(cx);
 465    }
 466
 467    fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 468        if matches!(self.active_view, ActiveView::History) {
 469            if let Some(previous_view) = self.previous_view.take() {
 470                self.set_active_view(previous_view, window, cx);
 471            }
 472        } else {
 473            self.thread_store
 474                .update(cx, |thread_store, cx| thread_store.reload(cx))
 475                .detach_and_log_err(cx);
 476            self.set_active_view(ActiveView::History, window, cx);
 477        }
 478        cx.notify();
 479    }
 480
 481    pub(crate) fn open_saved_prompt_editor(
 482        &mut self,
 483        path: PathBuf,
 484        window: &mut Window,
 485        cx: &mut Context<Self>,
 486    ) -> Task<Result<()>> {
 487        let context = self
 488            .context_store
 489            .update(cx, |store, cx| store.open_local_context(path.clone(), cx));
 490        let fs = self.fs.clone();
 491        let project = self.project.clone();
 492        let workspace = self.workspace.clone();
 493
 494        let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err().flatten();
 495
 496        cx.spawn_in(window, async move |this, cx| {
 497            let context = context.await?;
 498            this.update_in(cx, |this, window, cx| {
 499                let editor = cx.new(|cx| {
 500                    ContextEditor::for_context(
 501                        context,
 502                        fs,
 503                        workspace,
 504                        project,
 505                        lsp_adapter_delegate,
 506                        window,
 507                        cx,
 508                    )
 509                });
 510                this.set_active_view(ActiveView::PromptEditor, window, cx);
 511                this.context_editor = Some(editor);
 512
 513                anyhow::Ok(())
 514            })??;
 515            Ok(())
 516        })
 517    }
 518
 519    pub(crate) fn open_thread(
 520        &mut self,
 521        thread_id: &ThreadId,
 522        window: &mut Window,
 523        cx: &mut Context<Self>,
 524    ) -> Task<Result<()>> {
 525        let open_thread_task = self
 526            .thread_store
 527            .update(cx, |this, cx| this.open_thread(thread_id, cx));
 528
 529        cx.spawn_in(window, async move |this, cx| {
 530            let thread = open_thread_task.await?;
 531            this.update_in(cx, |this, window, cx| {
 532                let thread_view = ActiveView::thread(thread.clone(), window, cx);
 533                this.set_active_view(thread_view, window, cx);
 534                let message_editor_context_store = cx.new(|_cx| {
 535                    crate::context_store::ContextStore::new(
 536                        this.project.downgrade(),
 537                        Some(this.thread_store.downgrade()),
 538                    )
 539                });
 540                this.thread = cx.new(|cx| {
 541                    ActiveThread::new(
 542                        thread.clone(),
 543                        this.thread_store.clone(),
 544                        this.language_registry.clone(),
 545                        message_editor_context_store.clone(),
 546                        this.workspace.clone(),
 547                        window,
 548                        cx,
 549                    )
 550                });
 551                this.message_editor = cx.new(|cx| {
 552                    MessageEditor::new(
 553                        this.fs.clone(),
 554                        this.workspace.clone(),
 555                        message_editor_context_store,
 556                        this.thread_store.downgrade(),
 557                        thread,
 558                        window,
 559                        cx,
 560                    )
 561                });
 562                this.message_editor.focus_handle(cx).focus(window);
 563            })
 564        })
 565    }
 566
 567    pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
 568        match self.active_view {
 569            ActiveView::Configuration | ActiveView::History => {
 570                self.active_view =
 571                    ActiveView::thread(self.thread.read(cx).thread().clone(), window, cx);
 572                cx.notify();
 573            }
 574            _ => {}
 575        }
 576    }
 577
 578    pub fn open_agent_diff(
 579        &mut self,
 580        _: &OpenAgentDiff,
 581        window: &mut Window,
 582        cx: &mut Context<Self>,
 583    ) {
 584        let thread = self.thread.read(cx).thread().clone();
 585        self.workspace
 586            .update(cx, |workspace, cx| {
 587                AgentDiff::deploy_in_workspace(thread, workspace, window, cx)
 588            })
 589            .log_err();
 590    }
 591
 592    pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 593        let context_server_manager = self.thread_store.read(cx).context_server_manager();
 594        let tools = self.thread_store.read(cx).tools();
 595        let fs = self.fs.clone();
 596
 597        self.set_active_view(ActiveView::Configuration, window, cx);
 598        self.configuration =
 599            Some(cx.new(|cx| {
 600                AssistantConfiguration::new(fs, context_server_manager, tools, window, cx)
 601            }));
 602
 603        if let Some(configuration) = self.configuration.as_ref() {
 604            self.configuration_subscription = Some(cx.subscribe_in(
 605                configuration,
 606                window,
 607                Self::handle_assistant_configuration_event,
 608            ));
 609
 610            configuration.focus_handle(cx).focus(window);
 611        }
 612    }
 613
 614    pub(crate) fn open_active_thread_as_markdown(
 615        &mut self,
 616        _: &OpenActiveThreadAsMarkdown,
 617        window: &mut Window,
 618        cx: &mut Context<Self>,
 619    ) {
 620        let Some(workspace) = self
 621            .workspace
 622            .upgrade()
 623            .ok_or_else(|| anyhow!("workspace dropped"))
 624            .log_err()
 625        else {
 626            return;
 627        };
 628
 629        let markdown_language_task = workspace
 630            .read(cx)
 631            .app_state()
 632            .languages
 633            .language_for_name("Markdown");
 634        let thread = self.active_thread(cx);
 635        cx.spawn_in(window, async move |_this, cx| {
 636            let markdown_language = markdown_language_task.await?;
 637
 638            workspace.update_in(cx, |workspace, window, cx| {
 639                let thread = thread.read(cx);
 640                let markdown = thread.to_markdown(cx)?;
 641                let thread_summary = thread
 642                    .summary()
 643                    .map(|summary| summary.to_string())
 644                    .unwrap_or_else(|| "Thread".to_string());
 645
 646                let project = workspace.project().clone();
 647                let buffer = project.update(cx, |project, cx| {
 648                    project.create_local_buffer(&markdown, Some(markdown_language), cx)
 649                });
 650                let buffer = cx.new(|cx| {
 651                    MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone())
 652                });
 653
 654                workspace.add_item_to_active_pane(
 655                    Box::new(cx.new(|cx| {
 656                        let mut editor =
 657                            Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
 658                        editor.set_breadcrumb_header(thread_summary);
 659                        editor
 660                    })),
 661                    None,
 662                    true,
 663                    window,
 664                    cx,
 665                );
 666
 667                anyhow::Ok(())
 668            })
 669        })
 670        .detach_and_log_err(cx);
 671    }
 672
 673    fn handle_assistant_configuration_event(
 674        &mut self,
 675        _entity: &Entity<AssistantConfiguration>,
 676        event: &AssistantConfigurationEvent,
 677        window: &mut Window,
 678        cx: &mut Context<Self>,
 679    ) {
 680        match event {
 681            AssistantConfigurationEvent::NewThread(provider) => {
 682                if LanguageModelRegistry::read_global(cx)
 683                    .default_model()
 684                    .map_or(true, |model| model.provider.id() != provider.id())
 685                {
 686                    if let Some(model) = provider.default_model(cx) {
 687                        update_settings_file::<AssistantSettings>(
 688                            self.fs.clone(),
 689                            cx,
 690                            move |settings, _| settings.set_model(model),
 691                        );
 692                    }
 693                }
 694
 695                self.new_thread(&NewThread::default(), window, cx);
 696            }
 697        }
 698    }
 699
 700    pub(crate) fn active_thread(&self, cx: &App) -> Entity<Thread> {
 701        self.thread.read(cx).thread().clone()
 702    }
 703
 704    pub(crate) fn delete_thread(
 705        &mut self,
 706        thread_id: &ThreadId,
 707        cx: &mut Context<Self>,
 708    ) -> Task<Result<()>> {
 709        self.thread_store
 710            .update(cx, |this, cx| this.delete_thread(thread_id, cx))
 711    }
 712
 713    pub(crate) fn active_context_editor(&self) -> Option<Entity<ContextEditor>> {
 714        self.context_editor.clone()
 715    }
 716
 717    pub(crate) fn delete_context(
 718        &mut self,
 719        path: PathBuf,
 720        cx: &mut Context<Self>,
 721    ) -> Task<Result<()>> {
 722        self.context_store
 723            .update(cx, |this, cx| this.delete_local_context(path, cx))
 724    }
 725
 726    fn set_active_view(
 727        &mut self,
 728        new_view: ActiveView,
 729        window: &mut Window,
 730        cx: &mut Context<Self>,
 731    ) {
 732        let current_is_history = matches!(self.active_view, ActiveView::History);
 733        let new_is_history = matches!(new_view, ActiveView::History);
 734
 735        if current_is_history && !new_is_history {
 736            self.active_view = new_view;
 737        } else if !current_is_history && new_is_history {
 738            self.previous_view = Some(std::mem::replace(&mut self.active_view, new_view));
 739        } else {
 740            if !new_is_history {
 741                self.previous_view = None;
 742            }
 743            self.active_view = new_view;
 744        }
 745
 746        self.focus_handle(cx).focus(window);
 747    }
 748}
 749
 750impl Focusable for AssistantPanel {
 751    fn focus_handle(&self, cx: &App) -> FocusHandle {
 752        match self.active_view {
 753            ActiveView::Thread { .. } => self.message_editor.focus_handle(cx),
 754            ActiveView::History => self.history.focus_handle(cx),
 755            ActiveView::PromptEditor => {
 756                if let Some(context_editor) = self.context_editor.as_ref() {
 757                    context_editor.focus_handle(cx)
 758                } else {
 759                    cx.focus_handle()
 760                }
 761            }
 762            ActiveView::Configuration => {
 763                if let Some(configuration) = self.configuration.as_ref() {
 764                    configuration.focus_handle(cx)
 765                } else {
 766                    cx.focus_handle()
 767                }
 768            }
 769        }
 770    }
 771}
 772
 773impl EventEmitter<PanelEvent> for AssistantPanel {}
 774
 775impl Panel for AssistantPanel {
 776    fn persistent_name() -> &'static str {
 777        "AgentPanel"
 778    }
 779
 780    fn position(&self, _window: &Window, cx: &App) -> DockPosition {
 781        match AssistantSettings::get_global(cx).dock {
 782            AssistantDockPosition::Left => DockPosition::Left,
 783            AssistantDockPosition::Bottom => DockPosition::Bottom,
 784            AssistantDockPosition::Right => DockPosition::Right,
 785        }
 786    }
 787
 788    fn position_is_valid(&self, _: DockPosition) -> bool {
 789        true
 790    }
 791
 792    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
 793        settings::update_settings_file::<AssistantSettings>(
 794            self.fs.clone(),
 795            cx,
 796            move |settings, _| {
 797                let dock = match position {
 798                    DockPosition::Left => AssistantDockPosition::Left,
 799                    DockPosition::Bottom => AssistantDockPosition::Bottom,
 800                    DockPosition::Right => AssistantDockPosition::Right,
 801                };
 802                settings.set_dock(dock);
 803            },
 804        );
 805    }
 806
 807    fn size(&self, window: &Window, cx: &App) -> Pixels {
 808        let settings = AssistantSettings::get_global(cx);
 809        match self.position(window, cx) {
 810            DockPosition::Left | DockPosition::Right => {
 811                self.width.unwrap_or(settings.default_width)
 812            }
 813            DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
 814        }
 815    }
 816
 817    fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
 818        match self.position(window, cx) {
 819            DockPosition::Left | DockPosition::Right => self.width = size,
 820            DockPosition::Bottom => self.height = size,
 821        }
 822        cx.notify();
 823    }
 824
 825    fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
 826
 827    fn remote_id() -> Option<proto::PanelId> {
 828        Some(proto::PanelId::AssistantPanel)
 829    }
 830
 831    fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
 832        (self.enabled(cx) && AssistantSettings::get_global(cx).button)
 833            .then_some(IconName::ZedAssistant)
 834    }
 835
 836    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
 837        Some("Agent Panel")
 838    }
 839
 840    fn toggle_action(&self) -> Box<dyn Action> {
 841        Box::new(ToggleFocus)
 842    }
 843
 844    fn activation_priority(&self) -> u32 {
 845        3
 846    }
 847
 848    fn enabled(&self, cx: &App) -> bool {
 849        AssistantSettings::get_global(cx).enabled
 850    }
 851}
 852
 853impl AssistantPanel {
 854    fn render_title_view(&self, _window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
 855        const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
 856
 857        let content = match &self.active_view {
 858            ActiveView::Thread {
 859                change_title_editor,
 860                ..
 861            } => {
 862                let active_thread = self.thread.read(cx);
 863                let is_empty = active_thread.is_empty();
 864
 865                let summary = active_thread.summary(cx);
 866
 867                if is_empty {
 868                    Label::new(Thread::DEFAULT_SUMMARY.clone())
 869                        .truncate()
 870                        .ml_2()
 871                        .into_any_element()
 872                } else if summary.is_none() {
 873                    Label::new(LOADING_SUMMARY_PLACEHOLDER)
 874                        .ml_2()
 875                        .truncate()
 876                        .into_any_element()
 877                } else {
 878                    div()
 879                        .ml_2()
 880                        .w_full()
 881                        .child(change_title_editor.clone())
 882                        .into_any_element()
 883                }
 884            }
 885            ActiveView::PromptEditor => {
 886                let title = self
 887                    .context_editor
 888                    .as_ref()
 889                    .map(|context_editor| {
 890                        SharedString::from(context_editor.read(cx).title(cx).to_string())
 891                    })
 892                    .unwrap_or_else(|| SharedString::from(LOADING_SUMMARY_PLACEHOLDER));
 893
 894                Label::new(title).ml_2().truncate().into_any_element()
 895            }
 896            ActiveView::History => Label::new("History").truncate().into_any_element(),
 897            ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
 898        };
 899
 900        h_flex()
 901            .key_context("TitleEditor")
 902            .id("TitleEditor")
 903            .flex_grow()
 904            .w_full()
 905            .max_w_full()
 906            .overflow_x_scroll()
 907            .child(content)
 908            .into_any()
 909    }
 910
 911    fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 912        let active_thread = self.thread.read(cx);
 913        let thread = active_thread.thread().read(cx);
 914        let token_usage = thread.total_token_usage(cx);
 915        let thread_id = thread.id().clone();
 916
 917        let is_generating = thread.is_generating();
 918        let is_empty = active_thread.is_empty();
 919        let focus_handle = self.focus_handle(cx);
 920
 921        let is_history = matches!(self.active_view, ActiveView::History);
 922
 923        let show_token_count = match &self.active_view {
 924            ActiveView::Thread { .. } => !is_empty,
 925            ActiveView::PromptEditor => self.context_editor.is_some(),
 926            _ => false,
 927        };
 928
 929        let go_back_button = match &self.active_view {
 930            ActiveView::History | ActiveView::Configuration => Some(
 931                div().pl_1().child(
 932                    IconButton::new("go-back", IconName::ArrowLeft)
 933                        .icon_size(IconSize::Small)
 934                        .on_click(cx.listener(|this, _, window, cx| {
 935                            this.go_back(&workspace::GoBack, window, cx);
 936                        }))
 937                        .tooltip({
 938                            let focus_handle = focus_handle.clone();
 939                            move |window, cx| {
 940                                Tooltip::for_action_in(
 941                                    "Go Back",
 942                                    &workspace::GoBack,
 943                                    &focus_handle,
 944                                    window,
 945                                    cx,
 946                                )
 947                            }
 948                        }),
 949                ),
 950            ),
 951            _ => None,
 952        };
 953
 954        h_flex()
 955            .id("assistant-toolbar")
 956            .h(Tab::container_height(cx))
 957            .max_w_full()
 958            .flex_none()
 959            .justify_between()
 960            .gap_2()
 961            .bg(cx.theme().colors().tab_bar_background)
 962            .border_b_1()
 963            .border_color(cx.theme().colors().border)
 964            .child(
 965                h_flex()
 966                    .w_full()
 967                    .gap_1()
 968                    .children(go_back_button)
 969                    .child(self.render_title_view(window, cx)),
 970            )
 971            .child(
 972                h_flex()
 973                    .h_full()
 974                    .gap_2()
 975                    .when(show_token_count, |parent| match self.active_view {
 976                        ActiveView::Thread { .. } => {
 977                            if token_usage.total == 0 {
 978                                return parent;
 979                            }
 980
 981                            let token_color = match token_usage.ratio {
 982                                TokenUsageRatio::Normal => Color::Muted,
 983                                TokenUsageRatio::Warning => Color::Warning,
 984                                TokenUsageRatio::Exceeded => Color::Error,
 985                            };
 986
 987                            parent.child(
 988                                h_flex()
 989                                    .flex_shrink_0()
 990                                    .gap_0p5()
 991                                    .child(
 992                                        Label::new(assistant_context_editor::humanize_token_count(
 993                                            token_usage.total,
 994                                        ))
 995                                        .size(LabelSize::Small)
 996                                        .color(token_color)
 997                                        .map(|label| {
 998                                            if is_generating {
 999                                                label
1000                                                    .with_animation(
1001                                                        "used-tokens-label",
1002                                                        Animation::new(Duration::from_secs(2))
1003                                                            .repeat()
1004                                                            .with_easing(pulsating_between(
1005                                                                0.6, 1.,
1006                                                            )),
1007                                                        |label, delta| label.alpha(delta),
1008                                                    )
1009                                                    .into_any()
1010                                            } else {
1011                                                label.into_any_element()
1012                                            }
1013                                        }),
1014                                    )
1015                                    .child(
1016                                        Label::new("/").size(LabelSize::Small).color(Color::Muted),
1017                                    )
1018                                    .child(
1019                                        Label::new(assistant_context_editor::humanize_token_count(
1020                                            token_usage.max,
1021                                        ))
1022                                        .size(LabelSize::Small)
1023                                        .color(Color::Muted),
1024                                    ),
1025                            )
1026                        }
1027                        ActiveView::PromptEditor => {
1028                            let Some(editor) = self.context_editor.as_ref() else {
1029                                return parent;
1030                            };
1031                            let Some(element) = render_remaining_tokens(editor, cx) else {
1032                                return parent;
1033                            };
1034                            parent.child(element)
1035                        }
1036                        _ => parent,
1037                    })
1038                    .child(
1039                        h_flex()
1040                            .h_full()
1041                            .gap(DynamicSpacing::Base02.rems(cx))
1042                            .px(DynamicSpacing::Base08.rems(cx))
1043                            .border_l_1()
1044                            .border_color(cx.theme().colors().border)
1045                            .child(
1046                                IconButton::new("new", IconName::Plus)
1047                                    .icon_size(IconSize::Small)
1048                                    .style(ButtonStyle::Subtle)
1049                                    .tooltip(move |window, cx| {
1050                                        Tooltip::for_action_in(
1051                                            "New Thread",
1052                                            &NewThread::default(),
1053                                            &focus_handle,
1054                                            window,
1055                                            cx,
1056                                        )
1057                                    })
1058                                    .on_click(move |_event, window, cx| {
1059                                        window.dispatch_action(
1060                                            NewThread::default().boxed_clone(),
1061                                            cx,
1062                                        );
1063                                    }),
1064                            )
1065                            .child(
1066                                IconButton::new("open-history", IconName::HistoryRerun)
1067                                    .icon_size(IconSize::Small)
1068                                    .toggle_state(is_history)
1069                                    .selected_icon_color(Color::Accent)
1070                                    .tooltip({
1071                                        let focus_handle = self.focus_handle(cx);
1072                                        move |window, cx| {
1073                                            Tooltip::for_action_in(
1074                                                "History",
1075                                                &OpenHistory,
1076                                                &focus_handle,
1077                                                window,
1078                                                cx,
1079                                            )
1080                                        }
1081                                    })
1082                                    .on_click(move |_event, window, cx| {
1083                                        window.dispatch_action(OpenHistory.boxed_clone(), cx);
1084                                    }),
1085                            )
1086                            .child(
1087                                PopoverMenu::new("assistant-menu")
1088                                    .trigger_with_tooltip(
1089                                        IconButton::new("new", IconName::Ellipsis)
1090                                            .icon_size(IconSize::Small)
1091                                            .style(ButtonStyle::Subtle),
1092                                        Tooltip::text("Toggle Agent Menu"),
1093                                    )
1094                                    .anchor(Corner::TopRight)
1095                                    .with_handle(self.assistant_dropdown_menu_handle.clone())
1096                                    .menu(move |window, cx| {
1097                                        Some(ContextMenu::build(
1098                                            window,
1099                                            cx,
1100                                            |menu, _window, _cx| {
1101                                                menu
1102                                                    .when(!is_empty, |menu| {
1103                                                        menu.action(
1104                                                            "Start New From Summary",
1105                                                            Box::new(NewThread {
1106                                                                from_thread_id: Some(thread_id.clone()),
1107                                                            }),
1108                                                        ).separator()
1109                                                    })
1110                                                    .action(
1111                                                    "New Text Thread",
1112                                                    NewTextThread.boxed_clone(),
1113                                                )
1114                                                .action("Settings", OpenConfiguration.boxed_clone())
1115                                                .separator()
1116                                                .action(
1117                                                    "Install MCPs",
1118                                                    zed_actions::Extensions {
1119                                                        category_filter: Some(
1120                                                            zed_actions::ExtensionCategoryFilter::ContextServers,
1121                                                        ),
1122                                                    }
1123                                                    .boxed_clone(),
1124                                                )
1125                                            },
1126                                        ))
1127                                    }),
1128                            ),
1129                    ),
1130            )
1131    }
1132
1133    fn render_active_thread_or_empty_state(
1134        &self,
1135        window: &mut Window,
1136        cx: &mut Context<Self>,
1137    ) -> AnyElement {
1138        if self.thread.read(cx).is_empty() {
1139            return self
1140                .render_thread_empty_state(window, cx)
1141                .into_any_element();
1142        }
1143
1144        self.thread.clone().into_any_element()
1145    }
1146
1147    fn configuration_error(&self, cx: &App) -> Option<ConfigurationError> {
1148        let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
1149            return Some(ConfigurationError::NoProvider);
1150        };
1151
1152        if !model.provider.is_authenticated(cx) {
1153            return Some(ConfigurationError::ProviderNotAuthenticated);
1154        }
1155
1156        if model.provider.must_accept_terms(cx) {
1157            return Some(ConfigurationError::ProviderPendingTermsAcceptance(
1158                model.provider,
1159            ));
1160        }
1161
1162        None
1163    }
1164
1165    fn render_thread_empty_state(
1166        &self,
1167        window: &mut Window,
1168        cx: &mut Context<Self>,
1169    ) -> impl IntoElement {
1170        let recent_history = self
1171            .history_store
1172            .update(cx, |this, cx| this.recent_entries(6, cx));
1173
1174        let configuration_error = self.configuration_error(cx);
1175        let no_error = configuration_error.is_none();
1176        let focus_handle = self.focus_handle(cx);
1177
1178        v_flex()
1179            .size_full()
1180            .when(recent_history.is_empty(), |this| {
1181                let configuration_error_ref = &configuration_error;
1182                this.child(
1183                    v_flex()
1184                        .size_full()
1185                        .max_w_80()
1186                        .mx_auto()
1187                        .justify_center()
1188                        .items_center()
1189                        .gap_1()
1190                        .child(
1191                            h_flex().child(
1192                                Headline::new("Welcome to the Agent Panel")
1193                            ),
1194                        )
1195                        .when(no_error, |parent| {
1196                            parent
1197                                .child(
1198                                    h_flex().child(
1199                                        Label::new("Ask and build anything.")
1200                                            .color(Color::Muted)
1201                                            .mb_2p5(),
1202                                    ),
1203                                )
1204                                .child(
1205                                    Button::new("new-thread", "Start New Thread")
1206                                        .icon(IconName::Plus)
1207                                        .icon_position(IconPosition::Start)
1208                                        .icon_size(IconSize::Small)
1209                                        .icon_color(Color::Muted)
1210                                        .full_width()
1211                                        .key_binding(KeyBinding::for_action_in(
1212                                            &NewThread::default(),
1213                                            &focus_handle,
1214                                            window,
1215                                            cx,
1216                                        ))
1217                                        .on_click(|_event, window, cx| {
1218                                            window.dispatch_action(NewThread::default().boxed_clone(), cx)
1219                                        }),
1220                                )
1221                                .child(
1222                                    Button::new("context", "Add Context")
1223                                        .icon(IconName::FileCode)
1224                                        .icon_position(IconPosition::Start)
1225                                        .icon_size(IconSize::Small)
1226                                        .icon_color(Color::Muted)
1227                                        .full_width()
1228                                        .key_binding(KeyBinding::for_action_in(
1229                                            &ToggleContextPicker,
1230                                            &focus_handle,
1231                                            window,
1232                                            cx,
1233                                        ))
1234                                        .on_click(|_event, window, cx| {
1235                                            window.dispatch_action(ToggleContextPicker.boxed_clone(), cx)
1236                                        }),
1237                                )
1238                                .child(
1239                                    Button::new("mode", "Switch Model")
1240                                        .icon(IconName::DatabaseZap)
1241                                        .icon_position(IconPosition::Start)
1242                                        .icon_size(IconSize::Small)
1243                                        .icon_color(Color::Muted)
1244                                        .full_width()
1245                                        .key_binding(KeyBinding::for_action_in(
1246                                            &ToggleModelSelector,
1247                                            &focus_handle,
1248                                            window,
1249                                            cx,
1250                                        ))
1251                                        .on_click(|_event, window, cx| {
1252                                            window.dispatch_action(ToggleModelSelector.boxed_clone(), cx)
1253                                        }),
1254                                )
1255                                .child(
1256                                    Button::new("settings", "View Settings")
1257                                        .icon(IconName::Settings)
1258                                        .icon_position(IconPosition::Start)
1259                                        .icon_size(IconSize::Small)
1260                                        .icon_color(Color::Muted)
1261                                        .full_width()
1262                                        .key_binding(KeyBinding::for_action_in(
1263                                            &OpenConfiguration,
1264                                            &focus_handle,
1265                                            window,
1266                                            cx,
1267                                        ))
1268                                        .on_click(|_event, window, cx| {
1269                                            window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
1270                                        }),
1271                                )
1272                        })
1273                        .map(|parent| {
1274                            match configuration_error_ref {
1275                                Some(ConfigurationError::ProviderNotAuthenticated)
1276                                | Some(ConfigurationError::NoProvider) => {
1277                                    parent
1278                                        .child(
1279                                            h_flex().child(
1280                                                Label::new("To start using the agent, configure at least one LLM provider.")
1281                                                    .color(Color::Muted)
1282                                                    .mb_2p5()
1283                                            )
1284                                        )
1285                                        .child(
1286                                            Button::new("settings", "Configure a Provider")
1287                                                .icon(IconName::Settings)
1288                                                .icon_position(IconPosition::Start)
1289                                                .icon_size(IconSize::Small)
1290                                                .icon_color(Color::Muted)
1291                                                .full_width()
1292                                                .key_binding(KeyBinding::for_action_in(
1293                                                    &OpenConfiguration,
1294                                                    &focus_handle,
1295                                                    window,
1296                                                    cx,
1297                                                ))
1298                                                .on_click(|_event, window, cx| {
1299                                                    window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
1300                                                }),
1301                                        )
1302                                }
1303                                Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
1304                                    parent.children(
1305                                        provider.render_accept_terms(
1306                                            LanguageModelProviderTosView::ThreadFreshStart,
1307                                            cx,
1308                                        ),
1309                                    )
1310                                }
1311                                None => parent,
1312                            }
1313                        })
1314                )
1315            })
1316            .when(!recent_history.is_empty(), |parent| {
1317                let focus_handle = focus_handle.clone();
1318                let configuration_error_ref = &configuration_error;
1319
1320                parent
1321                    .overflow_hidden()
1322                    .p_1p5()
1323                    .justify_end()
1324                    .gap_1()
1325                    .child(
1326                        h_flex()
1327                            .pl_1p5()
1328                            .pb_1()
1329                            .w_full()
1330                            .justify_between()
1331                            .border_b_1()
1332                            .border_color(cx.theme().colors().border_variant)
1333                            .child(
1334                                Label::new("Past Interactions")
1335                                    .size(LabelSize::Small)
1336                                    .color(Color::Muted),
1337                            )
1338                            .child(
1339                                Button::new("view-history", "View All")
1340                                    .style(ButtonStyle::Subtle)
1341                                    .label_size(LabelSize::Small)
1342                                    .key_binding(
1343                                        KeyBinding::for_action_in(
1344                                            &OpenHistory,
1345                                            &self.focus_handle(cx),
1346                                            window,
1347                                            cx,
1348                                        ).map(|kb| kb.size(rems_from_px(12.))),
1349                                    )
1350                                    .on_click(move |_event, window, cx| {
1351                                        window.dispatch_action(OpenHistory.boxed_clone(), cx);
1352                                    }),
1353                            ),
1354                    )
1355                    .child(
1356                        v_flex()
1357                            .gap_1()
1358                            .children(
1359                                recent_history.into_iter().map(|entry| {
1360                                    // TODO: Add keyboard navigation.
1361                                    match entry {
1362                                        HistoryEntry::Thread(thread) => {
1363                                            PastThread::new(thread, cx.entity().downgrade(), false, vec![])
1364                                                .into_any_element()
1365                                        }
1366                                        HistoryEntry::Context(context) => {
1367                                            PastContext::new(context, cx.entity().downgrade(), false, vec![])
1368                                                .into_any_element()
1369                                        }
1370                                    }
1371                                }),
1372                            )
1373                    )
1374                    .map(|parent| {
1375                        match configuration_error_ref {
1376                            Some(ConfigurationError::ProviderNotAuthenticated)
1377                            | Some(ConfigurationError::NoProvider) => {
1378                                parent
1379                                    .child(
1380                                        Banner::new()
1381                                            .severity(ui::Severity::Warning)
1382                                            .children(
1383                                                Label::new(
1384                                                    "Configure at least one LLM provider to start using the panel.",
1385                                                )
1386                                                .size(LabelSize::Small),
1387                                            )
1388                                            .action_slot(
1389                                                Button::new("settings", "Configure Provider")
1390                                                    .style(ButtonStyle::Tinted(ui::TintColor::Warning))
1391                                                    .label_size(LabelSize::Small)
1392                                                    .key_binding(
1393                                                        KeyBinding::for_action_in(
1394                                                            &OpenConfiguration,
1395                                                            &focus_handle,
1396                                                            window,
1397                                                            cx,
1398                                                        )
1399                                                        .map(|kb| kb.size(rems_from_px(12.))),
1400                                                    )
1401                                                    .on_click(|_event, window, cx| {
1402                                                        window.dispatch_action(
1403                                                            OpenConfiguration.boxed_clone(),
1404                                                            cx,
1405                                                        )
1406                                                    }),
1407                                            ),
1408                                    )
1409                            }
1410                            Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
1411                                parent
1412                                    .child(
1413                                        Banner::new()
1414                                            .severity(ui::Severity::Warning)
1415                                            .children(
1416                                                h_flex()
1417                                                    .w_full()
1418                                                    .children(
1419                                                        provider.render_accept_terms(
1420                                                            LanguageModelProviderTosView::ThreadtEmptyState,
1421                                                            cx,
1422                                                        ),
1423                                                    ),
1424                                            ),
1425                                    )
1426                            }
1427                            None => parent,
1428                        }
1429                    })
1430            })
1431    }
1432
1433    fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
1434        let last_error = self.thread.read(cx).last_error()?;
1435
1436        Some(
1437            div()
1438                .absolute()
1439                .right_3()
1440                .bottom_12()
1441                .max_w_96()
1442                .py_2()
1443                .px_3()
1444                .elevation_2(cx)
1445                .occlude()
1446                .child(match last_error {
1447                    ThreadError::PaymentRequired => self.render_payment_required_error(cx),
1448                    ThreadError::MaxMonthlySpendReached => {
1449                        self.render_max_monthly_spend_reached_error(cx)
1450                    }
1451                    ThreadError::Message { header, message } => {
1452                        self.render_error_message(header, message, cx)
1453                    }
1454                })
1455                .into_any(),
1456        )
1457    }
1458
1459    fn render_payment_required_error(&self, cx: &mut Context<Self>) -> AnyElement {
1460        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.";
1461
1462        v_flex()
1463            .gap_0p5()
1464            .child(
1465                h_flex()
1466                    .gap_1p5()
1467                    .items_center()
1468                    .child(Icon::new(IconName::XCircle).color(Color::Error))
1469                    .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
1470            )
1471            .child(
1472                div()
1473                    .id("error-message")
1474                    .max_h_24()
1475                    .overflow_y_scroll()
1476                    .child(Label::new(ERROR_MESSAGE)),
1477            )
1478            .child(
1479                h_flex()
1480                    .justify_end()
1481                    .mt_1()
1482                    .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
1483                        |this, _, _, cx| {
1484                            this.thread.update(cx, |this, _cx| {
1485                                this.clear_last_error();
1486                            });
1487
1488                            cx.open_url(&zed_urls::account_url(cx));
1489                            cx.notify();
1490                        },
1491                    )))
1492                    .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1493                        |this, _, _, cx| {
1494                            this.thread.update(cx, |this, _cx| {
1495                                this.clear_last_error();
1496                            });
1497
1498                            cx.notify();
1499                        },
1500                    ))),
1501            )
1502            .into_any()
1503    }
1504
1505    fn render_max_monthly_spend_reached_error(&self, cx: &mut Context<Self>) -> AnyElement {
1506        const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
1507
1508        v_flex()
1509            .gap_0p5()
1510            .child(
1511                h_flex()
1512                    .gap_1p5()
1513                    .items_center()
1514                    .child(Icon::new(IconName::XCircle).color(Color::Error))
1515                    .child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
1516            )
1517            .child(
1518                div()
1519                    .id("error-message")
1520                    .max_h_24()
1521                    .overflow_y_scroll()
1522                    .child(Label::new(ERROR_MESSAGE)),
1523            )
1524            .child(
1525                h_flex()
1526                    .justify_end()
1527                    .mt_1()
1528                    .child(
1529                        Button::new("subscribe", "Update Monthly Spend Limit").on_click(
1530                            cx.listener(|this, _, _, cx| {
1531                                this.thread.update(cx, |this, _cx| {
1532                                    this.clear_last_error();
1533                                });
1534
1535                                cx.open_url(&zed_urls::account_url(cx));
1536                                cx.notify();
1537                            }),
1538                        ),
1539                    )
1540                    .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1541                        |this, _, _, cx| {
1542                            this.thread.update(cx, |this, _cx| {
1543                                this.clear_last_error();
1544                            });
1545
1546                            cx.notify();
1547                        },
1548                    ))),
1549            )
1550            .into_any()
1551    }
1552
1553    fn render_error_message(
1554        &self,
1555        header: SharedString,
1556        message: SharedString,
1557        cx: &mut Context<Self>,
1558    ) -> AnyElement {
1559        v_flex()
1560            .gap_0p5()
1561            .child(
1562                h_flex()
1563                    .gap_1p5()
1564                    .items_center()
1565                    .child(Icon::new(IconName::XCircle).color(Color::Error))
1566                    .child(Label::new(header).weight(FontWeight::MEDIUM)),
1567            )
1568            .child(
1569                div()
1570                    .id("error-message")
1571                    .max_h_32()
1572                    .overflow_y_scroll()
1573                    .child(Label::new(message)),
1574            )
1575            .child(
1576                h_flex()
1577                    .justify_end()
1578                    .mt_1()
1579                    .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1580                        |this, _, _, cx| {
1581                            this.thread.update(cx, |this, _cx| {
1582                                this.clear_last_error();
1583                            });
1584
1585                            cx.notify();
1586                        },
1587                    ))),
1588            )
1589            .into_any()
1590    }
1591
1592    fn key_context(&self) -> KeyContext {
1593        let mut key_context = KeyContext::new_with_defaults();
1594        key_context.add("AgentPanel");
1595        if matches!(self.active_view, ActiveView::PromptEditor) {
1596            key_context.add("prompt_editor");
1597        }
1598        key_context
1599    }
1600}
1601
1602impl Render for AssistantPanel {
1603    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1604        v_flex()
1605            .key_context(self.key_context())
1606            .justify_between()
1607            .size_full()
1608            .on_action(cx.listener(Self::cancel))
1609            .on_action(cx.listener(|this, action: &NewThread, window, cx| {
1610                this.new_thread(action, window, cx);
1611            }))
1612            .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
1613                this.open_history(window, cx);
1614            }))
1615            .on_action(cx.listener(|this, _: &OpenConfiguration, window, cx| {
1616                this.open_configuration(window, cx);
1617            }))
1618            .on_action(cx.listener(Self::open_active_thread_as_markdown))
1619            .on_action(cx.listener(Self::deploy_prompt_library))
1620            .on_action(cx.listener(Self::open_agent_diff))
1621            .on_action(cx.listener(Self::go_back))
1622            .child(self.render_toolbar(window, cx))
1623            .map(|parent| match self.active_view {
1624                ActiveView::Thread { .. } => parent
1625                    .child(self.render_active_thread_or_empty_state(window, cx))
1626                    .child(h_flex().child(self.message_editor.clone()))
1627                    .children(self.render_last_error(cx)),
1628                ActiveView::History => parent.child(self.history.clone()),
1629                ActiveView::PromptEditor => parent.children(self.context_editor.clone()),
1630                ActiveView::Configuration => parent.children(self.configuration.clone()),
1631            })
1632    }
1633}
1634
1635struct PromptLibraryInlineAssist {
1636    workspace: WeakEntity<Workspace>,
1637}
1638
1639impl PromptLibraryInlineAssist {
1640    pub fn new(workspace: WeakEntity<Workspace>) -> Self {
1641        Self { workspace }
1642    }
1643}
1644
1645impl prompt_library::InlineAssistDelegate for PromptLibraryInlineAssist {
1646    fn assist(
1647        &self,
1648        prompt_editor: &Entity<Editor>,
1649        _initial_prompt: Option<String>,
1650        window: &mut Window,
1651        cx: &mut Context<PromptLibrary>,
1652    ) {
1653        InlineAssistant::update_global(cx, |assistant, cx| {
1654            let Some(project) = self
1655                .workspace
1656                .upgrade()
1657                .map(|workspace| workspace.read(cx).project().downgrade())
1658            else {
1659                return;
1660            };
1661            assistant.assist(
1662                &prompt_editor,
1663                self.workspace.clone(),
1664                project,
1665                None,
1666                window,
1667                cx,
1668            )
1669        })
1670    }
1671
1672    fn focus_assistant_panel(
1673        &self,
1674        workspace: &mut Workspace,
1675        window: &mut Window,
1676        cx: &mut Context<Workspace>,
1677    ) -> bool {
1678        workspace
1679            .focus_panel::<AssistantPanel>(window, cx)
1680            .is_some()
1681    }
1682}
1683
1684pub struct ConcreteAssistantPanelDelegate;
1685
1686impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
1687    fn active_context_editor(
1688        &self,
1689        workspace: &mut Workspace,
1690        _window: &mut Window,
1691        cx: &mut Context<Workspace>,
1692    ) -> Option<Entity<ContextEditor>> {
1693        let panel = workspace.panel::<AssistantPanel>(cx)?;
1694        panel.update(cx, |panel, _cx| panel.context_editor.clone())
1695    }
1696
1697    fn open_saved_context(
1698        &self,
1699        workspace: &mut Workspace,
1700        path: std::path::PathBuf,
1701        window: &mut Window,
1702        cx: &mut Context<Workspace>,
1703    ) -> Task<Result<()>> {
1704        let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
1705            return Task::ready(Err(anyhow!("Agent panel not found")));
1706        };
1707
1708        panel.update(cx, |panel, cx| {
1709            panel.open_saved_prompt_editor(path, window, cx)
1710        })
1711    }
1712
1713    fn open_remote_context(
1714        &self,
1715        _workspace: &mut Workspace,
1716        _context_id: assistant_context_editor::ContextId,
1717        _window: &mut Window,
1718        _cx: &mut Context<Workspace>,
1719    ) -> Task<Result<Entity<ContextEditor>>> {
1720        Task::ready(Err(anyhow!("opening remote context not implemented")))
1721    }
1722
1723    fn quote_selection(
1724        &self,
1725        _workspace: &mut Workspace,
1726        _creases: Vec<(String, String)>,
1727        _window: &mut Window,
1728        _cx: &mut Context<Workspace>,
1729    ) {
1730    }
1731}