codex-mobile-bridge 1.0.1

Remote bridge and service manager for codex-mobile.
Documentation
use super::history::enrich_thread_history_from_rollout;
use super::*;

impl BridgeState {
    pub(super) fn thread_response_payload(
        &self,
        runtime_id: &str,
        result: &Value,
        archived: bool,
    ) -> Result<Value> {
        let thread_value = result.get("thread").context("返回缺少 thread 字段")?;
        let thread = normalize_thread(runtime_id, thread_value, archived)?;
        self.storage.upsert_thread_index(&thread)?;
        let _ = self.storage.record_directory_usage(Path::new(&thread.cwd));
        let _ = self.emit_directory_state();
        let thread = self.storage.get_thread_index(&thread.id)?.unwrap_or(thread);
        let effective_cwd = result
            .get("cwd")
            .and_then(Value::as_str)
            .map(ToString::to_string)
            .unwrap_or_else(|| thread.cwd.clone());
        let runtime_codex_home = self
            .storage
            .get_runtime(runtime_id)?
            .and_then(|runtime| runtime.codex_home);
        let mut render_thread = thread_value.clone();
        enrich_thread_history_from_rollout(&mut render_thread, runtime_codex_home.as_deref());
        let render_snapshot = self.cache_thread_render_snapshot(build_thread_render_snapshot(
            runtime_id,
            &render_thread,
        )?);
        let active_turn = extract_active_turn_payload(&render_thread);

        Ok(json!({
            "runtimeId": runtime_id,
            "thread": thread,
            "effectiveCwd": effective_cwd,
            "renderSnapshot": render_snapshot,
            "activeTurn": active_turn,
        }))
    }
}

fn extract_active_turn_payload(thread: &Value) -> Option<Value> {
    let thread_active = thread
        .get("status")
        .and_then(|status| {
            status
                .get("type")
                .and_then(Value::as_str)
                .or_else(|| status.as_str())
        })
        .is_some_and(|status| status == "active");
    if !thread_active {
        return None;
    }
    let turns = thread.get("turns").and_then(Value::as_array)?;
    let active_turn = turns.iter().rev().find(|turn| {
        turn.get("status")
            .and_then(Value::as_str)
            .is_some_and(|status| status == "inProgress")
    })?;
    let turn_id = active_turn.get("id").and_then(Value::as_str)?;
    Some(json!({
        "turnId": turn_id,
        "startedAtMs": active_turn
            .get("startedAt")
            .and_then(thread_started_at_ms),
    }))
}

fn thread_started_at_ms(value: &Value) -> Option<i64> {
    let started_at = value
        .as_i64()
        .or_else(|| value.as_u64().and_then(|raw| i64::try_from(raw).ok()))?;
    if started_at >= 10_000_000_000 {
        Some(started_at)
    } else {
        started_at.checked_mul(1_000)
    }
}

#[cfg(test)]
mod tests {
    use serde_json::json;

    use super::extract_active_turn_payload;

    #[test]
    fn extract_active_turn_payload_converts_seconds_to_millis() {
        let thread = json!({
            "status": {
                "type": "active"
            },
            "turns": [
                {
                    "id": "turn-complete",
                    "status": "completed",
                    "startedAt": 1_710_000_000
                },
                {
                    "id": "turn-active",
                    "status": "inProgress",
                    "startedAt": 1_710_000_123
                }
            ]
        });

        assert_eq!(
            extract_active_turn_payload(&thread),
            Some(json!({
                "turnId": "turn-active",
                "startedAtMs": 1_710_000_123_000_i64,
            })),
        );
    }
}