codex-mobile-bridge 0.2.6

Remote bridge and service manager for codex-mobile.
Documentation
use anyhow::Result;
use serde_json::{json, Value};

use super::super::helpers::{optional_string, required_string};
use super::content;
use super::content::{format_plan_payload, timeline_entry_status};
use super::metadata::{
    timeline_collapse_hint, timeline_has_visible_content, timeline_lifecycle_info,
    timeline_metadata, timeline_render_kind, timeline_stream_metadata,
};
use super::metadata_merge::finalize_timeline_entries;
use super::normalize::{canonical_timeline_entry_type, timeline_entry_id};
use super::payload::timeline_payload_from_thread_item;
use super::semantic::timeline_semantic_from_item;
use super::summary::timeline_summary_from_item;
use crate::bridge_protocol::TimelineEntry;

pub(super) fn timeline_entries_from_thread(
    runtime_id: &str,
    thread: &Value,
) -> Result<Vec<TimelineEntry>> {
    let thread_id = required_string(thread, "id")?.to_string();
    let mut entries = Vec::new();

    for turn in thread
        .get("turns")
        .and_then(Value::as_array)
        .into_iter()
        .flatten()
    {
        let turn_id = optional_string(turn, "id");
        for item in turn
            .get("items")
            .and_then(Value::as_array)
            .into_iter()
            .flatten()
        {
            if let Some(entry) = timeline_entry_from_thread_item(
                runtime_id,
                &thread_id,
                turn_id.as_deref(),
                item,
                "thread_item",
                false,
                true,
            ) {
                entries.push(entry);
            }
        }
    }

    finalize_timeline_entries(&mut entries);
    Ok(entries)
}

pub(super) fn timeline_entry_from_thread_item(
    runtime_id: &str,
    thread_id: &str,
    turn_id: Option<&str>,
    item: &Value,
    source_kind: &str,
    is_streaming: bool,
    authoritative: bool,
) -> Option<TimelineEntry> {
    let item_id = optional_string(item, "id");
    let raw_type = required_string(item, "type").ok()?.to_string();
    let entry_type = canonical_timeline_entry_type(&raw_type, item);
    let render_kind = timeline_render_kind(&entry_type);
    let stream_phase = optional_string(item, "phase");
    let payload = timeline_payload_from_thread_item(&entry_type, item);
    let semantic = timeline_semantic_from_item(&raw_type, &entry_type, item, &payload);
    let summary = timeline_summary_from_item(&entry_type, item, &payload, semantic.as_ref());
    let text = content::timeline_text_from_thread_item(&entry_type, item);
    let entry_status = timeline_entry_status(&entry_type, item, is_streaming);
    let lifecycle = timeline_lifecycle_info(
        authoritative,
        entry_status.as_deref(),
        timeline_has_visible_content(&text, &payload, summary.as_ref()),
    );
    let metadata = timeline_metadata(
        source_kind,
        &raw_type,
        render_kind,
        timeline_collapse_hint(render_kind, &entry_type),
        timeline_stream_metadata(
            is_streaming,
            authoritative,
            stream_phase.clone(),
            None,
            None,
        ),
        Some(&lifecycle),
        payload,
        semantic.as_ref(),
        summary.as_ref(),
        item.clone(),
    );

    Some(TimelineEntry {
        id: timeline_entry_id(turn_id, item_id.as_deref(), &entry_type),
        runtime_id: runtime_id.to_string(),
        thread_id: thread_id.to_string(),
        turn_id: turn_id.map(ToOwned::to_owned),
        item_id,
        entry_type: entry_type.clone(),
        title: content::timeline_entry_title(&entry_type, item),
        text,
        status: entry_status,
        metadata,
    })
}

pub(super) fn timeline_entry_from_plan_update(
    runtime_id: &str,
    thread_id: &str,
    turn_id: &str,
    explanation: Option<String>,
    plan: Value,
) -> TimelineEntry {
    let item_id = format!("turn-plan:{turn_id}");
    TimelineEntry {
        id: timeline_entry_id(Some(turn_id), Some(&item_id), "plan"),
        runtime_id: runtime_id.to_string(),
        thread_id: thread_id.to_string(),
        turn_id: Some(turn_id.to_string()),
        item_id: Some(item_id),
        entry_type: "plan".to_string(),
        title: Some("执行计划".to_string()),
        text: format_plan_payload(explanation.as_deref(), plan.as_array()),
        status: Some("inProgress".to_string()),
        metadata: timeline_metadata(
            "stream_event",
            "turn/plan/updated",
            "plan",
            timeline_collapse_hint("plan", "plan"),
            timeline_stream_metadata(true, false, None, None, None),
            None,
            json!({
                "explanation": explanation,
                "plan": plan,
            }),
            None,
            None,
            json!({
                "explanation": explanation,
                "plan": plan,
            }),
        ),
    }
}