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