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