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