codex-mobile-bridge 0.2.11

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

use super::super::helpers::optional_string;
use super::diff::{change_path, summarize_change_counts, summarize_diff_text};
use super::metadata::{TimelineSemanticInfo, TimelineSummaryInfo};
use super::semantic::{
    command_action_query, command_action_target, detail_label,
    extract_query_from_web_search_action, extract_targets_from_payload, first_search_query,
    first_shell_command, payload_string, push_unique, semantic_title,
};

pub(super) fn timeline_summary_from_item(
    entry_type: &str,
    item: &Value,
    payload: &Value,
    semantic: Option<&TimelineSemanticInfo>,
) -> Option<TimelineSummaryInfo> {
    let semantic = semantic?;
    match semantic.kind.as_str() {
        "ran" => Some(timeline_summary_for_ran(item, payload, semantic)),
        "explored" => Some(timeline_summary_for_explored(item, payload, semantic)),
        "edited" => Some(timeline_summary_for_edited(
            entry_type, item, payload, semantic,
        )),
        "waited" => Some(timeline_summary_for_waited(payload, semantic)),
        _ => None,
    }
}

pub(super) fn timeline_summary_from_diff_text(diff: &str) -> TimelineSummaryInfo {
    let (primary_path, file_count, add_lines, remove_lines) = summarize_diff_text(diff);
    TimelineSummaryInfo {
        title: Some("Edited".to_string()),
        label: Some("Edited".to_string()),
        primary_path,
        file_count: Some(file_count),
        add_lines: Some(add_lines),
        remove_lines: Some(remove_lines),
        ..TimelineSummaryInfo::default()
    }
}

fn timeline_summary_for_ran(
    item: &Value,
    payload: &Value,
    semantic: &TimelineSemanticInfo,
) -> TimelineSummaryInfo {
    TimelineSummaryInfo {
        title: Some(semantic_title(&semantic.kind).to_string()),
        label: Some(detail_label(semantic.detail.as_deref()).to_string()),
        command: payload_string(payload, "command")
            .or_else(|| first_shell_command(payload))
            .or_else(|| optional_string(item, "command")),
        ..TimelineSummaryInfo::default()
    }
}

fn timeline_summary_for_explored(
    item: &Value,
    payload: &Value,
    semantic: &TimelineSemanticInfo,
) -> TimelineSummaryInfo {
    let mut summary = TimelineSummaryInfo {
        title: Some(semantic_title(&semantic.kind).to_string()),
        label: Some(detail_label(semantic.detail.as_deref()).to_string()),
        command: payload_string(payload, "command")
            .or_else(|| first_shell_command(payload))
            .or_else(|| optional_string(item, "command")),
        query: payload_string(payload, "query")
            .or_else(|| first_search_query(payload))
            .or_else(|| extract_query_from_web_search_action(payload)),
        ..TimelineSummaryInfo::default()
    };

    if let Some(actions) = payload.get("commandActions").and_then(Value::as_array) {
        for action in actions {
            if let Some(target) = command_action_target(action) {
                push_unique(&mut summary.targets, target);
            }
            if summary.query.is_none() {
                summary.query = command_action_query(action);
            }
        }
    }

    if summary.targets.is_empty() {
        extract_targets_from_payload(payload)
            .into_iter()
            .for_each(|target| push_unique(&mut summary.targets, target));
    }
    if summary.command.is_none() {
        summary.command = optional_string(item, "command");
    }
    summary
}

fn timeline_summary_for_edited(
    entry_type: &str,
    item: &Value,
    payload: &Value,
    semantic: &TimelineSemanticInfo,
) -> TimelineSummaryInfo {
    if entry_type == "fileChange" {
        let changes = payload
            .get("changes")
            .and_then(Value::as_array)
            .map(Vec::as_slice)
            .unwrap_or(&[]);
        return timeline_summary_from_file_changes(changes, semantic);
    }
    if entry_type == "diff" {
        return timeline_summary_from_diff_text(&pretty_json_value(
            item.get("diff").unwrap_or(&Value::Null),
        ));
    }

    let patch_text = payload_string(payload, "operation")
        .or_else(|| payload_string(payload, "output"))
        .unwrap_or_default();
    let mut summary = timeline_summary_from_diff_text(&patch_text);
    summary.title = Some(semantic_title(&semantic.kind).to_string());
    summary.label = Some(semantic_title(&semantic.kind).to_string());
    summary
}

fn timeline_summary_for_waited(
    payload: &Value,
    semantic: &TimelineSemanticInfo,
) -> TimelineSummaryInfo {
    let wait_count = payload
        .get("receiverThreadIds")
        .and_then(Value::as_array)
        .map(|items| items.len() as i64)
        .or_else(|| {
            payload
                .get("agentsStates")
                .and_then(Value::as_object)
                .map(|items| items.len() as i64)
        });
    TimelineSummaryInfo {
        title: Some(semantic_title(&semantic.kind).to_string()),
        label: Some(detail_label(semantic.detail.as_deref()).to_string()),
        wait_count,
        ..TimelineSummaryInfo::default()
    }
}

fn timeline_summary_from_file_changes(
    changes: &[Value],
    semantic: &TimelineSemanticInfo,
) -> TimelineSummaryInfo {
    let mut summary = TimelineSummaryInfo {
        title: Some(semantic_title(&semantic.kind).to_string()),
        label: Some(semantic_title(&semantic.kind).to_string()),
        file_count: Some(changes.len() as i64),
        ..TimelineSummaryInfo::default()
    };
    let mut add_lines = 0_i64;
    let mut remove_lines = 0_i64;
    for change in changes {
        if summary.primary_path.is_none() {
            summary.primary_path = change_path(change);
        }
        let (added, removed) = summarize_change_counts(change);
        add_lines += added;
        remove_lines += removed;
    }
    summary.add_lines = Some(add_lines);
    summary.remove_lines = Some(remove_lines);
    summary
}

fn pretty_json_value(value: &Value) -> String {
    match value {
        Value::Null => String::new(),
        Value::String(text) => text.clone(),
        _ => serde_json::to_string_pretty(value).unwrap_or_default(),
    }
}