assistant_panel.rs

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