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