ACP debug tools pane (#36768)
Adds a new "acp: open debug tools" action that opens a new workspace item with a log of ACP messages for the active connection. Release Notes: - N/A
This commit is contained in:
parent
72bd248544
commit
18ac4ac5ef
12 changed files with 574 additions and 17 deletions
27
Cargo.lock
generated
27
Cargo.lock
generated
|
@ -39,6 +39,26 @@ dependencies = [
|
||||||
"workspace-hack",
|
"workspace-hack",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "acp_tools"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"agent-client-protocol",
|
||||||
|
"collections",
|
||||||
|
"gpui",
|
||||||
|
"language",
|
||||||
|
"markdown",
|
||||||
|
"project",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"settings",
|
||||||
|
"theme",
|
||||||
|
"ui",
|
||||||
|
"util",
|
||||||
|
"workspace",
|
||||||
|
"workspace-hack",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "action_log"
|
name = "action_log"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -171,11 +191,12 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "agent-client-protocol"
|
name = "agent-client-protocol"
|
||||||
version = "0.0.30"
|
version = "0.0.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5f792e009ba59b137ee1db560bc37e567887ad4b5af6f32181d381fff690e2d4"
|
checksum = "289eb34ee17213dadcca47eedadd386a5e7678094095414e475965d1bcca2860"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"async-broadcast",
|
||||||
"futures 0.3.31",
|
"futures 0.3.31",
|
||||||
"log",
|
"log",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
|
@ -264,6 +285,7 @@ name = "agent_servers"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"acp_thread",
|
"acp_thread",
|
||||||
|
"acp_tools",
|
||||||
"action_log",
|
"action_log",
|
||||||
"agent-client-protocol",
|
"agent-client-protocol",
|
||||||
"agent_settings",
|
"agent_settings",
|
||||||
|
@ -20417,6 +20439,7 @@ dependencies = [
|
||||||
name = "zed"
|
name = "zed"
|
||||||
version = "0.202.0"
|
version = "0.202.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"acp_tools",
|
||||||
"activity_indicator",
|
"activity_indicator",
|
||||||
"agent",
|
"agent",
|
||||||
"agent_servers",
|
"agent_servers",
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
|
"crates/acp_tools",
|
||||||
"crates/acp_thread",
|
"crates/acp_thread",
|
||||||
"crates/action_log",
|
"crates/action_log",
|
||||||
"crates/activity_indicator",
|
"crates/activity_indicator",
|
||||||
|
@ -227,6 +228,7 @@ edition = "2024"
|
||||||
# Workspace member crates
|
# Workspace member crates
|
||||||
#
|
#
|
||||||
|
|
||||||
|
acp_tools = { path = "crates/acp_tools" }
|
||||||
acp_thread = { path = "crates/acp_thread" }
|
acp_thread = { path = "crates/acp_thread" }
|
||||||
action_log = { path = "crates/action_log" }
|
action_log = { path = "crates/action_log" }
|
||||||
agent = { path = "crates/agent" }
|
agent = { path = "crates/agent" }
|
||||||
|
@ -425,7 +427,7 @@ zlog_settings = { path = "crates/zlog_settings" }
|
||||||
#
|
#
|
||||||
|
|
||||||
agentic-coding-protocol = "0.0.10"
|
agentic-coding-protocol = "0.0.10"
|
||||||
agent-client-protocol = "0.0.30"
|
agent-client-protocol = "0.0.31"
|
||||||
aho-corasick = "1.1"
|
aho-corasick = "1.1"
|
||||||
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
|
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
|
||||||
any_vec = "0.14"
|
any_vec = "0.14"
|
||||||
|
|
30
crates/acp_tools/Cargo.toml
Normal file
30
crates/acp_tools/Cargo.toml
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
[package]
|
||||||
|
name = "acp_tools"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
|
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/acp_tools.rs"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
agent-client-protocol.workspace = true
|
||||||
|
collections.workspace = true
|
||||||
|
gpui.workspace = true
|
||||||
|
language.workspace= true
|
||||||
|
markdown.workspace = true
|
||||||
|
project.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
settings.workspace = true
|
||||||
|
theme.workspace = true
|
||||||
|
ui.workspace = true
|
||||||
|
util.workspace = true
|
||||||
|
workspace-hack.workspace = true
|
||||||
|
workspace.workspace = true
|
1
crates/acp_tools/LICENSE-GPL
Symbolic link
1
crates/acp_tools/LICENSE-GPL
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../LICENSE-GPL
|
494
crates/acp_tools/src/acp_tools.rs
Normal file
494
crates/acp_tools/src/acp_tools.rs
Normal file
|
@ -0,0 +1,494 @@
|
||||||
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
collections::HashSet,
|
||||||
|
fmt::Display,
|
||||||
|
rc::{Rc, Weak},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
use agent_client_protocol as acp;
|
||||||
|
use collections::HashMap;
|
||||||
|
use gpui::{
|
||||||
|
App, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global, ListAlignment, ListState,
|
||||||
|
StyleRefinement, Subscription, Task, TextStyleRefinement, Window, actions, list, prelude::*,
|
||||||
|
};
|
||||||
|
use language::LanguageRegistry;
|
||||||
|
use markdown::{CodeBlockRenderer, Markdown, MarkdownElement, MarkdownStyle};
|
||||||
|
use project::Project;
|
||||||
|
use settings::Settings;
|
||||||
|
use theme::ThemeSettings;
|
||||||
|
use ui::prelude::*;
|
||||||
|
use util::ResultExt as _;
|
||||||
|
use workspace::{Item, Workspace};
|
||||||
|
|
||||||
|
actions!(acp, [OpenDebugTools]);
|
||||||
|
|
||||||
|
pub fn init(cx: &mut App) {
|
||||||
|
cx.observe_new(
|
||||||
|
|workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
|
||||||
|
workspace.register_action(|workspace, _: &OpenDebugTools, window, cx| {
|
||||||
|
let acp_tools =
|
||||||
|
Box::new(cx.new(|cx| AcpTools::new(workspace.project().clone(), cx)));
|
||||||
|
workspace.add_item_to_active_pane(acp_tools, None, true, window, cx);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GlobalAcpConnectionRegistry(Entity<AcpConnectionRegistry>);
|
||||||
|
|
||||||
|
impl Global for GlobalAcpConnectionRegistry {}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct AcpConnectionRegistry {
|
||||||
|
active_connection: RefCell<Option<ActiveConnection>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ActiveConnection {
|
||||||
|
server_name: &'static str,
|
||||||
|
connection: Weak<acp::ClientSideConnection>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AcpConnectionRegistry {
|
||||||
|
pub fn default_global(cx: &mut App) -> Entity<Self> {
|
||||||
|
if cx.has_global::<GlobalAcpConnectionRegistry>() {
|
||||||
|
cx.global::<GlobalAcpConnectionRegistry>().0.clone()
|
||||||
|
} else {
|
||||||
|
let registry = cx.new(|_cx| AcpConnectionRegistry::default());
|
||||||
|
cx.set_global(GlobalAcpConnectionRegistry(registry.clone()));
|
||||||
|
registry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_active_connection(
|
||||||
|
&self,
|
||||||
|
server_name: &'static str,
|
||||||
|
connection: &Rc<acp::ClientSideConnection>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.active_connection.replace(Some(ActiveConnection {
|
||||||
|
server_name,
|
||||||
|
connection: Rc::downgrade(connection),
|
||||||
|
}));
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AcpTools {
|
||||||
|
project: Entity<Project>,
|
||||||
|
focus_handle: FocusHandle,
|
||||||
|
expanded: HashSet<usize>,
|
||||||
|
watched_connection: Option<WatchedConnection>,
|
||||||
|
connection_registry: Entity<AcpConnectionRegistry>,
|
||||||
|
_subscription: Subscription,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WatchedConnection {
|
||||||
|
server_name: &'static str,
|
||||||
|
messages: Vec<WatchedConnectionMessage>,
|
||||||
|
list_state: ListState,
|
||||||
|
connection: Weak<acp::ClientSideConnection>,
|
||||||
|
incoming_request_methods: HashMap<i32, Arc<str>>,
|
||||||
|
outgoing_request_methods: HashMap<i32, Arc<str>>,
|
||||||
|
_task: Task<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AcpTools {
|
||||||
|
fn new(project: Entity<Project>, cx: &mut Context<Self>) -> Self {
|
||||||
|
let connection_registry = AcpConnectionRegistry::default_global(cx);
|
||||||
|
|
||||||
|
let subscription = cx.observe(&connection_registry, |this, _, cx| {
|
||||||
|
this.update_connection(cx);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut this = Self {
|
||||||
|
project,
|
||||||
|
focus_handle: cx.focus_handle(),
|
||||||
|
expanded: HashSet::default(),
|
||||||
|
watched_connection: None,
|
||||||
|
connection_registry,
|
||||||
|
_subscription: subscription,
|
||||||
|
};
|
||||||
|
this.update_connection(cx);
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_connection(&mut self, cx: &mut Context<Self>) {
|
||||||
|
let active_connection = self.connection_registry.read(cx).active_connection.borrow();
|
||||||
|
let Some(active_connection) = active_connection.as_ref() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(watched_connection) = self.watched_connection.as_ref() {
|
||||||
|
if Weak::ptr_eq(
|
||||||
|
&watched_connection.connection,
|
||||||
|
&active_connection.connection,
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(connection) = active_connection.connection.upgrade() {
|
||||||
|
let mut receiver = connection.subscribe();
|
||||||
|
let task = cx.spawn(async move |this, cx| {
|
||||||
|
while let Ok(message) = receiver.recv().await {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.push_stream_message(message, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.watched_connection = Some(WatchedConnection {
|
||||||
|
server_name: active_connection.server_name,
|
||||||
|
messages: vec![],
|
||||||
|
list_state: ListState::new(0, ListAlignment::Bottom, px(2048.)),
|
||||||
|
connection: active_connection.connection.clone(),
|
||||||
|
incoming_request_methods: HashMap::default(),
|
||||||
|
outgoing_request_methods: HashMap::default(),
|
||||||
|
_task: task,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_stream_message(&mut self, stream_message: acp::StreamMessage, cx: &mut Context<Self>) {
|
||||||
|
let Some(connection) = self.watched_connection.as_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let language_registry = self.project.read(cx).languages().clone();
|
||||||
|
let index = connection.messages.len();
|
||||||
|
|
||||||
|
let (request_id, method, message_type, params) = match stream_message.message {
|
||||||
|
acp::StreamMessageContent::Request { id, method, params } => {
|
||||||
|
let method_map = match stream_message.direction {
|
||||||
|
acp::StreamMessageDirection::Incoming => {
|
||||||
|
&mut connection.incoming_request_methods
|
||||||
|
}
|
||||||
|
acp::StreamMessageDirection::Outgoing => {
|
||||||
|
&mut connection.outgoing_request_methods
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
method_map.insert(id, method.clone());
|
||||||
|
(Some(id), method.into(), MessageType::Request, Ok(params))
|
||||||
|
}
|
||||||
|
acp::StreamMessageContent::Response { id, result } => {
|
||||||
|
let method_map = match stream_message.direction {
|
||||||
|
acp::StreamMessageDirection::Incoming => {
|
||||||
|
&mut connection.outgoing_request_methods
|
||||||
|
}
|
||||||
|
acp::StreamMessageDirection::Outgoing => {
|
||||||
|
&mut connection.incoming_request_methods
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(method) = method_map.remove(&id) {
|
||||||
|
(Some(id), method.into(), MessageType::Response, result)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
Some(id),
|
||||||
|
"[unrecognized response]".into(),
|
||||||
|
MessageType::Response,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
acp::StreamMessageContent::Notification { method, params } => {
|
||||||
|
(None, method.into(), MessageType::Notification, Ok(params))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let message = WatchedConnectionMessage {
|
||||||
|
name: method,
|
||||||
|
message_type,
|
||||||
|
request_id,
|
||||||
|
direction: stream_message.direction,
|
||||||
|
collapsed_params_md: match params.as_ref() {
|
||||||
|
Ok(params) => params
|
||||||
|
.as_ref()
|
||||||
|
.map(|params| collapsed_params_md(params, &language_registry, cx)),
|
||||||
|
Err(err) => {
|
||||||
|
if let Ok(err) = &serde_json::to_value(err) {
|
||||||
|
Some(collapsed_params_md(&err, &language_registry, cx))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
expanded_params_md: None,
|
||||||
|
params,
|
||||||
|
};
|
||||||
|
|
||||||
|
connection.messages.push(message);
|
||||||
|
connection.list_state.splice(index..index, 1);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_message(
|
||||||
|
&mut self,
|
||||||
|
index: usize,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> AnyElement {
|
||||||
|
let Some(connection) = self.watched_connection.as_ref() else {
|
||||||
|
return Empty.into_any();
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(message) = connection.messages.get(index) else {
|
||||||
|
return Empty.into_any();
|
||||||
|
};
|
||||||
|
|
||||||
|
let base_size = TextSize::Editor.rems(cx);
|
||||||
|
|
||||||
|
let theme_settings = ThemeSettings::get_global(cx);
|
||||||
|
let text_style = window.text_style();
|
||||||
|
|
||||||
|
let colors = cx.theme().colors();
|
||||||
|
let expanded = self.expanded.contains(&index);
|
||||||
|
|
||||||
|
v_flex()
|
||||||
|
.w_full()
|
||||||
|
.px_4()
|
||||||
|
.py_3()
|
||||||
|
.border_color(colors.border)
|
||||||
|
.border_b_1()
|
||||||
|
.gap_2()
|
||||||
|
.items_start()
|
||||||
|
.font_buffer(cx)
|
||||||
|
.text_size(base_size)
|
||||||
|
.id(index)
|
||||||
|
.group("message")
|
||||||
|
.hover(|this| this.bg(colors.element_background.opacity(0.5)))
|
||||||
|
.on_click(cx.listener(move |this, _, _, cx| {
|
||||||
|
if this.expanded.contains(&index) {
|
||||||
|
this.expanded.remove(&index);
|
||||||
|
} else {
|
||||||
|
this.expanded.insert(index);
|
||||||
|
let Some(connection) = &mut this.watched_connection else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(message) = connection.messages.get_mut(index) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
message.expanded(this.project.read(cx).languages().clone(), cx);
|
||||||
|
connection.list_state.scroll_to_reveal_item(index);
|
||||||
|
}
|
||||||
|
cx.notify()
|
||||||
|
}))
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.w_full()
|
||||||
|
.gap_2()
|
||||||
|
.items_center()
|
||||||
|
.flex_shrink_0()
|
||||||
|
.child(match message.direction {
|
||||||
|
acp::StreamMessageDirection::Incoming => {
|
||||||
|
ui::Icon::new(ui::IconName::ArrowDown).color(Color::Error)
|
||||||
|
}
|
||||||
|
acp::StreamMessageDirection::Outgoing => {
|
||||||
|
ui::Icon::new(ui::IconName::ArrowUp).color(Color::Success)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
Label::new(message.name.clone())
|
||||||
|
.buffer_font(cx)
|
||||||
|
.color(Color::Muted),
|
||||||
|
)
|
||||||
|
.child(div().flex_1())
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.child(ui::Chip::new(message.message_type.to_string()))
|
||||||
|
.visible_on_hover("message"),
|
||||||
|
)
|
||||||
|
.children(
|
||||||
|
message
|
||||||
|
.request_id
|
||||||
|
.map(|req_id| div().child(ui::Chip::new(req_id.to_string()))),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
// I'm aware using markdown is a hack. Trying to get something working for the demo.
|
||||||
|
// Will clean up soon!
|
||||||
|
.when_some(
|
||||||
|
if expanded {
|
||||||
|
message.expanded_params_md.clone()
|
||||||
|
} else {
|
||||||
|
message.collapsed_params_md.clone()
|
||||||
|
},
|
||||||
|
|this, params| {
|
||||||
|
this.child(
|
||||||
|
div().pl_6().w_full().child(
|
||||||
|
MarkdownElement::new(
|
||||||
|
params,
|
||||||
|
MarkdownStyle {
|
||||||
|
base_text_style: text_style,
|
||||||
|
selection_background_color: colors.element_selection_background,
|
||||||
|
syntax: cx.theme().syntax().clone(),
|
||||||
|
code_block_overflow_x_scroll: true,
|
||||||
|
code_block: StyleRefinement {
|
||||||
|
text: Some(TextStyleRefinement {
|
||||||
|
font_family: Some(
|
||||||
|
theme_settings.buffer_font.family.clone(),
|
||||||
|
),
|
||||||
|
font_size: Some((base_size * 0.8).into()),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.code_block_renderer(
|
||||||
|
CodeBlockRenderer::Default {
|
||||||
|
copy_button: false,
|
||||||
|
copy_button_on_hover: expanded,
|
||||||
|
border: false,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WatchedConnectionMessage {
|
||||||
|
name: SharedString,
|
||||||
|
request_id: Option<i32>,
|
||||||
|
direction: acp::StreamMessageDirection,
|
||||||
|
message_type: MessageType,
|
||||||
|
params: Result<Option<serde_json::Value>, acp::Error>,
|
||||||
|
collapsed_params_md: Option<Entity<Markdown>>,
|
||||||
|
expanded_params_md: Option<Entity<Markdown>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WatchedConnectionMessage {
|
||||||
|
fn expanded(&mut self, language_registry: Arc<LanguageRegistry>, cx: &mut App) {
|
||||||
|
let params_md = match &self.params {
|
||||||
|
Ok(Some(params)) => Some(expanded_params_md(params, &language_registry, cx)),
|
||||||
|
Err(err) => {
|
||||||
|
if let Some(err) = &serde_json::to_value(err).log_err() {
|
||||||
|
Some(expanded_params_md(&err, &language_registry, cx))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
self.expanded_params_md = params_md;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collapsed_params_md(
|
||||||
|
params: &serde_json::Value,
|
||||||
|
language_registry: &Arc<LanguageRegistry>,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Entity<Markdown> {
|
||||||
|
let params_json = serde_json::to_string(params).unwrap_or_default();
|
||||||
|
let mut spaced_out_json = String::with_capacity(params_json.len() + params_json.len() / 4);
|
||||||
|
|
||||||
|
for ch in params_json.chars() {
|
||||||
|
match ch {
|
||||||
|
'{' => spaced_out_json.push_str("{ "),
|
||||||
|
'}' => spaced_out_json.push_str(" }"),
|
||||||
|
':' => spaced_out_json.push_str(": "),
|
||||||
|
',' => spaced_out_json.push_str(", "),
|
||||||
|
c => spaced_out_json.push(c),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let params_md = format!("```json\n{}\n```", spaced_out_json);
|
||||||
|
cx.new(|cx| Markdown::new(params_md.into(), Some(language_registry.clone()), None, cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expanded_params_md(
|
||||||
|
params: &serde_json::Value,
|
||||||
|
language_registry: &Arc<LanguageRegistry>,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Entity<Markdown> {
|
||||||
|
let params_json = serde_json::to_string_pretty(params).unwrap_or_default();
|
||||||
|
let params_md = format!("```json\n{}\n```", params_json);
|
||||||
|
cx.new(|cx| Markdown::new(params_md.into(), Some(language_registry.clone()), None, cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
enum MessageType {
|
||||||
|
Request,
|
||||||
|
Response,
|
||||||
|
Notification,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for MessageType {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
MessageType::Request => write!(f, "Request"),
|
||||||
|
MessageType::Response => write!(f, "Response"),
|
||||||
|
MessageType::Notification => write!(f, "Notification"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AcpToolsEvent {}
|
||||||
|
|
||||||
|
impl EventEmitter<AcpToolsEvent> for AcpTools {}
|
||||||
|
|
||||||
|
impl Item for AcpTools {
|
||||||
|
type Event = AcpToolsEvent;
|
||||||
|
|
||||||
|
fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
|
||||||
|
format!(
|
||||||
|
"ACP: {}",
|
||||||
|
self.watched_connection
|
||||||
|
.as_ref()
|
||||||
|
.map_or("Disconnected", |connection| connection.server_name)
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
|
||||||
|
Some(ui::Icon::new(IconName::Thread))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Focusable for AcpTools {
|
||||||
|
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||||
|
self.focus_handle.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for AcpTools {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
v_flex()
|
||||||
|
.track_focus(&self.focus_handle)
|
||||||
|
.size_full()
|
||||||
|
.bg(cx.theme().colors().editor_background)
|
||||||
|
.child(match self.watched_connection.as_ref() {
|
||||||
|
Some(connection) => {
|
||||||
|
if connection.messages.is_empty() {
|
||||||
|
h_flex()
|
||||||
|
.size_full()
|
||||||
|
.justify_center()
|
||||||
|
.items_center()
|
||||||
|
.child("No messages recorded yet")
|
||||||
|
.into_any()
|
||||||
|
} else {
|
||||||
|
list(
|
||||||
|
connection.list_state.clone(),
|
||||||
|
cx.processor(Self::render_message),
|
||||||
|
)
|
||||||
|
.with_sizing_behavior(gpui::ListSizingBehavior::Auto)
|
||||||
|
.flex_grow()
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => h_flex()
|
||||||
|
.size_full()
|
||||||
|
.justify_center()
|
||||||
|
.items_center()
|
||||||
|
.child("No active connection")
|
||||||
|
.into_any(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ path = "src/agent_servers.rs"
|
||||||
doctest = false
|
doctest = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
acp_tools.workspace = true
|
||||||
acp_thread.workspace = true
|
acp_thread.workspace = true
|
||||||
action_log.workspace = true
|
action_log.workspace = true
|
||||||
agent-client-protocol.workspace = true
|
agent-client-protocol.workspace = true
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use acp_tools::AcpConnectionRegistry;
|
||||||
use action_log::ActionLog;
|
use action_log::ActionLog;
|
||||||
use agent_client_protocol::{self as acp, Agent as _, ErrorCode};
|
use agent_client_protocol::{self as acp, Agent as _, ErrorCode};
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
|
@ -101,6 +102,14 @@ impl AcpConnection {
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
|
let connection = Rc::new(connection);
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
AcpConnectionRegistry::default_global(cx).update(cx, |registry, cx| {
|
||||||
|
registry.set_active_connection(server_name, &connection, cx)
|
||||||
|
});
|
||||||
|
})?;
|
||||||
|
|
||||||
let response = connection
|
let response = connection
|
||||||
.initialize(acp::InitializeRequest {
|
.initialize(acp::InitializeRequest {
|
||||||
protocol_version: acp::VERSION,
|
protocol_version: acp::VERSION,
|
||||||
|
@ -119,7 +128,7 @@ impl AcpConnection {
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
auth_methods: response.auth_methods,
|
auth_methods: response.auth_methods,
|
||||||
connection: connection.into(),
|
connection,
|
||||||
server_name,
|
server_name,
|
||||||
sessions,
|
sessions,
|
||||||
prompt_capabilities: response.agent_capabilities.prompt_capabilities,
|
prompt_capabilities: response.agent_capabilities.prompt_capabilities,
|
||||||
|
|
|
@ -20,6 +20,7 @@ path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
activity_indicator.workspace = true
|
activity_indicator.workspace = true
|
||||||
|
acp_tools.workspace = true
|
||||||
agent.workspace = true
|
agent.workspace = true
|
||||||
agent_ui.workspace = true
|
agent_ui.workspace = true
|
||||||
agent_settings.workspace = true
|
agent_settings.workspace = true
|
||||||
|
|
|
@ -566,6 +566,7 @@ pub fn main() {
|
||||||
language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx);
|
language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx);
|
||||||
agent_settings::init(cx);
|
agent_settings::init(cx);
|
||||||
agent_servers::init(cx);
|
agent_servers::init(cx);
|
||||||
|
acp_tools::init(cx);
|
||||||
web_search::init(cx);
|
web_search::init(cx);
|
||||||
web_search_providers::init(app_state.client.clone(), cx);
|
web_search_providers::init(app_state.client.clone(), cx);
|
||||||
snippet_provider::init(cx);
|
snippet_provider::init(cx);
|
||||||
|
|
|
@ -4434,6 +4434,7 @@ mod tests {
|
||||||
assert_eq!(actions_without_namespace, Vec::<&str>::new());
|
assert_eq!(actions_without_namespace, Vec::<&str>::new());
|
||||||
|
|
||||||
let expected_namespaces = vec![
|
let expected_namespaces = vec![
|
||||||
|
"acp",
|
||||||
"activity_indicator",
|
"activity_indicator",
|
||||||
"agent",
|
"agent",
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
|
|
@ -15,13 +15,11 @@ SQUAWK_VERSION=0.26.0
|
||||||
SQUAWK_BIN="./target/squawk-$SQUAWK_VERSION"
|
SQUAWK_BIN="./target/squawk-$SQUAWK_VERSION"
|
||||||
SQUAWK_ARGS="--assume-in-transaction --config script/lib/squawk.toml"
|
SQUAWK_ARGS="--assume-in-transaction --config script/lib/squawk.toml"
|
||||||
|
|
||||||
if [ ! -f "$SQUAWK_BIN" ]; then
|
pkgutil --pkg-info com.apple.pkg.RosettaUpdateAuto || /usr/sbin/softwareupdate --install-rosetta --agree-to-license
|
||||||
pkgutil --pkg-info com.apple.pkg.RosettaUpdateAuto || /usr/sbin/softwareupdate --install-rosetta --agree-to-license
|
# When bootstrapping a brand new CI machine, the `target` directory may not exist yet.
|
||||||
# When bootstrapping a brand new CI machine, the `target` directory may not exist yet.
|
mkdir -p "./target"
|
||||||
mkdir -p "./target"
|
curl -L -o "$SQUAWK_BIN" "https://github.com/sbdchd/squawk/releases/download/v$SQUAWK_VERSION/squawk-darwin-x86_64"
|
||||||
curl -L -o "$SQUAWK_BIN" "https://github.com/sbdchd/squawk/releases/download/v$SQUAWK_VERSION/squawk-darwin-x86_64"
|
chmod +x "$SQUAWK_BIN"
|
||||||
chmod +x "$SQUAWK_BIN"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "$SQUAWK_GITHUB_TOKEN" ]; then
|
if [ -n "$SQUAWK_GITHUB_TOKEN" ]; then
|
||||||
export SQUAWK_GITHUB_REPO_OWNER=$(echo $GITHUB_REPOSITORY | awk -F/ '{print $1}')
|
export SQUAWK_GITHUB_REPO_OWNER=$(echo $GITHUB_REPOSITORY | awk -F/ '{print $1}')
|
||||||
|
|
|
@ -54,6 +54,7 @@ digest = { version = "0.10", features = ["mac", "oid", "std"] }
|
||||||
either = { version = "1", features = ["serde", "use_std"] }
|
either = { version = "1", features = ["serde", "use_std"] }
|
||||||
euclid = { version = "0.22" }
|
euclid = { version = "0.22" }
|
||||||
event-listener = { version = "5" }
|
event-listener = { version = "5" }
|
||||||
|
event-listener-strategy = { version = "0.5" }
|
||||||
flate2 = { version = "1", features = ["zlib-rs"] }
|
flate2 = { version = "1", features = ["zlib-rs"] }
|
||||||
form_urlencoded = { version = "1" }
|
form_urlencoded = { version = "1" }
|
||||||
futures = { version = "0.3", features = ["io-compat"] }
|
futures = { version = "0.3", features = ["io-compat"] }
|
||||||
|
@ -183,6 +184,7 @@ digest = { version = "0.10", features = ["mac", "oid", "std"] }
|
||||||
either = { version = "1", features = ["serde", "use_std"] }
|
either = { version = "1", features = ["serde", "use_std"] }
|
||||||
euclid = { version = "0.22" }
|
euclid = { version = "0.22" }
|
||||||
event-listener = { version = "5" }
|
event-listener = { version = "5" }
|
||||||
|
event-listener-strategy = { version = "0.5" }
|
||||||
flate2 = { version = "1", features = ["zlib-rs"] }
|
flate2 = { version = "1", features = ["zlib-rs"] }
|
||||||
form_urlencoded = { version = "1" }
|
form_urlencoded = { version = "1" }
|
||||||
futures = { version = "0.3", features = ["io-compat"] }
|
futures = { version = "0.3", features = ["io-compat"] }
|
||||||
|
@ -403,7 +405,6 @@ bytemuck = { version = "1", default-features = false, features = ["min_const_gen
|
||||||
cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] }
|
cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] }
|
||||||
codespan-reporting = { version = "0.12" }
|
codespan-reporting = { version = "0.12" }
|
||||||
crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] }
|
crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] }
|
||||||
event-listener-strategy = { version = "0.5" }
|
|
||||||
flume = { version = "0.11" }
|
flume = { version = "0.11" }
|
||||||
foldhash = { version = "0.1", default-features = false, features = ["std"] }
|
foldhash = { version = "0.1", default-features = false, features = ["std"] }
|
||||||
getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
|
getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
|
||||||
|
@ -444,7 +445,6 @@ bytemuck = { version = "1", default-features = false, features = ["min_const_gen
|
||||||
cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] }
|
cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] }
|
||||||
codespan-reporting = { version = "0.12" }
|
codespan-reporting = { version = "0.12" }
|
||||||
crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] }
|
crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] }
|
||||||
event-listener-strategy = { version = "0.5" }
|
|
||||||
flume = { version = "0.11" }
|
flume = { version = "0.11" }
|
||||||
foldhash = { version = "0.1", default-features = false, features = ["std"] }
|
foldhash = { version = "0.1", default-features = false, features = ["std"] }
|
||||||
getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
|
getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
|
||||||
|
@ -483,7 +483,6 @@ bytemuck = { version = "1", default-features = false, features = ["min_const_gen
|
||||||
cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] }
|
cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] }
|
||||||
codespan-reporting = { version = "0.12" }
|
codespan-reporting = { version = "0.12" }
|
||||||
crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] }
|
crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] }
|
||||||
event-listener-strategy = { version = "0.5" }
|
|
||||||
flume = { version = "0.11" }
|
flume = { version = "0.11" }
|
||||||
foldhash = { version = "0.1", default-features = false, features = ["std"] }
|
foldhash = { version = "0.1", default-features = false, features = ["std"] }
|
||||||
getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
|
getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
|
||||||
|
@ -524,7 +523,6 @@ bytemuck = { version = "1", default-features = false, features = ["min_const_gen
|
||||||
cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] }
|
cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] }
|
||||||
codespan-reporting = { version = "0.12" }
|
codespan-reporting = { version = "0.12" }
|
||||||
crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] }
|
crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] }
|
||||||
event-listener-strategy = { version = "0.5" }
|
|
||||||
flume = { version = "0.11" }
|
flume = { version = "0.11" }
|
||||||
foldhash = { version = "0.1", default-features = false, features = ["std"] }
|
foldhash = { version = "0.1", default-features = false, features = ["std"] }
|
||||||
getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
|
getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
|
||||||
|
@ -610,7 +608,6 @@ bytemuck = { version = "1", default-features = false, features = ["min_const_gen
|
||||||
cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] }
|
cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] }
|
||||||
codespan-reporting = { version = "0.12" }
|
codespan-reporting = { version = "0.12" }
|
||||||
crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] }
|
crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] }
|
||||||
event-listener-strategy = { version = "0.5" }
|
|
||||||
flume = { version = "0.11" }
|
flume = { version = "0.11" }
|
||||||
foldhash = { version = "0.1", default-features = false, features = ["std"] }
|
foldhash = { version = "0.1", default-features = false, features = ["std"] }
|
||||||
getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
|
getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
|
||||||
|
@ -651,7 +648,6 @@ bytemuck = { version = "1", default-features = false, features = ["min_const_gen
|
||||||
cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] }
|
cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] }
|
||||||
codespan-reporting = { version = "0.12" }
|
codespan-reporting = { version = "0.12" }
|
||||||
crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] }
|
crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] }
|
||||||
event-listener-strategy = { version = "0.5" }
|
|
||||||
flume = { version = "0.11" }
|
flume = { version = "0.11" }
|
||||||
foldhash = { version = "0.1", default-features = false, features = ["std"] }
|
foldhash = { version = "0.1", default-features = false, features = ["std"] }
|
||||||
getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
|
getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue