mobius-cli 0.11.2

The terminal client for a möbius gateway
Documentation
use mobius::Result;
use mobius_gateway::wire::{ClientMessage, ProfileSnapshot, ProviderInstance, ServerMessage};
use uuid::Uuid;

use super::catalog::GatewayAction;
use super::provider_instance_label;

pub(super) type PreparedAction = Box<ClientMessage>;

pub(super) fn prepare(action: GatewayAction) -> Result<PreparedAction> {
    match action {
        GatewayAction::Pair => Ok(send(|request_id| ClientMessage::CreatePairingCode {
            request_id,
        })),
        GatewayAction::Profile => Ok(send(|request_id| ClientMessage::GetProfile { request_id })),
    }
}

pub(super) fn render_response(
    message: &ServerMessage,
    provider_instances: &[ProviderInstance],
) -> Option<String> {
    match message {
        ServerMessage::Accepted { .. } => None,
        ServerMessage::Rejected { message, .. } | ServerMessage::Error { message, .. } => {
            Some(message.clone())
        }
        ServerMessage::ProviderCredentialSaved { provider, .. } => {
            Some(format!("{provider}: configured"))
        }
        ServerMessage::PairingCode {
            code, expires_at, ..
        } => Some(format!("one-time code {code} · expires {expires_at}")),
        ServerMessage::ProviderLoginStarted {
            provider,
            verification_url,
            user_code,
            ..
        } => Some(format!(
            "{provider} login · open {verification_url} · enter {user_code}"
        )),
        ServerMessage::ProviderLoginFinished { provider, .. } => {
            Some(format!("{provider} login complete"))
        }
        ServerMessage::Profile { profile, .. } => Some(render_profile(profile, provider_instances)),
        _ => None,
    }
}

fn request_id() -> String {
    Uuid::new_v4().to_string()
}

fn send(build: impl FnOnce(String) -> ClientMessage) -> PreparedAction {
    Box::new(build(request_id()))
}

fn render_profile(profile: &ProfileSnapshot, provider_instances: &[ProviderInstance]) -> String {
    let mut lines = vec![profile.user_name.as_deref().unwrap_or("user").into()];
    lines.extend(profile.daily_usage.iter().map(|day| {
        let provider =
            provider_instance_label(provider_instances, &day.provider).unwrap_or(&day.provider);
        format!(
            "day {} · {} · {} tokens · {} cached",
            day.unix_day, provider, day.usage.total_tokens, day.usage.cached_input_tokens
        )
    }));
    lines.join("\n")
}

#[cfg(test)]
mod tests {
    use mobius::protocol::TokenUsage;
    use mobius_gateway::wire::{AgentComposition, DailyUsage, RunStats};

    use super::*;

    #[test]
    fn profile_usage_names_each_provider() {
        let mut selection = AgentComposition::default().provider;
        selection.instance = "provider-instance".into();
        let instances = [ProviderInstance {
            label: "Work".into(),
            tint: Default::default(),
            configured: true,
            credential_hint: None,
            selection,
            model_ids: Vec::new(),
            reasoning_efforts: Vec::new(),
        }];
        let profile = ProfileSnapshot {
            user_name: Some("user".into()),
            daily_usage: vec![DailyUsage {
                unix_day: 7,
                provider: "provider-instance".into(),
                usage: TokenUsage {
                    total_tokens: 11,
                    ..TokenUsage::default()
                },
            }],
            run_stats: RunStats::default(),
            recent_run_groups: Vec::new(),
        };

        assert_eq!(
            render_profile(&profile, &instances),
            "user\nday 7 · Work · 11 tokens · 0 cached"
        );
    }

    #[test]
    fn generic_acceptance_is_not_transcript_content() {
        let accepted = ServerMessage::Accepted {
            request_id: "request".into(),
        };

        assert!(render_response(&accepted, &[]).is_none());
    }
}