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