assistant_panel.rs

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