a3s 0.10.8

a3s — A3S coding agent CLI; `a3s code` launches the interactive TUI
Documentation
use std::path::Path;

use serde_json::json;

use crate::commands::code::asset_types::AssetCommandOutput;
use crate::tui::{panels, remote_ui};

pub(crate) fn os_asset_category_query(category: &str, query: &str) -> String {
    let query = query.trim();
    if query.is_empty() {
        format!("category:{category}")
    } else {
        format!("category:{category} {query}")
    }
}

pub(crate) fn runtime_asset_query(category: &str, asset_hint: &str, query: &str) -> String {
    let category = category.trim();
    let asset_hint = asset_hint.trim();
    let query = query.trim();
    let mut parts = Vec::new();
    if !category.is_empty() {
        parts.push(format!("category:{category}"));
    }
    if !asset_hint.is_empty() {
        parts.push(asset_hint.to_string());
    }
    if !query.is_empty() {
        parts.push(query.to_string());
    }
    parts.join(" ")
}

pub(super) fn local_agents_output(query: &str, root: &Path) -> AssetCommandOutput {
    let rows = panels::agent::list_agents(root)
        .into_iter()
        .filter(|row| matches_query(&row.rel, query))
        .collect::<Vec<_>>();
    let mut human = format!(
        "{} local agent package(s) in {}\n",
        rows.len(),
        root.display()
    );
    for row in &rows {
        human.push_str(&format!(
            "{}\t{}\t{}\n",
            row.rel,
            row.definition_rel,
            row.path.display()
        ));
    }
    let assets = rows
        .iter()
        .map(|row| {
            json!({
                "id": row.rel,
                "path": path_value(&row.path),
                "definition": row.definition_rel,
                "definitionPath": path_value(&row.definition_path),
            })
        })
        .collect::<Vec<_>>();
    local_list_output("agent", query, root, assets, human)
}

pub(super) fn local_mcps_output(query: &str, root: &Path) -> AssetCommandOutput {
    let rows = panels::mcp::list_mcp_projects(root)
        .into_iter()
        .filter(|row| matches_query(&format!("{} {}", row.rel, row.name), query))
        .collect::<Vec<_>>();
    let mut human = format!("{} local MCP asset(s) in {}\n", rows.len(), root.display());
    for row in &rows {
        human.push_str(&format!(
            "{}\t{}\t{}\n",
            row.rel,
            row.name,
            row.path.display()
        ));
    }
    let assets = rows
        .iter()
        .map(|row| {
            json!({
                "id": row.rel,
                "name": row.name,
                "description": row.description,
                "path": path_value(&row.path),
            })
        })
        .collect::<Vec<_>>();
    local_list_output("mcp", query, root, assets, human)
}

pub(super) fn local_skills_output(query: &str, root: &Path) -> AssetCommandOutput {
    let rows = panels::skill::list_skill_assets(root)
        .into_iter()
        .filter(|row| matches_query(&format!("{} {}", row.rel, row.name), query))
        .collect::<Vec<_>>();
    let mut human = format!(
        "{} local skill asset(s) in {}\n",
        rows.len(),
        root.display()
    );
    for row in &rows {
        human.push_str(&format!(
            "{}\t{}\t{}\n",
            row.rel,
            row.name,
            row.path.display()
        ));
    }
    let assets = rows
        .iter()
        .map(|row| {
            json!({
                "id": row.rel,
                "name": row.name,
                "description": row.description,
                "path": path_value(&row.path),
            })
        })
        .collect::<Vec<_>>();
    local_list_output("skill", query, root, assets, human)
}

pub(super) fn local_flows_output(query: &str, root: &Path) -> AssetCommandOutput {
    let rows = panels::flow::list_flows(root)
        .into_iter()
        .filter(|row| matches_query(row, query))
        .collect::<Vec<_>>();
    let mut human = format!(
        "{} local workflow file(s) in {}\n",
        rows.len(),
        root.display()
    );
    for row in &rows {
        human.push_str(&format!("{}\t{}\n", row, root.join(row).display()));
    }
    let assets = rows
        .iter()
        .map(|row| json!({"id": row, "path": path_value(&root.join(row))}))
        .collect::<Vec<_>>();
    local_list_output("flow", query, root, assets, human)
}

pub(super) fn local_okf_output(query: &str, root: &Path) -> AssetCommandOutput {
    let rows = panels::okf::list_okf_packages(root)
        .into_iter()
        .filter(|row| matches_query(&format!("{} {}", row.rel, row.name), query))
        .collect::<Vec<_>>();
    let mut human = format!(
        "{} local OKF package(s) in {}\n",
        rows.len(),
        root.display()
    );
    for row in &rows {
        human.push_str(&format!(
            "{}\t{}\t{}\n",
            row.rel,
            row.name,
            row.path.display()
        ));
    }
    let assets = rows
        .iter()
        .map(|row| {
            json!({
                "id": row.rel,
                "name": row.name,
                "description": row.description,
                "path": path_value(&row.path),
            })
        })
        .collect::<Vec<_>>();
    local_list_output("okf", query, root, assets, human)
}

fn local_list_output(
    family: &'static str,
    query: &str,
    root: &Path,
    assets: Vec<serde_json::Value>,
    human: String,
) -> AssetCommandOutput {
    AssetCommandOutput::new(
        json!({
            "family": family,
            "location": "local",
            "query": query,
            "root": path_value(root),
            "assets": assets,
        }),
        human,
    )
}

pub(super) fn review_output(
    family: &'static str,
    path: &Path,
    prompt: String,
) -> AssetCommandOutput {
    let human = if prompt.ends_with('\n') {
        prompt.clone()
    } else {
        format!("{prompt}\n")
    };
    AssetCommandOutput::new(
        json!({
            "family": family,
            "path": path_value(path),
            "prompt": prompt,
        }),
        human,
    )
}

pub(super) fn path_value(path: &Path) -> serde_json::Value {
    if let Some(path) = path.to_str() {
        return serde_json::Value::String(path.to_string());
    }

    native_path_value(path)
}

#[cfg(unix)]
fn native_path_value(path: &Path) -> serde_json::Value {
    use std::os::unix::ffi::OsStrExt;

    let value = path
        .as_os_str()
        .as_bytes()
        .iter()
        .map(|byte| format!("{byte:02x}"))
        .collect::<String>();
    json!({
        "display": path.to_string_lossy(),
        "encoding": "unix-bytes-hex",
        "value": value,
    })
}

#[cfg(windows)]
fn native_path_value(path: &Path) -> serde_json::Value {
    use std::os::windows::ffi::OsStrExt;

    let value = path
        .as_os_str()
        .encode_wide()
        .map(|unit| format!("{unit:04x}"))
        .collect::<String>();
    json!({
        "display": path.to_string_lossy(),
        "encoding": "windows-wide-hex",
        "value": value,
    })
}

#[cfg(not(any(unix, windows)))]
fn native_path_value(path: &Path) -> serde_json::Value {
    json!({
        "display": path.to_string_lossy(),
        "encoding": "platform-native-lossy",
    })
}

fn matches_query(text: &str, query: &str) -> bool {
    let query = query.trim().to_ascii_lowercase();
    query.is_empty() || text.to_ascii_lowercase().contains(&query)
}

pub(super) fn os_note(note: &str, view: &remote_ui::ViewSpec, opened_with: Option<&str>) -> String {
    let mut human = format!("{note}\nview: {}\n", view.url);
    if let Some(width) = view.width {
        human.push_str(&format!("width: {width}"));
        if let Some(height) = view.height {
            human.push_str(&format!(", height: {height}"));
        }
        human.push('\n');
    }
    if let Some(opened_with) = opened_with {
        human.push_str(&format!("opened: {opened_with}\n"));
    }
    human
}

pub(super) fn open_if_requested(
    open_requested: bool,
    view: &remote_ui::ViewSpec,
) -> anyhow::Result<Option<String>> {
    if !open_requested {
        return Ok(None);
    }
    let opened = remote_ui::open_window(view)
        .map_err(|e| anyhow::anyhow!("could not open RemoteUI view: {e}"))?;
    let opened = match opened {
        remote_ui::OpenedWith::Webview => "webview",
        remote_ui::OpenedWith::Browser => "browser",
    };
    Ok(Some(opened.to_string()))
}

#[allow(clippy::too_many_arguments)]
pub(super) fn lifecycle_data(
    family: &'static str,
    action: &'static str,
    kind: Option<&'static str>,
    asset_name: &str,
    asset_id: &str,
    note: &str,
    view: &remote_ui::ViewSpec,
    opened_with: Option<String>,
) -> serde_json::Value {
    json!({
        "family": family,
        "action": action,
        "kind": kind,
        "assetName": asset_name,
        "assetId": asset_id,
        "note": note,
        "view": {
            "url": view.url,
            "width": view.width,
            "height": view.height,
            "embeddable": view.embeddable,
        },
        "openedWith": opened_with,
    })
}

pub(super) fn trim_col(value: &str, width: usize) -> String {
    let mut out = value.chars().take(width).collect::<String>();
    if value.chars().count() > width && width >= 1 {
        out.pop();
        out.push('~');
    }
    out
}