use crate::Assistant; use crate::assistant_configuration::{ConfigurationView, ConfigurationViewEvent}; use crate::{ DeployHistory, InlineAssistant, NewChat, terminal_inline_assistant::TerminalInlineAssistant, }; use anyhow::{Result, anyhow}; use assistant_context_editor::{ AssistantContext, AssistantPanelDelegate, ContextEditor, ContextEditorToolbarItem, ContextEditorToolbarItemEvent, ContextHistory, ContextId, ContextStore, ContextStoreEvent, DEFAULT_TAB_TITLE, InsertDraggedFiles, SlashCommandCompletionProvider, make_lsp_adapter_delegate, }; use assistant_settings::{AssistantDockPosition, AssistantSettings}; use assistant_slash_command::SlashCommandWorkingSet; use client::{Client, Status, proto}; use editor::{Editor, EditorEvent}; use fs::Fs; use gpui::{ Action, App, AsyncWindowContext, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Pixels, Render, Styled, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, }; use language::LanguageRegistry; use language_model::{ AuthenticateError, ConfiguredModel, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID, }; use project::Project; use prompt_library::{PromptLibrary, open_prompt_library}; use prompt_store::PromptBuilder; use search::{BufferSearchBar, buffer_search::DivRegistrar}; use settings::{Settings, update_settings_file}; use smol::stream::StreamExt; use std::{ops::ControlFlow, path::PathBuf, sync::Arc}; use terminal_view::{TerminalView, terminal_panel::TerminalPanel}; use ui::{ContextMenu, PopoverMenu, Tooltip, prelude::*}; use util::{ResultExt, maybe}; use workspace::DraggedTab; use workspace::{ DraggedSelection, Pane, ToggleZoom, Workspace, dock::{DockPosition, Panel, PanelEvent}, pane, }; use zed_actions::assistant::{InlineAssist, OpenPromptLibrary, ShowConfiguration, ToggleFocus}; pub fn init(cx: &mut App) { workspace::FollowableViewRegistry::register::(cx); cx.observe_new( |workspace: &mut Workspace, _window, _cx: &mut Context| { workspace .register_action(ContextEditor::quote_selection) .register_action(ContextEditor::insert_selection) .register_action(ContextEditor::copy_code) .register_action(ContextEditor::insert_dragged_files) .register_action(AssistantPanel::show_configuration) .register_action(AssistantPanel::create_new_context) .register_action(AssistantPanel::restart_context_servers); }, ) .detach(); cx.observe_new( |terminal_panel: &mut TerminalPanel, _, cx: &mut Context| { terminal_panel.set_assistant_enabled(Assistant::enabled(cx), cx); }, ) .detach(); } pub enum AssistantPanelEvent { ContextEdited, } pub struct AssistantPanel { pane: Entity, workspace: WeakEntity, width: Option, height: Option, project: Entity, context_store: Entity, languages: Arc, fs: Arc, subscriptions: Vec, model_summary_editor: Entity, authenticate_provider_task: Option<(LanguageModelProviderId, Task<()>)>, configuration_subscription: Option, client_status: Option, watch_client_status: Option>, pub(crate) show_zed_ai_notice: bool, } enum InlineAssistTarget { Editor(Entity, bool), Terminal(Entity), } impl AssistantPanel { pub fn load( workspace: WeakEntity, prompt_builder: Arc, cx: AsyncWindowContext, ) -> Task>> { cx.spawn(async move |cx| { let slash_commands = Arc::new(SlashCommandWorkingSet::default()); let context_store = workspace .update(cx, |workspace, cx| { let project = workspace.project().clone(); ContextStore::new(project, prompt_builder.clone(), slash_commands, cx) })? .await?; workspace.update_in(cx, |workspace, window, cx| { // TODO: deserialize state. cx.new(|cx| Self::new(workspace, context_store, window, cx)) }) }) } fn new( workspace: &Workspace, context_store: Entity, window: &mut Window, cx: &mut Context, ) -> Self { let model_summary_editor = cx.new(|cx| Editor::single_line(window, cx)); let context_editor_toolbar = cx.new(|_| ContextEditorToolbarItem::new(model_summary_editor.clone())); let pane = cx.new(|cx| { let mut pane = Pane::new( workspace.weak_handle(), workspace.project().clone(), Default::default(), None, NewChat.boxed_clone(), window, cx, ); let project = workspace.project().clone(); pane.set_custom_drop_handle(cx, move |_, dropped_item, window, cx| { let action = maybe!({ if project.read(cx).is_local() { if let Some(paths) = dropped_item.downcast_ref::() { return Some(InsertDraggedFiles::ExternalFiles(paths.paths().to_vec())); } } let project_paths = if let Some(tab) = dropped_item.downcast_ref::() { if tab.pane == cx.entity() { return None; } let item = tab.pane.read(cx).item_for_index(tab.ix); Some( item.and_then(|item| item.project_path(cx)) .into_iter() .collect::>(), ) } else if let Some(selection) = dropped_item.downcast_ref::() { Some( selection .items() .filter_map(|item| { project.read(cx).path_for_entry(item.entry_id, cx) }) .collect::>(), ) } else { None }?; let paths = project_paths .into_iter() .filter_map(|project_path| { let worktree = project .read(cx) .worktree_for_id(project_path.worktree_id, cx)?; let mut full_path = PathBuf::from(worktree.read(cx).root_name()); full_path.push(&project_path.path); Some(full_path) }) .collect::>(); Some(InsertDraggedFiles::ProjectPaths(paths)) }); if let Some(action) = action { window.dispatch_action(action.boxed_clone(), cx); } ControlFlow::Break(()) }); pane.set_can_navigate(true, cx); pane.display_nav_history_buttons(None); pane.set_should_display_tab_bar(|_, _| true); pane.set_render_tab_bar_buttons(cx, move |pane, _window, cx| { let focus_handle = pane.focus_handle(cx); let left_children = IconButton::new("history", IconName::HistoryRerun) .icon_size(IconSize::Small) .on_click(cx.listener({ let focus_handle = focus_handle.clone(); move |_, _, window, cx| { focus_handle.focus(window); window.dispatch_action(DeployHistory.boxed_clone(), cx) } })) .tooltip({ let focus_handle = focus_handle.clone(); move |window, cx| { Tooltip::for_action_in( "Open History", &DeployHistory, &focus_handle, window, cx, ) } }) .toggle_state( pane.active_item() .map_or(false, |item| item.downcast::().is_some()), ); let _pane = cx.entity().clone(); let right_children = h_flex() .gap(DynamicSpacing::Base02.rems(cx)) .child( IconButton::new("new-chat", IconName::Plus) .icon_size(IconSize::Small) .on_click(cx.listener(|_, _, window, cx| { window.dispatch_action(NewChat.boxed_clone(), cx) })) .tooltip(move |window, cx| { Tooltip::for_action_in( "New Chat", &NewChat, &focus_handle, window, cx, ) }), ) .child( PopoverMenu::new("assistant-panel-popover-menu") .trigger_with_tooltip( IconButton::new("menu", IconName::EllipsisVertical) .icon_size(IconSize::Small), Tooltip::text("Toggle Assistant Menu"), ) .menu(move |window, cx| { let zoom_label = if _pane.read(cx).is_zoomed() { "Zoom Out" } else { "Zoom In" }; let focus_handle = _pane.focus_handle(cx); Some(ContextMenu::build(window, cx, move |menu, _, _| { menu.context(focus_handle.clone()) .action("New Chat", Box::new(NewChat)) .action("History", Box::new(DeployHistory)) .action("Prompt Library", Box::new(OpenPromptLibrary)) .action("Configure", Box::new(ShowConfiguration)) .action(zoom_label, Box::new(ToggleZoom)) })) }), ) .into_any_element() .into(); (Some(left_children.into_any_element()), right_children) }); pane.toolbar().update(cx, |toolbar, cx| { toolbar.add_item(context_editor_toolbar.clone(), window, cx); toolbar.add_item( cx.new(|cx| { BufferSearchBar::new( Some(workspace.project().read(cx).languages().clone()), window, cx, ) }), window, cx, ) }); pane }); let subscriptions = vec![ cx.observe(&pane, |_, _, cx| cx.notify()), cx.subscribe_in(&pane, window, Self::handle_pane_event), cx.subscribe(&context_editor_toolbar, Self::handle_toolbar_event), cx.subscribe(&model_summary_editor, Self::handle_summary_editor_event), cx.subscribe_in(&context_store, window, Self::handle_context_store_event), cx.subscribe_in( &LanguageModelRegistry::global(cx), window, |this, _, event: &language_model::Event, window, cx| match event { language_model::Event::DefaultModelChanged | language_model::Event::InlineAssistantModelChanged | language_model::Event::CommitMessageModelChanged | language_model::Event::ThreadSummaryModelChanged => { this.completion_provider_changed(window, cx); } language_model::Event::ProviderStateChanged => { this.ensure_authenticated(window, cx); cx.notify() } language_model::Event::AddedProvider(_) | language_model::Event::RemovedProvider(_) => { this.ensure_authenticated(window, cx); } }, ), ]; let watch_client_status = Self::watch_client_status(workspace.client().clone(), window, cx); let mut this = Self { pane, workspace: workspace.weak_handle(), width: None, height: None, project: workspace.project().clone(), context_store, languages: workspace.app_state().languages.clone(), fs: workspace.app_state().fs.clone(), subscriptions, model_summary_editor, authenticate_provider_task: None, configuration_subscription: None, client_status: None, watch_client_status: Some(watch_client_status), show_zed_ai_notice: false, }; this.new_context(window, cx); this } pub fn toggle_focus( workspace: &mut Workspace, _: &ToggleFocus, window: &mut Window, cx: &mut Context, ) { if workspace .panel::(cx) .is_some_and(|panel| panel.read(cx).enabled(cx)) { workspace.toggle_panel_focus::(window, cx); } } fn watch_client_status( client: Arc, window: &mut Window, cx: &mut Context, ) -> Task<()> { let mut status_rx = client.status(); cx.spawn_in(window, async move |this, cx| { while let Some(status) = status_rx.next().await { this.update(cx, |this, cx| { if this.client_status.is_none() || this .client_status .map_or(false, |old_status| old_status != status) { this.update_zed_ai_notice_visibility(status, cx); } this.client_status = Some(status); }) .log_err(); } this.update(cx, |this, _cx| this.watch_client_status = None) .log_err(); }) } fn handle_pane_event( &mut self, pane: &Entity, event: &pane::Event, window: &mut Window, cx: &mut Context, ) { let update_model_summary = match event { pane::Event::Remove { .. } => { cx.emit(PanelEvent::Close); false } pane::Event::ZoomIn => { cx.emit(PanelEvent::ZoomIn); false } pane::Event::ZoomOut => { cx.emit(PanelEvent::ZoomOut); false } pane::Event::AddItem { item } => { self.workspace .update(cx, |workspace, cx| { item.added_to_pane(workspace, self.pane.clone(), window, cx) }) .ok(); true } pane::Event::ActivateItem { local, .. } => { if *local { self.workspace .update(cx, |workspace, cx| { workspace.unfollow_in_pane(&pane, window, cx); }) .ok(); } cx.emit(AssistantPanelEvent::ContextEdited); true } pane::Event::RemovedItem { .. } => { let has_configuration_view = self .pane .read(cx) .items_of_type::() .next() .is_some(); if !has_configuration_view { self.configuration_subscription = None; } cx.emit(AssistantPanelEvent::ContextEdited); true } _ => false, }; if update_model_summary { if let Some(editor) = self.active_context_editor(cx) { self.show_updated_summary(&editor, window, cx) } } } fn handle_summary_editor_event( &mut self, model_summary_editor: Entity, event: &EditorEvent, cx: &mut Context, ) { if matches!(event, EditorEvent::Edited { .. }) { if let Some(context_editor) = self.active_context_editor(cx) { let new_summary = model_summary_editor.read(cx).text(cx); context_editor.update(cx, |context_editor, cx| { context_editor.context().update(cx, |context, cx| { if context.summary().is_none() && (new_summary == DEFAULT_TAB_TITLE || new_summary.trim().is_empty()) { return; } context.custom_summary(new_summary, cx) }); }); } } } fn update_zed_ai_notice_visibility(&mut self, client_status: Status, cx: &mut Context) { let model = LanguageModelRegistry::read_global(cx).default_model(); // If we're signed out and don't have a provider configured, or we're signed-out AND Zed.dev is // the provider, we want to show a nudge to sign in. let show_zed_ai_notice = client_status.is_signed_out() && model.map_or(true, |model| model.provider.id().0 == ZED_CLOUD_PROVIDER_ID); self.show_zed_ai_notice = show_zed_ai_notice; cx.notify(); } fn handle_toolbar_event( &mut self, _: Entity, _: &ContextEditorToolbarItemEvent, cx: &mut Context, ) { if let Some(context_editor) = self.active_context_editor(cx) { context_editor.update(cx, |context_editor, cx| { context_editor.context().update(cx, |context, cx| { context.summarize(true, cx); }) }) } } fn handle_context_store_event( &mut self, _context_store: &Entity, event: &ContextStoreEvent, window: &mut Window, cx: &mut Context, ) { let ContextStoreEvent::ContextCreated(context_id) = event; let Some(context) = self .context_store .read(cx) .loaded_context_for_id(&context_id, cx) else { log::error!("no context found with ID: {}", context_id.to_proto()); return; }; let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx) .log_err() .flatten(); let editor = cx.new(|cx| { let mut editor = ContextEditor::for_context( context, self.fs.clone(), self.workspace.clone(), self.project.clone(), lsp_adapter_delegate, window, cx, ); editor.insert_default_prompt(window, cx); editor }); self.show_context(editor.clone(), window, cx); } fn completion_provider_changed(&mut self, window: &mut Window, cx: &mut Context) { if let Some(editor) = self.active_context_editor(cx) { editor.update(cx, |active_context, cx| { active_context .context() .update(cx, |context, cx| context.completion_provider_changed(cx)) }) } let Some(new_provider_id) = LanguageModelRegistry::read_global(cx) .default_model() .map(|default| default.provider.id()) else { return; }; if self .authenticate_provider_task .as_ref() .map_or(true, |(old_provider_id, _)| { *old_provider_id != new_provider_id }) { self.authenticate_provider_task = None; self.ensure_authenticated(window, cx); } if let Some(status) = self.client_status { self.update_zed_ai_notice_visibility(status, cx); } } fn ensure_authenticated(&mut self, window: &mut Window, cx: &mut Context) { if self.is_authenticated(cx) { return; } let Some(ConfiguredModel { provider, .. }) = LanguageModelRegistry::read_global(cx).default_model() else { return; }; let load_credentials = self.authenticate(cx); if self.authenticate_provider_task.is_none() { self.authenticate_provider_task = Some(( provider.id(), cx.spawn_in(window, async move |this, cx| { if let Some(future) = load_credentials { let _ = future.await; } this.update(cx, |this, _cx| { this.authenticate_provider_task = None; }) .log_err(); }), )); } } pub fn inline_assist( workspace: &mut Workspace, action: &InlineAssist, window: &mut Window, cx: &mut Context, ) { let Some(assistant_panel) = workspace .panel::(cx) .filter(|panel| panel.read(cx).enabled(cx)) else { return; }; let Some(inline_assist_target) = Self::resolve_inline_assist_target(workspace, &assistant_panel, window, cx) else { return; }; let initial_prompt = action.prompt.clone(); if assistant_panel.update(cx, |assistant, cx| assistant.is_authenticated(cx)) { match inline_assist_target { InlineAssistTarget::Editor(active_editor, include_context) => { InlineAssistant::update_global(cx, |assistant, cx| { assistant.assist( &active_editor, Some(cx.entity().downgrade()), include_context.then_some(&assistant_panel), initial_prompt, window, cx, ) }) } InlineAssistTarget::Terminal(active_terminal) => { TerminalInlineAssistant::update_global(cx, |assistant, cx| { assistant.assist( &active_terminal, Some(cx.entity().downgrade()), Some(&assistant_panel), initial_prompt, window, cx, ) }) } } } else { let assistant_panel = assistant_panel.downgrade(); cx.spawn_in(window, async move |workspace, cx| { let Some(task) = assistant_panel.update(cx, |assistant, cx| assistant.authenticate(cx))? else { let answer = cx .prompt( gpui::PromptLevel::Warning, "No language model provider configured", None, &["Configure", "Cancel"], ) .await .ok(); if let Some(answer) = answer { if answer == 0 { cx.update(|window, cx| { window.dispatch_action(Box::new(ShowConfiguration), cx) }) .ok(); } } return Ok(()); }; task.await?; if assistant_panel.update(cx, |panel, cx| panel.is_authenticated(cx))? { cx.update(|window, cx| match inline_assist_target { InlineAssistTarget::Editor(active_editor, include_context) => { let assistant_panel = if include_context { assistant_panel.upgrade() } else { None }; InlineAssistant::update_global(cx, |assistant, cx| { assistant.assist( &active_editor, Some(workspace), assistant_panel.as_ref(), initial_prompt, window, cx, ) }) } InlineAssistTarget::Terminal(active_terminal) => { TerminalInlineAssistant::update_global(cx, |assistant, cx| { assistant.assist( &active_terminal, Some(workspace), assistant_panel.upgrade().as_ref(), initial_prompt, window, cx, ) }) } })? } else { workspace.update_in(cx, |workspace, window, cx| { workspace.focus_panel::(window, cx) })?; } anyhow::Ok(()) }) .detach_and_log_err(cx) } } fn resolve_inline_assist_target( workspace: &mut Workspace, assistant_panel: &Entity, window: &mut Window, cx: &mut App, ) -> Option { if let Some(terminal_panel) = workspace.panel::(cx) { if terminal_panel .read(cx) .focus_handle(cx) .contains_focused(window, cx) { if let Some(terminal_view) = terminal_panel.read(cx).pane().and_then(|pane| { pane.read(cx) .active_item() .and_then(|t| t.downcast::()) }) { return Some(InlineAssistTarget::Terminal(terminal_view)); } } } let context_editor = assistant_panel .read(cx) .active_context_editor(cx) .and_then(|editor| { let editor = &editor.read(cx).editor().clone(); if editor.read(cx).is_focused(window) { Some(editor.clone()) } else { None } }); if let Some(context_editor) = context_editor { Some(InlineAssistTarget::Editor(context_editor, false)) } else if let Some(workspace_editor) = workspace .active_item(cx) .and_then(|item| item.act_as::(cx)) { Some(InlineAssistTarget::Editor(workspace_editor, true)) } else if let Some(terminal_view) = workspace .active_item(cx) .and_then(|item| item.act_as::(cx)) { Some(InlineAssistTarget::Terminal(terminal_view)) } else { None } } pub fn create_new_context( workspace: &mut Workspace, _: &NewChat, window: &mut Window, cx: &mut Context, ) { if let Some(panel) = workspace.panel::(cx) { let did_create_context = panel .update(cx, |panel, cx| { panel.new_context(window, cx)?; Some(()) }) .is_some(); if did_create_context { ContextEditor::quote_selection(workspace, &Default::default(), window, cx); } } } pub fn new_context( &mut self, window: &mut Window, cx: &mut Context, ) -> Option> { let project = self.project.read(cx); if project.is_via_collab() { let task = self .context_store .update(cx, |store, cx| store.create_remote_context(cx)); cx.spawn_in(window, async move |this, cx| { let context = task.await?; this.update_in(cx, |this, window, cx| { let workspace = this.workspace.clone(); let project = this.project.clone(); let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err().flatten(); let fs = this.fs.clone(); let project = this.project.clone(); let editor = cx.new(|cx| { ContextEditor::for_context( context, fs, workspace, project, lsp_adapter_delegate, window, cx, ) }); this.show_context(editor, window, cx); anyhow::Ok(()) })??; anyhow::Ok(()) }) .detach_and_log_err(cx); None } else { let context = self.context_store.update(cx, |store, cx| store.create(cx)); let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx) .log_err() .flatten(); let editor = cx.new(|cx| { let mut editor = ContextEditor::for_context( context, self.fs.clone(), self.workspace.clone(), self.project.clone(), lsp_adapter_delegate, window, cx, ); editor.insert_default_prompt(window, cx); editor }); self.show_context(editor.clone(), window, cx); let workspace = self.workspace.clone(); cx.spawn_in(window, async move |_, cx| { workspace .update_in(cx, |workspace, window, cx| { workspace.focus_panel::(window, cx); }) .ok(); }) .detach(); Some(editor) } } fn show_context( &mut self, context_editor: Entity, window: &mut Window, cx: &mut Context, ) { let focus = self.focus_handle(cx).contains_focused(window, cx); let prev_len = self.pane.read(cx).items_len(); self.pane.update(cx, |pane, cx| { pane.add_item( Box::new(context_editor.clone()), focus, focus, None, window, cx, ) }); if prev_len != self.pane.read(cx).items_len() { self.subscriptions.push(cx.subscribe_in( &context_editor, window, Self::handle_context_editor_event, )); } self.show_updated_summary(&context_editor, window, cx); cx.emit(AssistantPanelEvent::ContextEdited); cx.notify(); } fn show_updated_summary( &self, context_editor: &Entity, window: &mut Window, cx: &mut Context, ) { context_editor.update(cx, |context_editor, cx| { let new_summary = context_editor.title(cx).to_string(); self.model_summary_editor.update(cx, |summary_editor, cx| { if summary_editor.text(cx) != new_summary { summary_editor.set_text(new_summary, window, cx); } }); }); } fn handle_context_editor_event( &mut self, context_editor: &Entity, event: &EditorEvent, window: &mut Window, cx: &mut Context, ) { match event { EditorEvent::TitleChanged => { self.show_updated_summary(&context_editor, window, cx); cx.notify() } EditorEvent::Edited { .. } => { self.workspace .update(cx, |workspace, cx| { let is_via_ssh = workspace .project() .update(cx, |project, _| project.is_via_ssh()); workspace .client() .telemetry() .log_edit_event("assistant panel", is_via_ssh); }) .log_err(); cx.emit(AssistantPanelEvent::ContextEdited) } _ => {} } } fn show_configuration( workspace: &mut Workspace, _: &ShowConfiguration, window: &mut Window, cx: &mut Context, ) { let Some(panel) = workspace.panel::(cx) else { return; }; if !panel.focus_handle(cx).contains_focused(window, cx) { workspace.toggle_panel_focus::(window, cx); } panel.update(cx, |this, cx| { this.show_configuration_tab(window, cx); }) } fn show_configuration_tab(&mut self, window: &mut Window, cx: &mut Context) { let configuration_item_ix = self .pane .read(cx) .items() .position(|item| item.downcast::().is_some()); if let Some(configuration_item_ix) = configuration_item_ix { self.pane.update(cx, |pane, cx| { pane.activate_item(configuration_item_ix, true, true, window, cx); }); } else { let configuration = cx.new(|cx| ConfigurationView::new(window, cx)); self.configuration_subscription = Some(cx.subscribe_in( &configuration, window, |this, _, event: &ConfigurationViewEvent, window, cx| match event { ConfigurationViewEvent::NewProviderContextEditor(provider) => { if LanguageModelRegistry::read_global(cx) .default_model() .map_or(true, |default| default.provider.id() != provider.id()) { if let Some(model) = provider.default_model(cx) { update_settings_file::( this.fs.clone(), cx, move |settings, _| settings.set_model(model), ); } } this.new_context(window, cx); } }, )); self.pane.update(cx, |pane, cx| { pane.add_item(Box::new(configuration), true, true, None, window, cx); }); } } fn deploy_history(&mut self, _: &DeployHistory, window: &mut Window, cx: &mut Context) { let history_item_ix = self .pane .read(cx) .items() .position(|item| item.downcast::().is_some()); if let Some(history_item_ix) = history_item_ix { self.pane.update(cx, |pane, cx| { pane.activate_item(history_item_ix, true, true, window, cx); }); } else { let history = cx.new(|cx| { ContextHistory::new( self.project.clone(), self.context_store.clone(), self.workspace.clone(), window, cx, ) }); self.pane.update(cx, |pane, cx| { pane.add_item(Box::new(history), true, true, None, window, cx); }); } } fn deploy_prompt_library( &mut self, _: &OpenPromptLibrary, _window: &mut Window, cx: &mut Context, ) { open_prompt_library( self.languages.clone(), Box::new(PromptLibraryInlineAssist), Arc::new(|| { Box::new(SlashCommandCompletionProvider::new( Arc::new(SlashCommandWorkingSet::default()), None, None, )) }), cx, ) .detach_and_log_err(cx); } pub(crate) fn active_context_editor(&self, cx: &App) -> Option> { self.pane .read(cx) .active_item()? .downcast::() } pub fn active_context(&self, cx: &App) -> Option> { Some(self.active_context_editor(cx)?.read(cx).context().clone()) } pub fn open_saved_context( &mut self, path: PathBuf, window: &mut Window, cx: &mut Context, ) -> Task> { let existing_context = self.pane.read(cx).items().find_map(|item| { item.downcast::() .filter(|editor| editor.read(cx).context().read(cx).path() == Some(&path)) }); if let Some(existing_context) = existing_context { return cx.spawn_in(window, async move |this, cx| { this.update_in(cx, |this, window, cx| { this.show_context(existing_context, window, cx) }) }); } let context = self .context_store .update(cx, |store, cx| store.open_local_context(path.clone(), cx)); let fs = self.fs.clone(); let project = self.project.clone(); let workspace = self.workspace.clone(); let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err().flatten(); cx.spawn_in(window, async move |this, cx| { let context = context.await?; this.update_in(cx, |this, window, cx| { let editor = cx.new(|cx| { ContextEditor::for_context( context, fs, workspace, project, lsp_adapter_delegate, window, cx, ) }); this.show_context(editor, window, cx); anyhow::Ok(()) })??; Ok(()) }) } pub fn open_remote_context( &mut self, id: ContextId, window: &mut Window, cx: &mut Context, ) -> Task>> { let existing_context = self.pane.read(cx).items().find_map(|item| { item.downcast::() .filter(|editor| *editor.read(cx).context().read(cx).id() == id) }); if let Some(existing_context) = existing_context { return cx.spawn_in(window, async move |this, cx| { this.update_in(cx, |this, window, cx| { this.show_context(existing_context.clone(), window, cx) })?; Ok(existing_context) }); } let context = self .context_store .update(cx, |store, cx| store.open_remote_context(id, cx)); let fs = self.fs.clone(); let workspace = self.workspace.clone(); let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx) .log_err() .flatten(); cx.spawn_in(window, async move |this, cx| { let context = context.await?; this.update_in(cx, |this, window, cx| { let editor = cx.new(|cx| { ContextEditor::for_context( context, fs, workspace, this.project.clone(), lsp_adapter_delegate, window, cx, ) }); this.show_context(editor.clone(), window, cx); anyhow::Ok(editor) })? }) } fn is_authenticated(&mut self, cx: &mut Context) -> bool { LanguageModelRegistry::read_global(cx) .default_model() .map_or(false, |default| default.provider.is_authenticated(cx)) } fn authenticate( &mut self, cx: &mut Context, ) -> Option>> { LanguageModelRegistry::read_global(cx) .default_model() .map_or(None, |default| Some(default.provider.authenticate(cx))) } fn restart_context_servers( workspace: &mut Workspace, _action: &context_server::Restart, _: &mut Window, cx: &mut Context, ) { let Some(assistant_panel) = workspace.panel::(cx) else { return; }; assistant_panel.update(cx, |assistant_panel, cx| { assistant_panel .context_store .update(cx, |context_store, cx| { context_store.restart_context_servers(cx); }); }); } } impl Render for AssistantPanel { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let mut registrar = DivRegistrar::new( |panel, _, cx| { panel .pane .read(cx) .toolbar() .read(cx) .item_of_type::() }, cx, ); BufferSearchBar::register(&mut registrar); let registrar = registrar.into_div(); v_flex() .key_context("AssistantPanel") .size_full() .on_action(cx.listener(|this, _: &NewChat, window, cx| { this.new_context(window, cx); })) .on_action(cx.listener(|this, _: &ShowConfiguration, window, cx| { this.show_configuration_tab(window, cx) })) .on_action(cx.listener(AssistantPanel::deploy_history)) .on_action(cx.listener(AssistantPanel::deploy_prompt_library)) .child(registrar.size_full().child(self.pane.clone())) .into_any_element() } } impl Panel for AssistantPanel { fn persistent_name() -> &'static str { "AssistantPanel" } fn position(&self, _: &Window, cx: &App) -> DockPosition { match AssistantSettings::get_global(cx).dock { AssistantDockPosition::Left => DockPosition::Left, AssistantDockPosition::Bottom => DockPosition::Bottom, AssistantDockPosition::Right => DockPosition::Right, } } fn position_is_valid(&self, _: DockPosition) -> bool { true } fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context) { settings::update_settings_file::( self.fs.clone(), cx, move |settings, _| { let dock = match position { DockPosition::Left => AssistantDockPosition::Left, DockPosition::Bottom => AssistantDockPosition::Bottom, DockPosition::Right => AssistantDockPosition::Right, }; settings.set_dock(dock); }, ); } fn size(&self, window: &Window, cx: &App) -> Pixels { let settings = AssistantSettings::get_global(cx); match self.position(window, cx) { DockPosition::Left | DockPosition::Right => { self.width.unwrap_or(settings.default_width) } DockPosition::Bottom => self.height.unwrap_or(settings.default_height), } } fn set_size(&mut self, size: Option, window: &mut Window, cx: &mut Context) { match self.position(window, cx) { DockPosition::Left | DockPosition::Right => self.width = size, DockPosition::Bottom => self.height = size, } cx.notify(); } fn is_zoomed(&self, _: &Window, cx: &App) -> bool { self.pane.read(cx).is_zoomed() } fn set_zoomed(&mut self, zoomed: bool, _: &mut Window, cx: &mut Context) { self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx)); } fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context) { if active { if self.pane.read(cx).items_len() == 0 { self.new_context(window, cx); } self.ensure_authenticated(window, cx); } } fn pane(&self) -> Option> { Some(self.pane.clone()) } fn remote_id() -> Option { Some(proto::PanelId::AssistantPanel) } fn icon(&self, _: &Window, cx: &App) -> Option { (self.enabled(cx) && AssistantSettings::get_global(cx).button) .then_some(IconName::ZedAssistant) } fn icon_tooltip(&self, _: &Window, _: &App) -> Option<&'static str> { Some("Assistant Panel") } fn toggle_action(&self) -> Box { Box::new(ToggleFocus) } fn activation_priority(&self) -> u32 { 4 } fn enabled(&self, cx: &App) -> bool { Assistant::enabled(cx) } } impl EventEmitter for AssistantPanel {} impl EventEmitter for AssistantPanel {} impl Focusable for AssistantPanel { fn focus_handle(&self, cx: &App) -> FocusHandle { self.pane.focus_handle(cx) } } struct PromptLibraryInlineAssist; impl prompt_library::InlineAssistDelegate for PromptLibraryInlineAssist { fn assist( &self, prompt_editor: &Entity, initial_prompt: Option, window: &mut Window, cx: &mut Context, ) { InlineAssistant::update_global(cx, |assistant, cx| { assistant.assist(&prompt_editor, None, None, initial_prompt, window, cx) }) } fn focus_assistant_panel( &self, workspace: &mut Workspace, window: &mut Window, cx: &mut Context, ) -> bool { workspace .focus_panel::(window, cx) .is_some() } } pub struct ConcreteAssistantPanelDelegate; impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate { fn active_context_editor( &self, workspace: &mut Workspace, _window: &mut Window, cx: &mut Context, ) -> Option> { let panel = workspace.panel::(cx)?; panel.read(cx).active_context_editor(cx) } fn open_saved_context( &self, workspace: &mut Workspace, path: PathBuf, window: &mut Window, cx: &mut Context, ) -> Task> { let Some(panel) = workspace.panel::(cx) else { return Task::ready(Err(anyhow!("no Assistant panel found"))); }; panel.update(cx, |panel, cx| panel.open_saved_context(path, window, cx)) } fn open_remote_context( &self, workspace: &mut Workspace, context_id: ContextId, window: &mut Window, cx: &mut Context, ) -> Task>> { let Some(panel) = workspace.panel::(cx) else { return Task::ready(Err(anyhow!("no Assistant panel found"))); }; panel.update(cx, |panel, cx| { panel.open_remote_context(context_id, window, cx) }) } fn quote_selection( &self, workspace: &mut Workspace, creases: Vec<(String, String)>, window: &mut Window, cx: &mut Context, ) { let Some(panel) = workspace.panel::(cx) else { return; }; if !panel.focus_handle(cx).contains_focused(window, cx) { workspace.toggle_panel_focus::(window, cx); } panel.update(cx, |_, cx| { // Wait to create a new context until the workspace is no longer // being updated. cx.defer_in(window, move |panel, window, cx| { if let Some(context) = panel .active_context_editor(cx) .or_else(|| panel.new_context(window, cx)) { context.update(cx, |context, cx| context.quote_creases(creases, window, cx)); }; }); }); } } #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum WorkflowAssistStatus { Pending, Confirmed, Done, Idle, }