We finally have a cancellation mechanism to use! Made the non side-effectful handlers stop their work if we get a cancel request notification. Release Notes: - N/A
304 lines
9.6 KiB
Rust
304 lines
9.6 KiB
Rust
use acp_thread::AgentSessionModes;
|
|
use agent_client_protocol::schema::v1 as acp;
|
|
use agent_servers::AgentServer;
|
|
|
|
use fs::Fs;
|
|
use gpui::{Context, Entity, WeakEntity, Window, prelude::*};
|
|
|
|
use std::{rc::Rc, sync::Arc};
|
|
use ui::{
|
|
Button, ContextMenu, ContextMenuEntry, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip,
|
|
prelude::*,
|
|
};
|
|
|
|
use crate::{CycleModeSelector, ToggleProfileSelector, ui::documentation_aside_side};
|
|
|
|
pub struct ModeSelector {
|
|
connection: Rc<dyn AgentSessionModes>,
|
|
agent_server: Rc<dyn AgentServer>,
|
|
menu_handle: PopoverMenuHandle<ContextMenu>,
|
|
fs: Arc<dyn Fs>,
|
|
setting_mode: bool,
|
|
}
|
|
|
|
impl ModeSelector {
|
|
pub fn new(
|
|
session_modes: Rc<dyn AgentSessionModes>,
|
|
agent_server: Rc<dyn AgentServer>,
|
|
fs: Arc<dyn Fs>,
|
|
) -> Self {
|
|
Self {
|
|
connection: session_modes,
|
|
agent_server,
|
|
menu_handle: PopoverMenuHandle::default(),
|
|
fs,
|
|
setting_mode: false,
|
|
}
|
|
}
|
|
|
|
pub fn menu_handle(&self) -> PopoverMenuHandle<ContextMenu> {
|
|
self.menu_handle.clone()
|
|
}
|
|
|
|
pub fn cycle_mode(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
|
let all_modes = self.connection.all_modes();
|
|
if all_modes.is_empty() {
|
|
return;
|
|
}
|
|
|
|
let current_mode = self.connection.current_mode();
|
|
|
|
let current_index = all_modes
|
|
.iter()
|
|
.position(|mode| mode.id.0 == current_mode.0)
|
|
.unwrap_or(0);
|
|
|
|
if let Some(next_mode) = all_modes.get((current_index + 1) % all_modes.len()) {
|
|
self.set_mode(next_mode.id.clone(), cx);
|
|
}
|
|
}
|
|
|
|
pub fn mode(&self) -> acp::SessionModeId {
|
|
self.connection.current_mode()
|
|
}
|
|
|
|
pub fn set_mode(&mut self, mode: acp::SessionModeId, cx: &mut Context<Self>) {
|
|
self.agent_server
|
|
.set_default_mode(Some(mode.clone()), self.fs.clone(), cx);
|
|
|
|
let task = self.connection.set_mode(mode, cx);
|
|
self.setting_mode = true;
|
|
cx.notify();
|
|
|
|
cx.spawn(async move |this: WeakEntity<ModeSelector>, cx| {
|
|
if let Err(err) = task.await {
|
|
log::error!("Failed to set session mode: {:?}", err);
|
|
}
|
|
this.update(cx, |this, cx| {
|
|
this.setting_mode = false;
|
|
cx.notify();
|
|
})
|
|
.ok();
|
|
})
|
|
.detach();
|
|
}
|
|
|
|
fn build_context_menu(
|
|
&self,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Entity<ContextMenu> {
|
|
let weak_self = cx.weak_entity();
|
|
|
|
ContextMenu::build(window, cx, move |mut menu, _window, cx| {
|
|
let all_modes = self.connection.all_modes();
|
|
let current_mode = self.connection.current_mode();
|
|
|
|
let side = documentation_aside_side(cx);
|
|
|
|
for mode in all_modes {
|
|
let is_selected = &mode.id == ¤t_mode;
|
|
let entry = ContextMenuEntry::new(mode.name.clone())
|
|
.toggleable(IconPosition::End, is_selected);
|
|
|
|
let entry = if let Some(description) = &mode.description {
|
|
entry.documentation_aside(side, {
|
|
let description = description.clone();
|
|
|
|
move |_| Label::new(description.clone()).into_any_element()
|
|
})
|
|
} else {
|
|
entry
|
|
};
|
|
|
|
menu.push_item(entry.handler({
|
|
let mode_id = mode.id.clone();
|
|
let weak_self = weak_self.clone();
|
|
move |_window, cx| {
|
|
weak_self
|
|
.update(cx, |this, cx| {
|
|
this.set_mode(mode_id.clone(), cx);
|
|
})
|
|
.ok();
|
|
}
|
|
}));
|
|
}
|
|
|
|
menu.key_context("ModeSelector")
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Render for ModeSelector {
|
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let current_mode_id = self.connection.current_mode();
|
|
let current_mode_name = self
|
|
.connection
|
|
.all_modes()
|
|
.iter()
|
|
.find(|mode| mode.id == current_mode_id)
|
|
.map(|mode| mode.name.clone())
|
|
.unwrap_or_else(|| "Unknown".into());
|
|
|
|
let this = cx.weak_entity();
|
|
|
|
let icon = if self.menu_handle.is_deployed() {
|
|
IconName::ChevronUp
|
|
} else {
|
|
IconName::ChevronDown
|
|
};
|
|
|
|
let trigger_button = Button::new("mode-selector-trigger", current_mode_name)
|
|
.label_size(LabelSize::Small)
|
|
.color(Color::Muted)
|
|
.end_icon(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
|
|
.disabled(self.setting_mode);
|
|
|
|
PopoverMenu::new("mode-selector")
|
|
.trigger_with_tooltip(
|
|
trigger_button,
|
|
Tooltip::element({
|
|
move |_window, cx| {
|
|
v_flex()
|
|
.gap_1()
|
|
.child(
|
|
h_flex()
|
|
.gap_2()
|
|
.justify_between()
|
|
.child(Label::new("Change Mode"))
|
|
.child(KeyBinding::for_action(&ToggleProfileSelector, cx)),
|
|
)
|
|
.child(
|
|
h_flex()
|
|
.pt_1()
|
|
.gap_2()
|
|
.border_t_1()
|
|
.border_color(cx.theme().colors().border_variant)
|
|
.justify_between()
|
|
.child(Label::new("Cycle Through Modes"))
|
|
.child(KeyBinding::for_action(&CycleModeSelector, cx)),
|
|
)
|
|
.into_any()
|
|
}
|
|
}),
|
|
)
|
|
.anchor(gpui::Anchor::BottomRight)
|
|
.with_handle(self.menu_handle.clone())
|
|
.offset(gpui::Point {
|
|
x: px(0.0),
|
|
y: px(-2.0),
|
|
})
|
|
.menu(move |window, cx| {
|
|
this.update(cx, |this, cx| this.build_context_menu(window, cx))
|
|
.ok()
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use acp_thread::AgentConnection;
|
|
use fs::FakeFs;
|
|
use gpui::{App, Task, TestAppContext};
|
|
use parking_lot::Mutex;
|
|
use project::{AgentId, Project};
|
|
use std::{any::Any, cell::RefCell};
|
|
|
|
#[gpui::test]
|
|
fn setting_mode_saves_selected_mode_as_default(cx: &mut TestAppContext) {
|
|
let agent_server = Rc::new(TestAgentServer::default());
|
|
let session_modes = Rc::new(TestSessionModes::new());
|
|
let fs: Arc<dyn Fs> = FakeFs::new(cx.executor());
|
|
|
|
cx.update(|cx| {
|
|
let session_modes: Rc<dyn AgentSessionModes> = session_modes.clone();
|
|
let agent_server: Rc<dyn AgentServer> = agent_server.clone();
|
|
let selector = cx.new(|_| ModeSelector::new(session_modes, agent_server, fs));
|
|
|
|
selector.update(cx, |selector, cx| {
|
|
selector.set_mode(acp::SessionModeId::new("manual"), cx);
|
|
});
|
|
});
|
|
|
|
assert_eq!(
|
|
agent_server.saved_defaults.lock().as_slice(),
|
|
&[Some(acp::SessionModeId::new("manual"))]
|
|
);
|
|
assert_eq!(
|
|
session_modes.set_modes.borrow().as_slice(),
|
|
&[acp::SessionModeId::new("manual")]
|
|
);
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct TestAgentServer {
|
|
saved_defaults: Arc<Mutex<Vec<Option<acp::SessionModeId>>>>,
|
|
}
|
|
|
|
impl AgentServer for TestAgentServer {
|
|
fn logo(&self) -> IconName {
|
|
IconName::ZedAssistant
|
|
}
|
|
|
|
fn agent_id(&self) -> AgentId {
|
|
AgentId::new("test-agent")
|
|
}
|
|
|
|
fn connect(
|
|
&self,
|
|
_delegate: agent_servers::AgentServerDelegate,
|
|
_project: Entity<Project>,
|
|
_cx: &mut App,
|
|
) -> Task<anyhow::Result<Rc<dyn AgentConnection>>> {
|
|
Task::ready(Err(anyhow::anyhow!("test agent server cannot connect")))
|
|
}
|
|
|
|
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
|
|
self
|
|
}
|
|
|
|
fn set_default_mode(
|
|
&self,
|
|
mode_id: Option<acp::SessionModeId>,
|
|
_fs: Arc<dyn Fs>,
|
|
_cx: &mut App,
|
|
) {
|
|
self.saved_defaults.lock().push(mode_id);
|
|
}
|
|
}
|
|
|
|
struct TestSessionModes {
|
|
current_mode: RefCell<acp::SessionModeId>,
|
|
set_modes: RefCell<Vec<acp::SessionModeId>>,
|
|
}
|
|
|
|
impl TestSessionModes {
|
|
fn new() -> Self {
|
|
Self {
|
|
current_mode: RefCell::new(acp::SessionModeId::new("auto")),
|
|
set_modes: RefCell::new(Vec::new()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl AgentSessionModes for TestSessionModes {
|
|
fn current_mode(&self) -> acp::SessionModeId {
|
|
self.current_mode.borrow().clone()
|
|
}
|
|
|
|
fn all_modes(&self) -> Vec<acp::SessionMode> {
|
|
vec![
|
|
acp::SessionMode::new("auto", "Auto"),
|
|
acp::SessionMode::new("manual", "Manual"),
|
|
]
|
|
}
|
|
|
|
fn set_mode(&self, mode: acp::SessionModeId, _cx: &mut App) -> Task<anyhow::Result<()>> {
|
|
*self.current_mode.borrow_mut() = mode.clone();
|
|
self.set_modes.borrow_mut().push(mode);
|
|
Task::ready(Ok(()))
|
|
}
|
|
}
|
|
}
|