use crate::SettingsUI;
use crate::section::{collapsing_section, section_matches};
use par_term_config::CustomAcpAgentConfig;
use std::collections::{HashMap, HashSet};
fn set_run_command(agent: &mut CustomAcpAgentConfig, key: &str, value: String) {
let value = value.trim().to_string();
if value.is_empty() {
agent.run_command.remove(key);
} else {
agent.run_command.insert(key.to_string(), value);
}
}
fn next_env_placeholder_key(env: &HashMap<String, String>) -> String {
let base = "NEW_ENV_VAR";
if !env.contains_key(base) {
return base.to_string();
}
let mut idx = 2usize;
loop {
let candidate = format!("{base}_{idx}");
if !env.contains_key(&candidate) {
return candidate;
}
idx += 1;
}
}
pub(super) fn show_custom_agents_section(
ui: &mut egui::Ui,
settings: &mut SettingsUI,
changes_this_frame: &mut bool,
collapsed: &mut HashSet<String>,
) {
if section_matches(
&settings.search_query.trim().to_lowercase(),
"Custom Agents",
&[
"custom",
"acp",
"agent",
"identity",
"run command",
"env",
"environment",
"install command",
"connector",
],
) {
collapsing_section(
ui,
"Custom Agents",
"ai_inspector_custom_agents",
false,
collapsed,
|ui| {
ui.label(
"Define additional ACP agents directly in config. \
Entries override bundled/discovered agents with the same identity.",
);
ui.add_space(6.0);
let mut remove_index: Option<usize> = None;
for i in 0..settings
.config
.ai_inspector
.ai_inspector_custom_agents
.len()
{
let mut changed = false;
let mut request_remove = false;
ui.group(|ui| {
ui.push_id(format!("custom_agent_{i}"), |ui| {
let agent =
&mut settings.config.ai_inspector.ai_inspector_custom_agents[i];
show_agent_header(ui, agent, &mut request_remove);
changed |= show_agent_identity_fields(ui, agent);
changed |= show_agent_run_commands(ui, agent);
changed |= show_agent_env_vars(ui, agent);
});
});
if changed {
settings.has_changes = true;
*changes_this_frame = true;
}
if request_remove {
remove_index = Some(i);
}
ui.add_space(6.0);
}
if let Some(idx) = remove_index {
settings
.config
.ai_inspector
.ai_inspector_custom_agents
.remove(idx);
settings.has_changes = true;
*changes_this_frame = true;
}
if settings
.config
.ai_inspector
.ai_inspector_custom_agents
.is_empty()
{
ui.label("No custom agents defined.");
}
if ui.button("Add Custom Agent").clicked() {
settings
.config
.ai_inspector
.ai_inspector_custom_agents
.push(CustomAcpAgentConfig {
identity: format!(
"custom.agent.{}",
settings
.config
.ai_inspector
.ai_inspector_custom_agents
.len()
+ 1
),
name: "Custom ACP Agent".to_string(),
short_name: "custom".to_string(),
protocol: "acp".to_string(),
r#type: "coding".to_string(),
active: Some(true),
run_command: std::collections::HashMap::from([(
"*".to_string(),
"your-agent-acp".to_string(),
)]),
env: std::collections::HashMap::new(),
ollama_context_length: None,
install_command: None,
actions: std::collections::HashMap::new(),
});
settings.has_changes = true;
*changes_this_frame = true;
}
},
);
}
}
fn show_agent_header(ui: &mut egui::Ui, agent: &CustomAcpAgentConfig, request_remove: &mut bool) {
ui.horizontal(|ui| {
ui.strong("Agent".to_string());
if !agent.identity.trim().is_empty() {
ui.label(format!("({})", agent.identity));
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button("Remove").clicked() {
*request_remove = true;
}
});
});
}
fn show_agent_identity_fields(ui: &mut egui::Ui, agent: &mut CustomAcpAgentConfig) -> bool {
let mut changed = false;
changed |= ui
.text_edit_singleline(&mut agent.identity)
.on_hover_text(
"Unique agent ID (usually a domain-like string), \
used for selection and overrides.",
)
.changed();
if agent.identity.trim().is_empty() {
ui.colored_label(
egui::Color32::from_rgb(255, 193, 7),
"Identity is required.",
);
} else {
ui.label("Identity");
}
changed |= ui
.text_edit_singleline(&mut agent.name)
.on_hover_text("Display name shown in agent selectors.")
.changed();
ui.label("Name");
changed |= ui
.text_edit_singleline(&mut agent.short_name)
.on_hover_text("Compact label used in tighter UI surfaces.")
.changed();
ui.label("Short name");
ui.horizontal(|ui| {
let active = agent.active.get_or_insert(true);
changed |= ui
.checkbox(active, "Active")
.on_hover_text("Inactive agents are hidden from the UI.")
.changed();
if agent.protocol != "acp" {
agent.protocol = "acp".to_string();
changed = true;
}
ui.label("Protocol")
.on_hover_text("ACP is currently the only supported protocol.");
ui.add_enabled(
false,
egui::TextEdit::singleline(&mut agent.protocol).desired_width(56.0),
)
.on_hover_text("Read-only: only `acp` is supported right now.");
ui.label("Type")
.on_hover_text("Agent category label (for organization/filtering).");
changed |= ui
.add(egui::TextEdit::singleline(&mut agent.r#type).desired_width(100.0))
.on_hover_text("Typically `coding`.")
.changed();
});
ui.label("Install command (optional)");
let mut install_command = agent.install_command.clone().unwrap_or_default();
if ui
.text_edit_singleline(&mut install_command)
.on_hover_text("Shown when connector is missing. Example: npm/brew/pip install command.")
.changed()
{
let trimmed = install_command.trim().to_string();
agent.install_command = if trimmed.is_empty() {
None
} else {
Some(trimmed)
};
changed = true;
}
changed
}
fn show_agent_run_commands(ui: &mut egui::Ui, agent: &mut CustomAcpAgentConfig) -> bool {
let mut changed = false;
ui.add_space(4.0);
ui.strong("Run commands");
let mut wildcard = agent.run_command.get("*").cloned().unwrap_or_default();
ui.horizontal(|ui| {
ui.label("*")
.on_hover_text("Default command for all platforms.");
if ui
.text_edit_singleline(&mut wildcard)
.on_hover_text("Command used to launch the ACP connector.")
.changed()
{
set_run_command(agent, "*", wildcard.clone());
changed = true;
}
});
let mut macos = agent.run_command.get("macos").cloned().unwrap_or_default();
ui.horizontal(|ui| {
ui.label("macos")
.on_hover_text("Optional macOS-specific override command.");
if ui
.text_edit_singleline(&mut macos)
.on_hover_text("Leave empty to fall back to `*`.")
.changed()
{
set_run_command(agent, "macos", macos.clone());
changed = true;
}
});
let mut linux = agent.run_command.get("linux").cloned().unwrap_or_default();
ui.horizontal(|ui| {
ui.label("linux")
.on_hover_text("Optional Linux-specific override command.");
if ui
.text_edit_singleline(&mut linux)
.on_hover_text("Leave empty to fall back to `*`.")
.changed()
{
set_run_command(agent, "linux", linux.clone());
changed = true;
}
});
let mut windows = agent
.run_command
.get("windows")
.cloned()
.unwrap_or_default();
ui.horizontal(|ui| {
ui.label("windows")
.on_hover_text("Optional Windows-specific override command.");
if ui
.text_edit_singleline(&mut windows)
.on_hover_text("Leave empty to fall back to `*`.")
.changed()
{
set_run_command(agent, "windows", windows.clone());
changed = true;
}
});
if agent.run_command.is_empty() {
ui.colored_label(
egui::Color32::from_rgb(255, 152, 0),
"At least one run command is required.",
);
}
changed
}
fn show_agent_env_vars(ui: &mut egui::Ui, agent: &mut CustomAcpAgentConfig) -> bool {
let mut changed = false;
ui.add_space(4.0);
ui.strong("Environment variables");
ui.label("These key/value pairs are injected into the ACP subprocess.");
ui.add_space(4.0);
ui.horizontal(|ui| {
ui.label("Ollama context").on_hover_text(
"Optional helper for Ollama-backed agents. Sets \
OLLAMA_CONTEXT_LENGTH on the ACP subprocess unless you \
already define OLLAMA_CONTEXT_LENGTH in Env Vars.",
);
let mut ctx_text = agent
.ollama_context_length
.map(|v| v.to_string())
.unwrap_or_default();
let response = ui
.add(
egui::TextEdit::singleline(&mut ctx_text)
.desired_width(100.0)
.hint_text("e.g. 32768"),
)
.on_hover_text(
"Context window token limit to expose as \
OLLAMA_CONTEXT_LENGTH. Leave blank to disable. \
Note: if your Ollama server runs outside this ACP process \
(for example a separate `ollama serve` / `ollama launch`), \
set the same value in that server environment too.",
);
if response.changed() {
let trimmed = ctx_text.trim();
let parsed = if trimmed.is_empty() {
Some(None)
} else {
trimmed.parse::<u32>().ok().map(Some)
};
if let Some(value) = parsed {
agent.ollama_context_length = value.filter(|v| *v > 0);
changed = true;
}
}
if agent.env.contains_key("OLLAMA_CONTEXT_LENGTH") {
ui.label("(env override)").on_hover_text(
"Env Vars already defines OLLAMA_CONTEXT_LENGTH. \
That value takes precedence over this helper field.",
);
}
});
let mut env_rows: Vec<(String, String)> = agent
.env
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
env_rows.sort_by(|a, b| a.0.cmp(&b.0));
let mut remove_env_index: Option<usize> = None;
for (idx, (key, value)) in env_rows.iter_mut().enumerate() {
ui.horizontal(|ui| {
ui.label("KEY")
.on_hover_text("Environment variable name injected into the ACP subprocess.");
if ui
.text_edit_singleline(key)
.on_hover_text("Example: ANTHROPIC_BASE_URL")
.changed()
{
changed = true;
}
ui.label("VALUE")
.on_hover_text("Environment variable value for this key.");
if ui
.text_edit_singleline(value)
.on_hover_text("Example: http://127.0.0.1:11434")
.changed()
{
changed = true;
}
if ui.small_button("Remove").clicked() {
remove_env_index = Some(idx);
changed = true;
}
});
}
if let Some(idx) = remove_env_index {
env_rows.remove(idx);
}
if ui.small_button("Add Env Var").clicked() {
let key = next_env_placeholder_key(&agent.env);
env_rows.push((key, String::new()));
changed = true;
}
if changed {
agent.env = env_rows
.into_iter()
.filter_map(|(k, v)| {
let key = k.trim().to_string();
if key.is_empty() { None } else { Some((key, v)) }
})
.collect();
}
changed
}