assistant_panel.rs

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