terminal-info 1.4.2

An extensible terminal information CLI and developer toolbox
Documentation
use serde::Deserialize;

use crate::ai::agent::ApprovalKind;
use crate::config::AiAgentCliConfig;

#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum AgentAdapterKind {
    Codex,
    ClaudeCode,
    Gemini,
    Generic,
}

impl AgentAdapterKind {
    pub fn label(self) -> &'static str {
        match self {
            Self::Codex => "codex",
            Self::ClaudeCode => "claude_code",
            Self::Gemini => "gemini",
            Self::Generic => "generic",
        }
    }

    pub fn from_config(value: Option<&str>) -> Self {
        match value.unwrap_or("generic") {
            "codex" => Self::Codex,
            "claude_code" | "claude" => Self::ClaudeCode,
            "gemini" => Self::Gemini,
            _ => Self::Generic,
        }
    }
}

#[derive(Debug, Clone)]
pub struct ConfiguredAgent {
    pub id: String,
    pub adapter: AgentAdapterKind,
    pub config: AiAgentCliConfig,
}

pub trait AgentAdapter: Send + Sync {
    fn kind(&self) -> AgentAdapterKind;
    fn display_name(&self) -> &'static str;
    fn default_command(&self) -> &'static str;
    fn supports_pause_resume(&self) -> bool {
        true
    }
    fn supports_local_intercept(&self) -> bool {
        false
    }
    fn parse_line(&self, line: &str) -> Vec<AdapterFrame>;
}

#[derive(Debug, Clone)]
pub enum AdapterFrame {
    Log {
        level: String,
        message: String,
    },
    Event {
        event_type: String,
        message: Option<String>,
    },
    Approval {
        kind: ApprovalKind,
        action: String,
        details: Option<String>,
    },
    Task {
        description: String,
    },
}

#[derive(Debug, Deserialize)]
struct StructuredLine {
    #[serde(rename = "type")]
    kind: String,
    level: Option<String>,
    event_type: Option<String>,
    message: Option<String>,
    action: Option<String>,
    details: Option<String>,
    approval_kind: Option<String>,
    description: Option<String>,
}

struct GenericAdapter {
    kind: AgentAdapterKind,
    display_name: &'static str,
    default_command: &'static str,
    local_intercept: bool,
}

impl AgentAdapter for GenericAdapter {
    fn kind(&self) -> AgentAdapterKind {
        self.kind
    }

    fn display_name(&self) -> &'static str {
        self.display_name
    }

    fn default_command(&self) -> &'static str {
        self.default_command
    }

    fn supports_local_intercept(&self) -> bool {
        self.local_intercept
    }

    fn parse_line(&self, line: &str) -> Vec<AdapterFrame> {
        parse_structured_line(line)
    }
}

pub fn adapter_for(kind: AgentAdapterKind) -> Box<dyn AgentAdapter> {
    match kind {
        AgentAdapterKind::Codex => Box::new(GenericAdapter {
            kind,
            display_name: "Codex CLI",
            default_command: "codex",
            local_intercept: true,
        }),
        AgentAdapterKind::ClaudeCode => Box::new(GenericAdapter {
            kind,
            display_name: "Claude Code",
            default_command: "claude",
            local_intercept: true,
        }),
        AgentAdapterKind::Gemini => Box::new(GenericAdapter {
            kind,
            display_name: "Gemini CLI",
            default_command: "gemini",
            local_intercept: true,
        }),
        AgentAdapterKind::Generic => Box::new(GenericAdapter {
            kind,
            display_name: "Generic Agent CLI",
            default_command: "",
            local_intercept: false,
        }),
    }
}

pub fn parse_structured_line(line: &str) -> Vec<AdapterFrame> {
    let payload = line
        .strip_prefix("TINFO:")
        .or_else(|| line.strip_prefix("[tinfo]"))
        .unwrap_or(line)
        .trim();

    let Ok(parsed) = serde_json::from_str::<StructuredLine>(payload) else {
        return Vec::new();
    };

    match parsed.kind.as_str() {
        "log" => vec![AdapterFrame::Log {
            level: parsed.level.unwrap_or_else(|| "info".to_string()),
            message: parsed.message.unwrap_or_default(),
        }],
        "event" => vec![AdapterFrame::Event {
            event_type: parsed.event_type.unwrap_or_else(|| "output_stream".to_string()),
            message: parsed.message,
        }],
        "approval" => vec![AdapterFrame::Approval {
            kind: parse_approval_kind(parsed.approval_kind.as_deref()),
            action: parsed.action.unwrap_or_else(|| "approval requested".to_string()),
            details: parsed.details,
        }],
        "task" => vec![AdapterFrame::Task {
            description: parsed.description.unwrap_or_else(|| "task".to_string()),
        }],
        _ => Vec::new(),
    }
}

fn parse_approval_kind(value: Option<&str>) -> ApprovalKind {
    match value.unwrap_or("other") {
        "shell_command" => ApprovalKind::ShellCommand,
        "file_write" => ApprovalKind::FileWrite,
        "network_call" => ApprovalKind::NetworkCall,
        "package_install" => ApprovalKind::PackageInstall,
        _ => ApprovalKind::Other,
    }
}