Files
oak-gpui/crates/assistant/src/assistant_panel.rs
T
Agus ZubiagaandAntonio Scandurra 47a89ad243 assistant: Edit files tool (#26506)
Exposes a new "edit files" tool that the model can use to apply
modifications to files in the project. The main model provides
instructions and the tool uses a separate "editor" model (Claude 3.5 by
default) to generate search/replace blocks like Aider does:

````markdown
mathweb/flask/app.py
```python
<<<<<<< SEARCH
from flask import Flask
=======
import math
from flask import Flask
>>>>>>> REPLACE
```
````

The search/replace blocks are parsed and applied as they stream in. If a
block fails to parse, the tool will apply the other edits and report an
error pointing to the part of the input where it occurred. This should
allow the model to fix it.


Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-03-12 12:30:47 +00:00

1438 lines
50 KiB
Rust

use crate::assistant_configuration::{ConfigurationView, ConfigurationViewEvent};
use crate::{
terminal_inline_assistant::TerminalInlineAssistant, DeployHistory, InlineAssistant, NewChat,
};
use anyhow::{anyhow, Result};
use assistant_context_editor::{
make_lsp_adapter_delegate, AssistantContext, AssistantPanelDelegate, ContextEditor,
ContextEditorToolbarItem, ContextEditorToolbarItemEvent, ContextHistory, ContextId,
ContextStore, ContextStoreEvent, InsertDraggedFiles, SlashCommandCompletionProvider,
DEFAULT_TAB_TITLE,
};
use assistant_settings::{AssistantDockPosition, AssistantSettings};
use assistant_slash_command::SlashCommandWorkingSet;
use client::{proto, Client, Status};
use editor::{Editor, EditorEvent};
use fs::Fs;
use gpui::{
prelude::*, Action, App, AsyncWindowContext, Entity, EventEmitter, ExternalPaths, FocusHandle,
Focusable, InteractiveElement, IntoElement, ParentElement, Pixels, Render, Styled,
Subscription, Task, UpdateGlobal, WeakEntity,
};
use language::LanguageRegistry;
use language_model::{
AuthenticateError, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
};
use project::Project;
use prompt_library::{open_prompt_library, PromptLibrary};
use prompt_store::PromptBuilder;
use search::{buffer_search::DivRegistrar, BufferSearchBar};
use settings::{update_settings_file, Settings};
use smol::stream::StreamExt;
use std::{ops::ControlFlow, path::PathBuf, sync::Arc};
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
use ui::{prelude::*, ContextMenu, PopoverMenu, Tooltip};
use util::{maybe, ResultExt};
use workspace::DraggedTab;
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
pane, DraggedSelection, Pane, ShowConfiguration, ToggleZoom, Workspace,
};
use zed_actions::assistant::{DeployPromptLibrary, InlineAssist, ToggleFocus};
pub fn init(cx: &mut App) {
workspace::FollowableViewRegistry::register::<ContextEditor>(cx);
cx.observe_new(
|workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
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<TerminalPanel>| {
let settings = AssistantSettings::get_global(cx);
terminal_panel.set_assistant_enabled(settings.enabled, cx);
},
)
.detach();
}
pub enum AssistantPanelEvent {
ContextEdited,
}
pub struct AssistantPanel {
pane: Entity<Pane>,
workspace: WeakEntity<Workspace>,
width: Option<Pixels>,
height: Option<Pixels>,
project: Entity<Project>,
context_store: Entity<ContextStore>,
languages: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
subscriptions: Vec<Subscription>,
model_summary_editor: Entity<Editor>,
authenticate_provider_task: Option<(LanguageModelProviderId, Task<()>)>,
configuration_subscription: Option<Subscription>,
client_status: Option<client::Status>,
watch_client_status: Option<Task<()>>,
pub(crate) show_zed_ai_notice: bool,
}
enum InlineAssistTarget {
Editor(Entity<Editor>, bool),
Terminal(Entity<TerminalView>),
}
impl AssistantPanel {
pub fn load(
workspace: WeakEntity<Workspace>,
prompt_builder: Arc<PromptBuilder>,
cx: AsyncWindowContext,
) -> Task<Result<Entity<Self>>> {
cx.spawn(|mut cx| async move {
let slash_commands = Arc::new(SlashCommandWorkingSet::default());
let context_store = workspace
.update(&mut cx, |workspace, cx| {
let project = workspace.project().clone();
ContextStore::new(project, prompt_builder.clone(), slash_commands, cx)
})?
.await?;
workspace.update_in(&mut cx, |workspace, window, cx| {
// TODO: deserialize state.
cx.new(|cx| Self::new(workspace, context_store, window, cx))
})
})
}
fn new(
workspace: &Workspace,
context_store: Entity<ContextStore>,
window: &mut Window,
cx: &mut Context<Self>,
) -> 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::<ExternalPaths>() {
return Some(InsertDraggedFiles::ExternalFiles(paths.paths().to_vec()));
}
}
let project_paths = if let Some(tab) = dropped_item.downcast_ref::<DraggedTab>()
{
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::<Vec<_>>(),
)
} else if let Some(selection) = dropped_item.downcast_ref::<DraggedSelection>()
{
Some(
selection
.items()
.filter_map(|item| {
project.read(cx).path_for_entry(item.entry_id, cx)
})
.collect::<Vec<_>>(),
)
} 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::<Vec<_>>();
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::<ContextHistory>().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(DeployPromptLibrary))
.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::ActiveModelChanged
| language_model::Event::EditorModelChanged => {
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<Workspace>,
) {
let settings = AssistantSettings::get_global(cx);
if !settings.enabled {
return;
}
workspace.toggle_panel_focus::<Self>(window, cx);
}
fn watch_client_status(
client: Arc<Client>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<()> {
let mut status_rx = client.status();
cx.spawn_in(window, |this, mut cx| async move {
while let Some(status) = status_rx.next().await {
this.update(&mut 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(&mut cx, |this, _cx| this.watch_client_status = None)
.log_err();
})
}
fn handle_pane_event(
&mut self,
pane: &Entity<Pane>,
event: &pane::Event,
window: &mut Window,
cx: &mut Context<Self>,
) {
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::<ConfigurationView>()
.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<Editor>,
event: &EditorEvent,
cx: &mut Context<Self>,
) {
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<Self>) {
let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
// 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()
&& active_provider.map_or(true, |provider| 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<ContextEditorToolbarItem>,
_: &ContextEditorToolbarItemEvent,
cx: &mut Context<Self>,
) {
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<ContextStore>,
event: &ContextStoreEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
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<Self>) {
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)
.active_provider()
.map(|p| p.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<Self>) {
if self.is_authenticated(cx) {
return;
}
let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() 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, |this, mut cx| async move {
if let Some(future) = load_credentials {
let _ = future.await;
}
this.update(&mut 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<Workspace>,
) {
let settings = AssistantSettings::get_global(cx);
if !settings.enabled {
return;
}
let Some(assistant_panel) = workspace.panel::<AssistantPanel>(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, |workspace, mut cx| async move {
let Some(task) =
assistant_panel.update(&mut 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(&mut 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(&mut cx, |workspace, window, cx| {
workspace.focus_panel::<AssistantPanel>(window, cx)
})?;
}
anyhow::Ok(())
})
.detach_and_log_err(cx)
}
}
fn resolve_inline_assist_target(
workspace: &mut Workspace,
assistant_panel: &Entity<AssistantPanel>,
window: &mut Window,
cx: &mut App,
) -> Option<InlineAssistTarget> {
if let Some(terminal_panel) = workspace.panel::<TerminalPanel>(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::<TerminalView>())
}) {
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::<Editor>(cx))
{
Some(InlineAssistTarget::Editor(workspace_editor, true))
} else if let Some(terminal_view) = workspace
.active_item(cx)
.and_then(|item| item.act_as::<TerminalView>(cx))
{
Some(InlineAssistTarget::Terminal(terminal_view))
} else {
None
}
}
pub fn create_new_context(
workspace: &mut Workspace,
_: &NewChat,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
if let Some(panel) = workspace.panel::<AssistantPanel>(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<Self>,
) -> Option<Entity<ContextEditor>> {
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, |this, mut cx| async move {
let context = task.await?;
this.update_in(&mut 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, move |_, mut cx| async move {
workspace
.update_in(&mut cx, |workspace, window, cx| {
workspace.focus_panel::<AssistantPanel>(window, cx);
})
.ok();
})
.detach();
Some(editor)
}
}
fn show_context(
&mut self,
context_editor: Entity<ContextEditor>,
window: &mut Window,
cx: &mut Context<Self>,
) {
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<ContextEditor>,
window: &mut Window,
cx: &mut Context<Self>,
) {
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<ContextEditor>,
event: &EditorEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
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<Workspace>,
) {
let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
return;
};
if !panel.focus_handle(cx).contains_focused(window, cx) {
workspace.toggle_panel_focus::<AssistantPanel>(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<Self>) {
let configuration_item_ix = self
.pane
.read(cx)
.items()
.position(|item| item.downcast::<ConfigurationView>().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)
.active_provider()
.map_or(true, |p| p.id() != provider.id())
{
if let Some(model) = provider.default_model(cx) {
update_settings_file::<AssistantSettings>(
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<Self>) {
let history_item_ix = self
.pane
.read(cx)
.items()
.position(|item| item.downcast::<ContextHistory>().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,
_: &DeployPromptLibrary,
_window: &mut Window,
cx: &mut Context<Self>,
) {
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<Entity<ContextEditor>> {
self.pane
.read(cx)
.active_item()?
.downcast::<ContextEditor>()
}
pub fn active_context(&self, cx: &App) -> Option<Entity<AssistantContext>> {
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<Self>,
) -> Task<Result<()>> {
let existing_context = self.pane.read(cx).items().find_map(|item| {
item.downcast::<ContextEditor>()
.filter(|editor| editor.read(cx).context().read(cx).path() == Some(&path))
});
if let Some(existing_context) = existing_context {
return cx.spawn_in(window, |this, mut cx| async move {
this.update_in(&mut 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, |this, mut cx| async move {
let context = context.await?;
this.update_in(&mut 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<Self>,
) -> Task<Result<Entity<ContextEditor>>> {
let existing_context = self.pane.read(cx).items().find_map(|item| {
item.downcast::<ContextEditor>()
.filter(|editor| *editor.read(cx).context().read(cx).id() == id)
});
if let Some(existing_context) = existing_context {
return cx.spawn_in(window, |this, mut cx| async move {
this.update_in(&mut 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, |this, mut cx| async move {
let context = context.await?;
this.update_in(&mut 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<Self>) -> bool {
LanguageModelRegistry::read_global(cx)
.active_provider()
.map_or(false, |provider| provider.is_authenticated(cx))
}
fn authenticate(
&mut self,
cx: &mut Context<Self>,
) -> Option<Task<Result<(), AuthenticateError>>> {
LanguageModelRegistry::read_global(cx)
.active_provider()
.map_or(None, |provider| Some(provider.authenticate(cx)))
}
fn restart_context_servers(
workspace: &mut Workspace,
_action: &context_server::Restart,
_: &mut Window,
cx: &mut Context<Workspace>,
) {
let Some(assistant_panel) = workspace.panel::<AssistantPanel>(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<Self>) -> impl IntoElement {
let mut registrar = DivRegistrar::new(
|panel, _, cx| {
panel
.pane
.read(cx)
.toolbar()
.read(cx)
.item_of_type::<BufferSearchBar>()
},
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<Self>) {
settings::update_settings_file::<AssistantSettings>(
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<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
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>) {
self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx));
}
fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
if active {
if self.pane.read(cx).items_len() == 0 {
self.new_context(window, cx);
}
self.ensure_authenticated(window, cx);
}
}
fn pane(&self) -> Option<Entity<Pane>> {
Some(self.pane.clone())
}
fn remote_id() -> Option<proto::PanelId> {
Some(proto::PanelId::AssistantPanel)
}
fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
let settings = AssistantSettings::get_global(cx);
if !settings.enabled || !settings.button {
return None;
}
Some(IconName::ZedAssistant)
}
fn icon_tooltip(&self, _: &Window, _: &App) -> Option<&'static str> {
Some("Assistant Panel")
}
fn toggle_action(&self) -> Box<dyn Action> {
Box::new(ToggleFocus)
}
fn activation_priority(&self) -> u32 {
4
}
}
impl EventEmitter<PanelEvent> for AssistantPanel {}
impl EventEmitter<AssistantPanelEvent> 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<Editor>,
initial_prompt: Option<String>,
window: &mut Window,
cx: &mut Context<PromptLibrary>,
) {
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<Workspace>,
) -> bool {
workspace
.focus_panel::<AssistantPanel>(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<Workspace>,
) -> Option<Entity<ContextEditor>> {
let panel = workspace.panel::<AssistantPanel>(cx)?;
panel.read(cx).active_context_editor(cx)
}
fn open_saved_context(
&self,
workspace: &mut Workspace,
path: PathBuf,
window: &mut Window,
cx: &mut Context<Workspace>,
) -> Task<Result<()>> {
let Some(panel) = workspace.panel::<AssistantPanel>(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<Workspace>,
) -> Task<Result<Entity<ContextEditor>>> {
let Some(panel) = workspace.panel::<AssistantPanel>(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<Workspace>,
) {
let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
return;
};
if !panel.focus_handle(cx).contains_focused(window, cx) {
workspace.toggle_panel_focus::<AssistantPanel>(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,
}