assistant_panel.rs

   1use crate::{
   2    terminal_inline_assistant::TerminalInlineAssistant, DeployHistory, DeployPromptLibrary,
   3    InlineAssistant, NewContext,
   4};
   5use anyhow::{anyhow, Result};
   6use assistant_context_editor::{
   7    make_lsp_adapter_delegate, AssistantPanelDelegate, Context, ContextEditor,
   8    ContextEditorToolbarItem, ContextEditorToolbarItemEvent, ContextHistory, ContextId,
   9    ContextStore, ContextStoreEvent, InsertDraggedFiles, SlashCommandCompletionProvider,
  10    ToggleModelSelector, DEFAULT_TAB_TITLE,
  11};
  12use assistant_settings::{AssistantDockPosition, AssistantSettings};
  13use assistant_slash_command::SlashCommandWorkingSet;
  14use assistant_tool::ToolWorkingSet;
  15use client::{proto, Client, Status};
  16use collections::HashMap;
  17use editor::{Editor, EditorEvent};
  18use fs::Fs;
  19use gpui::{
  20    canvas, div, prelude::*, Action, AnyView, AppContext, AsyncWindowContext, EventEmitter,
  21    ExternalPaths, FocusHandle, FocusableView, InteractiveElement, IntoElement, Model,
  22    ParentElement, Pixels, Render, SharedString, StatefulInteractiveElement, Styled, Subscription,
  23    Task, UpdateGlobal, View, WeakView,
  24};
  25use language::LanguageRegistry;
  26use language_model::{
  27    LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
  28};
  29use language_model_selector::LanguageModelSelector;
  30use project::Project;
  31use prompt_library::{open_prompt_library, PromptBuilder, PromptLibrary};
  32use search::{buffer_search::DivRegistrar, BufferSearchBar};
  33use settings::{update_settings_file, Settings};
  34use smol::stream::StreamExt;
  35use std::{ops::ControlFlow, path::PathBuf, sync::Arc};
  36use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
  37use ui::{prelude::*, ContextMenu, ElevationIndex, PopoverMenu, PopoverMenuHandle, Tooltip};
  38use util::{maybe, ResultExt};
  39use workspace::DraggedTab;
  40use workspace::{
  41    dock::{DockPosition, Panel, PanelEvent},
  42    item::Item,
  43    pane, DraggedSelection, Pane, ShowConfiguration, ToggleZoom, Workspace,
  44};
  45use zed_actions::assistant::{InlineAssist, ToggleFocus};
  46
  47pub fn init(cx: &mut AppContext) {
  48    workspace::FollowableViewRegistry::register::<ContextEditor>(cx);
  49    cx.observe_new_views(
  50        |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
  51            workspace
  52                .register_action(ContextEditor::quote_selection)
  53                .register_action(ContextEditor::insert_selection)
  54                .register_action(ContextEditor::copy_code)
  55                .register_action(ContextEditor::insert_dragged_files)
  56                .register_action(AssistantPanel::show_configuration)
  57                .register_action(AssistantPanel::create_new_context)
  58                .register_action(AssistantPanel::restart_context_servers);
  59        },
  60    )
  61    .detach();
  62
  63    cx.observe_new_views(
  64        |terminal_panel: &mut TerminalPanel, cx: &mut ViewContext<TerminalPanel>| {
  65            let settings = AssistantSettings::get_global(cx);
  66            terminal_panel.set_assistant_enabled(settings.enabled, cx);
  67        },
  68    )
  69    .detach();
  70}
  71
  72pub enum AssistantPanelEvent {
  73    ContextEdited,
  74}
  75
  76pub struct AssistantPanel {
  77    pane: View<Pane>,
  78    workspace: WeakView<Workspace>,
  79    width: Option<Pixels>,
  80    height: Option<Pixels>,
  81    project: Model<Project>,
  82    context_store: Model<ContextStore>,
  83    languages: Arc<LanguageRegistry>,
  84    fs: Arc<dyn Fs>,
  85    subscriptions: Vec<Subscription>,
  86    model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
  87    model_summary_editor: View<Editor>,
  88    authenticate_provider_task: Option<(LanguageModelProviderId, Task<()>)>,
  89    configuration_subscription: Option<Subscription>,
  90    client_status: Option<client::Status>,
  91    watch_client_status: Option<Task<()>>,
  92    pub(crate) show_zed_ai_notice: bool,
  93}
  94
  95enum InlineAssistTarget {
  96    Editor(View<Editor>, bool),
  97    Terminal(View<TerminalView>),
  98}
  99
 100impl AssistantPanel {
 101    pub fn load(
 102        workspace: WeakView<Workspace>,
 103        prompt_builder: Arc<PromptBuilder>,
 104        cx: AsyncWindowContext,
 105    ) -> Task<Result<View<Self>>> {
 106        cx.spawn(|mut cx| async move {
 107            let slash_commands = Arc::new(SlashCommandWorkingSet::default());
 108            let tools = Arc::new(ToolWorkingSet::default());
 109            let context_store = workspace
 110                .update(&mut cx, |workspace, cx| {
 111                    let project = workspace.project().clone();
 112                    ContextStore::new(project, prompt_builder.clone(), slash_commands, tools, cx)
 113                })?
 114                .await?;
 115
 116            workspace.update(&mut cx, |workspace, cx| {
 117                // TODO: deserialize state.
 118                cx.new_view(|cx| Self::new(workspace, context_store, cx))
 119            })
 120        })
 121    }
 122
 123    fn new(
 124        workspace: &Workspace,
 125        context_store: Model<ContextStore>,
 126        cx: &mut ViewContext<Self>,
 127    ) -> Self {
 128        let model_selector_menu_handle = PopoverMenuHandle::default();
 129        let model_summary_editor = cx.new_view(Editor::single_line);
 130        let context_editor_toolbar = cx.new_view(|cx| {
 131            ContextEditorToolbarItem::new(
 132                workspace,
 133                model_selector_menu_handle.clone(),
 134                model_summary_editor.clone(),
 135                cx,
 136            )
 137        });
 138
 139        let pane = cx.new_view(|cx| {
 140            let mut pane = Pane::new(
 141                workspace.weak_handle(),
 142                workspace.project().clone(),
 143                Default::default(),
 144                None,
 145                NewContext.boxed_clone(),
 146                cx,
 147            );
 148
 149            let project = workspace.project().clone();
 150            pane.set_custom_drop_handle(cx, move |_, dropped_item, cx| {
 151                let action = maybe!({
 152                    if project.read(cx).is_local() {
 153                        if let Some(paths) = dropped_item.downcast_ref::<ExternalPaths>() {
 154                            return Some(InsertDraggedFiles::ExternalFiles(paths.paths().to_vec()));
 155                        }
 156                    }
 157
 158                    let project_paths = if let Some(tab) = dropped_item.downcast_ref::<DraggedTab>()
 159                    {
 160                        if &tab.pane == cx.view() {
 161                            return None;
 162                        }
 163                        let item = tab.pane.read(cx).item_for_index(tab.ix);
 164                        Some(
 165                            item.and_then(|item| item.project_path(cx))
 166                                .into_iter()
 167                                .collect::<Vec<_>>(),
 168                        )
 169                    } else if let Some(selection) = dropped_item.downcast_ref::<DraggedSelection>()
 170                    {
 171                        Some(
 172                            selection
 173                                .items()
 174                                .filter_map(|item| {
 175                                    project.read(cx).path_for_entry(item.entry_id, cx)
 176                                })
 177                                .collect::<Vec<_>>(),
 178                        )
 179                    } else {
 180                        None
 181                    }?;
 182
 183                    let paths = project_paths
 184                        .into_iter()
 185                        .filter_map(|project_path| {
 186                            let worktree = project
 187                                .read(cx)
 188                                .worktree_for_id(project_path.worktree_id, cx)?;
 189
 190                            let mut full_path = PathBuf::from(worktree.read(cx).root_name());
 191                            full_path.push(&project_path.path);
 192                            Some(full_path)
 193                        })
 194                        .collect::<Vec<_>>();
 195
 196                    Some(InsertDraggedFiles::ProjectPaths(paths))
 197                });
 198
 199                if let Some(action) = action {
 200                    cx.dispatch_action(action.boxed_clone());
 201                }
 202
 203                ControlFlow::Break(())
 204            });
 205
 206            pane.set_can_navigate(true, cx);
 207            pane.display_nav_history_buttons(None);
 208            pane.set_should_display_tab_bar(|_| true);
 209            pane.set_render_tab_bar_buttons(cx, move |pane, cx| {
 210                let focus_handle = pane.focus_handle(cx);
 211                let left_children = IconButton::new("history", IconName::HistoryRerun)
 212                    .icon_size(IconSize::Small)
 213                    .on_click(cx.listener({
 214                        let focus_handle = focus_handle.clone();
 215                        move |_, _, cx| {
 216                            focus_handle.focus(cx);
 217                            cx.dispatch_action(DeployHistory.boxed_clone())
 218                        }
 219                    }))
 220                    .tooltip({
 221                        let focus_handle = focus_handle.clone();
 222                        move |cx| {
 223                            Tooltip::for_action_in(
 224                                "Open History",
 225                                &DeployHistory,
 226                                &focus_handle,
 227                                cx,
 228                            )
 229                        }
 230                    })
 231                    .toggle_state(
 232                        pane.active_item()
 233                            .map_or(false, |item| item.downcast::<ContextHistory>().is_some()),
 234                    );
 235                let _pane = cx.view().clone();
 236                let right_children = h_flex()
 237                    .gap(DynamicSpacing::Base02.rems(cx))
 238                    .child(
 239                        IconButton::new("new-chat", IconName::Plus)
 240                            .icon_size(IconSize::Small)
 241                            .on_click(
 242                                cx.listener(|_, _, cx| {
 243                                    cx.dispatch_action(NewContext.boxed_clone())
 244                                }),
 245                            )
 246                            .tooltip(move |cx| {
 247                                Tooltip::for_action_in("New Chat", &NewContext, &focus_handle, cx)
 248                            }),
 249                    )
 250                    .child(
 251                        PopoverMenu::new("assistant-panel-popover-menu")
 252                            .trigger(
 253                                IconButton::new("menu", IconName::EllipsisVertical)
 254                                    .icon_size(IconSize::Small)
 255                                    .tooltip(|cx| Tooltip::text("Toggle Assistant Menu", cx)),
 256                            )
 257                            .menu(move |cx| {
 258                                let zoom_label = if _pane.read(cx).is_zoomed() {
 259                                    "Zoom Out"
 260                                } else {
 261                                    "Zoom In"
 262                                };
 263                                let focus_handle = _pane.focus_handle(cx);
 264                                Some(ContextMenu::build(cx, move |menu, _| {
 265                                    menu.context(focus_handle.clone())
 266                                        .action("New Chat", Box::new(NewContext))
 267                                        .action("History", Box::new(DeployHistory))
 268                                        .action("Prompt Library", Box::new(DeployPromptLibrary))
 269                                        .action("Configure", Box::new(ShowConfiguration))
 270                                        .action(zoom_label, Box::new(ToggleZoom))
 271                                }))
 272                            }),
 273                    )
 274                    .into_any_element()
 275                    .into();
 276
 277                (Some(left_children.into_any_element()), right_children)
 278            });
 279            pane.toolbar().update(cx, |toolbar, cx| {
 280                toolbar.add_item(context_editor_toolbar.clone(), cx);
 281                toolbar.add_item(cx.new_view(BufferSearchBar::new), cx)
 282            });
 283            pane
 284        });
 285
 286        let subscriptions = vec![
 287            cx.observe(&pane, |_, _, cx| cx.notify()),
 288            cx.subscribe(&pane, Self::handle_pane_event),
 289            cx.subscribe(&context_editor_toolbar, Self::handle_toolbar_event),
 290            cx.subscribe(&model_summary_editor, Self::handle_summary_editor_event),
 291            cx.subscribe(&context_store, Self::handle_context_store_event),
 292            cx.subscribe(
 293                &LanguageModelRegistry::global(cx),
 294                |this, _, event: &language_model::Event, cx| match event {
 295                    language_model::Event::ActiveModelChanged => {
 296                        this.completion_provider_changed(cx);
 297                    }
 298                    language_model::Event::ProviderStateChanged => {
 299                        this.ensure_authenticated(cx);
 300                        cx.notify()
 301                    }
 302                    language_model::Event::AddedProvider(_)
 303                    | language_model::Event::RemovedProvider(_) => {
 304                        this.ensure_authenticated(cx);
 305                    }
 306                },
 307            ),
 308        ];
 309
 310        let watch_client_status = Self::watch_client_status(workspace.client().clone(), cx);
 311
 312        let mut this = Self {
 313            pane,
 314            workspace: workspace.weak_handle(),
 315            width: None,
 316            height: None,
 317            project: workspace.project().clone(),
 318            context_store,
 319            languages: workspace.app_state().languages.clone(),
 320            fs: workspace.app_state().fs.clone(),
 321            subscriptions,
 322            model_selector_menu_handle,
 323            model_summary_editor,
 324            authenticate_provider_task: None,
 325            configuration_subscription: None,
 326            client_status: None,
 327            watch_client_status: Some(watch_client_status),
 328            show_zed_ai_notice: false,
 329        };
 330        this.new_context(cx);
 331        this
 332    }
 333
 334    pub fn toggle_focus(
 335        workspace: &mut Workspace,
 336        _: &ToggleFocus,
 337        cx: &mut ViewContext<Workspace>,
 338    ) {
 339        let settings = AssistantSettings::get_global(cx);
 340        if !settings.enabled {
 341            return;
 342        }
 343
 344        workspace.toggle_panel_focus::<Self>(cx);
 345    }
 346
 347    fn watch_client_status(client: Arc<Client>, cx: &mut ViewContext<Self>) -> Task<()> {
 348        let mut status_rx = client.status();
 349
 350        cx.spawn(|this, mut cx| async move {
 351            while let Some(status) = status_rx.next().await {
 352                this.update(&mut cx, |this, cx| {
 353                    if this.client_status.is_none()
 354                        || this
 355                            .client_status
 356                            .map_or(false, |old_status| old_status != status)
 357                    {
 358                        this.update_zed_ai_notice_visibility(status, cx);
 359                    }
 360                    this.client_status = Some(status);
 361                })
 362                .log_err();
 363            }
 364            this.update(&mut cx, |this, _cx| this.watch_client_status = None)
 365                .log_err();
 366        })
 367    }
 368
 369    fn handle_pane_event(
 370        &mut self,
 371        pane: View<Pane>,
 372        event: &pane::Event,
 373        cx: &mut ViewContext<Self>,
 374    ) {
 375        let update_model_summary = match event {
 376            pane::Event::Remove { .. } => {
 377                cx.emit(PanelEvent::Close);
 378                false
 379            }
 380            pane::Event::ZoomIn => {
 381                cx.emit(PanelEvent::ZoomIn);
 382                false
 383            }
 384            pane::Event::ZoomOut => {
 385                cx.emit(PanelEvent::ZoomOut);
 386                false
 387            }
 388
 389            pane::Event::AddItem { item } => {
 390                self.workspace
 391                    .update(cx, |workspace, cx| {
 392                        item.added_to_pane(workspace, self.pane.clone(), cx)
 393                    })
 394                    .ok();
 395                true
 396            }
 397
 398            pane::Event::ActivateItem { local, .. } => {
 399                if *local {
 400                    self.workspace
 401                        .update(cx, |workspace, cx| {
 402                            workspace.unfollow_in_pane(&pane, cx);
 403                        })
 404                        .ok();
 405                }
 406                cx.emit(AssistantPanelEvent::ContextEdited);
 407                true
 408            }
 409            pane::Event::RemovedItem { .. } => {
 410                let has_configuration_view = self
 411                    .pane
 412                    .read(cx)
 413                    .items_of_type::<ConfigurationView>()
 414                    .next()
 415                    .is_some();
 416
 417                if !has_configuration_view {
 418                    self.configuration_subscription = None;
 419                }
 420
 421                cx.emit(AssistantPanelEvent::ContextEdited);
 422                true
 423            }
 424
 425            _ => false,
 426        };
 427
 428        if update_model_summary {
 429            if let Some(editor) = self.active_context_editor(cx) {
 430                self.show_updated_summary(&editor, cx)
 431            }
 432        }
 433    }
 434
 435    fn handle_summary_editor_event(
 436        &mut self,
 437        model_summary_editor: View<Editor>,
 438        event: &EditorEvent,
 439        cx: &mut ViewContext<Self>,
 440    ) {
 441        if matches!(event, EditorEvent::Edited { .. }) {
 442            if let Some(context_editor) = self.active_context_editor(cx) {
 443                let new_summary = model_summary_editor.read(cx).text(cx);
 444                context_editor.update(cx, |context_editor, cx| {
 445                    context_editor.context().update(cx, |context, cx| {
 446                        if context.summary().is_none()
 447                            && (new_summary == DEFAULT_TAB_TITLE || new_summary.trim().is_empty())
 448                        {
 449                            return;
 450                        }
 451                        context.custom_summary(new_summary, cx)
 452                    });
 453                });
 454            }
 455        }
 456    }
 457
 458    fn update_zed_ai_notice_visibility(
 459        &mut self,
 460        client_status: Status,
 461        cx: &mut ViewContext<Self>,
 462    ) {
 463        let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
 464
 465        // If we're signed out and don't have a provider configured, or we're signed-out AND Zed.dev is
 466        // the provider, we want to show a nudge to sign in.
 467        let show_zed_ai_notice = client_status.is_signed_out()
 468            && active_provider.map_or(true, |provider| provider.id().0 == ZED_CLOUD_PROVIDER_ID);
 469
 470        self.show_zed_ai_notice = show_zed_ai_notice;
 471        cx.notify();
 472    }
 473
 474    fn handle_toolbar_event(
 475        &mut self,
 476        _: View<ContextEditorToolbarItem>,
 477        _: &ContextEditorToolbarItemEvent,
 478        cx: &mut ViewContext<Self>,
 479    ) {
 480        if let Some(context_editor) = self.active_context_editor(cx) {
 481            context_editor.update(cx, |context_editor, cx| {
 482                context_editor.context().update(cx, |context, cx| {
 483                    context.summarize(true, cx);
 484                })
 485            })
 486        }
 487    }
 488
 489    fn handle_context_store_event(
 490        &mut self,
 491        _context_store: Model<ContextStore>,
 492        event: &ContextStoreEvent,
 493        cx: &mut ViewContext<Self>,
 494    ) {
 495        let ContextStoreEvent::ContextCreated(context_id) = event;
 496        let Some(context) = self
 497            .context_store
 498            .read(cx)
 499            .loaded_context_for_id(&context_id, cx)
 500        else {
 501            log::error!("no context found with ID: {}", context_id.to_proto());
 502            return;
 503        };
 504        let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
 505            .log_err()
 506            .flatten();
 507
 508        let editor = cx.new_view(|cx| {
 509            let mut editor = ContextEditor::for_context(
 510                context,
 511                self.fs.clone(),
 512                self.workspace.clone(),
 513                self.project.clone(),
 514                lsp_adapter_delegate,
 515                cx,
 516            );
 517            editor.insert_default_prompt(cx);
 518            editor
 519        });
 520
 521        self.show_context(editor.clone(), cx);
 522    }
 523
 524    fn completion_provider_changed(&mut self, cx: &mut ViewContext<Self>) {
 525        if let Some(editor) = self.active_context_editor(cx) {
 526            editor.update(cx, |active_context, cx| {
 527                active_context
 528                    .context()
 529                    .update(cx, |context, cx| context.completion_provider_changed(cx))
 530            })
 531        }
 532
 533        let Some(new_provider_id) = LanguageModelRegistry::read_global(cx)
 534            .active_provider()
 535            .map(|p| p.id())
 536        else {
 537            return;
 538        };
 539
 540        if self
 541            .authenticate_provider_task
 542            .as_ref()
 543            .map_or(true, |(old_provider_id, _)| {
 544                *old_provider_id != new_provider_id
 545            })
 546        {
 547            self.authenticate_provider_task = None;
 548            self.ensure_authenticated(cx);
 549        }
 550
 551        if let Some(status) = self.client_status {
 552            self.update_zed_ai_notice_visibility(status, cx);
 553        }
 554    }
 555
 556    fn ensure_authenticated(&mut self, cx: &mut ViewContext<Self>) {
 557        if self.is_authenticated(cx) {
 558            return;
 559        }
 560
 561        let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else {
 562            return;
 563        };
 564
 565        let load_credentials = self.authenticate(cx);
 566
 567        if self.authenticate_provider_task.is_none() {
 568            self.authenticate_provider_task = Some((
 569                provider.id(),
 570                cx.spawn(|this, mut cx| async move {
 571                    if let Some(future) = load_credentials {
 572                        let _ = future.await;
 573                    }
 574                    this.update(&mut cx, |this, _cx| {
 575                        this.authenticate_provider_task = None;
 576                    })
 577                    .log_err();
 578                }),
 579            ));
 580        }
 581    }
 582
 583    pub fn inline_assist(
 584        workspace: &mut Workspace,
 585        action: &InlineAssist,
 586        cx: &mut ViewContext<Workspace>,
 587    ) {
 588        let settings = AssistantSettings::get_global(cx);
 589        if !settings.enabled {
 590            return;
 591        }
 592
 593        let Some(assistant_panel) = workspace.panel::<AssistantPanel>(cx) else {
 594            return;
 595        };
 596
 597        let Some(inline_assist_target) =
 598            Self::resolve_inline_assist_target(workspace, &assistant_panel, cx)
 599        else {
 600            return;
 601        };
 602
 603        let initial_prompt = action.prompt.clone();
 604
 605        if assistant_panel.update(cx, |assistant, cx| assistant.is_authenticated(cx)) {
 606            match inline_assist_target {
 607                InlineAssistTarget::Editor(active_editor, include_context) => {
 608                    InlineAssistant::update_global(cx, |assistant, cx| {
 609                        assistant.assist(
 610                            &active_editor,
 611                            Some(cx.view().downgrade()),
 612                            include_context.then_some(&assistant_panel),
 613                            initial_prompt,
 614                            cx,
 615                        )
 616                    })
 617                }
 618                InlineAssistTarget::Terminal(active_terminal) => {
 619                    TerminalInlineAssistant::update_global(cx, |assistant, cx| {
 620                        assistant.assist(
 621                            &active_terminal,
 622                            Some(cx.view().downgrade()),
 623                            Some(&assistant_panel),
 624                            initial_prompt,
 625                            cx,
 626                        )
 627                    })
 628                }
 629            }
 630        } else {
 631            let assistant_panel = assistant_panel.downgrade();
 632            cx.spawn(|workspace, mut cx| async move {
 633                let Some(task) =
 634                    assistant_panel.update(&mut cx, |assistant, cx| assistant.authenticate(cx))?
 635                else {
 636                    let answer = cx
 637                        .prompt(
 638                            gpui::PromptLevel::Warning,
 639                            "No language model provider configured",
 640                            None,
 641                            &["Configure", "Cancel"],
 642                        )
 643                        .await
 644                        .ok();
 645                    if let Some(answer) = answer {
 646                        if answer == 0 {
 647                            cx.update(|cx| cx.dispatch_action(Box::new(ShowConfiguration)))
 648                                .ok();
 649                        }
 650                    }
 651                    return Ok(());
 652                };
 653                task.await?;
 654                if assistant_panel.update(&mut cx, |panel, cx| panel.is_authenticated(cx))? {
 655                    cx.update(|cx| match inline_assist_target {
 656                        InlineAssistTarget::Editor(active_editor, include_context) => {
 657                            let assistant_panel = if include_context {
 658                                assistant_panel.upgrade()
 659                            } else {
 660                                None
 661                            };
 662                            InlineAssistant::update_global(cx, |assistant, cx| {
 663                                assistant.assist(
 664                                    &active_editor,
 665                                    Some(workspace),
 666                                    assistant_panel.as_ref(),
 667                                    initial_prompt,
 668                                    cx,
 669                                )
 670                            })
 671                        }
 672                        InlineAssistTarget::Terminal(active_terminal) => {
 673                            TerminalInlineAssistant::update_global(cx, |assistant, cx| {
 674                                assistant.assist(
 675                                    &active_terminal,
 676                                    Some(workspace),
 677                                    assistant_panel.upgrade().as_ref(),
 678                                    initial_prompt,
 679                                    cx,
 680                                )
 681                            })
 682                        }
 683                    })?
 684                } else {
 685                    workspace.update(&mut cx, |workspace, cx| {
 686                        workspace.focus_panel::<AssistantPanel>(cx)
 687                    })?;
 688                }
 689
 690                anyhow::Ok(())
 691            })
 692            .detach_and_log_err(cx)
 693        }
 694    }
 695
 696    fn resolve_inline_assist_target(
 697        workspace: &mut Workspace,
 698        assistant_panel: &View<AssistantPanel>,
 699        cx: &mut WindowContext,
 700    ) -> Option<InlineAssistTarget> {
 701        if let Some(terminal_panel) = workspace.panel::<TerminalPanel>(cx) {
 702            if terminal_panel
 703                .read(cx)
 704                .focus_handle(cx)
 705                .contains_focused(cx)
 706            {
 707                if let Some(terminal_view) = terminal_panel.read(cx).pane().and_then(|pane| {
 708                    pane.read(cx)
 709                        .active_item()
 710                        .and_then(|t| t.downcast::<TerminalView>())
 711                }) {
 712                    return Some(InlineAssistTarget::Terminal(terminal_view));
 713                }
 714            }
 715        }
 716        let context_editor =
 717            assistant_panel
 718                .read(cx)
 719                .active_context_editor(cx)
 720                .and_then(|editor| {
 721                    let editor = &editor.read(cx).editor().clone();
 722                    if editor.read(cx).is_focused(cx) {
 723                        Some(editor.clone())
 724                    } else {
 725                        None
 726                    }
 727                });
 728
 729        if let Some(context_editor) = context_editor {
 730            Some(InlineAssistTarget::Editor(context_editor, false))
 731        } else if let Some(workspace_editor) = workspace
 732            .active_item(cx)
 733            .and_then(|item| item.act_as::<Editor>(cx))
 734        {
 735            Some(InlineAssistTarget::Editor(workspace_editor, true))
 736        } else if let Some(terminal_view) = workspace
 737            .active_item(cx)
 738            .and_then(|item| item.act_as::<TerminalView>(cx))
 739        {
 740            Some(InlineAssistTarget::Terminal(terminal_view))
 741        } else {
 742            None
 743        }
 744    }
 745
 746    pub fn create_new_context(
 747        workspace: &mut Workspace,
 748        _: &NewContext,
 749        cx: &mut ViewContext<Workspace>,
 750    ) {
 751        if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
 752            let did_create_context = panel
 753                .update(cx, |panel, cx| {
 754                    panel.new_context(cx)?;
 755
 756                    Some(())
 757                })
 758                .is_some();
 759            if did_create_context {
 760                ContextEditor::quote_selection(workspace, &Default::default(), cx);
 761            }
 762        }
 763    }
 764
 765    pub fn new_context(&mut self, cx: &mut ViewContext<Self>) -> Option<View<ContextEditor>> {
 766        let project = self.project.read(cx);
 767        if project.is_via_collab() {
 768            let task = self
 769                .context_store
 770                .update(cx, |store, cx| store.create_remote_context(cx));
 771
 772            cx.spawn(|this, mut cx| async move {
 773                let context = task.await?;
 774
 775                this.update(&mut cx, |this, cx| {
 776                    let workspace = this.workspace.clone();
 777                    let project = this.project.clone();
 778                    let lsp_adapter_delegate =
 779                        make_lsp_adapter_delegate(&project, cx).log_err().flatten();
 780
 781                    let fs = this.fs.clone();
 782                    let project = this.project.clone();
 783
 784                    let editor = cx.new_view(|cx| {
 785                        ContextEditor::for_context(
 786                            context,
 787                            fs,
 788                            workspace,
 789                            project,
 790                            lsp_adapter_delegate,
 791                            cx,
 792                        )
 793                    });
 794
 795                    this.show_context(editor, cx);
 796
 797                    anyhow::Ok(())
 798                })??;
 799
 800                anyhow::Ok(())
 801            })
 802            .detach_and_log_err(cx);
 803
 804            None
 805        } else {
 806            let context = self.context_store.update(cx, |store, cx| store.create(cx));
 807            let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
 808                .log_err()
 809                .flatten();
 810
 811            let editor = cx.new_view(|cx| {
 812                let mut editor = ContextEditor::for_context(
 813                    context,
 814                    self.fs.clone(),
 815                    self.workspace.clone(),
 816                    self.project.clone(),
 817                    lsp_adapter_delegate,
 818                    cx,
 819                );
 820                editor.insert_default_prompt(cx);
 821                editor
 822            });
 823
 824            self.show_context(editor.clone(), cx);
 825            let workspace = self.workspace.clone();
 826            cx.spawn(move |_, mut cx| async move {
 827                workspace
 828                    .update(&mut cx, |workspace, cx| {
 829                        workspace.focus_panel::<AssistantPanel>(cx);
 830                    })
 831                    .ok();
 832            })
 833            .detach();
 834            Some(editor)
 835        }
 836    }
 837
 838    fn show_context(&mut self, context_editor: View<ContextEditor>, cx: &mut ViewContext<Self>) {
 839        let focus = self.focus_handle(cx).contains_focused(cx);
 840        let prev_len = self.pane.read(cx).items_len();
 841        self.pane.update(cx, |pane, cx| {
 842            pane.add_item(Box::new(context_editor.clone()), focus, focus, None, cx)
 843        });
 844
 845        if prev_len != self.pane.read(cx).items_len() {
 846            self.subscriptions
 847                .push(cx.subscribe(&context_editor, Self::handle_context_editor_event));
 848        }
 849
 850        self.show_updated_summary(&context_editor, cx);
 851
 852        cx.emit(AssistantPanelEvent::ContextEdited);
 853        cx.notify();
 854    }
 855
 856    fn show_updated_summary(
 857        &self,
 858        context_editor: &View<ContextEditor>,
 859        cx: &mut ViewContext<Self>,
 860    ) {
 861        context_editor.update(cx, |context_editor, cx| {
 862            let new_summary = context_editor.title(cx).to_string();
 863            self.model_summary_editor.update(cx, |summary_editor, cx| {
 864                if summary_editor.text(cx) != new_summary {
 865                    summary_editor.set_text(new_summary, cx);
 866                }
 867            });
 868        });
 869    }
 870
 871    fn handle_context_editor_event(
 872        &mut self,
 873        context_editor: View<ContextEditor>,
 874        event: &EditorEvent,
 875        cx: &mut ViewContext<Self>,
 876    ) {
 877        match event {
 878            EditorEvent::TitleChanged => {
 879                self.show_updated_summary(&context_editor, cx);
 880                cx.notify()
 881            }
 882            EditorEvent::Edited { .. } => {
 883                self.workspace
 884                    .update(cx, |workspace, cx| {
 885                        let is_via_ssh = workspace
 886                            .project()
 887                            .update(cx, |project, _| project.is_via_ssh());
 888
 889                        workspace
 890                            .client()
 891                            .telemetry()
 892                            .log_edit_event("assistant panel", is_via_ssh);
 893                    })
 894                    .log_err();
 895                cx.emit(AssistantPanelEvent::ContextEdited)
 896            }
 897            _ => {}
 898        }
 899    }
 900
 901    fn show_configuration(
 902        workspace: &mut Workspace,
 903        _: &ShowConfiguration,
 904        cx: &mut ViewContext<Workspace>,
 905    ) {
 906        let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
 907            return;
 908        };
 909
 910        if !panel.focus_handle(cx).contains_focused(cx) {
 911            workspace.toggle_panel_focus::<AssistantPanel>(cx);
 912        }
 913
 914        panel.update(cx, |this, cx| {
 915            this.show_configuration_tab(cx);
 916        })
 917    }
 918
 919    fn show_configuration_tab(&mut self, cx: &mut ViewContext<Self>) {
 920        let configuration_item_ix = self
 921            .pane
 922            .read(cx)
 923            .items()
 924            .position(|item| item.downcast::<ConfigurationView>().is_some());
 925
 926        if let Some(configuration_item_ix) = configuration_item_ix {
 927            self.pane.update(cx, |pane, cx| {
 928                pane.activate_item(configuration_item_ix, true, true, cx);
 929            });
 930        } else {
 931            let configuration = cx.new_view(ConfigurationView::new);
 932            self.configuration_subscription = Some(cx.subscribe(
 933                &configuration,
 934                |this, _, event: &ConfigurationViewEvent, cx| match event {
 935                    ConfigurationViewEvent::NewProviderContextEditor(provider) => {
 936                        if LanguageModelRegistry::read_global(cx)
 937                            .active_provider()
 938                            .map_or(true, |p| p.id() != provider.id())
 939                        {
 940                            if let Some(model) = provider.provided_models(cx).first().cloned() {
 941                                update_settings_file::<AssistantSettings>(
 942                                    this.fs.clone(),
 943                                    cx,
 944                                    move |settings, _| settings.set_model(model),
 945                                );
 946                            }
 947                        }
 948
 949                        this.new_context(cx);
 950                    }
 951                },
 952            ));
 953            self.pane.update(cx, |pane, cx| {
 954                pane.add_item(Box::new(configuration), true, true, None, cx);
 955            });
 956        }
 957    }
 958
 959    fn deploy_history(&mut self, _: &DeployHistory, cx: &mut ViewContext<Self>) {
 960        let history_item_ix = self
 961            .pane
 962            .read(cx)
 963            .items()
 964            .position(|item| item.downcast::<ContextHistory>().is_some());
 965
 966        if let Some(history_item_ix) = history_item_ix {
 967            self.pane.update(cx, |pane, cx| {
 968                pane.activate_item(history_item_ix, true, true, cx);
 969            });
 970        } else {
 971            let history = cx.new_view(|cx| {
 972                ContextHistory::new(
 973                    self.project.clone(),
 974                    self.context_store.clone(),
 975                    self.workspace.clone(),
 976                    cx,
 977                )
 978            });
 979            self.pane.update(cx, |pane, cx| {
 980                pane.add_item(Box::new(history), true, true, None, cx);
 981            });
 982        }
 983    }
 984
 985    fn deploy_prompt_library(&mut self, _: &DeployPromptLibrary, cx: &mut ViewContext<Self>) {
 986        open_prompt_library(
 987            self.languages.clone(),
 988            Box::new(PromptLibraryInlineAssist),
 989            Arc::new(|| {
 990                Box::new(SlashCommandCompletionProvider::new(
 991                    Arc::new(SlashCommandWorkingSet::default()),
 992                    None,
 993                    None,
 994                ))
 995            }),
 996            cx,
 997        )
 998        .detach_and_log_err(cx);
 999    }
1000
1001    fn toggle_model_selector(&mut self, _: &ToggleModelSelector, cx: &mut ViewContext<Self>) {
1002        self.model_selector_menu_handle.toggle(cx);
1003    }
1004
1005    pub(crate) fn active_context_editor(&self, cx: &AppContext) -> Option<View<ContextEditor>> {
1006        self.pane
1007            .read(cx)
1008            .active_item()?
1009            .downcast::<ContextEditor>()
1010    }
1011
1012    pub fn active_context(&self, cx: &AppContext) -> Option<Model<Context>> {
1013        Some(self.active_context_editor(cx)?.read(cx).context().clone())
1014    }
1015
1016    pub fn open_saved_context(
1017        &mut self,
1018        path: PathBuf,
1019        cx: &mut ViewContext<Self>,
1020    ) -> Task<Result<()>> {
1021        let existing_context = self.pane.read(cx).items().find_map(|item| {
1022            item.downcast::<ContextEditor>()
1023                .filter(|editor| editor.read(cx).context().read(cx).path() == Some(&path))
1024        });
1025        if let Some(existing_context) = existing_context {
1026            return cx.spawn(|this, mut cx| async move {
1027                this.update(&mut cx, |this, cx| this.show_context(existing_context, cx))
1028            });
1029        }
1030
1031        let context = self
1032            .context_store
1033            .update(cx, |store, cx| store.open_local_context(path.clone(), cx));
1034        let fs = self.fs.clone();
1035        let project = self.project.clone();
1036        let workspace = self.workspace.clone();
1037
1038        let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err().flatten();
1039
1040        cx.spawn(|this, mut cx| async move {
1041            let context = context.await?;
1042            this.update(&mut cx, |this, cx| {
1043                let editor = cx.new_view(|cx| {
1044                    ContextEditor::for_context(
1045                        context,
1046                        fs,
1047                        workspace,
1048                        project,
1049                        lsp_adapter_delegate,
1050                        cx,
1051                    )
1052                });
1053                this.show_context(editor, cx);
1054                anyhow::Ok(())
1055            })??;
1056            Ok(())
1057        })
1058    }
1059
1060    pub fn open_remote_context(
1061        &mut self,
1062        id: ContextId,
1063        cx: &mut ViewContext<Self>,
1064    ) -> Task<Result<View<ContextEditor>>> {
1065        let existing_context = self.pane.read(cx).items().find_map(|item| {
1066            item.downcast::<ContextEditor>()
1067                .filter(|editor| *editor.read(cx).context().read(cx).id() == id)
1068        });
1069        if let Some(existing_context) = existing_context {
1070            return cx.spawn(|this, mut cx| async move {
1071                this.update(&mut cx, |this, cx| {
1072                    this.show_context(existing_context.clone(), cx)
1073                })?;
1074                Ok(existing_context)
1075            });
1076        }
1077
1078        let context = self
1079            .context_store
1080            .update(cx, |store, cx| store.open_remote_context(id, cx));
1081        let fs = self.fs.clone();
1082        let workspace = self.workspace.clone();
1083        let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
1084            .log_err()
1085            .flatten();
1086
1087        cx.spawn(|this, mut cx| async move {
1088            let context = context.await?;
1089            this.update(&mut cx, |this, cx| {
1090                let editor = cx.new_view(|cx| {
1091                    ContextEditor::for_context(
1092                        context,
1093                        fs,
1094                        workspace,
1095                        this.project.clone(),
1096                        lsp_adapter_delegate,
1097                        cx,
1098                    )
1099                });
1100                this.show_context(editor.clone(), cx);
1101                anyhow::Ok(editor)
1102            })?
1103        })
1104    }
1105
1106    fn is_authenticated(&mut self, cx: &mut ViewContext<Self>) -> bool {
1107        LanguageModelRegistry::read_global(cx)
1108            .active_provider()
1109            .map_or(false, |provider| provider.is_authenticated(cx))
1110    }
1111
1112    fn authenticate(&mut self, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
1113        LanguageModelRegistry::read_global(cx)
1114            .active_provider()
1115            .map_or(None, |provider| Some(provider.authenticate(cx)))
1116    }
1117
1118    fn restart_context_servers(
1119        workspace: &mut Workspace,
1120        _action: &context_server::Restart,
1121        cx: &mut ViewContext<Workspace>,
1122    ) {
1123        let Some(assistant_panel) = workspace.panel::<AssistantPanel>(cx) else {
1124            return;
1125        };
1126
1127        assistant_panel.update(cx, |assistant_panel, cx| {
1128            assistant_panel
1129                .context_store
1130                .update(cx, |context_store, cx| {
1131                    context_store.restart_context_servers(cx);
1132                });
1133        });
1134    }
1135}
1136
1137impl Render for AssistantPanel {
1138    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1139        let mut registrar = DivRegistrar::new(
1140            |panel, cx| {
1141                panel
1142                    .pane
1143                    .read(cx)
1144                    .toolbar()
1145                    .read(cx)
1146                    .item_of_type::<BufferSearchBar>()
1147            },
1148            cx,
1149        );
1150        BufferSearchBar::register(&mut registrar);
1151        let registrar = registrar.into_div();
1152
1153        v_flex()
1154            .key_context("AssistantPanel")
1155            .size_full()
1156            .on_action(cx.listener(|this, _: &NewContext, cx| {
1157                this.new_context(cx);
1158            }))
1159            .on_action(
1160                cx.listener(|this, _: &ShowConfiguration, cx| this.show_configuration_tab(cx)),
1161            )
1162            .on_action(cx.listener(AssistantPanel::deploy_history))
1163            .on_action(cx.listener(AssistantPanel::deploy_prompt_library))
1164            .on_action(cx.listener(AssistantPanel::toggle_model_selector))
1165            .child(registrar.size_full().child(self.pane.clone()))
1166            .into_any_element()
1167    }
1168}
1169
1170impl Panel for AssistantPanel {
1171    fn persistent_name() -> &'static str {
1172        "AssistantPanel"
1173    }
1174
1175    fn position(&self, cx: &WindowContext) -> DockPosition {
1176        match AssistantSettings::get_global(cx).dock {
1177            AssistantDockPosition::Left => DockPosition::Left,
1178            AssistantDockPosition::Bottom => DockPosition::Bottom,
1179            AssistantDockPosition::Right => DockPosition::Right,
1180        }
1181    }
1182
1183    fn position_is_valid(&self, _: DockPosition) -> bool {
1184        true
1185    }
1186
1187    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
1188        settings::update_settings_file::<AssistantSettings>(
1189            self.fs.clone(),
1190            cx,
1191            move |settings, _| {
1192                let dock = match position {
1193                    DockPosition::Left => AssistantDockPosition::Left,
1194                    DockPosition::Bottom => AssistantDockPosition::Bottom,
1195                    DockPosition::Right => AssistantDockPosition::Right,
1196                };
1197                settings.set_dock(dock);
1198            },
1199        );
1200    }
1201
1202    fn size(&self, cx: &WindowContext) -> Pixels {
1203        let settings = AssistantSettings::get_global(cx);
1204        match self.position(cx) {
1205            DockPosition::Left | DockPosition::Right => {
1206                self.width.unwrap_or(settings.default_width)
1207            }
1208            DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1209        }
1210    }
1211
1212    fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
1213        match self.position(cx) {
1214            DockPosition::Left | DockPosition::Right => self.width = size,
1215            DockPosition::Bottom => self.height = size,
1216        }
1217        cx.notify();
1218    }
1219
1220    fn is_zoomed(&self, cx: &WindowContext) -> bool {
1221        self.pane.read(cx).is_zoomed()
1222    }
1223
1224    fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
1225        self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx));
1226    }
1227
1228    fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
1229        if active {
1230            if self.pane.read(cx).items_len() == 0 {
1231                self.new_context(cx);
1232            }
1233
1234            self.ensure_authenticated(cx);
1235        }
1236    }
1237
1238    fn pane(&self) -> Option<View<Pane>> {
1239        Some(self.pane.clone())
1240    }
1241
1242    fn remote_id() -> Option<proto::PanelId> {
1243        Some(proto::PanelId::AssistantPanel)
1244    }
1245
1246    fn icon(&self, cx: &WindowContext) -> Option<IconName> {
1247        let settings = AssistantSettings::get_global(cx);
1248        if !settings.enabled || !settings.button {
1249            return None;
1250        }
1251
1252        Some(IconName::ZedAssistant)
1253    }
1254
1255    fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
1256        Some("Assistant Panel")
1257    }
1258
1259    fn toggle_action(&self) -> Box<dyn Action> {
1260        Box::new(ToggleFocus)
1261    }
1262
1263    fn activation_priority(&self) -> u32 {
1264        4
1265    }
1266}
1267
1268impl EventEmitter<PanelEvent> for AssistantPanel {}
1269impl EventEmitter<AssistantPanelEvent> for AssistantPanel {}
1270
1271impl FocusableView for AssistantPanel {
1272    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
1273        self.pane.focus_handle(cx)
1274    }
1275}
1276
1277struct PromptLibraryInlineAssist;
1278
1279impl prompt_library::InlineAssistDelegate for PromptLibraryInlineAssist {
1280    fn assist(
1281        &self,
1282        prompt_editor: &View<Editor>,
1283        initial_prompt: Option<String>,
1284        cx: &mut ViewContext<PromptLibrary>,
1285    ) {
1286        InlineAssistant::update_global(cx, |assistant, cx| {
1287            assistant.assist(&prompt_editor, None, None, initial_prompt, cx)
1288        })
1289    }
1290
1291    fn focus_assistant_panel(
1292        &self,
1293        workspace: &mut Workspace,
1294        cx: &mut ViewContext<Workspace>,
1295    ) -> bool {
1296        workspace.focus_panel::<AssistantPanel>(cx).is_some()
1297    }
1298}
1299
1300pub struct ConcreteAssistantPanelDelegate;
1301
1302impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
1303    fn active_context_editor(
1304        &self,
1305        workspace: &mut Workspace,
1306        cx: &mut ViewContext<Workspace>,
1307    ) -> Option<View<ContextEditor>> {
1308        let panel = workspace.panel::<AssistantPanel>(cx)?;
1309        panel.read(cx).active_context_editor(cx)
1310    }
1311
1312    fn open_saved_context(
1313        &self,
1314        workspace: &mut Workspace,
1315        path: PathBuf,
1316        cx: &mut ViewContext<Workspace>,
1317    ) -> Task<Result<()>> {
1318        let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
1319            return Task::ready(Err(anyhow!("no Assistant panel found")));
1320        };
1321
1322        panel.update(cx, |panel, cx| panel.open_saved_context(path, cx))
1323    }
1324
1325    fn open_remote_context(
1326        &self,
1327        workspace: &mut Workspace,
1328        context_id: ContextId,
1329        cx: &mut ViewContext<Workspace>,
1330    ) -> Task<Result<View<ContextEditor>>> {
1331        let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
1332            return Task::ready(Err(anyhow!("no Assistant panel found")));
1333        };
1334
1335        panel.update(cx, |panel, cx| panel.open_remote_context(context_id, cx))
1336    }
1337
1338    fn quote_selection(
1339        &self,
1340        workspace: &mut Workspace,
1341        creases: Vec<(String, String)>,
1342        cx: &mut ViewContext<Workspace>,
1343    ) {
1344        let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
1345            return;
1346        };
1347
1348        if !panel.focus_handle(cx).contains_focused(cx) {
1349            workspace.toggle_panel_focus::<AssistantPanel>(cx);
1350        }
1351
1352        panel.update(cx, |_, cx| {
1353            // Wait to create a new context until the workspace is no longer
1354            // being updated.
1355            cx.defer(move |panel, cx| {
1356                if let Some(context) = panel
1357                    .active_context_editor(cx)
1358                    .or_else(|| panel.new_context(cx))
1359                {
1360                    context.update(cx, |context, cx| context.quote_creases(creases, cx));
1361                };
1362            });
1363        });
1364    }
1365}
1366
1367#[derive(Debug, PartialEq, Eq, Clone, Copy)]
1368pub enum WorkflowAssistStatus {
1369    Pending,
1370    Confirmed,
1371    Done,
1372    Idle,
1373}
1374
1375pub struct ConfigurationView {
1376    focus_handle: FocusHandle,
1377    configuration_views: HashMap<LanguageModelProviderId, AnyView>,
1378    _registry_subscription: Subscription,
1379}
1380
1381impl ConfigurationView {
1382    fn new(cx: &mut ViewContext<Self>) -> Self {
1383        let focus_handle = cx.focus_handle();
1384
1385        let registry_subscription = cx.subscribe(
1386            &LanguageModelRegistry::global(cx),
1387            |this, _, event: &language_model::Event, cx| match event {
1388                language_model::Event::AddedProvider(provider_id) => {
1389                    let provider = LanguageModelRegistry::read_global(cx).provider(provider_id);
1390                    if let Some(provider) = provider {
1391                        this.add_configuration_view(&provider, cx);
1392                    }
1393                }
1394                language_model::Event::RemovedProvider(provider_id) => {
1395                    this.remove_configuration_view(provider_id);
1396                }
1397                _ => {}
1398            },
1399        );
1400
1401        let mut this = Self {
1402            focus_handle,
1403            configuration_views: HashMap::default(),
1404            _registry_subscription: registry_subscription,
1405        };
1406        this.build_configuration_views(cx);
1407        this
1408    }
1409
1410    fn build_configuration_views(&mut self, cx: &mut ViewContext<Self>) {
1411        let providers = LanguageModelRegistry::read_global(cx).providers();
1412        for provider in providers {
1413            self.add_configuration_view(&provider, cx);
1414        }
1415    }
1416
1417    fn remove_configuration_view(&mut self, provider_id: &LanguageModelProviderId) {
1418        self.configuration_views.remove(provider_id);
1419    }
1420
1421    fn add_configuration_view(
1422        &mut self,
1423        provider: &Arc<dyn LanguageModelProvider>,
1424        cx: &mut ViewContext<Self>,
1425    ) {
1426        let configuration_view = provider.configuration_view(cx);
1427        self.configuration_views
1428            .insert(provider.id(), configuration_view);
1429    }
1430
1431    fn render_provider_view(
1432        &mut self,
1433        provider: &Arc<dyn LanguageModelProvider>,
1434        cx: &mut ViewContext<Self>,
1435    ) -> Div {
1436        let provider_id = provider.id().0.clone();
1437        let provider_name = provider.name().0.clone();
1438        let configuration_view = self.configuration_views.get(&provider.id()).cloned();
1439
1440        let open_new_context = cx.listener({
1441            let provider = provider.clone();
1442            move |_, _, cx| {
1443                cx.emit(ConfigurationViewEvent::NewProviderContextEditor(
1444                    provider.clone(),
1445                ))
1446            }
1447        });
1448
1449        v_flex()
1450            .gap_2()
1451            .child(
1452                h_flex()
1453                    .justify_between()
1454                    .child(Headline::new(provider_name.clone()).size(HeadlineSize::Small))
1455                    .when(provider.is_authenticated(cx), move |this| {
1456                        this.child(
1457                            h_flex().justify_end().child(
1458                                Button::new(
1459                                    SharedString::from(format!("new-context-{provider_id}")),
1460                                    "Open New Chat",
1461                                )
1462                                .icon_position(IconPosition::Start)
1463                                .icon(IconName::Plus)
1464                                .style(ButtonStyle::Filled)
1465                                .layer(ElevationIndex::ModalSurface)
1466                                .on_click(open_new_context),
1467                            ),
1468                        )
1469                    }),
1470            )
1471            .child(
1472                div()
1473                    .p(DynamicSpacing::Base08.rems(cx))
1474                    .bg(cx.theme().colors().surface_background)
1475                    .border_1()
1476                    .border_color(cx.theme().colors().border_variant)
1477                    .rounded_md()
1478                    .when(configuration_view.is_none(), |this| {
1479                        this.child(div().child(Label::new(format!(
1480                            "No configuration view for {}",
1481                            provider_name
1482                        ))))
1483                    })
1484                    .when_some(configuration_view, |this, configuration_view| {
1485                        this.child(configuration_view)
1486                    }),
1487            )
1488    }
1489}
1490
1491impl Render for ConfigurationView {
1492    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1493        let providers = LanguageModelRegistry::read_global(cx).providers();
1494        let provider_views = providers
1495            .into_iter()
1496            .map(|provider| self.render_provider_view(&provider, cx))
1497            .collect::<Vec<_>>();
1498
1499        let mut element = v_flex()
1500            .id("assistant-configuration-view")
1501            .track_focus(&self.focus_handle(cx))
1502            .bg(cx.theme().colors().editor_background)
1503            .size_full()
1504            .overflow_y_scroll()
1505            .child(
1506                v_flex()
1507                    .p(DynamicSpacing::Base16.rems(cx))
1508                    .border_b_1()
1509                    .border_color(cx.theme().colors().border)
1510                    .gap_1()
1511                    .child(Headline::new("Configure your Assistant").size(HeadlineSize::Medium))
1512                    .child(
1513                        Label::new(
1514                            "At least one LLM provider must be configured to use the Assistant.",
1515                        )
1516                        .color(Color::Muted),
1517                    ),
1518            )
1519            .child(
1520                v_flex()
1521                    .p(DynamicSpacing::Base16.rems(cx))
1522                    .mt_1()
1523                    .gap_6()
1524                    .flex_1()
1525                    .children(provider_views),
1526            )
1527            .into_any();
1528
1529        // We use a canvas here to get scrolling to work in the ConfigurationView. It's a workaround
1530        // because we couldn't the element to take up the size of the parent.
1531        canvas(
1532            move |bounds, cx| {
1533                element.prepaint_as_root(bounds.origin, bounds.size.into(), cx);
1534                element
1535            },
1536            |_, mut element, cx| {
1537                element.paint(cx);
1538            },
1539        )
1540        .flex_1()
1541        .w_full()
1542    }
1543}
1544
1545pub enum ConfigurationViewEvent {
1546    NewProviderContextEditor(Arc<dyn LanguageModelProvider>),
1547}
1548
1549impl EventEmitter<ConfigurationViewEvent> for ConfigurationView {}
1550
1551impl FocusableView for ConfigurationView {
1552    fn focus_handle(&self, _: &AppContext) -> FocusHandle {
1553        self.focus_handle.clone()
1554    }
1555}
1556
1557impl Item for ConfigurationView {
1558    type Event = ConfigurationViewEvent;
1559
1560    fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
1561        Some("Configuration".into())
1562    }
1563}