dpc-tau-cli 0.2.1

A minimal Unix-first coding agent.
Documentation
//! Shared parsing for UI command subsets.

use std::num::NonZeroU8;

#[cfg(test)]
mod tests;

use tau_proto::Event;

fn is_reset_value(value: &str) -> bool {
    value == "reset"
}

fn parse_resettable<T>(value: &str) -> Result<Option<T>, String>
where
    T: std::str::FromStr,
    T::Err: std::fmt::Display,
{
    if is_reset_value(value) {
        return Ok(None);
    }
    value.parse::<T>().map(Some).map_err(|err| err.to_string())
}

pub(crate) fn parse_tree_navigation_target(
    arg: &str,
) -> Result<tau_proto::UiTreeNavigationTarget, ()> {
    let arg = arg.trim();
    if arg == "root" || arg == "0" {
        return Ok(tau_proto::UiTreeNavigationTarget::Root);
    }
    if let Some(rest) = arg.strip_prefix("node ") {
        let node_id = rest.trim().parse::<u64>().map_err(|_| ())?;
        return Ok(tau_proto::UiTreeNavigationTarget::Node(
            tau_proto::NodeId::new(node_id),
        ));
    }
    let anchor = arg.parse::<u64>().map_err(|_| ())?;
    if anchor == 0 {
        return Ok(tau_proto::UiTreeNavigationTarget::Root);
    }
    Ok(tau_proto::UiTreeNavigationTarget::PromptAnchor(anchor))
}

fn parse_service_tier_update(value: &str) -> Result<Option<tau_proto::ServiceTier>, String> {
    match value {
        "fast" => Ok(Some(tau_proto::ServiceTier::Fast)),
        "flex" => Ok(Some(tau_proto::ServiceTier::Flex)),
        "reset" => Ok(None),
        other => Err(format!(
            "unknown service tier `{other}`; expected fast/flex/reset"
        )),
    }
}

pub(crate) fn parse_tool_list_update(
    value: &str,
) -> Result<Option<Vec<tau_proto::ToolName>>, String> {
    if is_reset_value(value) {
        return Ok(None);
    }
    value
        .split(',')
        .map(str::trim)
        .filter(|name| !name.is_empty())
        .map(|name| {
            tau_proto::ToolName::try_new(name).ok_or_else(|| format!("invalid tool name: {name}"))
        })
        .collect::<Result<Vec<_>, _>>()
        .map(Some)
}

fn parse_tool_override_list_update(value: &str) -> Result<Vec<tau_proto::ToolName>, String> {
    Ok(parse_tool_list_update(value)?.unwrap_or_default())
}

fn parse_tool_group_list_update(value: &str) -> Result<Vec<tau_proto::ToolGroupName>, String> {
    if is_reset_value(value) {
        return Ok(Vec::new());
    }
    value
        .split(',')
        .map(str::trim)
        .filter(|name| !name.is_empty())
        .map(|name| {
            tau_proto::ToolGroupName::try_new(name)
                .ok_or_else(|| format!("invalid tool group name: {name}"))
        })
        .collect()
}

fn parse_compaction_threshold_update(value: &str) -> Result<Option<u64>, String> {
    if is_reset_value(value) {
        return Ok(None);
    }
    let threshold = value
        .parse::<u64>()
        .map_err(|_| "compaction-threshold must be a token count of at least 1000".to_owned())?;
    if threshold < 1000 {
        return Err("compaction-threshold must be a token count of at least 1000".to_owned());
    }
    Ok(Some(threshold))
}

fn parse_model_action(value: &str) -> Result<tau_proto::UiRoleUpdateAction, String> {
    Ok(tau_proto::UiRoleUpdateAction::SetModel {
        model: parse_resettable(value)?,
    })
}

fn parse_relative_role_setting(
    value: &str,
) -> Result<Option<tau_proto::UiRoleSettingAdjustment>, String> {
    let (direction, amount) = value
        .split_once(':')
        .map_or((value, 1), |(direction, amount)| {
            (direction, amount.parse::<u8>().unwrap_or(0))
        });
    if direction != "increase" && direction != "decrease" {
        return Ok(None);
    }
    if amount == 0 {
        return Err("relative setting amount must be a positive integer".to_owned());
    }
    let amount = NonZeroU8::new(amount).expect("checked positive amount");
    Ok(Some(match direction {
        "increase" => tau_proto::UiRoleSettingAdjustment::Increase(amount),
        "decrease" => tau_proto::UiRoleSettingAdjustment::Decrease(amount),
        _ => unreachable!("checked above"),
    }))
}

fn parse_effort_action(value: &str) -> Result<tau_proto::UiRoleUpdateAction, String> {
    if let Some((direction, amount)) = value.split_once(':')
        && matches!(direction, "increase" | "decrease")
    {
        let amount = amount
            .parse::<tau_proto::ReasoningIntensity>()
            .map_err(|error| error.to_string())?;
        if amount.millionths() <= 0 {
            return Err("relative reasoning intensity magnitude must be positive".to_owned());
        }
        let signed = if direction == "decrease" {
            amount.millionths().saturating_neg()
        } else {
            amount.millionths()
        };
        return Ok(tau_proto::UiRoleUpdateAction::AdjustEffort {
            adjustment: tau_proto::ReasoningIntensityDelta::new(signed)
                .expect("parsed effort delta is nonzero"),
        });
    }
    let effort = parse_effort_override(value)?;
    Ok(tau_proto::UiRoleUpdateAction::SetEffort { effort })
}

/// Parses one resettable absolute effort using the same validation as role
/// effort updates.
pub(crate) fn parse_effort_override(
    value: &str,
) -> Result<Option<tau_proto::ReasoningIntent>, String> {
    let effort = parse_resettable(value)?;
    if effort.is_some_and(|intent: tau_proto::ReasoningIntent| !intent.is_nominal()) {
        return Err("absolute reasoning intensity must be between 0.0 and 1.0".to_owned());
    }
    Ok(effort)
}

fn parse_verbosity_action(value: &str) -> Result<tau_proto::UiRoleUpdateAction, String> {
    if let Some(adjustment) = parse_relative_role_setting(value)? {
        return Ok(tau_proto::UiRoleUpdateAction::AdjustVerbosity { adjustment });
    }
    Ok(tau_proto::UiRoleUpdateAction::SetVerbosity {
        verbosity: parse_resettable(value)?,
    })
}

fn parse_thinking_summary_action(value: &str) -> Result<tau_proto::UiRoleUpdateAction, String> {
    if let Some(adjustment) = parse_relative_role_setting(value)? {
        return Ok(tau_proto::UiRoleUpdateAction::AdjustThinkingSummary { adjustment });
    }
    Ok(tau_proto::UiRoleUpdateAction::SetThinkingSummary {
        thinking_summary: parse_resettable(value)?,
    })
}

fn parse_service_tier_action(value: &str) -> Result<tau_proto::UiRoleUpdateAction, String> {
    Ok(tau_proto::UiRoleUpdateAction::SetServiceTier {
        service_tier: parse_service_tier_update(value)?,
    })
}

fn parse_compaction_threshold_action(value: &str) -> Result<tau_proto::UiRoleUpdateAction, String> {
    Ok(tau_proto::UiRoleUpdateAction::SetCompactionThreshold {
        compaction_threshold: parse_compaction_threshold_update(value)?
            .map(tau_proto::TokenCount::new),
    })
}

fn parse_tools_action(value: &str) -> Result<tau_proto::UiRoleUpdateAction, String> {
    Ok(tau_proto::UiRoleUpdateAction::SetTools {
        tools: parse_tool_list_update(value)?,
    })
}

fn parse_enable_tool_groups_action(value: &str) -> Result<tau_proto::UiRoleUpdateAction, String> {
    Ok(tau_proto::UiRoleUpdateAction::SetEnableToolGroups {
        enable_tool_groups: parse_tool_group_list_update(value)?,
    })
}

fn parse_disable_tool_groups_action(value: &str) -> Result<tau_proto::UiRoleUpdateAction, String> {
    Ok(tau_proto::UiRoleUpdateAction::SetDisableToolGroups {
        disable_tool_groups: parse_tool_group_list_update(value)?,
    })
}

fn parse_enable_tools_action(value: &str) -> Result<tau_proto::UiRoleUpdateAction, String> {
    Ok(tau_proto::UiRoleUpdateAction::SetEnableTools {
        enable_tools: parse_tool_override_list_update(value)?,
    })
}

fn parse_disable_tools_action(value: &str) -> Result<tau_proto::UiRoleUpdateAction, String> {
    Ok(tau_proto::UiRoleUpdateAction::SetDisableTools {
        disable_tools: parse_tool_override_list_update(value)?,
    })
}

pub(crate) fn parse_role_setting_update(
    setting: &str,
    value: &str,
) -> Result<tau_proto::UiRoleUpdateAction, String> {
    match setting {
        "model" => parse_model_action(value),
        "effort" => parse_effort_action(value),
        "verbosity" => parse_verbosity_action(value),
        "thinking-summary" => parse_thinking_summary_action(value),
        "service-tier" => parse_service_tier_action(value),
        "compaction-threshold" => parse_compaction_threshold_action(value),
        "tools" => parse_tools_action(value),
        "enable-tool-groups" => parse_enable_tool_groups_action(value),
        "disable-tool-groups" => parse_disable_tool_groups_action(value),
        "enable-tools" => parse_enable_tools_action(value),
        "disable-tools" => parse_disable_tools_action(value),
        _ => Err("unknown setting".to_owned()),
    }
}

pub(crate) fn parse_role_command(rest: &str) -> Result<Option<Event>, String> {
    let mut parts = rest.split_whitespace();
    let role = parts.next();
    let command = parts.next();
    let value = parts.next();
    let extra = parts.next();

    let Some(role) = role else {
        return Ok(None);
    };
    let Some(command) = command else {
        return Ok(Some(crate::ui_events::role_select(role)));
    };
    if command == "delete" {
        if value.is_some() {
            return Err(":role <role> delete takes no value".to_owned());
        }
        return Ok(Some(crate::ui_events::role_update(
            role,
            tau_proto::UiRoleUpdateAction::Delete,
        )));
    }
    let Some(value) = value else {
        return Err(":role <role> <setting> <value>".to_owned());
    };
    if extra.is_some() {
        return Err(":role: too many arguments".to_owned());
    }
    let action =
        parse_role_setting_update(command, value).map_err(|error| format!(":role: {error}"))?;
    Ok(Some(crate::ui_events::role_update(role, action)))
}