use std::path::Path;
use anyhow::Context as _;
use crate::bootstrap::{load_config_or_default, resolve_config_path};
use crate::cli::{AcpCommand, AcpModelConfigCommand, AcpSubagentCommand};
pub(crate) async fn handle_acp_command(
cmd: AcpCommand,
config_path: Option<&Path>,
) -> anyhow::Result<()> {
match cmd {
AcpCommand::RunAgent {
command,
prompt,
cwd,
timeout,
} => {
let span = tracing::info_span!("acp.client.session.run");
let _enter = span.enter();
let text = if let Some(p) = prompt {
p
} else {
use std::io::Read;
let mut buf = String::new();
std::io::stdin()
.read_to_string(&mut buf)
.context("reading prompt from stdin")?;
buf
};
let cfg = zeph_acp::client::SubagentConfig {
command,
process_cwd: cwd.clone(),
session_cwd: cwd,
prompt_timeout_secs: timeout,
handshake_timeout_secs: timeout.min(30),
auto_approve_permissions: true,
..zeph_acp::client::SubagentConfig::default()
};
let outcome = zeph_acp::run_session(cfg, text).await?;
println!("{}", outcome.text);
tracing::info!(stop_reason = ?outcome.stop_reason, "sub-agent session completed");
Ok(())
}
AcpCommand::Subagent {
command: AcpSubagentCommand::List,
} => {
println!("Sub-agent presets are configured under [acp.subagents] in config.toml.");
println!("Use `zeph acp run-agent --command <CMD> --prompt <TEXT>` for one-shot runs.");
Ok(())
}
AcpCommand::ModelConfig {
command: AcpModelConfigCommand::Show,
} => {
let config_file = resolve_config_path(config_path);
let config = load_config_or_default(&config_file)?;
print!("{}", render_model_config_table(&config, &config_file));
Ok(())
}
}
}
fn render_model_config_table(config: &zeph_core::config::Config, config_path: &Path) -> String {
use std::fmt::Write as _;
let default_preset = config.acp.model_config.default_temperature_preset;
let mut out = String::new();
let _ = writeln!(
out,
"ACP model_config presets (session/set_config_option, config_id=\"temperature\"):"
);
for preset in [
zeph_config::AcpTemperaturePreset::Precise,
zeph_config::AcpTemperaturePreset::Balanced,
zeph_config::AcpTemperaturePreset::Creative,
] {
let marker = if preset == default_preset {
" (default)"
} else {
""
};
let _ = writeln!(
out,
" {:<10} temperature = {}{marker}",
preset.as_str(),
preset.temperature()
);
}
let _ = writeln!(
out,
"Config: {} — [acp.model_config].default_temperature_preset",
config_path.display()
);
out
}
#[cfg(test)]
mod tests {
use super::{handle_acp_command, render_model_config_table};
use crate::cli::{AcpCommand, AcpModelConfigCommand};
#[tokio::test]
async fn model_config_show_loads_config_without_error() {
let dir = tempfile::tempdir().expect("tempdir");
let mut config = zeph_core::config::Config::default();
config.acp.model_config.default_temperature_preset =
zeph_config::AcpTemperaturePreset::Creative;
let toml = toml::to_string_pretty(&config).expect("serialize config");
let config_path = dir.path().join("config.toml");
std::fs::write(&config_path, toml).expect("write config");
let result = handle_acp_command(
AcpCommand::ModelConfig {
command: AcpModelConfigCommand::Show,
},
Some(&config_path),
)
.await;
assert!(result.is_ok(), "model-config show failed: {result:?}");
}
#[test]
fn render_marks_configured_creative_preset_as_default() {
let mut config = zeph_core::config::Config::default();
config.acp.model_config.default_temperature_preset =
zeph_config::AcpTemperaturePreset::Creative;
let table = render_model_config_table(&config, std::path::Path::new("config.toml"));
let creative_line = table
.lines()
.find(|l| l.contains("creative"))
.expect("creative line present");
assert!(
creative_line.contains("(default)"),
"creative line must carry the marker: {creative_line:?}"
);
for other in ["precise", "balanced"] {
let line = table.lines().find(|l| l.contains(other)).unwrap();
assert!(
!line.contains("(default)"),
"{other} line must not carry the marker: {line:?}"
);
}
}
#[test]
fn render_marks_balanced_as_default_when_unconfigured() {
let config = zeph_core::config::Config::default();
let table = render_model_config_table(&config, std::path::Path::new("config.toml"));
let balanced_line = table
.lines()
.find(|l| l.contains("balanced"))
.expect("balanced line present");
assert!(
balanced_line.contains("(default)"),
"balanced line must carry the marker: {balanced_line:?}"
);
for other in ["precise", "creative"] {
let line = table.lines().find(|l| l.contains(other)).unwrap();
assert!(
!line.contains("(default)"),
"{other} line must not carry the marker: {line:?}"
);
}
}
}