codex-mobile-bridge 0.2.9

Remote bridge and service manager for codex-mobile.
Documentation
use serde_json::Value;

use super::super::helpers::optional_string;
use super::diff::looks_like_patch_text;
use super::metadata::TimelineSemanticInfo;

pub(super) fn timeline_semantic_from_item(
    raw_type: &str,
    entry_type: &str,
    item: &Value,
    payload: &Value,
) -> Option<TimelineSemanticInfo> {
    match entry_type {
        "commandExecution" => {
            let actions = payload
                .get("commandActions")
                .and_then(Value::as_array)
                .map(Vec::as_slice)
                .unwrap_or(&[]);
            if let Some(detail) = explored_detail_from_command_actions(actions) {
                return Some(TimelineSemanticInfo::new(
                    "explored",
                    Some(detail.to_string()),
                    "high",
                    "primary",
                ));
            }
            Some(TimelineSemanticInfo::new(
                "ran",
                Some("run".to_string()),
                "high",
                "primary",
            ))
        }
        "webSearch" => Some(TimelineSemanticInfo::new(
            "explored",
            Some("search".to_string()),
            "high",
            "primary",
        )),
        "fileChange" | "diff" => Some(TimelineSemanticInfo::new("edited", None, "high", "primary")),
        "collabToolCall" => {
            let tool = payload_string(payload, "tool")
                .or_else(|| optional_string(item, "tool"))
                .unwrap_or_default();
            if normalize_token(&tool) == "wait" {
                return Some(TimelineSemanticInfo::new(
                    "waited",
                    Some("wait_agents".to_string()),
                    "high",
                    "primary",
                ));
            }
            None
        }
        "shell_call" | "local_shell_call" => classify_shell_like_semantic(payload, "primary"),
        "web_search_call" | "file_search_call" => Some(TimelineSemanticInfo::new(
            "explored",
            Some("search".to_string()),
            "low",
            "primary",
        )),
        "apply_patch_call" | "apply_patch_call_output" => {
            let patch_like = payload_string(payload, "operation")
                .or_else(|| payload_string(payload, "output"))
                .is_some_and(|text| looks_like_patch_text(&text));
            patch_like.then(|| {
                TimelineSemanticInfo::new(
                    "edited",
                    None,
                    "low",
                    if entry_type.ends_with("_output") {
                        "output"
                    } else {
                        "primary"
                    },
                )
            })
        }
        _ => {
            let normalized_raw_type = normalize_token(raw_type);
            if normalized_raw_type == "websearchcall" || normalized_raw_type == "filesearchcall" {
                return Some(TimelineSemanticInfo::new(
                    "explored",
                    Some("search".to_string()),
                    "low",
                    "primary",
                ));
            }
            None
        }
    }
}

pub(super) fn semantic_title(kind: &str) -> &'static str {
    match kind {
        "ran" => "Ran",
        "explored" => "Explored",
        "edited" => "Edited",
        "waited" => "Waited",
        _ => "Tool",
    }
}

pub(super) fn detail_label(detail: Option<&str>) -> &'static str {
    match detail.unwrap_or_default() {
        "read" => "Read",
        "list" => "List",
        "search" => "Search",
        "mixed" => "Mixed",
        "wait_agents" => "Wait",
        "run" | "" => "Run",
        _ => "Run",
    }
}

pub(super) fn payload_string(payload: &Value, key: &str) -> Option<String> {
    payload
        .get(key)
        .and_then(Value::as_str)
        .map(ToOwned::to_owned)
}

pub(super) fn first_shell_command(payload: &Value) -> Option<String> {
    payload_string(payload, "command").or_else(|| {
        payload
            .get("commands")
            .and_then(Value::as_array)
            .into_iter()
            .flatten()
            .find_map(|item| match item {
                Value::String(text) => Some(text.clone()),
                Value::Object(_) => item
                    .get("command")
                    .and_then(Value::as_str)
                    .map(ToOwned::to_owned),
                _ => None,
            })
    })
}

pub(super) fn first_search_query(payload: &Value) -> Option<String> {
    payload_string(payload, "query").or_else(|| {
        payload
            .get("queries")
            .and_then(Value::as_array)
            .into_iter()
            .flatten()
            .find_map(|item| match item {
                Value::String(text) => Some(text.clone()),
                Value::Object(_) => item
                    .get("query")
                    .or_else(|| item.get("text"))
                    .and_then(Value::as_str)
                    .map(ToOwned::to_owned),
                _ => None,
            })
    })
}

pub(super) fn extract_query_from_web_search_action(payload: &Value) -> Option<String> {
    let action = payload.get("action")?;
    action
        .get("query")
        .or_else(|| action.get("pattern"))
        .and_then(Value::as_str)
        .map(ToOwned::to_owned)
        .or_else(|| {
            action
                .get("queries")
                .and_then(Value::as_array)
                .into_iter()
                .flatten()
                .filter_map(Value::as_str)
                .map(ToOwned::to_owned)
                .next()
        })
}

pub(super) fn command_action_target(action: &Value) -> Option<String> {
    let action_type = normalize_token(&payload_string(action, "type").unwrap_or_default());
    match action_type.as_str() {
        "read" => payload_string(action, "name").or_else(|| payload_string(action, "path")),
        "listfiles" => payload_string(action, "path").or_else(|| payload_string(action, "command")),
        "search" => payload_string(action, "path"),
        _ => None,
    }
}

pub(super) fn command_action_query(action: &Value) -> Option<String> {
    let action_type = normalize_token(&payload_string(action, "type").unwrap_or_default());
    (action_type == "search")
        .then(|| payload_string(action, "query").or_else(|| payload_string(action, "command")))
        .flatten()
}

pub(super) fn extract_targets_from_payload(payload: &Value) -> Vec<String> {
    let mut targets = Vec::new();
    if let Some(path) = payload_string(payload, "path") {
        push_unique(&mut targets, path);
    }
    if let Some(items) = payload.get("queries").and_then(Value::as_array) {
        items.iter().for_each(|item| {
            if let Some(text) = item.as_str().map(ToOwned::to_owned) {
                push_unique(&mut targets, text);
            }
        });
    }
    targets
}

pub(super) fn normalize_token(value: &str) -> String {
    value
        .trim()
        .to_lowercase()
        .replace(['_', '-', '\n', '\r', '\t'], " ")
        .split_whitespace()
        .collect::<Vec<_>>()
        .join(" ")
}

pub(super) fn push_unique(values: &mut Vec<String>, candidate: String) {
    if candidate.trim().is_empty() || values.iter().any(|value| value == &candidate) {
        return;
    }
    values.push(candidate);
}

fn classify_shell_like_semantic(payload: &Value, role: &str) -> Option<TimelineSemanticInfo> {
    let command = first_shell_command(payload)?;
    let normalized = normalize_token(&unwrap_shell_wrapper(&command));
    let detail = if normalized.starts_with("cat ")
        || normalized.starts_with("sed ")
        || normalized.starts_with("head ")
        || normalized.starts_with("tail ")
        || normalized.starts_with("bat ")
        || normalized == "cat"
        || normalized == "sed"
        || normalized == "head"
        || normalized == "tail"
        || normalized == "bat"
    {
        "read"
    } else if normalized.starts_with("ls ")
        || normalized.starts_with("tree ")
        || normalized.starts_with("find ")
        || normalized.starts_with("fd ")
        || normalized == "ls"
        || normalized == "tree"
        || normalized == "find"
        || normalized == "fd"
    {
        "list"
    } else if normalized.starts_with("rg ")
        || normalized.starts_with("grep ")
        || normalized.starts_with("git grep ")
        || normalized.starts_with("findstr ")
        || normalized == "rg"
        || normalized == "grep"
        || normalized == "findstr"
    {
        "search"
    } else {
        "run"
    };
    let kind = if detail == "run" { "ran" } else { "explored" };
    Some(TimelineSemanticInfo::new(
        kind,
        Some(detail.to_string()),
        "low",
        role.to_string(),
    ))
}

fn explored_detail_from_command_actions(actions: &[Value]) -> Option<&'static str> {
    if actions.is_empty() {
        return None;
    }
    let mut has_read = false;
    let mut has_list = false;
    let mut has_search = false;
    for action in actions {
        match normalize_token(&payload_string(action, "type").unwrap_or_default()).as_str() {
            "read" => has_read = true,
            "listfiles" => has_list = true,
            "search" => has_search = true,
            _ => return None,
        }
    }
    Some(match (has_read, has_list, has_search) {
        (true, false, false) => "read",
        (false, true, false) => "list",
        (false, false, true) => "search",
        _ => "mixed",
    })
}

fn unwrap_shell_wrapper(command: &str) -> String {
    let trimmed = command.trim();
    for marker in [" -lc ", " -c "] {
        if let Some((_, script)) = trimmed.split_once(marker) {
            return script
                .trim()
                .trim_matches('\'')
                .trim_matches('"')
                .to_string();
        }
    }
    trimmed.to_string()
}