assistant_panel.rs

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