codex-mobile-bridge 0.3.6

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

use super::format::{extract_message_text, format_json_value, plan_steps, preview_text};
use super::{
    PREVIEW_LIMIT, StatusSurfaceState, ThreadRenderNode, ThreadRenderSnapshot, node_id_value,
    node_turn_id, optional_turn_id_from_params, render_node_id, render_notice_node,
    set_inline_message_from_last_node, upsert_node_by_id, working_status,
};
use crate::state::helpers::optional_string;

pub(super) fn upsert_plan_update_node(snapshot: &mut ThreadRenderSnapshot, params: &Value) {
    let turn_id = optional_string(params, "turnId");
    let item_id = turn_id
        .as_ref()
        .map(|turn_id| format!("turn-plan:{turn_id}"));
    upsert_node_by_id(
        snapshot,
        ThreadRenderNode::PlanUpdate {
            id: render_node_id(turn_id.as_deref(), item_id.as_deref(), "plan-update"),
            turn_id,
            item_id,
            title: "Updated plan".to_string(),
            explanation: optional_string(params, "explanation"),
            steps: plan_steps(params.get("plan").unwrap_or(&Value::Null)),
            streaming: true,
        },
    );
}

pub(super) fn upsert_assistant_delta(snapshot: &mut ThreadRenderSnapshot, params: &Value) {
    let item_id = optional_string(params, "itemId");
    let turn_id = optional_string(params, "turnId");
    let delta = optional_string(params, "delta").unwrap_or_default();
    let node_id = render_node_id(turn_id.as_deref(), item_id.as_deref(), "assistant");
    match snapshot
        .nodes
        .iter_mut()
        .find(|node| node_id_value(node) == node_id)
    {
        Some(ThreadRenderNode::AssistantMarkdown {
            text, streaming, ..
        }) => {
            text.push_str(&delta);
            *streaming = true;
        }
        _ => upsert_node_by_id(
            snapshot,
            ThreadRenderNode::AssistantMarkdown {
                id: node_id,
                turn_id,
                item_id,
                text: delta,
                phase: None,
                streaming: true,
            },
        ),
    }
}

pub(super) fn upsert_plan_delta(snapshot: &mut ThreadRenderSnapshot, params: &Value) {
    let item_id = optional_string(params, "itemId");
    let turn_id = optional_string(params, "turnId");
    let delta = optional_string(params, "delta").unwrap_or_default();
    let node_id = render_node_id(turn_id.as_deref(), item_id.as_deref(), "proposed-plan");
    match snapshot
        .nodes
        .iter_mut()
        .find(|node| node_id_value(node) == node_id)
    {
        Some(ThreadRenderNode::ProposedPlan { text, .. }) => text.push_str(&delta),
        _ => upsert_node_by_id(
            snapshot,
            ThreadRenderNode::ProposedPlan {
                id: node_id,
                turn_id,
                item_id,
                title: "Proposed plan".to_string(),
                text: delta,
            },
        ),
    }
}

pub(super) fn upsert_exec_output_delta(snapshot: &mut ThreadRenderSnapshot, params: &Value) {
    let item_id = optional_string(params, "itemId");
    let turn_id = optional_string(params, "turnId");
    let delta = optional_string(params, "delta").unwrap_or_default();
    let node_id = render_node_id(turn_id.as_deref(), item_id.as_deref(), "exec");
    match snapshot
        .nodes
        .iter_mut()
        .find(|node| node_id_value(node) == node_id)
    {
        Some(ThreadRenderNode::ExecGroup {
            output_text, state, ..
        }) => {
            output_text.get_or_insert_with(String::new).push_str(&delta);
            *state = "inProgress".to_string();
        }
        _ => upsert_node_by_id(
            snapshot,
            ThreadRenderNode::ExecGroup {
                id: node_id,
                turn_id,
                item_id,
                title: "Running".to_string(),
                kind: String::new(),
                state: "inProgress".to_string(),
                commands: Vec::new(),
                output_text: Some(delta),
                exit_code: None,
            },
        ),
    }
}

pub(super) fn upsert_file_change_placeholder(snapshot: &mut ThreadRenderSnapshot, params: &Value) {
    let item_id = optional_string(params, "itemId");
    let turn_id = optional_string(params, "turnId");
    let node_id = render_node_id(turn_id.as_deref(), item_id.as_deref(), "file-change");
    if snapshot
        .nodes
        .iter()
        .any(|node| node_id_value(node) == node_id)
    {
        return;
    }
    upsert_node_by_id(
        snapshot,
        ThreadRenderNode::FileChange {
            id: node_id,
            turn_id,
            item_id,
            title: "Editing".to_string(),
            state: "inProgress".to_string(),
            changes: Vec::new(),
        },
    );
}

pub(super) fn upsert_mcp_progress(snapshot: &mut ThreadRenderSnapshot, params: &Value) {
    let item_id = optional_string(params, "itemId");
    let turn_id = optional_string(params, "turnId");
    let detail = optional_string(params, "message");
    let node_id = render_node_id(turn_id.as_deref(), item_id.as_deref(), "mcp");
    match snapshot
        .nodes
        .iter_mut()
        .find(|node| node_id_value(node) == node_id)
    {
        Some(ThreadRenderNode::McpToolCall {
            detail: current,
            state,
            ..
        }) => {
            *current = detail;
            *state = "inProgress".to_string();
        }
        _ => upsert_node_by_id(
            snapshot,
            ThreadRenderNode::McpToolCall {
                id: node_id,
                turn_id,
                item_id,
                title: "Calling".to_string(),
                state: "inProgress".to_string(),
                subtitle: "mcp tool".to_string(),
                detail,
            },
        ),
    }
}

pub(super) fn ensure_reasoning_node(snapshot: &mut ThreadRenderSnapshot, params: &Value) {
    let item_id = optional_string(params, "itemId");
    let turn_id = optional_string(params, "turnId");
    let node_id = render_node_id(turn_id.as_deref(), item_id.as_deref(), "reasoning");
    if snapshot
        .nodes
        .iter()
        .any(|node| node_id_value(node) == node_id)
    {
        return;
    }
    upsert_node_by_id(
        snapshot,
        ThreadRenderNode::ReasoningSummary {
            id: node_id,
            turn_id,
            item_id,
            title: "Thinking".to_string(),
            text: String::new(),
        },
    );
}

pub(super) fn append_reasoning_delta(
    snapshot: &mut ThreadRenderSnapshot,
    params: &Value,
    summary: bool,
) {
    ensure_reasoning_node(snapshot, params);
    let item_id = optional_string(params, "itemId");
    let turn_id = optional_string(params, "turnId");
    let delta = optional_string(params, "delta").unwrap_or_default();
    let node_id = render_node_id(turn_id.as_deref(), item_id.as_deref(), "reasoning");
    if let Some(ThreadRenderNode::ReasoningSummary { text, .. }) = snapshot
        .nodes
        .iter_mut()
        .find(|node| node_id_value(node) == node_id)
    {
        if summary || text.is_empty() {
            text.push_str(&delta);
        }
    }
}

pub(super) fn push_notice(
    snapshot: &mut ThreadRenderSnapshot,
    params: &Value,
    title: &str,
    detail: Option<String>,
) {
    let turn_id = optional_string(params, "turnId");
    let item_id = optional_string(params, "itemId");
    snapshot.nodes.push(render_notice_node(
        turn_id.as_deref(),
        item_id,
        title,
        detail,
    ));
    set_inline_message_from_last_node(snapshot);
}

pub(super) fn apply_turn_completed(snapshot: &mut ThreadRenderSnapshot, params: &Value) {
    let turn = params.get("turn").unwrap_or(params);
    let status = optional_string(turn, "status").unwrap_or_default();
    if status == "failed" {
        let detail = turn
            .get("error")
            .and_then(format_json_value)
            .map(|value| preview_text(&value, PREVIEW_LIMIT));
        set_status_surface(snapshot, working_status("Turn failed", detail));
    } else {
        snapshot.status_surface = None;
    }
    if let Some(turn_id) = optional_turn_id_from_params(params) {
        for node in &mut snapshot.nodes {
            if node_turn_id(node) == Some(turn_id.as_str())
                && let ThreadRenderNode::AssistantMarkdown { streaming, .. } = node
            {
                *streaming = false;
            }
        }
    }
}

pub(super) fn apply_thread_status(snapshot: &mut ThreadRenderSnapshot, params: &Value) {
    let kind = params
        .get("statusInfo")
        .and_then(|status| status.get("kind"))
        .and_then(Value::as_str)
        .or_else(|| params.get("status").and_then(Value::as_str))
        .unwrap_or_default();
    snapshot.status_surface = match kind {
        "active" | "inProgress" => Some(working_status("Working", None)),
        "failed" => Some(working_status("Failed", None)),
        _ => None,
    };
}

pub(super) fn set_status_surface(
    snapshot: &mut ThreadRenderSnapshot,
    status_surface: StatusSurfaceState,
) {
    snapshot.status_surface = Some(status_surface);
}

pub(super) fn extract_message(params: &Value) -> Option<String> {
    extract_message_text(params).or_else(|| {
        params
            .get("details")
            .and_then(format_json_value)
            .map(|value| preview_text(&value, PREVIEW_LIMIT))
    })
}