use std::path::Path;
use std::sync::{Arc, Mutex};
use crate::actions::route::RouteInput;
use anyhow::{Context, Result, bail, ensure};
use bitrouter_tui::machine::PromptCommand;
use tokio_util::sync::CancellationToken;
use crate::acp_cli::{
SessionHandle, SessionHost, SessionSelection, SpawnContext, lifecycle_cancelled,
};
use crate::actions::administration::{PolicyInput, PolicyView};
use crate::administration_target::{InspectionTarget, ReloadSubmission};
use crate::contexts::RemoteContext;
use crate::dashboard::SessionRequest;
use crate::output::{CliReport, Format, Output};
pub(crate) mod evolution;
pub(crate) struct AgentChoice {
pub id: String,
pub description: String,
}
struct InitialLaunch {
agent_id: String,
selection: SessionSelection,
routing: crate::acp_cli::RoutingOptions,
config: bitrouter_sdk::config::Config,
options: crate::acp_cli::LaunchOptions,
}
impl InitialLaunch {
fn matches(&self, request: &SessionRequest) -> bool {
self.agent_id == request.agent
&& self.selection == request.selection
&& self.routing == request.routing
}
}
pub(crate) struct CodeServices {
target: InspectionTarget,
pub label: String,
pub operations_only: bool,
pub can_reload: bool,
initial_launch: Mutex<Option<InitialLaunch>>,
}
pub(crate) struct OpenedSession {
pub handle: SessionHandle,
pub diagnostics: Vec<String>,
pub prompt_commands: Vec<PromptCommand>,
pub resumed: bool,
}
impl CodeServices {
pub fn commands(
&self,
client: Option<&bitrouter_sdk::acp::client::AcpClient>,
) -> Vec<bitrouter_tui::machine::Command> {
if let Some(client) = client {
return super::session::offered_commands(client);
}
crate::actions::ACTIONS
.iter()
.filter_map(|action| {
if self.operations_only && !operation_command_supported(action.id) {
return None;
}
let name = action.tui_command?;
let unavailable = (!matches!(action.requires, crate::actions::Requires::Nothing))
.then_some("Connect a routable ACP session to use this action");
Some(bitrouter_tui::machine::Command {
name,
action: action.id,
summary: super::session::summary_for(action.id),
unavailable,
})
})
.collect()
}
pub async fn open(
remote: Option<(String, RemoteContext)>,
config: Option<&Path>,
socket: Option<&Path>,
) -> Result<Arc<Self>> {
let operations_only = remote.is_some() || socket.is_some();
let target = InspectionTarget::resolve(remote, config, socket).await?;
let can_reload = target.control_authority().await?.reload;
let label = if operations_only {
if target.is_remote() {
format!(
"Remote operations · reports read-only · reload explicit · {}",
target.label()
)
} else {
format!(
"Local operations · reports read-only · reload explicit · {}",
target
.local_socket()
.context("local operations target has no socket")?
.display()
)
}
} else {
conversation_label(&std::env::current_dir()?)
};
Ok(Arc::new(Self {
target,
label,
operations_only,
can_reload,
initial_launch: Mutex::new(None),
}))
}
pub(crate) fn from_spawn_context(ctx: SpawnContext<'_>) -> Result<(Arc<Self>, SessionRequest)> {
let SpawnContext {
source,
config,
agent_id,
options,
routing,
} = ctx;
let request = SessionRequest {
agent: agent_id.to_string(),
selection: SessionSelection::New,
turn_timeout: options.turn_timeout.map(|timeout| timeout.as_secs()),
routing: routing.clone(),
};
let source = source.clone();
let socket = crate::daemon::socket_path_for(&source, &config);
let label = conversation_label(&std::env::current_dir()?);
let initial_launch = InitialLaunch {
agent_id: agent_id.to_string(),
selection: SessionSelection::New,
routing,
config,
options,
};
Ok((
Arc::new(Self {
target: InspectionTarget::Local { source, socket },
label,
operations_only: false,
can_reload: true,
initial_launch: Mutex::new(Some(initial_launch)),
}),
request,
))
}
pub async fn agents(&self) -> Result<Vec<AgentChoice>> {
if self.operations_only {
return Ok(Vec::new());
}
let Some(source) = self.target.local_source() else {
return Ok(Vec::new());
};
let config = crate::paths::load_config(source).await?;
Ok(crate::agents::list(&config)
.into_iter()
.filter_map(|row| {
agent_is_selectable(&row).then_some(AgentChoice {
id: row.id,
description: row.description,
})
})
.collect())
}
pub(crate) async fn start_with_cancel(
&self,
request: SessionRequest,
cancel: &CancellationToken,
) -> Result<OpenedSession> {
if cancel.is_cancelled() {
return Err(lifecycle_cancelled());
}
ensure!(
!self.operations_only,
"This target is operations-only; ACP execution is unavailable"
);
let Some(source) = self.target.local_source() else {
bail!("Remote ACP execution is unavailable");
};
let initial = self.take_initial_launch(&request)?;
let (config, agent_id, options, routing) = match initial {
Some(initial) => (
initial.config,
initial.agent_id,
initial.options,
initial.routing,
),
None => {
let config = tokio::select! {
biased;
_ = cancel.cancelled() => return Err(lifecycle_cancelled()),
result = crate::paths::load_config(source) => result?,
};
(
config,
request.agent.clone(),
crate::acp_cli::launch_options(request.turn_timeout),
request.routing.clone(),
)
}
};
let prompt_commands = super::session::prompt_commands(&config.chat)?;
let mut diagnostics = Vec::new();
let mut record_diagnostic = |message| diagnostics.push(message);
let host = tokio::select! {
biased;
_ = cancel.cancelled() => return Err(lifecycle_cancelled()),
result = SessionHost::prepare_with_diagnostics(
SpawnContext {
source,
config,
agent_id: &agent_id,
options,
routing,
},
false,
&mut record_diagnostic,
) => result?,
};
let resumed = matches!(
request.selection,
crate::acp_cli::SessionSelection::Resume(_)
);
let handle = host
.open_with_cancel(
&request.selection,
std::env::current_dir().context("resolving current directory")?,
cancel,
)
.await?;
Ok(OpenedSession {
handle,
diagnostics,
prompt_commands,
resumed,
})
}
fn take_initial_launch(&self, request: &SessionRequest) -> Result<Option<InitialLaunch>> {
let mut initial = self
.initial_launch
.lock()
.map_err(|_| anyhow::anyhow!("Code initial launch state is unavailable"))?;
if initial
.as_ref()
.is_some_and(|launch| launch.matches(request))
{
Ok(initial.take())
} else {
Ok(None)
}
}
pub async fn report(&self, action: &str, args: &[String]) -> Result<String> {
let report: Box<dyn CliReport> = match action {
"status" => {
ensure!(args.is_empty(), "usage: /status");
Box::new(self.target.status().await?)
}
"list_models" => Box::new(self.target.models(args.first().map(String::as_str)).await?),
"requests" => {
ensure!(args.is_empty(), "Host requests takes no arguments in Code");
Box::new(
self.target
.requests(crate::actions::requests::RequestFilters {
limit: 100,
..Default::default()
})
.await?,
)
}
"route" => {
let model = args.first().context("usage: /preview <model>")?;
Box::new(
self.target
.route(RouteInput {
model: model.clone(),
prompt: None,
})
.await?,
)
}
"providers_list" => {
ensure!(args.is_empty(), "usage: /providers list");
Box::new(self.target.providers().await?)
}
"observe_status" => {
ensure!(args.is_empty(), "usage: /observe status");
Box::new(self.target.observe().await?)
}
"policy_status" => {
ensure!(args.is_empty(), "usage: /policy status");
Box::new(self.target.policy(PolicyInput::default()).await?)
}
"policy_show" => {
ensure!(args.len() <= 1, "usage: /policy show [name]");
Box::new(
self.target
.policy(PolicyInput {
view: PolicyView::Active,
name: args.first().cloned(),
})
.await?,
)
}
"agents_list" => {
ensure!(args.is_empty(), "usage: /agents list");
Box::new(self.target.agents().await?)
}
"reload_state" => {
ensure!(args.is_empty(), "usage: /reload state");
Box::new(self.target.reload_state().await?)
}
"reload" => {
ensure!(args.is_empty(), "usage: /reload");
match self.target.reload().await? {
ReloadSubmission::Local(report) => Box::new(report),
ReloadSubmission::Remote(report) => Box::new(report),
}
}
_ => bail!("This target does not offer `{action}`"),
};
String::from_utf8(Output::new(Format::Human).render_to_vec(report.as_ref()))
.context("rendering the requested report")
}
}
fn conversation_label(workspace: &Path) -> String {
let short = workspace
.file_name()
.unwrap_or(workspace.as_os_str())
.to_string_lossy();
format!("{} code · {short}", bitrouter_sdk::invocation::name())
}
fn operation_command_supported(action: &str) -> bool {
matches!(action, "status" | "list_models" | "route")
}
fn agent_is_selectable(row: &crate::agents::ListRow) -> bool {
row.configured
|| crate::harness::by_id(&row.id).is_some_and(|harness| harness.acp_command.is_some())
}
#[cfg(test)]
mod tests {
use super::{
CodeServices, agent_is_selectable, conversation_label, operation_command_supported,
};
use crate::acp_cli::{LaunchOptions, RoutingOptions, SessionSelection, SpawnContext};
use crate::dashboard::SessionRequest;
use crate::paths::ConfigSource;
fn request(agent: &str) -> SessionRequest {
SessionRequest {
agent: agent.to_string(),
selection: SessionSelection::New,
turn_timeout: None,
routing: RoutingOptions::default(),
}
}
#[test]
fn conversation_welcome_uses_only_the_short_workspace_name() {
assert_eq!(
conversation_label(std::path::Path::new("/workspaces/bitrouter")),
"bro code · bitrouter"
);
}
#[test]
fn initial_launch_waits_for_its_matching_agent_request() -> anyhow::Result<()> {
let directory = std::env::temp_dir().join("bitrouter-code-test");
let source = ConfigSource::Default { home: directory };
let (services, initial_request) = CodeServices::from_spawn_context(SpawnContext {
source: &source,
config: bitrouter_sdk::config::Config::default(),
agent_id: "initial",
options: LaunchOptions {
strip_inherited_env: vec!["UNRELATED_AGENT_TOKEN".to_string()],
turn_timeout: Some(std::time::Duration::from_millis(1_501)),
terminal_auth: true,
..LaunchOptions::default()
},
routing: RoutingOptions::default(),
})?;
assert_eq!(initial_request.agent, "initial");
assert!(services.take_initial_launch(&request("other"))?.is_none());
let initial = services
.take_initial_launch(&request("initial"))?
.ok_or_else(|| anyhow::anyhow!("matching initial launch was not retained"))?;
assert_eq!(
initial.options.strip_inherited_env,
vec!["UNRELATED_AGENT_TOKEN".to_string()]
);
assert_eq!(
initial.options.turn_timeout,
Some(std::time::Duration::from_millis(1_501))
);
assert!(initial.options.terminal_auth);
assert!(services.take_initial_launch(&request("initial"))?.is_none());
Ok(())
}
#[test]
fn operation_targets_offer_only_their_read_only_reports() {
for action in ["status", "list_models", "route"] {
assert!(operation_command_supported(action));
}
for action in ["commands", "route_set", "route_reset", "skills_search"] {
assert!(!operation_command_supported(action));
}
}
#[test]
fn configured_agent_remains_selectable_without_catalog_acp() {
let configured = crate::agents::ListRow {
id: "configured-native-name".to_string(),
configured: true,
in_catalog: false,
description: "configured ACP transport".to_string(),
};
let unconfigured = crate::agents::ListRow {
configured: false,
..configured.clone()
};
assert!(agent_is_selectable(&configured));
assert!(!agent_is_selectable(&unconfigured));
}
}