assistant_panel.rs

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