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