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