assistant_panel.rs

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