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