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