assistant_panel.rs

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