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 = cx.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                self.message_editor.focus_handle(cx).focus(window);
 573                cx.notify();
 574            }
 575            _ => {}
 576        }
 577    }
 578
 579    pub fn open_agent_diff(
 580        &mut self,
 581        _: &OpenAgentDiff,
 582        window: &mut Window,
 583        cx: &mut Context<Self>,
 584    ) {
 585        let thread = self.thread.read(cx).thread().clone();
 586        self.workspace
 587            .update(cx, |workspace, cx| {
 588                AgentDiff::deploy_in_workspace(thread, workspace, window, cx)
 589            })
 590            .log_err();
 591    }
 592
 593    pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 594        let context_server_manager = self.thread_store.read(cx).context_server_manager();
 595        let tools = self.thread_store.read(cx).tools();
 596        let fs = self.fs.clone();
 597
 598        self.set_active_view(ActiveView::Configuration, window, cx);
 599        self.configuration =
 600            Some(cx.new(|cx| {
 601                AssistantConfiguration::new(fs, context_server_manager, tools, window, cx)
 602            }));
 603
 604        if let Some(configuration) = self.configuration.as_ref() {
 605            self.configuration_subscription = Some(cx.subscribe_in(
 606                configuration,
 607                window,
 608                Self::handle_assistant_configuration_event,
 609            ));
 610
 611            configuration.focus_handle(cx).focus(window);
 612        }
 613    }
 614
 615    pub(crate) fn open_active_thread_as_markdown(
 616        &mut self,
 617        _: &OpenActiveThreadAsMarkdown,
 618        window: &mut Window,
 619        cx: &mut Context<Self>,
 620    ) {
 621        let Some(workspace) = self
 622            .workspace
 623            .upgrade()
 624            .ok_or_else(|| anyhow!("workspace dropped"))
 625            .log_err()
 626        else {
 627            return;
 628        };
 629
 630        let markdown_language_task = workspace
 631            .read(cx)
 632            .app_state()
 633            .languages
 634            .language_for_name("Markdown");
 635        let thread = self.active_thread(cx);
 636        cx.spawn_in(window, async move |_this, cx| {
 637            let markdown_language = markdown_language_task.await?;
 638
 639            workspace.update_in(cx, |workspace, window, cx| {
 640                let thread = thread.read(cx);
 641                let markdown = thread.to_markdown(cx)?;
 642                let thread_summary = thread
 643                    .summary()
 644                    .map(|summary| summary.to_string())
 645                    .unwrap_or_else(|| "Thread".to_string());
 646
 647                let project = workspace.project().clone();
 648                let buffer = project.update(cx, |project, cx| {
 649                    project.create_local_buffer(&markdown, Some(markdown_language), cx)
 650                });
 651                let buffer = cx.new(|cx| {
 652                    MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone())
 653                });
 654
 655                workspace.add_item_to_active_pane(
 656                    Box::new(cx.new(|cx| {
 657                        let mut editor =
 658                            Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
 659                        editor.set_breadcrumb_header(thread_summary);
 660                        editor
 661                    })),
 662                    None,
 663                    true,
 664                    window,
 665                    cx,
 666                );
 667
 668                anyhow::Ok(())
 669            })
 670        })
 671        .detach_and_log_err(cx);
 672    }
 673
 674    fn handle_assistant_configuration_event(
 675        &mut self,
 676        _entity: &Entity<AssistantConfiguration>,
 677        event: &AssistantConfigurationEvent,
 678        window: &mut Window,
 679        cx: &mut Context<Self>,
 680    ) {
 681        match event {
 682            AssistantConfigurationEvent::NewThread(provider) => {
 683                if LanguageModelRegistry::read_global(cx)
 684                    .default_model()
 685                    .map_or(true, |model| model.provider.id() != provider.id())
 686                {
 687                    if let Some(model) = provider.default_model(cx) {
 688                        update_settings_file::<AssistantSettings>(
 689                            self.fs.clone(),
 690                            cx,
 691                            move |settings, _| settings.set_model(model),
 692                        );
 693                    }
 694                }
 695
 696                self.new_thread(&NewThread::default(), window, cx);
 697            }
 698        }
 699    }
 700
 701    pub(crate) fn active_thread(&self, cx: &App) -> Entity<Thread> {
 702        self.thread.read(cx).thread().clone()
 703    }
 704
 705    pub(crate) fn delete_thread(
 706        &mut self,
 707        thread_id: &ThreadId,
 708        cx: &mut Context<Self>,
 709    ) -> Task<Result<()>> {
 710        self.thread_store
 711            .update(cx, |this, cx| this.delete_thread(thread_id, cx))
 712    }
 713
 714    pub(crate) fn active_context_editor(&self) -> Option<Entity<ContextEditor>> {
 715        self.context_editor.clone()
 716    }
 717
 718    pub(crate) fn delete_context(
 719        &mut self,
 720        path: PathBuf,
 721        cx: &mut Context<Self>,
 722    ) -> Task<Result<()>> {
 723        self.context_store
 724            .update(cx, |this, cx| this.delete_local_context(path, cx))
 725    }
 726
 727    fn set_active_view(
 728        &mut self,
 729        new_view: ActiveView,
 730        window: &mut Window,
 731        cx: &mut Context<Self>,
 732    ) {
 733        let current_is_history = matches!(self.active_view, ActiveView::History);
 734        let new_is_history = matches!(new_view, ActiveView::History);
 735
 736        if current_is_history && !new_is_history {
 737            self.active_view = new_view;
 738        } else if !current_is_history && new_is_history {
 739            self.previous_view = Some(std::mem::replace(&mut self.active_view, new_view));
 740        } else {
 741            if !new_is_history {
 742                self.previous_view = None;
 743            }
 744            self.active_view = new_view;
 745        }
 746
 747        self.focus_handle(cx).focus(window);
 748    }
 749}
 750
 751impl Focusable for AssistantPanel {
 752    fn focus_handle(&self, cx: &App) -> FocusHandle {
 753        match self.active_view {
 754            ActiveView::Thread { .. } => self.message_editor.focus_handle(cx),
 755            ActiveView::History => self.history.focus_handle(cx),
 756            ActiveView::PromptEditor => {
 757                if let Some(context_editor) = self.context_editor.as_ref() {
 758                    context_editor.focus_handle(cx)
 759                } else {
 760                    cx.focus_handle()
 761                }
 762            }
 763            ActiveView::Configuration => {
 764                if let Some(configuration) = self.configuration.as_ref() {
 765                    configuration.focus_handle(cx)
 766                } else {
 767                    cx.focus_handle()
 768                }
 769            }
 770        }
 771    }
 772}
 773
 774impl EventEmitter<PanelEvent> for AssistantPanel {}
 775
 776impl Panel for AssistantPanel {
 777    fn persistent_name() -> &'static str {
 778        "AgentPanel"
 779    }
 780
 781    fn position(&self, _window: &Window, cx: &App) -> DockPosition {
 782        match AssistantSettings::get_global(cx).dock {
 783            AssistantDockPosition::Left => DockPosition::Left,
 784            AssistantDockPosition::Bottom => DockPosition::Bottom,
 785            AssistantDockPosition::Right => DockPosition::Right,
 786        }
 787    }
 788
 789    fn position_is_valid(&self, _: DockPosition) -> bool {
 790        true
 791    }
 792
 793    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
 794        settings::update_settings_file::<AssistantSettings>(
 795            self.fs.clone(),
 796            cx,
 797            move |settings, _| {
 798                let dock = match position {
 799                    DockPosition::Left => AssistantDockPosition::Left,
 800                    DockPosition::Bottom => AssistantDockPosition::Bottom,
 801                    DockPosition::Right => AssistantDockPosition::Right,
 802                };
 803                settings.set_dock(dock);
 804            },
 805        );
 806    }
 807
 808    fn size(&self, window: &Window, cx: &App) -> Pixels {
 809        let settings = AssistantSettings::get_global(cx);
 810        match self.position(window, cx) {
 811            DockPosition::Left | DockPosition::Right => {
 812                self.width.unwrap_or(settings.default_width)
 813            }
 814            DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
 815        }
 816    }
 817
 818    fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
 819        match self.position(window, cx) {
 820            DockPosition::Left | DockPosition::Right => self.width = size,
 821            DockPosition::Bottom => self.height = size,
 822        }
 823        cx.notify();
 824    }
 825
 826    fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
 827
 828    fn remote_id() -> Option<proto::PanelId> {
 829        Some(proto::PanelId::AssistantPanel)
 830    }
 831
 832    fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
 833        (self.enabled(cx) && AssistantSettings::get_global(cx).button)
 834            .then_some(IconName::ZedAssistant)
 835    }
 836
 837    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
 838        Some("Agent Panel")
 839    }
 840
 841    fn toggle_action(&self) -> Box<dyn Action> {
 842        Box::new(ToggleFocus)
 843    }
 844
 845    fn activation_priority(&self) -> u32 {
 846        3
 847    }
 848
 849    fn enabled(&self, cx: &App) -> bool {
 850        AssistantSettings::get_global(cx).enabled
 851    }
 852}
 853
 854impl AssistantPanel {
 855    fn render_title_view(&self, _window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
 856        const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
 857
 858        let content = match &self.active_view {
 859            ActiveView::Thread {
 860                change_title_editor,
 861                ..
 862            } => {
 863                let active_thread = self.thread.read(cx);
 864                let is_empty = active_thread.is_empty();
 865
 866                let summary = active_thread.summary(cx);
 867
 868                if is_empty {
 869                    Label::new(Thread::DEFAULT_SUMMARY.clone())
 870                        .truncate()
 871                        .ml_2()
 872                        .into_any_element()
 873                } else if summary.is_none() {
 874                    Label::new(LOADING_SUMMARY_PLACEHOLDER)
 875                        .ml_2()
 876                        .truncate()
 877                        .into_any_element()
 878                } else {
 879                    div()
 880                        .ml_2()
 881                        .w_full()
 882                        .child(change_title_editor.clone())
 883                        .into_any_element()
 884                }
 885            }
 886            ActiveView::PromptEditor => {
 887                let title = self
 888                    .context_editor
 889                    .as_ref()
 890                    .map(|context_editor| {
 891                        SharedString::from(context_editor.read(cx).title(cx).to_string())
 892                    })
 893                    .unwrap_or_else(|| SharedString::from(LOADING_SUMMARY_PLACEHOLDER));
 894
 895                Label::new(title).ml_2().truncate().into_any_element()
 896            }
 897            ActiveView::History => Label::new("History").truncate().into_any_element(),
 898            ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
 899        };
 900
 901        h_flex()
 902            .key_context("TitleEditor")
 903            .id("TitleEditor")
 904            .flex_grow()
 905            .w_full()
 906            .max_w_full()
 907            .overflow_x_scroll()
 908            .child(content)
 909            .into_any()
 910    }
 911
 912    fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 913        let active_thread = self.thread.read(cx);
 914        let thread = active_thread.thread().read(cx);
 915        let token_usage = thread.total_token_usage(cx);
 916        let thread_id = thread.id().clone();
 917
 918        let is_generating = thread.is_generating();
 919        let is_empty = active_thread.is_empty();
 920        let focus_handle = self.focus_handle(cx);
 921
 922        let is_history = matches!(self.active_view, ActiveView::History);
 923
 924        let show_token_count = match &self.active_view {
 925            ActiveView::Thread { .. } => !is_empty,
 926            ActiveView::PromptEditor => self.context_editor.is_some(),
 927            _ => false,
 928        };
 929
 930        let go_back_button = match &self.active_view {
 931            ActiveView::History | ActiveView::Configuration => Some(
 932                div().pl_1().child(
 933                    IconButton::new("go-back", IconName::ArrowLeft)
 934                        .icon_size(IconSize::Small)
 935                        .on_click(cx.listener(|this, _, window, cx| {
 936                            this.go_back(&workspace::GoBack, window, cx);
 937                        }))
 938                        .tooltip({
 939                            let focus_handle = focus_handle.clone();
 940                            move |window, cx| {
 941                                Tooltip::for_action_in(
 942                                    "Go Back",
 943                                    &workspace::GoBack,
 944                                    &focus_handle,
 945                                    window,
 946                                    cx,
 947                                )
 948                            }
 949                        }),
 950                ),
 951            ),
 952            _ => None,
 953        };
 954
 955        h_flex()
 956            .id("assistant-toolbar")
 957            .h(Tab::container_height(cx))
 958            .max_w_full()
 959            .flex_none()
 960            .justify_between()
 961            .gap_2()
 962            .bg(cx.theme().colors().tab_bar_background)
 963            .border_b_1()
 964            .border_color(cx.theme().colors().border)
 965            .child(
 966                h_flex()
 967                    .w_full()
 968                    .gap_1()
 969                    .children(go_back_button)
 970                    .child(self.render_title_view(window, cx)),
 971            )
 972            .child(
 973                h_flex()
 974                    .h_full()
 975                    .gap_2()
 976                    .when(show_token_count, |parent| match self.active_view {
 977                        ActiveView::Thread { .. } => {
 978                            if token_usage.total == 0 {
 979                                return parent;
 980                            }
 981
 982                            let token_color = match token_usage.ratio {
 983                                TokenUsageRatio::Normal => Color::Muted,
 984                                TokenUsageRatio::Warning => Color::Warning,
 985                                TokenUsageRatio::Exceeded => Color::Error,
 986                            };
 987
 988                            parent.child(
 989                                h_flex()
 990                                    .flex_shrink_0()
 991                                    .gap_0p5()
 992                                    .child(
 993                                        Label::new(assistant_context_editor::humanize_token_count(
 994                                            token_usage.total,
 995                                        ))
 996                                        .size(LabelSize::Small)
 997                                        .color(token_color)
 998                                        .map(|label| {
 999                                            if is_generating {
1000                                                label
1001                                                    .with_animation(
1002                                                        "used-tokens-label",
1003                                                        Animation::new(Duration::from_secs(2))
1004                                                            .repeat()
1005                                                            .with_easing(pulsating_between(
1006                                                                0.6, 1.,
1007                                                            )),
1008                                                        |label, delta| label.alpha(delta),
1009                                                    )
1010                                                    .into_any()
1011                                            } else {
1012                                                label.into_any_element()
1013                                            }
1014                                        }),
1015                                    )
1016                                    .child(
1017                                        Label::new("/").size(LabelSize::Small).color(Color::Muted),
1018                                    )
1019                                    .child(
1020                                        Label::new(assistant_context_editor::humanize_token_count(
1021                                            token_usage.max,
1022                                        ))
1023                                        .size(LabelSize::Small)
1024                                        .color(Color::Muted),
1025                                    ),
1026                            )
1027                        }
1028                        ActiveView::PromptEditor => {
1029                            let Some(editor) = self.context_editor.as_ref() else {
1030                                return parent;
1031                            };
1032                            let Some(element) = render_remaining_tokens(editor, cx) else {
1033                                return parent;
1034                            };
1035                            parent.child(element)
1036                        }
1037                        _ => parent,
1038                    })
1039                    .child(
1040                        h_flex()
1041                            .h_full()
1042                            .gap(DynamicSpacing::Base02.rems(cx))
1043                            .px(DynamicSpacing::Base08.rems(cx))
1044                            .border_l_1()
1045                            .border_color(cx.theme().colors().border)
1046                            .child(
1047                                IconButton::new("new", IconName::Plus)
1048                                    .icon_size(IconSize::Small)
1049                                    .style(ButtonStyle::Subtle)
1050                                    .tooltip(move |window, cx| {
1051                                        Tooltip::for_action_in(
1052                                            "New Thread",
1053                                            &NewThread::default(),
1054                                            &focus_handle,
1055                                            window,
1056                                            cx,
1057                                        )
1058                                    })
1059                                    .on_click(move |_event, window, cx| {
1060                                        window.dispatch_action(
1061                                            NewThread::default().boxed_clone(),
1062                                            cx,
1063                                        );
1064                                    }),
1065                            )
1066                            .child(
1067                                IconButton::new("open-history", IconName::HistoryRerun)
1068                                    .icon_size(IconSize::Small)
1069                                    .toggle_state(is_history)
1070                                    .selected_icon_color(Color::Accent)
1071                                    .tooltip({
1072                                        let focus_handle = self.focus_handle(cx);
1073                                        move |window, cx| {
1074                                            Tooltip::for_action_in(
1075                                                "History",
1076                                                &OpenHistory,
1077                                                &focus_handle,
1078                                                window,
1079                                                cx,
1080                                            )
1081                                        }
1082                                    })
1083                                    .on_click(move |_event, window, cx| {
1084                                        window.dispatch_action(OpenHistory.boxed_clone(), cx);
1085                                    }),
1086                            )
1087                            .child(
1088                                PopoverMenu::new("assistant-menu")
1089                                    .trigger_with_tooltip(
1090                                        IconButton::new("new", IconName::Ellipsis)
1091                                            .icon_size(IconSize::Small)
1092                                            .style(ButtonStyle::Subtle),
1093                                        Tooltip::text("Toggle Agent Menu"),
1094                                    )
1095                                    .anchor(Corner::TopRight)
1096                                    .with_handle(self.assistant_dropdown_menu_handle.clone())
1097                                    .menu(move |window, cx| {
1098                                        Some(ContextMenu::build(
1099                                            window,
1100                                            cx,
1101                                            |menu, _window, _cx| {
1102                                                menu
1103                                                    .when(!is_empty, |menu| {
1104                                                        menu.action(
1105                                                            "Start New From Summary",
1106                                                            Box::new(NewThread {
1107                                                                from_thread_id: Some(thread_id.clone()),
1108                                                            }),
1109                                                        ).separator()
1110                                                    })
1111                                                    .action(
1112                                                    "New Text Thread",
1113                                                    NewTextThread.boxed_clone(),
1114                                                )
1115                                                .action("Settings", OpenConfiguration.boxed_clone())
1116                                                .separator()
1117                                                .action(
1118                                                    "Install MCPs",
1119                                                    zed_actions::Extensions {
1120                                                        category_filter: Some(
1121                                                            zed_actions::ExtensionCategoryFilter::ContextServers,
1122                                                        ),
1123                                                    }
1124                                                    .boxed_clone(),
1125                                                )
1126                                            },
1127                                        ))
1128                                    }),
1129                            ),
1130                    ),
1131            )
1132    }
1133
1134    fn render_active_thread_or_empty_state(
1135        &self,
1136        window: &mut Window,
1137        cx: &mut Context<Self>,
1138    ) -> AnyElement {
1139        if self.thread.read(cx).is_empty() {
1140            return self
1141                .render_thread_empty_state(window, cx)
1142                .into_any_element();
1143        }
1144
1145        self.thread.clone().into_any_element()
1146    }
1147
1148    fn configuration_error(&self, cx: &App) -> Option<ConfigurationError> {
1149        let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
1150            return Some(ConfigurationError::NoProvider);
1151        };
1152
1153        if !model.provider.is_authenticated(cx) {
1154            return Some(ConfigurationError::ProviderNotAuthenticated);
1155        }
1156
1157        if model.provider.must_accept_terms(cx) {
1158            return Some(ConfigurationError::ProviderPendingTermsAcceptance(
1159                model.provider,
1160            ));
1161        }
1162
1163        None
1164    }
1165
1166    fn render_thread_empty_state(
1167        &self,
1168        window: &mut Window,
1169        cx: &mut Context<Self>,
1170    ) -> impl IntoElement {
1171        let recent_history = self
1172            .history_store
1173            .update(cx, |this, cx| this.recent_entries(6, cx));
1174
1175        let configuration_error = self.configuration_error(cx);
1176        let no_error = configuration_error.is_none();
1177        let focus_handle = self.focus_handle(cx);
1178
1179        v_flex()
1180            .size_full()
1181            .when(recent_history.is_empty(), |this| {
1182                let configuration_error_ref = &configuration_error;
1183                this.child(
1184                    v_flex()
1185                        .size_full()
1186                        .max_w_80()
1187                        .mx_auto()
1188                        .justify_center()
1189                        .items_center()
1190                        .gap_1()
1191                        .child(
1192                            h_flex().child(
1193                                Headline::new("Welcome to the Agent Panel")
1194                            ),
1195                        )
1196                        .when(no_error, |parent| {
1197                            parent
1198                                .child(
1199                                    h_flex().child(
1200                                        Label::new("Ask and build anything.")
1201                                            .color(Color::Muted)
1202                                            .mb_2p5(),
1203                                    ),
1204                                )
1205                                .child(
1206                                    Button::new("new-thread", "Start New Thread")
1207                                        .icon(IconName::Plus)
1208                                        .icon_position(IconPosition::Start)
1209                                        .icon_size(IconSize::Small)
1210                                        .icon_color(Color::Muted)
1211                                        .full_width()
1212                                        .key_binding(KeyBinding::for_action_in(
1213                                            &NewThread::default(),
1214                                            &focus_handle,
1215                                            window,
1216                                            cx,
1217                                        ))
1218                                        .on_click(|_event, window, cx| {
1219                                            window.dispatch_action(NewThread::default().boxed_clone(), cx)
1220                                        }),
1221                                )
1222                                .child(
1223                                    Button::new("context", "Add Context")
1224                                        .icon(IconName::FileCode)
1225                                        .icon_position(IconPosition::Start)
1226                                        .icon_size(IconSize::Small)
1227                                        .icon_color(Color::Muted)
1228                                        .full_width()
1229                                        .key_binding(KeyBinding::for_action_in(
1230                                            &ToggleContextPicker,
1231                                            &focus_handle,
1232                                            window,
1233                                            cx,
1234                                        ))
1235                                        .on_click(|_event, window, cx| {
1236                                            window.dispatch_action(ToggleContextPicker.boxed_clone(), cx)
1237                                        }),
1238                                )
1239                                .child(
1240                                    Button::new("mode", "Switch Model")
1241                                        .icon(IconName::DatabaseZap)
1242                                        .icon_position(IconPosition::Start)
1243                                        .icon_size(IconSize::Small)
1244                                        .icon_color(Color::Muted)
1245                                        .full_width()
1246                                        .key_binding(KeyBinding::for_action_in(
1247                                            &ToggleModelSelector,
1248                                            &focus_handle,
1249                                            window,
1250                                            cx,
1251                                        ))
1252                                        .on_click(|_event, window, cx| {
1253                                            window.dispatch_action(ToggleModelSelector.boxed_clone(), cx)
1254                                        }),
1255                                )
1256                                .child(
1257                                    Button::new("settings", "View Settings")
1258                                        .icon(IconName::Settings)
1259                                        .icon_position(IconPosition::Start)
1260                                        .icon_size(IconSize::Small)
1261                                        .icon_color(Color::Muted)
1262                                        .full_width()
1263                                        .key_binding(KeyBinding::for_action_in(
1264                                            &OpenConfiguration,
1265                                            &focus_handle,
1266                                            window,
1267                                            cx,
1268                                        ))
1269                                        .on_click(|_event, window, cx| {
1270                                            window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
1271                                        }),
1272                                )
1273                        })
1274                        .map(|parent| {
1275                            match configuration_error_ref {
1276                                Some(ConfigurationError::ProviderNotAuthenticated)
1277                                | Some(ConfigurationError::NoProvider) => {
1278                                    parent
1279                                        .child(
1280                                            h_flex().child(
1281                                                Label::new("To start using the agent, configure at least one LLM provider.")
1282                                                    .color(Color::Muted)
1283                                                    .mb_2p5()
1284                                            )
1285                                        )
1286                                        .child(
1287                                            Button::new("settings", "Configure a Provider")
1288                                                .icon(IconName::Settings)
1289                                                .icon_position(IconPosition::Start)
1290                                                .icon_size(IconSize::Small)
1291                                                .icon_color(Color::Muted)
1292                                                .full_width()
1293                                                .key_binding(KeyBinding::for_action_in(
1294                                                    &OpenConfiguration,
1295                                                    &focus_handle,
1296                                                    window,
1297                                                    cx,
1298                                                ))
1299                                                .on_click(|_event, window, cx| {
1300                                                    window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
1301                                                }),
1302                                        )
1303                                }
1304                                Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
1305                                    parent.children(
1306                                        provider.render_accept_terms(
1307                                            LanguageModelProviderTosView::ThreadFreshStart,
1308                                            cx,
1309                                        ),
1310                                    )
1311                                }
1312                                None => parent,
1313                            }
1314                        })
1315                )
1316            })
1317            .when(!recent_history.is_empty(), |parent| {
1318                let focus_handle = focus_handle.clone();
1319                let configuration_error_ref = &configuration_error;
1320
1321                parent
1322                    .overflow_hidden()
1323                    .p_1p5()
1324                    .justify_end()
1325                    .gap_1()
1326                    .child(
1327                        h_flex()
1328                            .pl_1p5()
1329                            .pb_1()
1330                            .w_full()
1331                            .justify_between()
1332                            .border_b_1()
1333                            .border_color(cx.theme().colors().border_variant)
1334                            .child(
1335                                Label::new("Past Interactions")
1336                                    .size(LabelSize::Small)
1337                                    .color(Color::Muted),
1338                            )
1339                            .child(
1340                                Button::new("view-history", "View All")
1341                                    .style(ButtonStyle::Subtle)
1342                                    .label_size(LabelSize::Small)
1343                                    .key_binding(
1344                                        KeyBinding::for_action_in(
1345                                            &OpenHistory,
1346                                            &self.focus_handle(cx),
1347                                            window,
1348                                            cx,
1349                                        ).map(|kb| kb.size(rems_from_px(12.))),
1350                                    )
1351                                    .on_click(move |_event, window, cx| {
1352                                        window.dispatch_action(OpenHistory.boxed_clone(), cx);
1353                                    }),
1354                            ),
1355                    )
1356                    .child(
1357                        v_flex()
1358                            .gap_1()
1359                            .children(
1360                                recent_history.into_iter().map(|entry| {
1361                                    // TODO: Add keyboard navigation.
1362                                    match entry {
1363                                        HistoryEntry::Thread(thread) => {
1364                                            PastThread::new(thread, cx.entity().downgrade(), false, vec![])
1365                                                .into_any_element()
1366                                        }
1367                                        HistoryEntry::Context(context) => {
1368                                            PastContext::new(context, cx.entity().downgrade(), false, vec![])
1369                                                .into_any_element()
1370                                        }
1371                                    }
1372                                }),
1373                            )
1374                    )
1375                    .map(|parent| {
1376                        match configuration_error_ref {
1377                            Some(ConfigurationError::ProviderNotAuthenticated)
1378                            | Some(ConfigurationError::NoProvider) => {
1379                                parent
1380                                    .child(
1381                                        Banner::new()
1382                                            .severity(ui::Severity::Warning)
1383                                            .children(
1384                                                Label::new(
1385                                                    "Configure at least one LLM provider to start using the panel.",
1386                                                )
1387                                                .size(LabelSize::Small),
1388                                            )
1389                                            .action_slot(
1390                                                Button::new("settings", "Configure Provider")
1391                                                    .style(ButtonStyle::Tinted(ui::TintColor::Warning))
1392                                                    .label_size(LabelSize::Small)
1393                                                    .key_binding(
1394                                                        KeyBinding::for_action_in(
1395                                                            &OpenConfiguration,
1396                                                            &focus_handle,
1397                                                            window,
1398                                                            cx,
1399                                                        )
1400                                                        .map(|kb| kb.size(rems_from_px(12.))),
1401                                                    )
1402                                                    .on_click(|_event, window, cx| {
1403                                                        window.dispatch_action(
1404                                                            OpenConfiguration.boxed_clone(),
1405                                                            cx,
1406                                                        )
1407                                                    }),
1408                                            ),
1409                                    )
1410                            }
1411                            Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
1412                                parent
1413                                    .child(
1414                                        Banner::new()
1415                                            .severity(ui::Severity::Warning)
1416                                            .children(
1417                                                h_flex()
1418                                                    .w_full()
1419                                                    .children(
1420                                                        provider.render_accept_terms(
1421                                                            LanguageModelProviderTosView::ThreadtEmptyState,
1422                                                            cx,
1423                                                        ),
1424                                                    ),
1425                                            ),
1426                                    )
1427                            }
1428                            None => parent,
1429                        }
1430                    })
1431            })
1432    }
1433
1434    fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
1435        let last_error = self.thread.read(cx).last_error()?;
1436
1437        Some(
1438            div()
1439                .absolute()
1440                .right_3()
1441                .bottom_12()
1442                .max_w_96()
1443                .py_2()
1444                .px_3()
1445                .elevation_2(cx)
1446                .occlude()
1447                .child(match last_error {
1448                    ThreadError::PaymentRequired => self.render_payment_required_error(cx),
1449                    ThreadError::MaxMonthlySpendReached => {
1450                        self.render_max_monthly_spend_reached_error(cx)
1451                    }
1452                    ThreadError::Message { header, message } => {
1453                        self.render_error_message(header, message, cx)
1454                    }
1455                })
1456                .into_any(),
1457        )
1458    }
1459
1460    fn render_payment_required_error(&self, cx: &mut Context<Self>) -> AnyElement {
1461        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.";
1462
1463        v_flex()
1464            .gap_0p5()
1465            .child(
1466                h_flex()
1467                    .gap_1p5()
1468                    .items_center()
1469                    .child(Icon::new(IconName::XCircle).color(Color::Error))
1470                    .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
1471            )
1472            .child(
1473                div()
1474                    .id("error-message")
1475                    .max_h_24()
1476                    .overflow_y_scroll()
1477                    .child(Label::new(ERROR_MESSAGE)),
1478            )
1479            .child(
1480                h_flex()
1481                    .justify_end()
1482                    .mt_1()
1483                    .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
1484                        |this, _, _, cx| {
1485                            this.thread.update(cx, |this, _cx| {
1486                                this.clear_last_error();
1487                            });
1488
1489                            cx.open_url(&zed_urls::account_url(cx));
1490                            cx.notify();
1491                        },
1492                    )))
1493                    .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1494                        |this, _, _, cx| {
1495                            this.thread.update(cx, |this, _cx| {
1496                                this.clear_last_error();
1497                            });
1498
1499                            cx.notify();
1500                        },
1501                    ))),
1502            )
1503            .into_any()
1504    }
1505
1506    fn render_max_monthly_spend_reached_error(&self, cx: &mut Context<Self>) -> AnyElement {
1507        const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
1508
1509        v_flex()
1510            .gap_0p5()
1511            .child(
1512                h_flex()
1513                    .gap_1p5()
1514                    .items_center()
1515                    .child(Icon::new(IconName::XCircle).color(Color::Error))
1516                    .child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
1517            )
1518            .child(
1519                div()
1520                    .id("error-message")
1521                    .max_h_24()
1522                    .overflow_y_scroll()
1523                    .child(Label::new(ERROR_MESSAGE)),
1524            )
1525            .child(
1526                h_flex()
1527                    .justify_end()
1528                    .mt_1()
1529                    .child(
1530                        Button::new("subscribe", "Update Monthly Spend Limit").on_click(
1531                            cx.listener(|this, _, _, cx| {
1532                                this.thread.update(cx, |this, _cx| {
1533                                    this.clear_last_error();
1534                                });
1535
1536                                cx.open_url(&zed_urls::account_url(cx));
1537                                cx.notify();
1538                            }),
1539                        ),
1540                    )
1541                    .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1542                        |this, _, _, cx| {
1543                            this.thread.update(cx, |this, _cx| {
1544                                this.clear_last_error();
1545                            });
1546
1547                            cx.notify();
1548                        },
1549                    ))),
1550            )
1551            .into_any()
1552    }
1553
1554    fn render_error_message(
1555        &self,
1556        header: SharedString,
1557        message: SharedString,
1558        cx: &mut Context<Self>,
1559    ) -> AnyElement {
1560        v_flex()
1561            .gap_0p5()
1562            .child(
1563                h_flex()
1564                    .gap_1p5()
1565                    .items_center()
1566                    .child(Icon::new(IconName::XCircle).color(Color::Error))
1567                    .child(Label::new(header).weight(FontWeight::MEDIUM)),
1568            )
1569            .child(
1570                div()
1571                    .id("error-message")
1572                    .max_h_32()
1573                    .overflow_y_scroll()
1574                    .child(Label::new(message)),
1575            )
1576            .child(
1577                h_flex()
1578                    .justify_end()
1579                    .mt_1()
1580                    .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1581                        |this, _, _, cx| {
1582                            this.thread.update(cx, |this, _cx| {
1583                                this.clear_last_error();
1584                            });
1585
1586                            cx.notify();
1587                        },
1588                    ))),
1589            )
1590            .into_any()
1591    }
1592
1593    fn key_context(&self) -> KeyContext {
1594        let mut key_context = KeyContext::new_with_defaults();
1595        key_context.add("AgentPanel");
1596        if matches!(self.active_view, ActiveView::PromptEditor) {
1597            key_context.add("prompt_editor");
1598        }
1599        key_context
1600    }
1601}
1602
1603impl Render for AssistantPanel {
1604    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1605        v_flex()
1606            .key_context(self.key_context())
1607            .justify_between()
1608            .size_full()
1609            .on_action(cx.listener(Self::cancel))
1610            .on_action(cx.listener(|this, action: &NewThread, window, cx| {
1611                this.new_thread(action, window, cx);
1612            }))
1613            .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
1614                this.open_history(window, cx);
1615            }))
1616            .on_action(cx.listener(|this, _: &OpenConfiguration, window, cx| {
1617                this.open_configuration(window, cx);
1618            }))
1619            .on_action(cx.listener(Self::open_active_thread_as_markdown))
1620            .on_action(cx.listener(Self::deploy_prompt_library))
1621            .on_action(cx.listener(Self::open_agent_diff))
1622            .on_action(cx.listener(Self::go_back))
1623            .child(self.render_toolbar(window, cx))
1624            .map(|parent| match self.active_view {
1625                ActiveView::Thread { .. } => parent
1626                    .child(self.render_active_thread_or_empty_state(window, cx))
1627                    .child(h_flex().child(self.message_editor.clone()))
1628                    .children(self.render_last_error(cx)),
1629                ActiveView::History => parent.child(self.history.clone()),
1630                ActiveView::PromptEditor => parent.children(self.context_editor.clone()),
1631                ActiveView::Configuration => parent.children(self.configuration.clone()),
1632            })
1633    }
1634}
1635
1636struct PromptLibraryInlineAssist {
1637    workspace: WeakEntity<Workspace>,
1638}
1639
1640impl PromptLibraryInlineAssist {
1641    pub fn new(workspace: WeakEntity<Workspace>) -> Self {
1642        Self { workspace }
1643    }
1644}
1645
1646impl prompt_library::InlineAssistDelegate for PromptLibraryInlineAssist {
1647    fn assist(
1648        &self,
1649        prompt_editor: &Entity<Editor>,
1650        _initial_prompt: Option<String>,
1651        window: &mut Window,
1652        cx: &mut Context<PromptLibrary>,
1653    ) {
1654        InlineAssistant::update_global(cx, |assistant, cx| {
1655            let Some(project) = self
1656                .workspace
1657                .upgrade()
1658                .map(|workspace| workspace.read(cx).project().downgrade())
1659            else {
1660                return;
1661            };
1662            assistant.assist(
1663                &prompt_editor,
1664                self.workspace.clone(),
1665                project,
1666                None,
1667                window,
1668                cx,
1669            )
1670        })
1671    }
1672
1673    fn focus_assistant_panel(
1674        &self,
1675        workspace: &mut Workspace,
1676        window: &mut Window,
1677        cx: &mut Context<Workspace>,
1678    ) -> bool {
1679        workspace
1680            .focus_panel::<AssistantPanel>(window, cx)
1681            .is_some()
1682    }
1683}
1684
1685pub struct ConcreteAssistantPanelDelegate;
1686
1687impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
1688    fn active_context_editor(
1689        &self,
1690        workspace: &mut Workspace,
1691        _window: &mut Window,
1692        cx: &mut Context<Workspace>,
1693    ) -> Option<Entity<ContextEditor>> {
1694        let panel = workspace.panel::<AssistantPanel>(cx)?;
1695        panel.update(cx, |panel, _cx| panel.context_editor.clone())
1696    }
1697
1698    fn open_saved_context(
1699        &self,
1700        workspace: &mut Workspace,
1701        path: std::path::PathBuf,
1702        window: &mut Window,
1703        cx: &mut Context<Workspace>,
1704    ) -> Task<Result<()>> {
1705        let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
1706            return Task::ready(Err(anyhow!("Agent panel not found")));
1707        };
1708
1709        panel.update(cx, |panel, cx| {
1710            panel.open_saved_prompt_editor(path, window, cx)
1711        })
1712    }
1713
1714    fn open_remote_context(
1715        &self,
1716        _workspace: &mut Workspace,
1717        _context_id: assistant_context_editor::ContextId,
1718        _window: &mut Window,
1719        _cx: &mut Context<Workspace>,
1720    ) -> Task<Result<Entity<ContextEditor>>> {
1721        Task::ready(Err(anyhow!("opening remote context not implemented")))
1722    }
1723
1724    fn quote_selection(
1725        &self,
1726        _workspace: &mut Workspace,
1727        _creases: Vec<(String, String)>,
1728        _window: &mut Window,
1729        _cx: &mut Context<Workspace>,
1730    ) {
1731    }
1732}