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