agentmux 0.2.0

Multi-agent coordination runtime with inter-agent messaging across CLI, MCP, tmux, and ACP.
Documentation
use std::{
    env,
    path::PathBuf,
    process::{Command, Stdio},
};

use crate::{
    configuration::load_bundle_configuration,
    runtime::{
        association::WorkspaceContext,
        bootstrap::{BootstrapOptions, bootstrap_relay, resolve_relay_program},
        error::RuntimeError,
        paths::BundleRuntimePaths,
        paths::RuntimeRoots,
        starter::ensure_starter_configuration_layout,
        tui_session::resolve_tui_session_identity,
    },
};

use super::{RuntimeArguments, TuiArguments, shared};

pub(super) fn run_agentmux_tui(arguments: &[String]) -> Result<(), RuntimeError> {
    if arguments
        .iter()
        .any(|value| value == "--help" || value == "-h")
    {
        print_tui_help();
        return Ok(());
    }

    let parsed = parse_tui_arguments(arguments)?;
    let current_directory = env::current_dir()
        .map_err(|source| RuntimeError::io("resolve current working directory", source))?;
    let workspace = WorkspaceContext::discover(&current_directory)?;
    let roots = shared::resolve_roots(&parsed.runtime, &workspace, None)?;
    ensure_starter_configuration_layout(&roots.configuration_root)?;
    let resolved_session = resolve_tui_session_identity(
        &roots.configuration_root,
        &workspace.workspace_root,
        parsed.bundle_name.as_deref(),
        parsed.session_selector.as_deref(),
    )?;
    load_bundle_configuration(&roots.configuration_root, &resolved_session.bundle_name)
        .map_err(shared::map_bundle_load_error)?;
    let paths = BundleRuntimePaths::resolve(&roots.state_root, &resolved_session.bundle_name)?;
    ensure_tui_relay_available(&roots, &paths)?;
    crate::tui::run(crate::tui::TuiLaunchOptions {
        bundle_name: resolved_session.bundle_name,
        sender_session: resolved_session.session_id,
        relay_socket: paths.relay_socket,
        look_lines: parsed.lines,
    })
}

fn parse_tui_arguments(arguments: &[String]) -> Result<TuiArguments, RuntimeError> {
    let mut parsed = TuiArguments {
        bundle_name: None,
        session_selector: None,
        lines: None,
        runtime: RuntimeArguments::default(),
    };
    let mut index = 0usize;
    while index < arguments.len() {
        if shared::parse_runtime_flag(arguments, &mut index, &mut parsed.runtime)? {
            index += 1;
            continue;
        }
        match arguments[index].as_str() {
            "--bundle" | "--bundle-name" => {
                parsed.bundle_name = Some(shared::take_value(arguments, &mut index, "--bundle")?);
            }
            "--as-session" => {
                parsed.session_selector =
                    Some(shared::take_value(arguments, &mut index, "--as-session")?);
            }
            "--lines" => {
                let value = shared::take_value(arguments, &mut index, "--lines")?;
                parsed.lines = Some(shared::parse_look_lines(value.as_str())?);
            }
            unknown => {
                return Err(RuntimeError::InvalidArgument {
                    argument: unknown.to_string(),
                    message: "unknown argument".to_string(),
                });
            }
        }
        index += 1;
    }
    Ok(parsed)
}

pub(super) fn print_tui_help() {
    println!(
        "Usage: agentmux tui [--bundle NAME] [--as-session NAME] [--lines N] [--config-directory PATH] [--state-directory PATH] [--inscriptions-directory PATH|--logs-directory PATH] [--repository-root PATH]"
    );
}

fn ensure_tui_relay_available(
    roots: &RuntimeRoots,
    paths: &BundleRuntimePaths,
) -> Result<(), RuntimeError> {
    let relay_program = resolve_relay_program()?;
    let configuration_root = roots.configuration_root.clone();
    let state_root = roots.state_root.clone();
    let inscriptions_root = roots.inscriptions_root.clone();
    let relay_command = relay_program.clone();
    bootstrap_relay(paths, BootstrapOptions::default(), move || {
        spawn_relay_host_for_tui(
            relay_command.clone(),
            configuration_root.clone(),
            state_root.clone(),
            inscriptions_root.clone(),
        )
    })?;
    Ok(())
}

fn spawn_relay_host_for_tui(
    relay_program: PathBuf,
    configuration_root: PathBuf,
    state_root: PathBuf,
    inscriptions_root: PathBuf,
) -> Result<(), RuntimeError> {
    let spawn_result = Command::new(&relay_program)
        .arg("host")
        .arg("relay")
        .arg("--config-directory")
        .arg(configuration_root)
        .arg("--state-directory")
        .arg(state_root)
        .arg("--inscriptions-directory")
        .arg(inscriptions_root)
        .stdin(Stdio::null())
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn();
    match spawn_result {
        Ok(_) => Ok(()),
        Err(source) => Err(RuntimeError::RelaySpawnFailure {
            command: relay_program,
            source,
        }),
    }
}