assistant_panel.rs

   1use std::path::PathBuf;
   2use std::sync::Arc;
   3
   4use anyhow::{anyhow, Result};
   5use assistant_context_editor::{
   6    make_lsp_adapter_delegate, AssistantPanelDelegate, ConfigurationError, ContextEditor,
   7    ContextHistory, SlashCommandCompletionProvider,
   8};
   9use assistant_settings::{AssistantDockPosition, AssistantSettings};
  10use assistant_slash_command::SlashCommandWorkingSet;
  11use assistant_tool::ToolWorkingSet;
  12use client::zed_urls;
  13use editor::Editor;
  14use fs::Fs;
  15use gpui::{
  16    prelude::*, px, svg, Action, AnyElement, AppContext, AsyncWindowContext, Corner, EventEmitter,
  17    FocusHandle, FocusableView, FontWeight, Model, Pixels, Subscription, Task, UpdateGlobal, View,
  18    ViewContext, WeakView, WindowContext,
  19};
  20use language::LanguageRegistry;
  21use language_model::LanguageModelRegistry;
  22use project::Project;
  23use prompt_library::{open_prompt_library, PromptBuilder, PromptLibrary};
  24use settings::{update_settings_file, Settings};
  25use time::UtcOffset;
  26use ui::{prelude::*, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, Tab, Tooltip};
  27use util::ResultExt as _;
  28use workspace::dock::{DockPosition, Panel, PanelEvent};
  29use workspace::Workspace;
  30use zed_actions::assistant::{DeployPromptLibrary, ToggleFocus};
  31
  32use crate::active_thread::ActiveThread;
  33use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent};
  34use crate::message_editor::MessageEditor;
  35use crate::thread::{Thread, ThreadError, ThreadId};
  36use crate::thread_history::{PastThread, ThreadHistory};
  37use crate::thread_store::ThreadStore;
  38use crate::{
  39    InlineAssistant, NewPromptEditor, NewThread, OpenConfiguration, OpenHistory,
  40    OpenPromptEditorHistory,
  41};
  42
  43pub fn init(cx: &mut AppContext) {
  44    cx.observe_new_views(
  45        |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
  46            workspace
  47                .register_action(|workspace, _: &NewThread, cx| {
  48                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
  49                        panel.update(cx, |panel, cx| panel.new_thread(cx));
  50                        workspace.focus_panel::<AssistantPanel>(cx);
  51                    }
  52                })
  53                .register_action(|workspace, _: &OpenHistory, cx| {
  54                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
  55                        workspace.focus_panel::<AssistantPanel>(cx);
  56                        panel.update(cx, |panel, cx| panel.open_history(cx));
  57                    }
  58                })
  59                .register_action(|workspace, _: &NewPromptEditor, cx| {
  60                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
  61                        workspace.focus_panel::<AssistantPanel>(cx);
  62                        panel.update(cx, |panel, cx| panel.new_prompt_editor(cx));
  63                    }
  64                })
  65                .register_action(|workspace, _: &OpenPromptEditorHistory, cx| {
  66                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
  67                        workspace.focus_panel::<AssistantPanel>(cx);
  68                        panel.update(cx, |panel, cx| panel.open_prompt_editor_history(cx));
  69                    }
  70                })
  71                .register_action(|workspace, _: &OpenConfiguration, cx| {
  72                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
  73                        workspace.focus_panel::<AssistantPanel>(cx);
  74                        panel.update(cx, |panel, cx| panel.open_configuration(cx));
  75                    }
  76                });
  77        },
  78    )
  79    .detach();
  80}
  81
  82enum ActiveView {
  83    Thread,
  84    PromptEditor,
  85    History,
  86    PromptEditorHistory,
  87    Configuration,
  88}
  89
  90pub struct AssistantPanel {
  91    workspace: WeakView<Workspace>,
  92    project: Model<Project>,
  93    fs: Arc<dyn Fs>,
  94    language_registry: Arc<LanguageRegistry>,
  95    thread_store: Model<ThreadStore>,
  96    thread: View<ActiveThread>,
  97    message_editor: View<MessageEditor>,
  98    context_store: Model<assistant_context_editor::ContextStore>,
  99    context_editor: Option<View<ContextEditor>>,
 100    context_history: Option<View<ContextHistory>>,
 101    configuration: Option<View<AssistantConfiguration>>,
 102    configuration_subscription: Option<Subscription>,
 103    tools: Arc<ToolWorkingSet>,
 104    local_timezone: UtcOffset,
 105    active_view: ActiveView,
 106    history: View<ThreadHistory>,
 107    new_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
 108    open_history_context_menu_handle: PopoverMenuHandle<ContextMenu>,
 109    width: Option<Pixels>,
 110    height: Option<Pixels>,
 111}
 112
 113impl AssistantPanel {
 114    pub fn load(
 115        workspace: WeakView<Workspace>,
 116        prompt_builder: Arc<PromptBuilder>,
 117        cx: AsyncWindowContext,
 118    ) -> Task<Result<View<Self>>> {
 119        cx.spawn(|mut cx| async move {
 120            let tools = Arc::new(ToolWorkingSet::default());
 121            let thread_store = workspace
 122                .update(&mut cx, |workspace, cx| {
 123                    let project = workspace.project().clone();
 124                    ThreadStore::new(project, tools.clone(), cx)
 125                })?
 126                .await?;
 127
 128            let slash_commands = Arc::new(SlashCommandWorkingSet::default());
 129            let context_store = workspace
 130                .update(&mut cx, |workspace, cx| {
 131                    let project = workspace.project().clone();
 132                    assistant_context_editor::ContextStore::new(
 133                        project,
 134                        prompt_builder.clone(),
 135                        slash_commands,
 136                        tools.clone(),
 137                        cx,
 138                    )
 139                })?
 140                .await?;
 141
 142            workspace.update(&mut cx, |workspace, cx| {
 143                cx.new_view(|cx| Self::new(workspace, thread_store, context_store, tools, cx))
 144            })
 145        })
 146    }
 147
 148    fn new(
 149        workspace: &Workspace,
 150        thread_store: Model<ThreadStore>,
 151        context_store: Model<assistant_context_editor::ContextStore>,
 152        tools: Arc<ToolWorkingSet>,
 153        cx: &mut ViewContext<Self>,
 154    ) -> Self {
 155        let thread = thread_store.update(cx, |this, cx| this.create_thread(cx));
 156        let fs = workspace.app_state().fs.clone();
 157        let project = workspace.project().clone();
 158        let language_registry = project.read(cx).languages().clone();
 159        let workspace = workspace.weak_handle();
 160        let weak_self = cx.view().downgrade();
 161
 162        let message_editor = cx.new_view(|cx| {
 163            MessageEditor::new(
 164                fs.clone(),
 165                workspace.clone(),
 166                thread_store.downgrade(),
 167                thread.clone(),
 168                cx,
 169            )
 170        });
 171
 172        Self {
 173            active_view: ActiveView::Thread,
 174            workspace: workspace.clone(),
 175            project,
 176            fs: fs.clone(),
 177            language_registry: language_registry.clone(),
 178            thread_store: thread_store.clone(),
 179            thread: cx.new_view(|cx| {
 180                ActiveThread::new(
 181                    thread.clone(),
 182                    thread_store.clone(),
 183                    workspace,
 184                    language_registry,
 185                    tools.clone(),
 186                    cx,
 187                )
 188            }),
 189            message_editor,
 190            context_store,
 191            context_editor: None,
 192            context_history: None,
 193            configuration: None,
 194            configuration_subscription: None,
 195            tools,
 196            local_timezone: UtcOffset::from_whole_seconds(
 197                chrono::Local::now().offset().local_minus_utc(),
 198            )
 199            .unwrap(),
 200            history: cx.new_view(|cx| ThreadHistory::new(weak_self, thread_store, cx)),
 201            new_item_context_menu_handle: PopoverMenuHandle::default(),
 202            open_history_context_menu_handle: PopoverMenuHandle::default(),
 203            width: None,
 204            height: None,
 205        }
 206    }
 207
 208    pub fn toggle_focus(
 209        workspace: &mut Workspace,
 210        _: &ToggleFocus,
 211        cx: &mut ViewContext<Workspace>,
 212    ) {
 213        let settings = AssistantSettings::get_global(cx);
 214        if !settings.enabled {
 215            return;
 216        }
 217
 218        workspace.toggle_panel_focus::<Self>(cx);
 219    }
 220
 221    pub(crate) fn local_timezone(&self) -> UtcOffset {
 222        self.local_timezone
 223    }
 224
 225    pub(crate) fn thread_store(&self) -> &Model<ThreadStore> {
 226        &self.thread_store
 227    }
 228
 229    fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
 230        self.thread
 231            .update(cx, |thread, cx| thread.cancel_last_completion(cx));
 232    }
 233
 234    fn new_thread(&mut self, cx: &mut ViewContext<Self>) {
 235        let thread = self
 236            .thread_store
 237            .update(cx, |this, cx| this.create_thread(cx));
 238
 239        self.active_view = ActiveView::Thread;
 240        self.thread = cx.new_view(|cx| {
 241            ActiveThread::new(
 242                thread.clone(),
 243                self.thread_store.clone(),
 244                self.workspace.clone(),
 245                self.language_registry.clone(),
 246                self.tools.clone(),
 247                cx,
 248            )
 249        });
 250        self.message_editor = cx.new_view(|cx| {
 251            MessageEditor::new(
 252                self.fs.clone(),
 253                self.workspace.clone(),
 254                self.thread_store.downgrade(),
 255                thread,
 256                cx,
 257            )
 258        });
 259        self.message_editor.focus_handle(cx).focus(cx);
 260    }
 261
 262    fn new_prompt_editor(&mut self, cx: &mut ViewContext<Self>) {
 263        self.active_view = ActiveView::PromptEditor;
 264
 265        let context = self
 266            .context_store
 267            .update(cx, |context_store, cx| context_store.create(cx));
 268        let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
 269            .log_err()
 270            .flatten();
 271
 272        self.context_editor = Some(cx.new_view(|cx| {
 273            let mut editor = ContextEditor::for_context(
 274                context,
 275                self.fs.clone(),
 276                self.workspace.clone(),
 277                self.project.clone(),
 278                lsp_adapter_delegate,
 279                cx,
 280            );
 281            editor.insert_default_prompt(cx);
 282            editor
 283        }));
 284
 285        if let Some(context_editor) = self.context_editor.as_ref() {
 286            context_editor.focus_handle(cx).focus(cx);
 287        }
 288    }
 289
 290    fn deploy_prompt_library(&mut self, _: &DeployPromptLibrary, cx: &mut ViewContext<Self>) {
 291        open_prompt_library(
 292            self.language_registry.clone(),
 293            Box::new(PromptLibraryInlineAssist::new(self.workspace.clone())),
 294            Arc::new(|| {
 295                Box::new(SlashCommandCompletionProvider::new(
 296                    Arc::new(SlashCommandWorkingSet::default()),
 297                    None,
 298                    None,
 299                ))
 300            }),
 301            cx,
 302        )
 303        .detach_and_log_err(cx);
 304    }
 305
 306    fn open_history(&mut self, cx: &mut ViewContext<Self>) {
 307        self.active_view = ActiveView::History;
 308        self.history.focus_handle(cx).focus(cx);
 309        cx.notify();
 310    }
 311
 312    fn open_prompt_editor_history(&mut self, cx: &mut ViewContext<Self>) {
 313        self.active_view = ActiveView::PromptEditorHistory;
 314        self.context_history = Some(cx.new_view(|cx| {
 315            ContextHistory::new(
 316                self.project.clone(),
 317                self.context_store.clone(),
 318                self.workspace.clone(),
 319                cx,
 320            )
 321        }));
 322
 323        if let Some(context_history) = self.context_history.as_ref() {
 324            context_history.focus_handle(cx).focus(cx);
 325        }
 326
 327        cx.notify();
 328    }
 329
 330    fn open_saved_prompt_editor(
 331        &mut self,
 332        path: PathBuf,
 333        cx: &mut ViewContext<Self>,
 334    ) -> Task<Result<()>> {
 335        let context = self
 336            .context_store
 337            .update(cx, |store, cx| store.open_local_context(path.clone(), cx));
 338        let fs = self.fs.clone();
 339        let project = self.project.clone();
 340        let workspace = self.workspace.clone();
 341
 342        let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err().flatten();
 343
 344        cx.spawn(|this, mut cx| async move {
 345            let context = context.await?;
 346            this.update(&mut cx, |this, cx| {
 347                let editor = cx.new_view(|cx| {
 348                    ContextEditor::for_context(
 349                        context,
 350                        fs,
 351                        workspace,
 352                        project,
 353                        lsp_adapter_delegate,
 354                        cx,
 355                    )
 356                });
 357                this.active_view = ActiveView::PromptEditor;
 358                this.context_editor = Some(editor);
 359
 360                anyhow::Ok(())
 361            })??;
 362            Ok(())
 363        })
 364    }
 365
 366    pub(crate) fn open_thread(
 367        &mut self,
 368        thread_id: &ThreadId,
 369        cx: &mut ViewContext<Self>,
 370    ) -> Task<Result<()>> {
 371        let open_thread_task = self
 372            .thread_store
 373            .update(cx, |this, cx| this.open_thread(thread_id, cx));
 374
 375        cx.spawn(|this, mut cx| async move {
 376            let thread = open_thread_task.await?;
 377            this.update(&mut cx, |this, cx| {
 378                this.active_view = ActiveView::Thread;
 379                this.thread = cx.new_view(|cx| {
 380                    ActiveThread::new(
 381                        thread.clone(),
 382                        this.thread_store.clone(),
 383                        this.workspace.clone(),
 384                        this.language_registry.clone(),
 385                        this.tools.clone(),
 386                        cx,
 387                    )
 388                });
 389                this.message_editor = cx.new_view(|cx| {
 390                    MessageEditor::new(
 391                        this.fs.clone(),
 392                        this.workspace.clone(),
 393                        this.thread_store.downgrade(),
 394                        thread,
 395                        cx,
 396                    )
 397                });
 398                this.message_editor.focus_handle(cx).focus(cx);
 399            })
 400        })
 401    }
 402
 403    pub(crate) fn open_configuration(&mut self, cx: &mut ViewContext<Self>) {
 404        self.active_view = ActiveView::Configuration;
 405        self.configuration = Some(cx.new_view(AssistantConfiguration::new));
 406
 407        if let Some(configuration) = self.configuration.as_ref() {
 408            self.configuration_subscription =
 409                Some(cx.subscribe(configuration, Self::handle_assistant_configuration_event));
 410
 411            configuration.focus_handle(cx).focus(cx);
 412        }
 413    }
 414
 415    fn handle_assistant_configuration_event(
 416        &mut self,
 417        _view: View<AssistantConfiguration>,
 418        event: &AssistantConfigurationEvent,
 419        cx: &mut ViewContext<Self>,
 420    ) {
 421        match event {
 422            AssistantConfigurationEvent::NewThread(provider) => {
 423                if LanguageModelRegistry::read_global(cx)
 424                    .active_provider()
 425                    .map_or(true, |active_provider| {
 426                        active_provider.id() != provider.id()
 427                    })
 428                {
 429                    if let Some(model) = provider.provided_models(cx).first().cloned() {
 430                        update_settings_file::<AssistantSettings>(
 431                            self.fs.clone(),
 432                            cx,
 433                            move |settings, _| settings.set_model(model),
 434                        );
 435                    }
 436                }
 437
 438                self.new_thread(cx);
 439            }
 440        }
 441    }
 442
 443    pub(crate) fn active_thread(&self, cx: &AppContext) -> Model<Thread> {
 444        self.thread.read(cx).thread().clone()
 445    }
 446
 447    pub(crate) fn delete_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext<Self>) {
 448        self.thread_store
 449            .update(cx, |this, cx| this.delete_thread(thread_id, cx))
 450            .detach_and_log_err(cx);
 451    }
 452}
 453
 454impl FocusableView for AssistantPanel {
 455    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
 456        match self.active_view {
 457            ActiveView::Thread => self.message_editor.focus_handle(cx),
 458            ActiveView::History => self.history.focus_handle(cx),
 459            ActiveView::PromptEditor => {
 460                if let Some(context_editor) = self.context_editor.as_ref() {
 461                    context_editor.focus_handle(cx)
 462                } else {
 463                    cx.focus_handle()
 464                }
 465            }
 466            ActiveView::PromptEditorHistory => {
 467                if let Some(context_history) = self.context_history.as_ref() {
 468                    context_history.focus_handle(cx)
 469                } else {
 470                    cx.focus_handle()
 471                }
 472            }
 473            ActiveView::Configuration => {
 474                if let Some(configuration) = self.configuration.as_ref() {
 475                    configuration.focus_handle(cx)
 476                } else {
 477                    cx.focus_handle()
 478                }
 479            }
 480        }
 481    }
 482}
 483
 484impl EventEmitter<PanelEvent> for AssistantPanel {}
 485
 486impl Panel for AssistantPanel {
 487    fn persistent_name() -> &'static str {
 488        "AssistantPanel2"
 489    }
 490
 491    fn position(&self, cx: &WindowContext) -> DockPosition {
 492        match AssistantSettings::get_global(cx).dock {
 493            AssistantDockPosition::Left => DockPosition::Left,
 494            AssistantDockPosition::Bottom => DockPosition::Bottom,
 495            AssistantDockPosition::Right => DockPosition::Right,
 496        }
 497    }
 498
 499    fn position_is_valid(&self, _: DockPosition) -> bool {
 500        true
 501    }
 502
 503    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
 504        settings::update_settings_file::<AssistantSettings>(
 505            self.fs.clone(),
 506            cx,
 507            move |settings, _| {
 508                let dock = match position {
 509                    DockPosition::Left => AssistantDockPosition::Left,
 510                    DockPosition::Bottom => AssistantDockPosition::Bottom,
 511                    DockPosition::Right => AssistantDockPosition::Right,
 512                };
 513                settings.set_dock(dock);
 514            },
 515        );
 516    }
 517
 518    fn size(&self, cx: &WindowContext) -> Pixels {
 519        let settings = AssistantSettings::get_global(cx);
 520        match self.position(cx) {
 521            DockPosition::Left | DockPosition::Right => {
 522                self.width.unwrap_or(settings.default_width)
 523            }
 524            DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
 525        }
 526    }
 527
 528    fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
 529        match self.position(cx) {
 530            DockPosition::Left | DockPosition::Right => self.width = size,
 531            DockPosition::Bottom => self.height = size,
 532        }
 533        cx.notify();
 534    }
 535
 536    fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
 537
 538    fn remote_id() -> Option<proto::PanelId> {
 539        Some(proto::PanelId::AssistantPanel)
 540    }
 541
 542    fn icon(&self, cx: &WindowContext) -> Option<IconName> {
 543        let settings = AssistantSettings::get_global(cx);
 544        if !settings.enabled || !settings.button {
 545            return None;
 546        }
 547
 548        Some(IconName::ZedAssistant)
 549    }
 550
 551    fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
 552        Some("Assistant Panel")
 553    }
 554
 555    fn toggle_action(&self) -> Box<dyn Action> {
 556        Box::new(ToggleFocus)
 557    }
 558
 559    fn activation_priority(&self) -> u32 {
 560        3
 561    }
 562}
 563
 564impl AssistantPanel {
 565    fn render_toolbar(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
 566        let thread = self.thread.read(cx);
 567
 568        let title = match self.active_view {
 569            ActiveView::Thread => {
 570                if thread.is_empty() {
 571                    thread.summary_or_default(cx)
 572                } else {
 573                    thread
 574                        .summary(cx)
 575                        .unwrap_or_else(|| SharedString::from("Loading Summary…"))
 576                }
 577            }
 578            ActiveView::PromptEditor => self
 579                .context_editor
 580                .as_ref()
 581                .map(|context_editor| {
 582                    SharedString::from(context_editor.read(cx).title(cx).to_string())
 583                })
 584                .unwrap_or_else(|| SharedString::from("Loading Summary…")),
 585            ActiveView::History => "History / Thread".into(),
 586            ActiveView::PromptEditorHistory => "History / Prompt Editor".into(),
 587            ActiveView::Configuration => "Configuration".into(),
 588        };
 589
 590        h_flex()
 591            .id("assistant-toolbar")
 592            .px(DynamicSpacing::Base08.rems(cx))
 593            .h(Tab::container_height(cx))
 594            .flex_none()
 595            .justify_between()
 596            .gap(DynamicSpacing::Base08.rems(cx))
 597            .bg(cx.theme().colors().tab_bar_background)
 598            .border_b_1()
 599            .border_color(cx.theme().colors().border)
 600            .child(h_flex().child(Label::new(title)))
 601            .child(
 602                h_flex()
 603                    .h_full()
 604                    .pl_1p5()
 605                    .border_l_1()
 606                    .border_color(cx.theme().colors().border)
 607                    .gap(DynamicSpacing::Base02.rems(cx))
 608                    .child(
 609                        PopoverMenu::new("assistant-toolbar-new-popover-menu")
 610                            .trigger(
 611                                IconButton::new("new", IconName::Plus)
 612                                    .icon_size(IconSize::Small)
 613                                    .style(ButtonStyle::Subtle)
 614                                    .tooltip(|cx| Tooltip::text("New…", cx)),
 615                            )
 616                            .anchor(Corner::TopRight)
 617                            .with_handle(self.new_item_context_menu_handle.clone())
 618                            .menu(move |cx| {
 619                                Some(ContextMenu::build(cx, |menu, _| {
 620                                    menu.action("New Thread", NewThread.boxed_clone())
 621                                        .action("New Prompt Editor", NewPromptEditor.boxed_clone())
 622                                }))
 623                            }),
 624                    )
 625                    .child(
 626                        PopoverMenu::new("assistant-toolbar-history-popover-menu")
 627                            .trigger(
 628                                IconButton::new("open-history", IconName::HistoryRerun)
 629                                    .icon_size(IconSize::Small)
 630                                    .style(ButtonStyle::Subtle)
 631                                    .tooltip(|cx| Tooltip::text("History…", cx)),
 632                            )
 633                            .anchor(Corner::TopRight)
 634                            .with_handle(self.open_history_context_menu_handle.clone())
 635                            .menu(move |cx| {
 636                                Some(ContextMenu::build(cx, |menu, _| {
 637                                    menu.action("Thread History", OpenHistory.boxed_clone())
 638                                        .action(
 639                                            "Prompt Editor History",
 640                                            OpenPromptEditorHistory.boxed_clone(),
 641                                        )
 642                                }))
 643                            }),
 644                    )
 645                    .child(
 646                        IconButton::new("configure-assistant", IconName::Settings)
 647                            .icon_size(IconSize::Small)
 648                            .style(ButtonStyle::Subtle)
 649                            .tooltip(move |cx| Tooltip::text("Configure Assistant", cx))
 650                            .on_click(move |_event, cx| {
 651                                cx.dispatch_action(OpenConfiguration.boxed_clone());
 652                            }),
 653                    ),
 654            )
 655    }
 656
 657    fn render_active_thread_or_empty_state(&self, cx: &mut ViewContext<Self>) -> AnyElement {
 658        if self.thread.read(cx).is_empty() {
 659            return self.render_thread_empty_state(cx).into_any_element();
 660        }
 661
 662        self.thread.clone().into_any()
 663    }
 664
 665    fn configuration_error(&self, cx: &AppContext) -> Option<ConfigurationError> {
 666        let provider = LanguageModelRegistry::read_global(cx).active_provider();
 667        let is_authenticated = provider
 668            .as_ref()
 669            .map_or(false, |provider| provider.is_authenticated(cx));
 670
 671        if provider.is_some() && is_authenticated {
 672            return None;
 673        }
 674
 675        if !is_authenticated {
 676            return Some(ConfigurationError::ProviderNotAuthenticated);
 677        }
 678
 679        None
 680    }
 681
 682    fn render_thread_empty_state(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
 683        let recent_threads = self
 684            .thread_store
 685            .update(cx, |this, _cx| this.recent_threads(3));
 686
 687        let create_welcome_heading = || {
 688            h_flex()
 689                .w_full()
 690                .justify_center()
 691                .child(Headline::new("Welcome to the Assistant Panel").size(HeadlineSize::Small))
 692        };
 693
 694        v_flex()
 695            .gap_2()
 696            .child(
 697                v_flex().w_full().child(
 698                    svg()
 699                        .path("icons/logo_96.svg")
 700                        .text_color(cx.theme().colors().text)
 701                        .w(px(40.))
 702                        .h(px(40.))
 703                        .mx_auto()
 704                        .mb_4(),
 705                ),
 706            )
 707            .when(
 708                matches!(
 709                    self.configuration_error(cx),
 710                    Some(ConfigurationError::ProviderNotAuthenticated)
 711                ),
 712                |parent| {
 713                    parent.child(
 714                        v_flex()
 715                            .gap_0p5()
 716                            .child(create_welcome_heading())
 717                            .child(
 718                                h_flex().mb_2().w_full().justify_center().child(
 719                                    Label::new(
 720                                        "To start using the assistant, configure at least one LLM provider.",
 721                                    )
 722                                    .color(Color::Muted),
 723                                ),
 724                            )
 725                            .child(
 726                                h_flex().w_full().justify_center().child(
 727                                    Button::new("open-configuration", "Configure a Provider")
 728                                        .size(ButtonSize::Compact)
 729                                        .icon(Some(IconName::Sliders))
 730                                        .icon_size(IconSize::Small)
 731                                        .icon_position(IconPosition::Start)
 732                                        .on_click(cx.listener(|this, _, cx| {
 733                                            this.open_configuration(cx);
 734                                        })),
 735                                ),
 736                            ),
 737                    )
 738                },
 739            )
 740            .when(
 741                recent_threads.is_empty() && self.configuration_error(cx).is_none(),
 742                |parent| {
 743                    parent.child(
 744                        v_flex().gap_0p5().child(create_welcome_heading()).child(
 745                            h_flex().w_full().justify_center().child(
 746                                Label::new("Start typing to chat with your codebase")
 747                                    .color(Color::Muted),
 748                            ),
 749                        ),
 750                    )
 751                },
 752            )
 753            .when(!recent_threads.is_empty(), |parent| {
 754                parent
 755                    .child(
 756                        h_flex().w_full().justify_center().child(
 757                            Label::new("Recent Threads:")
 758                                .size(LabelSize::Small)
 759                                .color(Color::Muted),
 760                        ),
 761                    )
 762                    .child(v_flex().mx_auto().w_4_5().gap_2().children(
 763                        recent_threads.into_iter().map(|thread| {
 764                            // TODO: keyboard navigation
 765                            PastThread::new(thread, cx.view().downgrade(), false)
 766                        }),
 767                    ))
 768                    .child(
 769                        h_flex().w_full().justify_center().child(
 770                            Button::new("view-all-past-threads", "View All Past Threads")
 771                                .style(ButtonStyle::Subtle)
 772                                .label_size(LabelSize::Small)
 773                                .key_binding(KeyBinding::for_action_in(
 774                                    &OpenHistory,
 775                                    &self.focus_handle(cx),
 776                                    cx,
 777                                ))
 778                                .on_click(move |_event, cx| {
 779                                    cx.dispatch_action(OpenHistory.boxed_clone());
 780                                }),
 781                        ),
 782                    )
 783            })
 784    }
 785
 786    fn render_last_error(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
 787        let last_error = self.thread.read(cx).last_error()?;
 788
 789        Some(
 790            div()
 791                .absolute()
 792                .right_3()
 793                .bottom_12()
 794                .max_w_96()
 795                .py_2()
 796                .px_3()
 797                .elevation_2(cx)
 798                .occlude()
 799                .child(match last_error {
 800                    ThreadError::PaymentRequired => self.render_payment_required_error(cx),
 801                    ThreadError::MaxMonthlySpendReached => {
 802                        self.render_max_monthly_spend_reached_error(cx)
 803                    }
 804                    ThreadError::Message(error_message) => {
 805                        self.render_error_message(&error_message, cx)
 806                    }
 807                })
 808                .into_any(),
 809        )
 810    }
 811
 812    fn render_payment_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
 813        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.";
 814
 815        v_flex()
 816            .gap_0p5()
 817            .child(
 818                h_flex()
 819                    .gap_1p5()
 820                    .items_center()
 821                    .child(Icon::new(IconName::XCircle).color(Color::Error))
 822                    .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
 823            )
 824            .child(
 825                div()
 826                    .id("error-message")
 827                    .max_h_24()
 828                    .overflow_y_scroll()
 829                    .child(Label::new(ERROR_MESSAGE)),
 830            )
 831            .child(
 832                h_flex()
 833                    .justify_end()
 834                    .mt_1()
 835                    .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
 836                        |this, _, cx| {
 837                            this.thread.update(cx, |this, _cx| {
 838                                this.clear_last_error();
 839                            });
 840
 841                            cx.open_url(&zed_urls::account_url(cx));
 842                            cx.notify();
 843                        },
 844                    )))
 845                    .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
 846                        |this, _, cx| {
 847                            this.thread.update(cx, |this, _cx| {
 848                                this.clear_last_error();
 849                            });
 850
 851                            cx.notify();
 852                        },
 853                    ))),
 854            )
 855            .into_any()
 856    }
 857
 858    fn render_max_monthly_spend_reached_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
 859        const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
 860
 861        v_flex()
 862            .gap_0p5()
 863            .child(
 864                h_flex()
 865                    .gap_1p5()
 866                    .items_center()
 867                    .child(Icon::new(IconName::XCircle).color(Color::Error))
 868                    .child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
 869            )
 870            .child(
 871                div()
 872                    .id("error-message")
 873                    .max_h_24()
 874                    .overflow_y_scroll()
 875                    .child(Label::new(ERROR_MESSAGE)),
 876            )
 877            .child(
 878                h_flex()
 879                    .justify_end()
 880                    .mt_1()
 881                    .child(
 882                        Button::new("subscribe", "Update Monthly Spend Limit").on_click(
 883                            cx.listener(|this, _, cx| {
 884                                this.thread.update(cx, |this, _cx| {
 885                                    this.clear_last_error();
 886                                });
 887
 888                                cx.open_url(&zed_urls::account_url(cx));
 889                                cx.notify();
 890                            }),
 891                        ),
 892                    )
 893                    .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
 894                        |this, _, cx| {
 895                            this.thread.update(cx, |this, _cx| {
 896                                this.clear_last_error();
 897                            });
 898
 899                            cx.notify();
 900                        },
 901                    ))),
 902            )
 903            .into_any()
 904    }
 905
 906    fn render_error_message(
 907        &self,
 908        error_message: &SharedString,
 909        cx: &mut ViewContext<Self>,
 910    ) -> AnyElement {
 911        v_flex()
 912            .gap_0p5()
 913            .child(
 914                h_flex()
 915                    .gap_1p5()
 916                    .items_center()
 917                    .child(Icon::new(IconName::XCircle).color(Color::Error))
 918                    .child(
 919                        Label::new("Error interacting with language model")
 920                            .weight(FontWeight::MEDIUM),
 921                    ),
 922            )
 923            .child(
 924                div()
 925                    .id("error-message")
 926                    .max_h_32()
 927                    .overflow_y_scroll()
 928                    .child(Label::new(error_message.clone())),
 929            )
 930            .child(
 931                h_flex()
 932                    .justify_end()
 933                    .mt_1()
 934                    .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
 935                        |this, _, cx| {
 936                            this.thread.update(cx, |this, _cx| {
 937                                this.clear_last_error();
 938                            });
 939
 940                            cx.notify();
 941                        },
 942                    ))),
 943            )
 944            .into_any()
 945    }
 946}
 947
 948impl Render for AssistantPanel {
 949    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
 950        v_flex()
 951            .key_context("AssistantPanel2")
 952            .justify_between()
 953            .size_full()
 954            .on_action(cx.listener(Self::cancel))
 955            .on_action(cx.listener(|this, _: &NewThread, cx| {
 956                this.new_thread(cx);
 957            }))
 958            .on_action(cx.listener(|this, _: &OpenHistory, cx| {
 959                this.open_history(cx);
 960            }))
 961            .on_action(cx.listener(Self::deploy_prompt_library))
 962            .child(self.render_toolbar(cx))
 963            .map(|parent| match self.active_view {
 964                ActiveView::Thread => parent
 965                    .child(self.render_active_thread_or_empty_state(cx))
 966                    .child(
 967                        h_flex()
 968                            .border_t_1()
 969                            .border_color(cx.theme().colors().border)
 970                            .child(self.message_editor.clone()),
 971                    )
 972                    .children(self.render_last_error(cx)),
 973                ActiveView::History => parent.child(self.history.clone()),
 974                ActiveView::PromptEditor => parent.children(self.context_editor.clone()),
 975                ActiveView::PromptEditorHistory => parent.children(self.context_history.clone()),
 976                ActiveView::Configuration => parent.children(self.configuration.clone()),
 977            })
 978    }
 979}
 980
 981struct PromptLibraryInlineAssist {
 982    workspace: WeakView<Workspace>,
 983}
 984
 985impl PromptLibraryInlineAssist {
 986    pub fn new(workspace: WeakView<Workspace>) -> Self {
 987        Self { workspace }
 988    }
 989}
 990
 991impl prompt_library::InlineAssistDelegate for PromptLibraryInlineAssist {
 992    fn assist(
 993        &self,
 994        prompt_editor: &View<Editor>,
 995        _initial_prompt: Option<String>,
 996        cx: &mut ViewContext<PromptLibrary>,
 997    ) {
 998        InlineAssistant::update_global(cx, |assistant, cx| {
 999            assistant.assist(&prompt_editor, self.workspace.clone(), None, cx)
1000        })
1001    }
1002
1003    fn focus_assistant_panel(
1004        &self,
1005        workspace: &mut Workspace,
1006        cx: &mut ViewContext<Workspace>,
1007    ) -> bool {
1008        workspace.focus_panel::<AssistantPanel>(cx).is_some()
1009    }
1010}
1011
1012pub struct ConcreteAssistantPanelDelegate;
1013
1014impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
1015    fn active_context_editor(
1016        &self,
1017        workspace: &mut Workspace,
1018        cx: &mut ViewContext<Workspace>,
1019    ) -> Option<View<ContextEditor>> {
1020        let panel = workspace.panel::<AssistantPanel>(cx)?;
1021        panel.update(cx, |panel, _cx| panel.context_editor.clone())
1022    }
1023
1024    fn open_saved_context(
1025        &self,
1026        workspace: &mut Workspace,
1027        path: std::path::PathBuf,
1028        cx: &mut ViewContext<Workspace>,
1029    ) -> Task<Result<()>> {
1030        let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
1031            return Task::ready(Err(anyhow!("Assistant panel not found")));
1032        };
1033
1034        panel.update(cx, |panel, cx| panel.open_saved_prompt_editor(path, cx))
1035    }
1036
1037    fn open_remote_context(
1038        &self,
1039        _workspace: &mut Workspace,
1040        _context_id: assistant_context_editor::ContextId,
1041        _cx: &mut ViewContext<Workspace>,
1042    ) -> Task<Result<View<ContextEditor>>> {
1043        Task::ready(Err(anyhow!("opening remote context not implemented")))
1044    }
1045
1046    fn quote_selection(
1047        &self,
1048        _workspace: &mut Workspace,
1049        _creases: Vec<(String, String)>,
1050        _cx: &mut ViewContext<Workspace>,
1051    ) {
1052    }
1053}