assistant_panel.rs

   1use std::ops::Range;
   2use std::path::PathBuf;
   3use std::sync::Arc;
   4use std::time::Duration;
   5
   6use anyhow::{Result, anyhow};
   7use assistant_context_editor::{
   8    AssistantPanelDelegate, ConfigurationError, ContextEditor, SlashCommandCompletionProvider,
   9    humanize_token_count, make_lsp_adapter_delegate, render_remaining_tokens,
  10};
  11use assistant_settings::{AssistantDockPosition, AssistantSettings};
  12use assistant_slash_command::SlashCommandWorkingSet;
  13use assistant_tool::ToolWorkingSet;
  14
  15use client::zed_urls;
  16use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
  17use fs::Fs;
  18use gpui::{
  19    Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, Corner, Entity,
  20    EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext, Pixels, Subscription, Task,
  21    UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
  22};
  23use language::LanguageRegistry;
  24use language_model::{LanguageModelProviderTosView, LanguageModelRegistry};
  25use language_model_selector::ToggleModelSelector;
  26use project::Project;
  27use prompt_library::{PromptLibrary, open_prompt_library};
  28use prompt_store::PromptBuilder;
  29use proto::Plan;
  30use settings::{Settings, update_settings_file};
  31use time::UtcOffset;
  32use ui::{
  33    Banner, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, Tab, Tooltip, prelude::*,
  34};
  35use util::ResultExt as _;
  36use workspace::Workspace;
  37use workspace::dock::{DockPosition, Panel, PanelEvent};
  38use zed_actions::agent::OpenConfiguration;
  39use zed_actions::assistant::{OpenPromptLibrary, ToggleFocus};
  40
  41use crate::active_thread::{ActiveThread, ActiveThreadEvent};
  42use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent};
  43use crate::history_store::{HistoryEntry, HistoryStore};
  44use crate::message_editor::{MessageEditor, MessageEditorEvent};
  45use crate::thread::{Thread, ThreadError, ThreadId, TokenUsageRatio};
  46use crate::thread_history::{PastContext, PastThread, ThreadHistory};
  47use crate::thread_store::ThreadStore;
  48use crate::{
  49    AgentDiff, ExpandMessageEditor, InlineAssistant, NewTextThread, NewThread,
  50    OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ThreadEvent, ToggleContextPicker,
  51};
  52
  53pub fn init(cx: &mut App) {
  54    cx.observe_new(
  55        |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
  56            workspace
  57                .register_action(|workspace, action: &NewThread, window, cx| {
  58                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
  59                        panel.update(cx, |panel, cx| panel.new_thread(action, window, cx));
  60                        workspace.focus_panel::<AssistantPanel>(window, cx);
  61                    }
  62                })
  63                .register_action(|workspace, _: &OpenHistory, window, cx| {
  64                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
  65                        workspace.focus_panel::<AssistantPanel>(window, cx);
  66                        panel.update(cx, |panel, cx| panel.open_history(window, cx));
  67                    }
  68                })
  69                .register_action(|workspace, _: &OpenConfiguration, window, cx| {
  70                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
  71                        workspace.focus_panel::<AssistantPanel>(window, cx);
  72                        panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
  73                    }
  74                })
  75                .register_action(|workspace, _: &NewTextThread, window, cx| {
  76                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
  77                        workspace.focus_panel::<AssistantPanel>(window, cx);
  78                        panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx));
  79                    }
  80                })
  81                .register_action(|workspace, _: &OpenPromptLibrary, window, cx| {
  82                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
  83                        workspace.focus_panel::<AssistantPanel>(window, cx);
  84                        panel.update(cx, |panel, cx| {
  85                            panel.deploy_prompt_library(&OpenPromptLibrary, window, cx)
  86                        });
  87                    }
  88                })
  89                .register_action(|workspace, _: &OpenAgentDiff, window, cx| {
  90                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
  91                        workspace.focus_panel::<AssistantPanel>(window, cx);
  92                        let thread = panel.read(cx).thread.read(cx).thread().clone();
  93                        AgentDiff::deploy_in_workspace(thread, workspace, window, cx);
  94                    }
  95                })
  96                .register_action(|workspace, _: &ExpandMessageEditor, window, cx| {
  97                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
  98                        workspace.focus_panel::<AssistantPanel>(window, cx);
  99                        panel.update(cx, |panel, cx| {
 100                            panel.message_editor.update(cx, |editor, cx| {
 101                                editor.expand_message_editor(&ExpandMessageEditor, window, cx);
 102                            });
 103                        });
 104                    }
 105                });
 106        },
 107    )
 108    .detach();
 109}
 110
 111enum ActiveView {
 112    Thread {
 113        change_title_editor: Entity<Editor>,
 114        _subscriptions: Vec<gpui::Subscription>,
 115    },
 116    PromptEditor {
 117        context_editor: Entity<ContextEditor>,
 118    },
 119    History,
 120    Configuration,
 121}
 122
 123impl ActiveView {
 124    pub fn thread(thread: Entity<Thread>, window: &mut Window, cx: &mut App) -> Self {
 125        let summary = thread.read(cx).summary_or_default();
 126
 127        let editor = cx.new(|cx| {
 128            let mut editor = Editor::single_line(window, cx);
 129            editor.set_text(summary, window, cx);
 130            editor
 131        });
 132
 133        let subscriptions = vec![
 134            window.subscribe(&editor, cx, {
 135                {
 136                    let thread = thread.clone();
 137                    move |editor, event, window, cx| match event {
 138                        EditorEvent::BufferEdited => {
 139                            let new_summary = editor.read(cx).text(cx);
 140
 141                            thread.update(cx, |thread, cx| {
 142                                thread.set_summary(new_summary, cx);
 143                            })
 144                        }
 145                        EditorEvent::Blurred => {
 146                            if editor.read(cx).text(cx).is_empty() {
 147                                let summary = thread.read(cx).summary_or_default();
 148
 149                                editor.update(cx, |editor, cx| {
 150                                    editor.set_text(summary, window, cx);
 151                                });
 152                            }
 153                        }
 154                        _ => {}
 155                    }
 156                }
 157            }),
 158            window.subscribe(&thread, cx, {
 159                let editor = editor.clone();
 160                move |thread, event, window, cx| match event {
 161                    ThreadEvent::SummaryGenerated => {
 162                        let summary = thread.read(cx).summary_or_default();
 163
 164                        editor.update(cx, |editor, cx| {
 165                            editor.set_text(summary, window, cx);
 166                        })
 167                    }
 168                    _ => {}
 169                }
 170            }),
 171        ];
 172
 173        Self::Thread {
 174            change_title_editor: editor,
 175            _subscriptions: subscriptions,
 176        }
 177    }
 178}
 179
 180pub struct AssistantPanel {
 181    workspace: WeakEntity<Workspace>,
 182    project: Entity<Project>,
 183    fs: Arc<dyn Fs>,
 184    language_registry: Arc<LanguageRegistry>,
 185    thread_store: Entity<ThreadStore>,
 186    thread: Entity<ActiveThread>,
 187    message_editor: Entity<MessageEditor>,
 188    _active_thread_subscriptions: Vec<Subscription>,
 189    context_store: Entity<assistant_context_editor::ContextStore>,
 190    configuration: Option<Entity<AssistantConfiguration>>,
 191    configuration_subscription: Option<Subscription>,
 192    local_timezone: UtcOffset,
 193    active_view: ActiveView,
 194    previous_view: Option<ActiveView>,
 195    history_store: Entity<HistoryStore>,
 196    history: Entity<ThreadHistory>,
 197    assistant_dropdown_menu_handle: PopoverMenuHandle<ContextMenu>,
 198    width: Option<Pixels>,
 199    height: Option<Pixels>,
 200}
 201
 202impl AssistantPanel {
 203    pub fn load(
 204        workspace: WeakEntity<Workspace>,
 205        prompt_builder: Arc<PromptBuilder>,
 206        cx: AsyncWindowContext,
 207    ) -> Task<Result<Entity<Self>>> {
 208        cx.spawn(async move |cx| {
 209            let tools = cx.new(|_| ToolWorkingSet::default())?;
 210            let thread_store = workspace
 211                .update(cx, |workspace, cx| {
 212                    let project = workspace.project().clone();
 213                    ThreadStore::load(project, tools.clone(), prompt_builder.clone(), cx)
 214                })?
 215                .await;
 216
 217            let slash_commands = Arc::new(SlashCommandWorkingSet::default());
 218            let context_store = workspace
 219                .update(cx, |workspace, cx| {
 220                    let project = workspace.project().clone();
 221                    assistant_context_editor::ContextStore::new(
 222                        project,
 223                        prompt_builder.clone(),
 224                        slash_commands,
 225                        cx,
 226                    )
 227                })?
 228                .await?;
 229
 230            workspace.update_in(cx, |workspace, window, cx| {
 231                cx.new(|cx| Self::new(workspace, thread_store, context_store, window, cx))
 232            })
 233        })
 234    }
 235
 236    fn new(
 237        workspace: &Workspace,
 238        thread_store: Entity<ThreadStore>,
 239        context_store: Entity<assistant_context_editor::ContextStore>,
 240        window: &mut Window,
 241        cx: &mut Context<Self>,
 242    ) -> Self {
 243        let thread = thread_store.update(cx, |this, cx| this.create_thread(cx));
 244        let fs = workspace.app_state().fs.clone();
 245        let project = workspace.project();
 246        let language_registry = project.read(cx).languages().clone();
 247        let workspace = workspace.weak_handle();
 248        let weak_self = cx.entity().downgrade();
 249
 250        let message_editor_context_store = cx.new(|_cx| {
 251            crate::context_store::ContextStore::new(
 252                project.downgrade(),
 253                Some(thread_store.downgrade()),
 254            )
 255        });
 256
 257        let message_editor = cx.new(|cx| {
 258            MessageEditor::new(
 259                fs.clone(),
 260                workspace.clone(),
 261                message_editor_context_store.clone(),
 262                thread_store.downgrade(),
 263                thread.clone(),
 264                window,
 265                cx,
 266            )
 267        });
 268
 269        let message_editor_subscription =
 270            cx.subscribe(&message_editor, |_, _, event, cx| match event {
 271                MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
 272                    cx.notify();
 273                }
 274            });
 275
 276        let history_store =
 277            cx.new(|cx| HistoryStore::new(thread_store.clone(), context_store.clone(), cx));
 278
 279        cx.observe(&history_store, |_, _, cx| cx.notify()).detach();
 280
 281        let active_view = ActiveView::thread(thread.clone(), window, cx);
 282        let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
 283            if let ThreadEvent::MessageAdded(_) = &event {
 284                // needed to leave empty state
 285                cx.notify();
 286            }
 287        });
 288        let thread = cx.new(|cx| {
 289            ActiveThread::new(
 290                thread.clone(),
 291                thread_store.clone(),
 292                language_registry.clone(),
 293                message_editor_context_store.clone(),
 294                workspace.clone(),
 295                window,
 296                cx,
 297            )
 298        });
 299
 300        let active_thread_subscription = cx.subscribe(&thread, |_, _, event, cx| match &event {
 301            ActiveThreadEvent::EditingMessageTokenCountChanged => {
 302                cx.notify();
 303            }
 304        });
 305
 306        Self {
 307            active_view,
 308            workspace,
 309            project: project.clone(),
 310            fs: fs.clone(),
 311            language_registry,
 312            thread_store: thread_store.clone(),
 313            thread,
 314            message_editor,
 315            _active_thread_subscriptions: vec![
 316                thread_subscription,
 317                active_thread_subscription,
 318                message_editor_subscription,
 319            ],
 320            context_store,
 321            configuration: None,
 322            configuration_subscription: None,
 323            local_timezone: UtcOffset::from_whole_seconds(
 324                chrono::Local::now().offset().local_minus_utc(),
 325            )
 326            .unwrap(),
 327            previous_view: None,
 328            history_store: history_store.clone(),
 329            history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)),
 330            assistant_dropdown_menu_handle: PopoverMenuHandle::default(),
 331            width: None,
 332            height: None,
 333        }
 334    }
 335
 336    pub fn toggle_focus(
 337        workspace: &mut Workspace,
 338        _: &ToggleFocus,
 339        window: &mut Window,
 340        cx: &mut Context<Workspace>,
 341    ) {
 342        if workspace
 343            .panel::<Self>(cx)
 344            .is_some_and(|panel| panel.read(cx).enabled(cx))
 345        {
 346            workspace.toggle_panel_focus::<Self>(window, cx);
 347        }
 348    }
 349
 350    pub(crate) fn local_timezone(&self) -> UtcOffset {
 351        self.local_timezone
 352    }
 353
 354    pub(crate) fn thread_store(&self) -> &Entity<ThreadStore> {
 355        &self.thread_store
 356    }
 357
 358    fn cancel(
 359        &mut self,
 360        _: &editor::actions::Cancel,
 361        _window: &mut Window,
 362        cx: &mut Context<Self>,
 363    ) {
 364        self.thread
 365            .update(cx, |thread, cx| thread.cancel_last_completion(cx));
 366    }
 367
 368    fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {
 369        let thread = self
 370            .thread_store
 371            .update(cx, |this, cx| this.create_thread(cx));
 372
 373        let thread_view = ActiveView::thread(thread.clone(), window, cx);
 374        self.set_active_view(thread_view, window, cx);
 375
 376        let message_editor_context_store = cx.new(|_cx| {
 377            crate::context_store::ContextStore::new(
 378                self.project.downgrade(),
 379                Some(self.thread_store.downgrade()),
 380            )
 381        });
 382
 383        if let Some(other_thread_id) = action.from_thread_id.clone() {
 384            let other_thread_task = self
 385                .thread_store
 386                .update(cx, |this, cx| this.open_thread(&other_thread_id, cx));
 387
 388            cx.spawn({
 389                let context_store = message_editor_context_store.clone();
 390
 391                async move |_panel, cx| {
 392                    let other_thread = other_thread_task.await?;
 393
 394                    context_store.update(cx, |this, cx| {
 395                        this.add_thread(other_thread, false, cx);
 396                    })?;
 397                    anyhow::Ok(())
 398                }
 399            })
 400            .detach_and_log_err(cx);
 401        }
 402
 403        let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
 404            if let ThreadEvent::MessageAdded(_) = &event {
 405                // needed to leave empty state
 406                cx.notify();
 407            }
 408        });
 409
 410        self.thread = cx.new(|cx| {
 411            ActiveThread::new(
 412                thread.clone(),
 413                self.thread_store.clone(),
 414                self.language_registry.clone(),
 415                message_editor_context_store.clone(),
 416                self.workspace.clone(),
 417                window,
 418                cx,
 419            )
 420        });
 421
 422        let active_thread_subscription =
 423            cx.subscribe(&self.thread, |_, _, event, cx| match &event {
 424                ActiveThreadEvent::EditingMessageTokenCountChanged => {
 425                    cx.notify();
 426                }
 427            });
 428
 429        self.message_editor = cx.new(|cx| {
 430            MessageEditor::new(
 431                self.fs.clone(),
 432                self.workspace.clone(),
 433                message_editor_context_store,
 434                self.thread_store.downgrade(),
 435                thread,
 436                window,
 437                cx,
 438            )
 439        });
 440        self.message_editor.focus_handle(cx).focus(window);
 441
 442        let message_editor_subscription =
 443            cx.subscribe(&self.message_editor, |_, _, event, cx| match event {
 444                MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
 445                    cx.notify();
 446                }
 447            });
 448
 449        self._active_thread_subscriptions = vec![
 450            thread_subscription,
 451            active_thread_subscription,
 452            message_editor_subscription,
 453        ];
 454    }
 455
 456    fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 457        let context = self
 458            .context_store
 459            .update(cx, |context_store, cx| context_store.create(cx));
 460        let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
 461            .log_err()
 462            .flatten();
 463
 464        let context_editor = cx.new(|cx| {
 465            let mut editor = ContextEditor::for_context(
 466                context,
 467                self.fs.clone(),
 468                self.workspace.clone(),
 469                self.project.clone(),
 470                lsp_adapter_delegate,
 471                window,
 472                cx,
 473            );
 474            editor.insert_default_prompt(window, cx);
 475            editor
 476        });
 477
 478        self.set_active_view(
 479            ActiveView::PromptEditor {
 480                context_editor: context_editor.clone(),
 481            },
 482            window,
 483            cx,
 484        );
 485        context_editor.focus_handle(cx).focus(window);
 486    }
 487
 488    fn deploy_prompt_library(
 489        &mut self,
 490        _: &OpenPromptLibrary,
 491        _window: &mut Window,
 492        cx: &mut Context<Self>,
 493    ) {
 494        open_prompt_library(
 495            self.language_registry.clone(),
 496            Box::new(PromptLibraryInlineAssist::new(self.workspace.clone())),
 497            Arc::new(|| {
 498                Box::new(SlashCommandCompletionProvider::new(
 499                    Arc::new(SlashCommandWorkingSet::default()),
 500                    None,
 501                    None,
 502                ))
 503            }),
 504            cx,
 505        )
 506        .detach_and_log_err(cx);
 507    }
 508
 509    fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 510        if matches!(self.active_view, ActiveView::History) {
 511            if let Some(previous_view) = self.previous_view.take() {
 512                self.set_active_view(previous_view, window, cx);
 513            }
 514        } else {
 515            self.thread_store
 516                .update(cx, |thread_store, cx| thread_store.reload(cx))
 517                .detach_and_log_err(cx);
 518            self.set_active_view(ActiveView::History, window, cx);
 519        }
 520        cx.notify();
 521    }
 522
 523    pub(crate) fn open_saved_prompt_editor(
 524        &mut self,
 525        path: PathBuf,
 526        window: &mut Window,
 527        cx: &mut Context<Self>,
 528    ) -> Task<Result<()>> {
 529        let context = self
 530            .context_store
 531            .update(cx, |store, cx| store.open_local_context(path.clone(), cx));
 532        let fs = self.fs.clone();
 533        let project = self.project.clone();
 534        let workspace = self.workspace.clone();
 535
 536        let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err().flatten();
 537
 538        cx.spawn_in(window, async move |this, cx| {
 539            let context = context.await?;
 540            this.update_in(cx, |this, window, cx| {
 541                let editor = cx.new(|cx| {
 542                    ContextEditor::for_context(
 543                        context,
 544                        fs,
 545                        workspace,
 546                        project,
 547                        lsp_adapter_delegate,
 548                        window,
 549                        cx,
 550                    )
 551                });
 552                this.set_active_view(
 553                    ActiveView::PromptEditor {
 554                        context_editor: editor,
 555                    },
 556                    window,
 557                    cx,
 558                );
 559
 560                anyhow::Ok(())
 561            })??;
 562            Ok(())
 563        })
 564    }
 565
 566    pub(crate) fn open_thread(
 567        &mut self,
 568        thread_id: &ThreadId,
 569        window: &mut Window,
 570        cx: &mut Context<Self>,
 571    ) -> Task<Result<()>> {
 572        let open_thread_task = self
 573            .thread_store
 574            .update(cx, |this, cx| this.open_thread(thread_id, cx));
 575
 576        cx.spawn_in(window, async move |this, cx| {
 577            let thread = open_thread_task.await?;
 578            this.update_in(cx, |this, window, cx| {
 579                let thread_view = ActiveView::thread(thread.clone(), window, cx);
 580                this.set_active_view(thread_view, window, cx);
 581                let message_editor_context_store = cx.new(|_cx| {
 582                    crate::context_store::ContextStore::new(
 583                        this.project.downgrade(),
 584                        Some(this.thread_store.downgrade()),
 585                    )
 586                });
 587                let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
 588                    if let ThreadEvent::MessageAdded(_) = &event {
 589                        // needed to leave empty state
 590                        cx.notify();
 591                    }
 592                });
 593
 594                this.thread = cx.new(|cx| {
 595                    ActiveThread::new(
 596                        thread.clone(),
 597                        this.thread_store.clone(),
 598                        this.language_registry.clone(),
 599                        message_editor_context_store.clone(),
 600                        this.workspace.clone(),
 601                        window,
 602                        cx,
 603                    )
 604                });
 605
 606                let active_thread_subscription =
 607                    cx.subscribe(&this.thread, |_, _, event, cx| match &event {
 608                        ActiveThreadEvent::EditingMessageTokenCountChanged => {
 609                            cx.notify();
 610                        }
 611                    });
 612
 613                this.message_editor = cx.new(|cx| {
 614                    MessageEditor::new(
 615                        this.fs.clone(),
 616                        this.workspace.clone(),
 617                        message_editor_context_store,
 618                        this.thread_store.downgrade(),
 619                        thread,
 620                        window,
 621                        cx,
 622                    )
 623                });
 624                this.message_editor.focus_handle(cx).focus(window);
 625
 626                let message_editor_subscription =
 627                    cx.subscribe(&this.message_editor, |_, _, event, cx| match event {
 628                        MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
 629                            cx.notify();
 630                        }
 631                    });
 632
 633                this._active_thread_subscriptions = vec![
 634                    thread_subscription,
 635                    active_thread_subscription,
 636                    message_editor_subscription,
 637                ];
 638            })
 639        })
 640    }
 641
 642    pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
 643        match self.active_view {
 644            ActiveView::Configuration | ActiveView::History => {
 645                self.active_view =
 646                    ActiveView::thread(self.thread.read(cx).thread().clone(), window, cx);
 647                self.message_editor.focus_handle(cx).focus(window);
 648                cx.notify();
 649            }
 650            _ => {}
 651        }
 652    }
 653
 654    pub fn open_agent_diff(
 655        &mut self,
 656        _: &OpenAgentDiff,
 657        window: &mut Window,
 658        cx: &mut Context<Self>,
 659    ) {
 660        let thread = self.thread.read(cx).thread().clone();
 661        self.workspace
 662            .update(cx, |workspace, cx| {
 663                AgentDiff::deploy_in_workspace(thread, workspace, window, cx)
 664            })
 665            .log_err();
 666    }
 667
 668    pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 669        let context_server_manager = self.thread_store.read(cx).context_server_manager();
 670        let tools = self.thread_store.read(cx).tools();
 671        let fs = self.fs.clone();
 672
 673        self.set_active_view(ActiveView::Configuration, window, cx);
 674        self.configuration =
 675            Some(cx.new(|cx| {
 676                AssistantConfiguration::new(fs, context_server_manager, tools, window, cx)
 677            }));
 678
 679        if let Some(configuration) = self.configuration.as_ref() {
 680            self.configuration_subscription = Some(cx.subscribe_in(
 681                configuration,
 682                window,
 683                Self::handle_assistant_configuration_event,
 684            ));
 685
 686            configuration.focus_handle(cx).focus(window);
 687        }
 688    }
 689
 690    pub(crate) fn open_active_thread_as_markdown(
 691        &mut self,
 692        _: &OpenActiveThreadAsMarkdown,
 693        window: &mut Window,
 694        cx: &mut Context<Self>,
 695    ) {
 696        let Some(workspace) = self
 697            .workspace
 698            .upgrade()
 699            .ok_or_else(|| anyhow!("workspace dropped"))
 700            .log_err()
 701        else {
 702            return;
 703        };
 704
 705        let markdown_language_task = workspace
 706            .read(cx)
 707            .app_state()
 708            .languages
 709            .language_for_name("Markdown");
 710        let thread = self.active_thread(cx);
 711        cx.spawn_in(window, async move |_this, cx| {
 712            let markdown_language = markdown_language_task.await?;
 713
 714            workspace.update_in(cx, |workspace, window, cx| {
 715                let thread = thread.read(cx);
 716                let markdown = thread.to_markdown(cx)?;
 717                let thread_summary = thread
 718                    .summary()
 719                    .map(|summary| summary.to_string())
 720                    .unwrap_or_else(|| "Thread".to_string());
 721
 722                let project = workspace.project().clone();
 723                let buffer = project.update(cx, |project, cx| {
 724                    project.create_local_buffer(&markdown, Some(markdown_language), cx)
 725                });
 726                let buffer = cx.new(|cx| {
 727                    MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone())
 728                });
 729
 730                workspace.add_item_to_active_pane(
 731                    Box::new(cx.new(|cx| {
 732                        let mut editor =
 733                            Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
 734                        editor.set_breadcrumb_header(thread_summary);
 735                        editor
 736                    })),
 737                    None,
 738                    true,
 739                    window,
 740                    cx,
 741                );
 742
 743                anyhow::Ok(())
 744            })
 745        })
 746        .detach_and_log_err(cx);
 747    }
 748
 749    fn handle_assistant_configuration_event(
 750        &mut self,
 751        _entity: &Entity<AssistantConfiguration>,
 752        event: &AssistantConfigurationEvent,
 753        window: &mut Window,
 754        cx: &mut Context<Self>,
 755    ) {
 756        match event {
 757            AssistantConfigurationEvent::NewThread(provider) => {
 758                if LanguageModelRegistry::read_global(cx)
 759                    .default_model()
 760                    .map_or(true, |model| model.provider.id() != provider.id())
 761                {
 762                    if let Some(model) = provider.default_model(cx) {
 763                        update_settings_file::<AssistantSettings>(
 764                            self.fs.clone(),
 765                            cx,
 766                            move |settings, _| settings.set_model(model),
 767                        );
 768                    }
 769                }
 770
 771                self.new_thread(&NewThread::default(), window, cx);
 772            }
 773        }
 774    }
 775
 776    pub(crate) fn active_thread(&self, cx: &App) -> Entity<Thread> {
 777        self.thread.read(cx).thread().clone()
 778    }
 779
 780    pub(crate) fn delete_thread(
 781        &mut self,
 782        thread_id: &ThreadId,
 783        cx: &mut Context<Self>,
 784    ) -> Task<Result<()>> {
 785        self.thread_store
 786            .update(cx, |this, cx| this.delete_thread(thread_id, cx))
 787    }
 788
 789    pub(crate) fn has_active_thread(&self) -> bool {
 790        matches!(self.active_view, ActiveView::Thread { .. })
 791    }
 792
 793    pub(crate) fn active_context_editor(&self) -> Option<Entity<ContextEditor>> {
 794        match &self.active_view {
 795            ActiveView::PromptEditor { context_editor } => Some(context_editor.clone()),
 796            _ => None,
 797        }
 798    }
 799
 800    pub(crate) fn delete_context(
 801        &mut self,
 802        path: PathBuf,
 803        cx: &mut Context<Self>,
 804    ) -> Task<Result<()>> {
 805        self.context_store
 806            .update(cx, |this, cx| this.delete_local_context(path, cx))
 807    }
 808
 809    fn set_active_view(
 810        &mut self,
 811        new_view: ActiveView,
 812        window: &mut Window,
 813        cx: &mut Context<Self>,
 814    ) {
 815        let current_is_history = matches!(self.active_view, ActiveView::History);
 816        let new_is_history = matches!(new_view, ActiveView::History);
 817
 818        if current_is_history && !new_is_history {
 819            self.active_view = new_view;
 820        } else if !current_is_history && new_is_history {
 821            self.previous_view = Some(std::mem::replace(&mut self.active_view, new_view));
 822        } else {
 823            if !new_is_history {
 824                self.previous_view = None;
 825            }
 826            self.active_view = new_view;
 827        }
 828
 829        self.focus_handle(cx).focus(window);
 830    }
 831}
 832
 833impl Focusable for AssistantPanel {
 834    fn focus_handle(&self, cx: &App) -> FocusHandle {
 835        match &self.active_view {
 836            ActiveView::Thread { .. } => self.message_editor.focus_handle(cx),
 837            ActiveView::History => self.history.focus_handle(cx),
 838            ActiveView::PromptEditor { context_editor } => context_editor.focus_handle(cx),
 839            ActiveView::Configuration => {
 840                if let Some(configuration) = self.configuration.as_ref() {
 841                    configuration.focus_handle(cx)
 842                } else {
 843                    cx.focus_handle()
 844                }
 845            }
 846        }
 847    }
 848}
 849
 850impl EventEmitter<PanelEvent> for AssistantPanel {}
 851
 852impl Panel for AssistantPanel {
 853    fn persistent_name() -> &'static str {
 854        "AgentPanel"
 855    }
 856
 857    fn position(&self, _window: &Window, cx: &App) -> DockPosition {
 858        match AssistantSettings::get_global(cx).dock {
 859            AssistantDockPosition::Left => DockPosition::Left,
 860            AssistantDockPosition::Bottom => DockPosition::Bottom,
 861            AssistantDockPosition::Right => DockPosition::Right,
 862        }
 863    }
 864
 865    fn position_is_valid(&self, _: DockPosition) -> bool {
 866        true
 867    }
 868
 869    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
 870        settings::update_settings_file::<AssistantSettings>(
 871            self.fs.clone(),
 872            cx,
 873            move |settings, _| {
 874                let dock = match position {
 875                    DockPosition::Left => AssistantDockPosition::Left,
 876                    DockPosition::Bottom => AssistantDockPosition::Bottom,
 877                    DockPosition::Right => AssistantDockPosition::Right,
 878                };
 879                settings.set_dock(dock);
 880            },
 881        );
 882    }
 883
 884    fn size(&self, window: &Window, cx: &App) -> Pixels {
 885        let settings = AssistantSettings::get_global(cx);
 886        match self.position(window, cx) {
 887            DockPosition::Left | DockPosition::Right => {
 888                self.width.unwrap_or(settings.default_width)
 889            }
 890            DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
 891        }
 892    }
 893
 894    fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
 895        match self.position(window, cx) {
 896            DockPosition::Left | DockPosition::Right => self.width = size,
 897            DockPosition::Bottom => self.height = size,
 898        }
 899        cx.notify();
 900    }
 901
 902    fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
 903
 904    fn remote_id() -> Option<proto::PanelId> {
 905        Some(proto::PanelId::AssistantPanel)
 906    }
 907
 908    fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
 909        (self.enabled(cx) && AssistantSettings::get_global(cx).button)
 910            .then_some(IconName::ZedAssistant)
 911    }
 912
 913    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
 914        Some("Agent Panel")
 915    }
 916
 917    fn toggle_action(&self) -> Box<dyn Action> {
 918        Box::new(ToggleFocus)
 919    }
 920
 921    fn activation_priority(&self) -> u32 {
 922        3
 923    }
 924
 925    fn enabled(&self, cx: &App) -> bool {
 926        AssistantSettings::get_global(cx).enabled
 927    }
 928}
 929
 930impl AssistantPanel {
 931    fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
 932        const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
 933
 934        let content = match &self.active_view {
 935            ActiveView::Thread {
 936                change_title_editor,
 937                ..
 938            } => {
 939                let active_thread = self.thread.read(cx);
 940                let is_empty = active_thread.is_empty();
 941
 942                let summary = active_thread.summary(cx);
 943
 944                if is_empty {
 945                    Label::new(Thread::DEFAULT_SUMMARY.clone())
 946                        .truncate()
 947                        .ml_2()
 948                        .into_any_element()
 949                } else if summary.is_none() {
 950                    Label::new(LOADING_SUMMARY_PLACEHOLDER)
 951                        .ml_2()
 952                        .truncate()
 953                        .into_any_element()
 954                } else {
 955                    div()
 956                        .ml_2()
 957                        .w_full()
 958                        .child(change_title_editor.clone())
 959                        .into_any_element()
 960                }
 961            }
 962            ActiveView::PromptEditor { context_editor } => {
 963                let title = SharedString::from(context_editor.read(cx).title(cx).to_string());
 964                Label::new(title).ml_2().truncate().into_any_element()
 965            }
 966            ActiveView::History => Label::new("History").truncate().into_any_element(),
 967            ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
 968        };
 969
 970        h_flex()
 971            .key_context("TitleEditor")
 972            .id("TitleEditor")
 973            .flex_grow()
 974            .w_full()
 975            .max_w_full()
 976            .overflow_x_scroll()
 977            .child(content)
 978            .into_any()
 979    }
 980
 981    fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 982        let active_thread = self.thread.read(cx);
 983        let thread = active_thread.thread().read(cx);
 984        let thread_id = thread.id().clone();
 985        let is_empty = active_thread.is_empty();
 986        let is_history = matches!(self.active_view, ActiveView::History);
 987
 988        let show_token_count = match &self.active_view {
 989            ActiveView::Thread { .. } => !is_empty,
 990            ActiveView::PromptEditor { .. } => true,
 991            _ => false,
 992        };
 993
 994        let focus_handle = self.focus_handle(cx);
 995
 996        let go_back_button = match &self.active_view {
 997            ActiveView::History | ActiveView::Configuration => Some(
 998                div().pl_1().child(
 999                    IconButton::new("go-back", IconName::ArrowLeft)
1000                        .icon_size(IconSize::Small)
1001                        .on_click(cx.listener(|this, _, window, cx| {
1002                            this.go_back(&workspace::GoBack, window, cx);
1003                        }))
1004                        .tooltip({
1005                            let focus_handle = focus_handle.clone();
1006                            move |window, cx| {
1007                                Tooltip::for_action_in(
1008                                    "Go Back",
1009                                    &workspace::GoBack,
1010                                    &focus_handle,
1011                                    window,
1012                                    cx,
1013                                )
1014                            }
1015                        }),
1016                ),
1017            ),
1018            _ => None,
1019        };
1020
1021        h_flex()
1022            .id("assistant-toolbar")
1023            .h(Tab::container_height(cx))
1024            .max_w_full()
1025            .flex_none()
1026            .justify_between()
1027            .gap_2()
1028            .bg(cx.theme().colors().tab_bar_background)
1029            .border_b_1()
1030            .border_color(cx.theme().colors().border)
1031            .child(
1032                h_flex()
1033                    .w_full()
1034                    .gap_1()
1035                    .children(go_back_button)
1036                    .child(self.render_title_view(window, cx)),
1037            )
1038            .child(
1039                h_flex()
1040                    .h_full()
1041                    .gap_2()
1042                    .when(show_token_count, |parent|
1043                        parent.children(self.render_token_count(&thread, cx))
1044                    )
1045                    .child(
1046                        h_flex()
1047                            .h_full()
1048                            .gap(DynamicSpacing::Base02.rems(cx))
1049                            .px(DynamicSpacing::Base08.rems(cx))
1050                            .border_l_1()
1051                            .border_color(cx.theme().colors().border)
1052                            .child(
1053                                IconButton::new("new", IconName::Plus)
1054                                    .icon_size(IconSize::Small)
1055                                    .style(ButtonStyle::Subtle)
1056                                    .tooltip(move |window, cx| {
1057                                        Tooltip::for_action_in(
1058                                            "New Thread",
1059                                            &NewThread::default(),
1060                                            &focus_handle,
1061                                            window,
1062                                            cx,
1063                                        )
1064                                    })
1065                                    .on_click(move |_event, window, cx| {
1066                                        window.dispatch_action(
1067                                            NewThread::default().boxed_clone(),
1068                                            cx,
1069                                        );
1070                                    }),
1071                            )
1072                            .child(
1073                                IconButton::new("open-history", IconName::HistoryRerun)
1074                                    .icon_size(IconSize::Small)
1075                                    .toggle_state(is_history)
1076                                    .selected_icon_color(Color::Accent)
1077                                    .tooltip({
1078                                        let focus_handle = self.focus_handle(cx);
1079                                        move |window, cx| {
1080                                            Tooltip::for_action_in(
1081                                                "History",
1082                                                &OpenHistory,
1083                                                &focus_handle,
1084                                                window,
1085                                                cx,
1086                                            )
1087                                        }
1088                                    })
1089                                    .on_click(move |_event, window, cx| {
1090                                        window.dispatch_action(OpenHistory.boxed_clone(), cx);
1091                                    }),
1092                            )
1093                            .child(
1094                                PopoverMenu::new("assistant-menu")
1095                                    .trigger_with_tooltip(
1096                                        IconButton::new("new", IconName::Ellipsis)
1097                                            .icon_size(IconSize::Small)
1098                                            .style(ButtonStyle::Subtle),
1099                                        Tooltip::text("Toggle Agent Menu"),
1100                                    )
1101                                    .anchor(Corner::TopRight)
1102                                    .with_handle(self.assistant_dropdown_menu_handle.clone())
1103                                    .menu(move |window, cx| {
1104                                        Some(ContextMenu::build(
1105                                            window,
1106                                            cx,
1107                                            |menu, _window, _cx| {
1108                                                menu
1109                                                    .when(!is_empty, |menu| {
1110                                                        menu.action(
1111                                                            "Start New From Summary",
1112                                                            Box::new(NewThread {
1113                                                                from_thread_id: Some(thread_id.clone()),
1114                                                            }),
1115                                                        ).separator()
1116                                                    })
1117                                                    .action(
1118                                                    "New Text Thread",
1119                                                    NewTextThread.boxed_clone(),
1120                                                )
1121                                                .action("Prompt Library", Box::new(OpenPromptLibrary))
1122                                                .action("Settings", Box::new(OpenConfiguration))
1123                                                .separator()
1124                                                .action(
1125                                                    "Install MCPs",
1126                                                    Box::new(zed_actions::Extensions {
1127                                                        category_filter: Some(
1128                                                            zed_actions::ExtensionCategoryFilter::ContextServers,
1129                                                        ),
1130                                                        }),
1131                                                )
1132                                            },
1133                                        ))
1134                                    }),
1135                            ),
1136                    ),
1137            )
1138    }
1139
1140    fn render_token_count(&self, thread: &Thread, cx: &App) -> Option<AnyElement> {
1141        let is_generating = thread.is_generating();
1142        let message_editor = self.message_editor.read(cx);
1143
1144        let conversation_token_usage = thread.total_token_usage(cx);
1145        let (total_token_usage, is_estimating) = if let Some((editing_message_id, unsent_tokens)) =
1146            self.thread.read(cx).editing_message_id()
1147        {
1148            let combined = thread
1149                .token_usage_up_to_message(editing_message_id, cx)
1150                .add(unsent_tokens);
1151
1152            (combined, unsent_tokens > 0)
1153        } else {
1154            let unsent_tokens = message_editor.last_estimated_token_count().unwrap_or(0);
1155            let combined = conversation_token_usage.add(unsent_tokens);
1156
1157            (combined, unsent_tokens > 0)
1158        };
1159
1160        let is_waiting_to_update_token_count = message_editor.is_waiting_to_update_token_count();
1161
1162        match &self.active_view {
1163            ActiveView::Thread { .. } => {
1164                if total_token_usage.total == 0 {
1165                    return None;
1166                }
1167
1168                let token_color = match total_token_usage.ratio() {
1169                    TokenUsageRatio::Normal if is_estimating => Color::Default,
1170                    TokenUsageRatio::Normal => Color::Muted,
1171                    TokenUsageRatio::Warning => Color::Warning,
1172                    TokenUsageRatio::Exceeded => Color::Error,
1173                };
1174
1175                let token_count = h_flex()
1176                    .id("token-count")
1177                    .flex_shrink_0()
1178                    .gap_0p5()
1179                    .when(!is_generating && is_estimating, |parent| {
1180                        parent
1181                            .child(
1182                                h_flex()
1183                                    .mr_1()
1184                                    .size_2p5()
1185                                    .justify_center()
1186                                    .rounded_full()
1187                                    .bg(cx.theme().colors().text.opacity(0.1))
1188                                    .child(
1189                                        div().size_1().rounded_full().bg(cx.theme().colors().text),
1190                                    ),
1191                            )
1192                            .tooltip(move |window, cx| {
1193                                Tooltip::with_meta(
1194                                    "Estimated New Token Count",
1195                                    None,
1196                                    format!(
1197                                        "Current Conversation Tokens: {}",
1198                                        humanize_token_count(conversation_token_usage.total)
1199                                    ),
1200                                    window,
1201                                    cx,
1202                                )
1203                            })
1204                    })
1205                    .child(
1206                        Label::new(humanize_token_count(total_token_usage.total))
1207                            .size(LabelSize::Small)
1208                            .color(token_color)
1209                            .map(|label| {
1210                                if is_generating || is_waiting_to_update_token_count {
1211                                    label
1212                                        .with_animation(
1213                                            "used-tokens-label",
1214                                            Animation::new(Duration::from_secs(2))
1215                                                .repeat()
1216                                                .with_easing(pulsating_between(0.6, 1.)),
1217                                            |label, delta| label.alpha(delta),
1218                                        )
1219                                        .into_any()
1220                                } else {
1221                                    label.into_any_element()
1222                                }
1223                            }),
1224                    )
1225                    .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
1226                    .child(
1227                        Label::new(humanize_token_count(total_token_usage.max))
1228                            .size(LabelSize::Small)
1229                            .color(Color::Muted),
1230                    )
1231                    .into_any();
1232
1233                Some(token_count)
1234            }
1235            ActiveView::PromptEditor { context_editor } => {
1236                let element = render_remaining_tokens(context_editor, cx)?;
1237
1238                Some(element.into_any_element())
1239            }
1240            _ => None,
1241        }
1242    }
1243
1244    fn render_active_thread_or_empty_state(
1245        &self,
1246        window: &mut Window,
1247        cx: &mut Context<Self>,
1248    ) -> AnyElement {
1249        if self.thread.read(cx).is_empty() {
1250            return self
1251                .render_thread_empty_state(window, cx)
1252                .into_any_element();
1253        }
1254
1255        self.thread.clone().into_any_element()
1256    }
1257
1258    fn configuration_error(&self, cx: &App) -> Option<ConfigurationError> {
1259        let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
1260            return Some(ConfigurationError::NoProvider);
1261        };
1262
1263        if !model.provider.is_authenticated(cx) {
1264            return Some(ConfigurationError::ProviderNotAuthenticated);
1265        }
1266
1267        if model.provider.must_accept_terms(cx) {
1268            return Some(ConfigurationError::ProviderPendingTermsAcceptance(
1269                model.provider,
1270            ));
1271        }
1272
1273        None
1274    }
1275
1276    fn render_thread_empty_state(
1277        &self,
1278        window: &mut Window,
1279        cx: &mut Context<Self>,
1280    ) -> impl IntoElement {
1281        let recent_history = self
1282            .history_store
1283            .update(cx, |this, cx| this.recent_entries(6, cx));
1284
1285        let configuration_error = self.configuration_error(cx);
1286        let no_error = configuration_error.is_none();
1287        let focus_handle = self.focus_handle(cx);
1288
1289        v_flex()
1290            .size_full()
1291            .when(recent_history.is_empty(), |this| {
1292                let configuration_error_ref = &configuration_error;
1293                this.child(
1294                    v_flex()
1295                        .size_full()
1296                        .max_w_80()
1297                        .mx_auto()
1298                        .justify_center()
1299                        .items_center()
1300                        .gap_1()
1301                        .child(
1302                            h_flex().child(
1303                                Headline::new("Welcome to the Agent Panel")
1304                            ),
1305                        )
1306                        .when(no_error, |parent| {
1307                            parent
1308                                .child(
1309                                    h_flex().child(
1310                                        Label::new("Ask and build anything.")
1311                                            .color(Color::Muted)
1312                                            .mb_2p5(),
1313                                    ),
1314                                )
1315                                .child(
1316                                    Button::new("new-thread", "Start New Thread")
1317                                        .icon(IconName::Plus)
1318                                        .icon_position(IconPosition::Start)
1319                                        .icon_size(IconSize::Small)
1320                                        .icon_color(Color::Muted)
1321                                        .full_width()
1322                                        .key_binding(KeyBinding::for_action_in(
1323                                            &NewThread::default(),
1324                                            &focus_handle,
1325                                            window,
1326                                            cx,
1327                                        ))
1328                                        .on_click(|_event, window, cx| {
1329                                            window.dispatch_action(NewThread::default().boxed_clone(), cx)
1330                                        }),
1331                                )
1332                                .child(
1333                                    Button::new("context", "Add Context")
1334                                        .icon(IconName::FileCode)
1335                                        .icon_position(IconPosition::Start)
1336                                        .icon_size(IconSize::Small)
1337                                        .icon_color(Color::Muted)
1338                                        .full_width()
1339                                        .key_binding(KeyBinding::for_action_in(
1340                                            &ToggleContextPicker,
1341                                            &focus_handle,
1342                                            window,
1343                                            cx,
1344                                        ))
1345                                        .on_click(|_event, window, cx| {
1346                                            window.dispatch_action(ToggleContextPicker.boxed_clone(), cx)
1347                                        }),
1348                                )
1349                                .child(
1350                                    Button::new("mode", "Switch Model")
1351                                        .icon(IconName::DatabaseZap)
1352                                        .icon_position(IconPosition::Start)
1353                                        .icon_size(IconSize::Small)
1354                                        .icon_color(Color::Muted)
1355                                        .full_width()
1356                                        .key_binding(KeyBinding::for_action_in(
1357                                            &ToggleModelSelector,
1358                                            &focus_handle,
1359                                            window,
1360                                            cx,
1361                                        ))
1362                                        .on_click(|_event, window, cx| {
1363                                            window.dispatch_action(ToggleModelSelector.boxed_clone(), cx)
1364                                        }),
1365                                )
1366                                .child(
1367                                    Button::new("settings", "View Settings")
1368                                        .icon(IconName::Settings)
1369                                        .icon_position(IconPosition::Start)
1370                                        .icon_size(IconSize::Small)
1371                                        .icon_color(Color::Muted)
1372                                        .full_width()
1373                                        .key_binding(KeyBinding::for_action_in(
1374                                            &OpenConfiguration,
1375                                            &focus_handle,
1376                                            window,
1377                                            cx,
1378                                        ))
1379                                        .on_click(|_event, window, cx| {
1380                                            window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
1381                                        }),
1382                                )
1383                        })
1384                        .map(|parent| {
1385                            match configuration_error_ref {
1386                                Some(ConfigurationError::ProviderNotAuthenticated)
1387                                | Some(ConfigurationError::NoProvider) => {
1388                                    parent
1389                                        .child(
1390                                            h_flex().child(
1391                                                Label::new("To start using the agent, configure at least one LLM provider.")
1392                                                    .color(Color::Muted)
1393                                                    .mb_2p5()
1394                                            )
1395                                        )
1396                                        .child(
1397                                            Button::new("settings", "Configure a Provider")
1398                                                .icon(IconName::Settings)
1399                                                .icon_position(IconPosition::Start)
1400                                                .icon_size(IconSize::Small)
1401                                                .icon_color(Color::Muted)
1402                                                .full_width()
1403                                                .key_binding(KeyBinding::for_action_in(
1404                                                    &OpenConfiguration,
1405                                                    &focus_handle,
1406                                                    window,
1407                                                    cx,
1408                                                ))
1409                                                .on_click(|_event, window, cx| {
1410                                                    window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
1411                                                }),
1412                                        )
1413                                }
1414                                Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
1415                                    parent.children(
1416                                        provider.render_accept_terms(
1417                                            LanguageModelProviderTosView::ThreadFreshStart,
1418                                            cx,
1419                                        ),
1420                                    )
1421                                }
1422                                None => parent,
1423                            }
1424                        })
1425                )
1426            })
1427            .when(!recent_history.is_empty(), |parent| {
1428                let focus_handle = focus_handle.clone();
1429                let configuration_error_ref = &configuration_error;
1430
1431                parent
1432                    .overflow_hidden()
1433                    .p_1p5()
1434                    .justify_end()
1435                    .gap_1()
1436                    .child(
1437                        h_flex()
1438                            .pl_1p5()
1439                            .pb_1()
1440                            .w_full()
1441                            .justify_between()
1442                            .border_b_1()
1443                            .border_color(cx.theme().colors().border_variant)
1444                            .child(
1445                                Label::new("Past Interactions")
1446                                    .size(LabelSize::Small)
1447                                    .color(Color::Muted),
1448                            )
1449                            .child(
1450                                Button::new("view-history", "View All")
1451                                    .style(ButtonStyle::Subtle)
1452                                    .label_size(LabelSize::Small)
1453                                    .key_binding(
1454                                        KeyBinding::for_action_in(
1455                                            &OpenHistory,
1456                                            &self.focus_handle(cx),
1457                                            window,
1458                                            cx,
1459                                        ).map(|kb| kb.size(rems_from_px(12.))),
1460                                    )
1461                                    .on_click(move |_event, window, cx| {
1462                                        window.dispatch_action(OpenHistory.boxed_clone(), cx);
1463                                    }),
1464                            ),
1465                    )
1466                    .child(
1467                        v_flex()
1468                            .gap_1()
1469                            .children(
1470                                recent_history.into_iter().map(|entry| {
1471                                    // TODO: Add keyboard navigation.
1472                                    match entry {
1473                                        HistoryEntry::Thread(thread) => {
1474                                            PastThread::new(thread, cx.entity().downgrade(), false, vec![])
1475                                                .into_any_element()
1476                                        }
1477                                        HistoryEntry::Context(context) => {
1478                                            PastContext::new(context, cx.entity().downgrade(), false, vec![])
1479                                                .into_any_element()
1480                                        }
1481                                    }
1482                                }),
1483                            )
1484                    )
1485                    .map(|parent| {
1486                        match configuration_error_ref {
1487                            Some(ConfigurationError::ProviderNotAuthenticated)
1488                            | Some(ConfigurationError::NoProvider) => {
1489                                parent
1490                                    .child(
1491                                        Banner::new()
1492                                            .severity(ui::Severity::Warning)
1493                                            .children(
1494                                                Label::new(
1495                                                    "Configure at least one LLM provider to start using the panel.",
1496                                                )
1497                                                .size(LabelSize::Small),
1498                                            )
1499                                            .action_slot(
1500                                                Button::new("settings", "Configure Provider")
1501                                                    .style(ButtonStyle::Tinted(ui::TintColor::Warning))
1502                                                    .label_size(LabelSize::Small)
1503                                                    .key_binding(
1504                                                        KeyBinding::for_action_in(
1505                                                            &OpenConfiguration,
1506                                                            &focus_handle,
1507                                                            window,
1508                                                            cx,
1509                                                        )
1510                                                        .map(|kb| kb.size(rems_from_px(12.))),
1511                                                    )
1512                                                    .on_click(|_event, window, cx| {
1513                                                        window.dispatch_action(
1514                                                            OpenConfiguration.boxed_clone(),
1515                                                            cx,
1516                                                        )
1517                                                    }),
1518                                            ),
1519                                    )
1520                            }
1521                            Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
1522                                parent
1523                                    .child(
1524                                        Banner::new()
1525                                            .severity(ui::Severity::Warning)
1526                                            .children(
1527                                                h_flex()
1528                                                    .w_full()
1529                                                    .children(
1530                                                        provider.render_accept_terms(
1531                                                            LanguageModelProviderTosView::ThreadtEmptyState,
1532                                                            cx,
1533                                                        ),
1534                                                    ),
1535                                            ),
1536                                    )
1537                            }
1538                            None => parent,
1539                        }
1540                    })
1541            })
1542    }
1543
1544    fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
1545        let last_error = self.thread.read(cx).last_error()?;
1546
1547        Some(
1548            div()
1549                .absolute()
1550                .right_3()
1551                .bottom_12()
1552                .max_w_96()
1553                .py_2()
1554                .px_3()
1555                .elevation_2(cx)
1556                .occlude()
1557                .child(match last_error {
1558                    ThreadError::PaymentRequired => self.render_payment_required_error(cx),
1559                    ThreadError::MaxMonthlySpendReached => {
1560                        self.render_max_monthly_spend_reached_error(cx)
1561                    }
1562                    ThreadError::ModelRequestLimitReached { plan } => {
1563                        self.render_model_request_limit_reached_error(plan, cx)
1564                    }
1565                    ThreadError::Message { header, message } => {
1566                        self.render_error_message(header, message, cx)
1567                    }
1568                })
1569                .into_any(),
1570        )
1571    }
1572
1573    fn render_payment_required_error(&self, cx: &mut Context<Self>) -> AnyElement {
1574        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.";
1575
1576        v_flex()
1577            .gap_0p5()
1578            .child(
1579                h_flex()
1580                    .gap_1p5()
1581                    .items_center()
1582                    .child(Icon::new(IconName::XCircle).color(Color::Error))
1583                    .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
1584            )
1585            .child(
1586                div()
1587                    .id("error-message")
1588                    .max_h_24()
1589                    .overflow_y_scroll()
1590                    .child(Label::new(ERROR_MESSAGE)),
1591            )
1592            .child(
1593                h_flex()
1594                    .justify_end()
1595                    .mt_1()
1596                    .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
1597                        |this, _, _, cx| {
1598                            this.thread.update(cx, |this, _cx| {
1599                                this.clear_last_error();
1600                            });
1601
1602                            cx.open_url(&zed_urls::account_url(cx));
1603                            cx.notify();
1604                        },
1605                    )))
1606                    .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1607                        |this, _, _, cx| {
1608                            this.thread.update(cx, |this, _cx| {
1609                                this.clear_last_error();
1610                            });
1611
1612                            cx.notify();
1613                        },
1614                    ))),
1615            )
1616            .into_any()
1617    }
1618
1619    fn render_max_monthly_spend_reached_error(&self, cx: &mut Context<Self>) -> AnyElement {
1620        const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
1621
1622        v_flex()
1623            .gap_0p5()
1624            .child(
1625                h_flex()
1626                    .gap_1p5()
1627                    .items_center()
1628                    .child(Icon::new(IconName::XCircle).color(Color::Error))
1629                    .child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
1630            )
1631            .child(
1632                div()
1633                    .id("error-message")
1634                    .max_h_24()
1635                    .overflow_y_scroll()
1636                    .child(Label::new(ERROR_MESSAGE)),
1637            )
1638            .child(
1639                h_flex()
1640                    .justify_end()
1641                    .mt_1()
1642                    .child(
1643                        Button::new("subscribe", "Update Monthly Spend Limit").on_click(
1644                            cx.listener(|this, _, _, cx| {
1645                                this.thread.update(cx, |this, _cx| {
1646                                    this.clear_last_error();
1647                                });
1648
1649                                cx.open_url(&zed_urls::account_url(cx));
1650                                cx.notify();
1651                            }),
1652                        ),
1653                    )
1654                    .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1655                        |this, _, _, cx| {
1656                            this.thread.update(cx, |this, _cx| {
1657                                this.clear_last_error();
1658                            });
1659
1660                            cx.notify();
1661                        },
1662                    ))),
1663            )
1664            .into_any()
1665    }
1666
1667    fn render_model_request_limit_reached_error(
1668        &self,
1669        plan: Plan,
1670        cx: &mut Context<Self>,
1671    ) -> AnyElement {
1672        let error_message = match plan {
1673            Plan::ZedPro => {
1674                "Model request limit reached. Upgrade to usage-based billing for more requests."
1675            }
1676            Plan::ZedProTrial => {
1677                "Model request limit reached. Upgrade to Zed Pro for more requests."
1678            }
1679            Plan::Free => "Model request limit reached. Upgrade to Zed Pro for more requests.",
1680        };
1681        let call_to_action = match plan {
1682            Plan::ZedPro => "Upgrade to usage-based billing",
1683            Plan::ZedProTrial => "Upgrade to Zed Pro",
1684            Plan::Free => "Upgrade to Zed Pro",
1685        };
1686
1687        v_flex()
1688            .gap_0p5()
1689            .child(
1690                h_flex()
1691                    .gap_1p5()
1692                    .items_center()
1693                    .child(Icon::new(IconName::XCircle).color(Color::Error))
1694                    .child(Label::new("Model Request Limit Reached").weight(FontWeight::MEDIUM)),
1695            )
1696            .child(
1697                div()
1698                    .id("error-message")
1699                    .max_h_24()
1700                    .overflow_y_scroll()
1701                    .child(Label::new(error_message)),
1702            )
1703            .child(
1704                h_flex()
1705                    .justify_end()
1706                    .mt_1()
1707                    .child(
1708                        Button::new("subscribe", call_to_action).on_click(cx.listener(
1709                            |this, _, _, cx| {
1710                                this.thread.update(cx, |this, _cx| {
1711                                    this.clear_last_error();
1712                                });
1713
1714                                cx.open_url(&zed_urls::account_url(cx));
1715                                cx.notify();
1716                            },
1717                        )),
1718                    )
1719                    .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1720                        |this, _, _, cx| {
1721                            this.thread.update(cx, |this, _cx| {
1722                                this.clear_last_error();
1723                            });
1724
1725                            cx.notify();
1726                        },
1727                    ))),
1728            )
1729            .into_any()
1730    }
1731
1732    fn render_error_message(
1733        &self,
1734        header: SharedString,
1735        message: SharedString,
1736        cx: &mut Context<Self>,
1737    ) -> AnyElement {
1738        v_flex()
1739            .gap_0p5()
1740            .child(
1741                h_flex()
1742                    .gap_1p5()
1743                    .items_center()
1744                    .child(Icon::new(IconName::XCircle).color(Color::Error))
1745                    .child(Label::new(header).weight(FontWeight::MEDIUM)),
1746            )
1747            .child(
1748                div()
1749                    .id("error-message")
1750                    .max_h_32()
1751                    .overflow_y_scroll()
1752                    .child(Label::new(message)),
1753            )
1754            .child(
1755                h_flex()
1756                    .justify_end()
1757                    .mt_1()
1758                    .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1759                        |this, _, _, cx| {
1760                            this.thread.update(cx, |this, _cx| {
1761                                this.clear_last_error();
1762                            });
1763
1764                            cx.notify();
1765                        },
1766                    ))),
1767            )
1768            .into_any()
1769    }
1770
1771    fn key_context(&self) -> KeyContext {
1772        let mut key_context = KeyContext::new_with_defaults();
1773        key_context.add("AgentPanel");
1774        if matches!(self.active_view, ActiveView::PromptEditor { .. }) {
1775            key_context.add("prompt_editor");
1776        }
1777        key_context
1778    }
1779}
1780
1781impl Render for AssistantPanel {
1782    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1783        v_flex()
1784            .key_context(self.key_context())
1785            .justify_between()
1786            .size_full()
1787            .on_action(cx.listener(Self::cancel))
1788            .on_action(cx.listener(|this, action: &NewThread, window, cx| {
1789                this.new_thread(action, window, cx);
1790            }))
1791            .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
1792                this.open_history(window, cx);
1793            }))
1794            .on_action(cx.listener(|this, _: &OpenConfiguration, window, cx| {
1795                this.open_configuration(window, cx);
1796            }))
1797            .on_action(cx.listener(Self::open_active_thread_as_markdown))
1798            .on_action(cx.listener(Self::deploy_prompt_library))
1799            .on_action(cx.listener(Self::open_agent_diff))
1800            .on_action(cx.listener(Self::go_back))
1801            .child(self.render_toolbar(window, cx))
1802            .map(|parent| match &self.active_view {
1803                ActiveView::Thread { .. } => parent
1804                    .child(self.render_active_thread_or_empty_state(window, cx))
1805                    .child(h_flex().child(self.message_editor.clone()))
1806                    .children(self.render_last_error(cx)),
1807                ActiveView::History => parent.child(self.history.clone()),
1808                ActiveView::PromptEditor { context_editor } => parent.child(context_editor.clone()),
1809                ActiveView::Configuration => parent.children(self.configuration.clone()),
1810            })
1811    }
1812}
1813
1814struct PromptLibraryInlineAssist {
1815    workspace: WeakEntity<Workspace>,
1816}
1817
1818impl PromptLibraryInlineAssist {
1819    pub fn new(workspace: WeakEntity<Workspace>) -> Self {
1820        Self { workspace }
1821    }
1822}
1823
1824impl prompt_library::InlineAssistDelegate for PromptLibraryInlineAssist {
1825    fn assist(
1826        &self,
1827        prompt_editor: &Entity<Editor>,
1828        _initial_prompt: Option<String>,
1829        window: &mut Window,
1830        cx: &mut Context<PromptLibrary>,
1831    ) {
1832        InlineAssistant::update_global(cx, |assistant, cx| {
1833            let Some(project) = self
1834                .workspace
1835                .upgrade()
1836                .map(|workspace| workspace.read(cx).project().downgrade())
1837            else {
1838                return;
1839            };
1840            assistant.assist(
1841                &prompt_editor,
1842                self.workspace.clone(),
1843                project,
1844                None,
1845                window,
1846                cx,
1847            )
1848        })
1849    }
1850
1851    fn focus_assistant_panel(
1852        &self,
1853        workspace: &mut Workspace,
1854        window: &mut Window,
1855        cx: &mut Context<Workspace>,
1856    ) -> bool {
1857        workspace
1858            .focus_panel::<AssistantPanel>(window, cx)
1859            .is_some()
1860    }
1861}
1862
1863pub struct ConcreteAssistantPanelDelegate;
1864
1865impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
1866    fn active_context_editor(
1867        &self,
1868        workspace: &mut Workspace,
1869        _window: &mut Window,
1870        cx: &mut Context<Workspace>,
1871    ) -> Option<Entity<ContextEditor>> {
1872        let panel = workspace.panel::<AssistantPanel>(cx)?;
1873        panel.read(cx).active_context_editor()
1874    }
1875
1876    fn open_saved_context(
1877        &self,
1878        workspace: &mut Workspace,
1879        path: std::path::PathBuf,
1880        window: &mut Window,
1881        cx: &mut Context<Workspace>,
1882    ) -> Task<Result<()>> {
1883        let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
1884            return Task::ready(Err(anyhow!("Agent panel not found")));
1885        };
1886
1887        panel.update(cx, |panel, cx| {
1888            panel.open_saved_prompt_editor(path, window, cx)
1889        })
1890    }
1891
1892    fn open_remote_context(
1893        &self,
1894        _workspace: &mut Workspace,
1895        _context_id: assistant_context_editor::ContextId,
1896        _window: &mut Window,
1897        _cx: &mut Context<Workspace>,
1898    ) -> Task<Result<Entity<ContextEditor>>> {
1899        Task::ready(Err(anyhow!("opening remote context not implemented")))
1900    }
1901
1902    fn quote_selection(
1903        &self,
1904        workspace: &mut Workspace,
1905        selection_ranges: Vec<Range<Anchor>>,
1906        buffer: Entity<MultiBuffer>,
1907        window: &mut Window,
1908        cx: &mut Context<Workspace>,
1909    ) {
1910        let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
1911            return;
1912        };
1913
1914        if !panel.focus_handle(cx).contains_focused(window, cx) {
1915            workspace.toggle_panel_focus::<AssistantPanel>(window, cx);
1916        }
1917
1918        panel.update(cx, |_, cx| {
1919            // Wait to create a new context until the workspace is no longer
1920            // being updated.
1921            cx.defer_in(window, move |panel, window, cx| {
1922                if panel.has_active_thread() {
1923                    panel.thread.update(cx, |thread, cx| {
1924                        thread.context_store().update(cx, |store, cx| {
1925                            let buffer = buffer.read(cx);
1926                            let selection_ranges = selection_ranges
1927                                .into_iter()
1928                                .flat_map(|range| {
1929                                    let (start_buffer, start) =
1930                                        buffer.text_anchor_for_position(range.start, cx)?;
1931                                    let (end_buffer, end) =
1932                                        buffer.text_anchor_for_position(range.end, cx)?;
1933                                    if start_buffer != end_buffer {
1934                                        return None;
1935                                    }
1936                                    Some((start_buffer, start..end))
1937                                })
1938                                .collect::<Vec<_>>();
1939
1940                            for (buffer, range) in selection_ranges {
1941                                store.add_excerpt(range, buffer, cx).detach_and_log_err(cx);
1942                            }
1943                        })
1944                    })
1945                } else if let Some(context_editor) = panel.active_context_editor() {
1946                    let snapshot = buffer.read(cx).snapshot(cx);
1947                    let selection_ranges = selection_ranges
1948                        .into_iter()
1949                        .map(|range| range.to_point(&snapshot))
1950                        .collect::<Vec<_>>();
1951
1952                    context_editor.update(cx, |context_editor, cx| {
1953                        context_editor.quote_ranges(selection_ranges, snapshot, window, cx)
1954                    });
1955                }
1956            });
1957        });
1958    }
1959}