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