First attempt to add ACP support

This commit is contained in:
Ryan Ward
2026-07-31 11:09:32 -05:00
parent 1a0aac51b6
commit 7f4891ec7c
25 changed files with 1051 additions and 138 deletions
+165 -1
View File
@@ -54,6 +54,7 @@ use crate::ai::blocklist::agent_view::agent_input_footer::editor::{
};
use crate::ai::blocklist::BlocklistAIPermissions;
use crate::ai::execution_profiles::model_menu_items::available_model_menu_items;
#[cfg(not(target_family = "wasm"))]
use crate::ai::execution_profiles::profiles::{
AIExecutionProfilesModel, AIExecutionProfilesModelEvent, ClientProfileId,
};
@@ -689,6 +690,8 @@ pub struct AISettingsPageView {
// Profile views
profile_views: Vec<ViewHandle<ExecutionProfileView>>,
add_profile_button: ViewHandle<ActionButton>,
#[cfg(not(target_family = "wasm"))]
refresh_acp_button: ViewHandle<ActionButton>,
// Custom model router views (gated on FeatureFlag::CustomModelRouters)
#[cfg(feature = "local_fs")]
@@ -698,6 +701,85 @@ pub struct AISettingsPageView {
}
impl AISettingsPageView {
#[cfg(not(target_family = "wasm"))]
fn refresh_acp_discovery(&mut self, ctx: &mut ViewContext<Self>) {
let (config, agent_id) = {
let settings = AISettings::as_ref(ctx);
let Ok(config) = crate::ai::acp::AcpRuntimeModel::discovery_config(settings) else {
log::warn!("Could not resolve ACP launch configuration for discovery");
return;
};
(config, settings.acp_agent_id.value().clone())
};
/*
* The settings borrow must end before updating the runtime singleton.
*/
let manager =
match crate::ai::acp::AcpRuntimeModel::handle(ctx).update(ctx, |runtime, ctx| {
runtime.begin_discovery(ctx);
runtime.manager(config)
}) {
Ok(manager) => manager,
Err(error) => {
log::warn!("Could not start ACP discovery: {error}");
return;
}
};
let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("/"));
let _ = ctx.spawn(
async move { manager.discover_config_options(cwd, Vec::new()).await },
move |_me, result, ctx| {
match result {
Ok(options) => {
let option_count = options.len();
AISettings::handle(ctx).update(ctx, |settings, ctx| {
if let Err(error) =
crate::ai::acp::AcpRuntimeModel::upsert_agent_settings(
settings, &agent_id, options, ctx,
)
{
log::warn!("Failed to persist ACP discovery: {error}");
}
if let Err(error) =
crate::ai::acp::AcpRuntimeModel::mark_discovery_success(
settings,
&agent_id,
option_count,
ctx,
)
{
log::warn!("Failed to persist ACP discovery metadata: {error}");
}
});
crate::ai::acp::AcpRuntimeModel::handle(ctx).update(ctx, |runtime, ctx| {
runtime.finish_discovery_success(option_count, ctx);
});
}
Err(error) => {
log::warn!("ACP discovery failed: {error}");
let error_text = error.to_string();
AISettings::handle(ctx).update(ctx, |settings, ctx| {
if let Err(error) =
crate::ai::acp::AcpRuntimeModel::mark_discovery_failure(
settings,
&agent_id,
error_text.clone(),
ctx,
)
{
log::warn!("Failed to persist ACP discovery failure: {error}");
}
});
crate::ai::acp::AcpRuntimeModel::handle(ctx).update(ctx, |runtime, ctx| {
runtime.finish_discovery_failure(error_text, ctx);
});
}
}
ctx.notify();
},
);
}
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
let is_any_ai_enabled = AISettings::as_ref(ctx).is_any_ai_enabled(ctx);
@@ -1673,6 +1755,15 @@ impl AISettingsPageView {
})
});
#[cfg(not(target_family = "wasm"))]
let refresh_acp_button = ctx.add_typed_action_view(|_| {
ActionButton::new("Discover ACP options", SecondaryTheme)
.with_size(ButtonSize::Small)
.on_click(|ctx| {
ctx.dispatch_typed_action(AISettingsPageAction::RefreshAcpDiscovery)
})
});
add_profile_button.update(ctx, |button, ctx| {
button.set_disabled(!is_any_ai_enabled, ctx);
});
@@ -1781,6 +1872,8 @@ impl AISettingsPageView {
conversation_layout_dropdown,
profile_views,
add_profile_button,
#[cfg(not(target_family = "wasm"))]
refresh_acp_button,
#[cfg(feature = "local_fs")]
router_views,
#[cfg(feature = "local_fs")]
@@ -2716,6 +2809,7 @@ pub enum AISettingsPageAction {
ToggleBedrockCrossRegionInference,
ToggleOpenAIEnabled,
ToggleAcpEnabled,
RefreshAcpDiscovery,
FetchOpenAIModels,
ToggleFileBasedMcp,
ToggleIncludeAgentCommandsInHistory,
@@ -3482,6 +3576,10 @@ impl TypedActionView for AISettingsPageView {
ctx.notify();
}
}
AISettingsPageAction::RefreshAcpDiscovery => {
#[cfg(not(target_family = "wasm"))]
self.refresh_acp_discovery(ctx);
}
AISettingsPageAction::FetchOpenAIModels => {
// Trigger a fetch of models from the LiteLLM endpoint
self.fetch_litellm_models(ctx);
@@ -7592,6 +7690,8 @@ struct ACPSettingsWidget {
agent_id_editor: ViewHandle<EditorView>,
command_editor: ViewHandle<EditorView>,
args_editor: ViewHandle<EditorView>,
#[cfg(not(target_family = "wasm"))]
refresh_button: ViewHandle<ActionButton>,
}
impl ACPSettingsWidget {
@@ -7653,6 +7753,15 @@ impl ACPSettingsWidget {
}
});
#[cfg(not(target_family = "wasm"))]
let refresh_button = ctx.add_typed_action_view(|_| {
ActionButton::new("Discover ACP options", SecondaryTheme)
.with_size(ButtonSize::Small)
.on_click(|ctx| {
ctx.dispatch_typed_action(AISettingsPageAction::RefreshAcpDiscovery);
})
});
for editor in [
agent_id_editor.clone(),
command_editor.clone(),
@@ -7683,6 +7792,8 @@ impl ACPSettingsWidget {
agent_id_editor,
command_editor,
args_editor,
#[cfg(not(target_family = "wasm"))]
refresh_button,
}
}
@@ -7772,6 +7883,11 @@ impl SettingsWidget for ACPSettingsWidget {
let is_enabled = *settings.acp_enabled.value();
let mut column = Flex::column().with_spacing(16.);
#[cfg(not(target_family = "wasm"))]
let discovery_state = crate::ai::acp::AcpRuntimeModel::as_ref(app)
.discovery_state()
.clone();
column.add_child(build_sub_header(appearance, "Agent Client Protocol", None).finish());
column.add_child(render_ai_setting_toggle::<AcpEnabled>(
"Use an ACP agent for new conversations",
@@ -7824,13 +7940,61 @@ impl SettingsWidget for ACPSettingsWidget {
is_enabled,
app,
));
#[cfg(not(target_family = "wasm"))]
column.add_child(self.refresh_button.as_ref(app).render(app));
#[cfg(not(target_family = "wasm"))]
match discovery_state {
crate::ai::acp::AcpDiscoveryState::Idle => {}
crate::ai::acp::AcpDiscoveryState::Running => {
column.add_child(render_ai_setting_description(
"Discovering ACP configuration options...",
is_enabled,
app,
));
}
crate::ai::acp::AcpDiscoveryState::Succeeded { option_count } => {
column.add_child(render_ai_setting_description(
format!("ACP discovery succeeded with {option_count} configuration option(s)."),
is_enabled,
app,
));
}
crate::ai::acp::AcpDiscoveryState::Failed { message } => {
column.add_child(render_ai_setting_description(
format!("ACP discovery failed: {message}. Cached options were retained."),
is_enabled,
app,
));
}
}
let discovered = settings.acp_agents.value();
if let Some(agent) = discovered
.iter()
.find(|agent| agent.id.eq_ignore_ascii_case(settings.acp_agent_id.value()))
{
if let Some(timestamp) = &agent.discovery_timestamp {
column.add_child(render_ai_setting_description(
format!("Last ACP discovery attempt: {timestamp}"),
is_enabled,
app,
));
}
if let Some(source) = &agent.discovery_source {
column.add_child(render_ai_setting_description(
format!("Discovery source: {source}"),
is_enabled,
app,
));
}
if let Some(error) = &agent.discovery_error {
column.add_child(render_ai_setting_description(
format!("Last discovery error: {error}. Cached options are retained."),
is_enabled,
app,
));
}
column.add_child(render_ai_setting_description(
&format!("Discovered {} ACP configuration option(s) for {}. Options are refreshed from the running agent and cached in settings.toml.", agent.config_options.len(), agent.name),
format!("Discovered {} ACP configuration option(s) for {}. Options are refreshed from the running agent and cached in settings.toml.", agent.config_options.len(), agent.name),
is_enabled,
app,
));