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