codex-mobile-bridge 0.3.3

Remote bridge and service manager for codex-mobile.
Documentation
use std::path::Path;

use rusqlite::{OptionalExtension, params, params_from_iter};

use super::Storage;
use super::decode::decode_thread_row;
use crate::bridge_protocol::ThreadSummary;
use crate::directory::{directory_contains, normalize_absolute_directory};

impl Storage {
    pub fn upsert_thread_index(&self, thread: &ThreadSummary) -> anyhow::Result<()> {
        let conn = self.connect()?;
        conn.execute(
            "INSERT INTO thread_index (
                 thread_id, runtime_id, name, preview, cwd, status,
                 model_provider, source, created_at_ms, updated_at_ms, is_loaded, is_active,
                 archived, raw_json
             )
             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)
             ON CONFLICT(thread_id) DO UPDATE SET
                 runtime_id = excluded.runtime_id,
                 name = excluded.name,
                 preview = excluded.preview,
                 cwd = excluded.cwd,
                 status = excluded.status,
                 model_provider = excluded.model_provider,
                 source = excluded.source,
                 created_at_ms = excluded.created_at_ms,
                 updated_at_ms = excluded.updated_at_ms,
                 is_loaded = excluded.is_loaded,
                 is_active = excluded.is_active,
                 archived = excluded.archived,
                 raw_json = excluded.raw_json",
            params![
                thread.id,
                thread.runtime_id,
                thread.name,
                thread.preview,
                thread.cwd,
                thread.status,
                thread.model_provider,
                thread.source,
                thread.created_at,
                thread.updated_at,
                if thread.is_loaded { 1_i64 } else { 0_i64 },
                if thread.is_active { 1_i64 } else { 0_i64 },
                if thread.archived { 1_i64 } else { 0_i64 },
                serde_json::to_string(thread)?
            ],
        )?;
        Ok(())
    }

    pub fn get_thread_index(&self, thread_id: &str) -> anyhow::Result<Option<ThreadSummary>> {
        let conn = self.connect()?;
        let record = conn
            .query_row(
                "SELECT raw_json, archived FROM thread_index WHERE thread_id = ?1",
                params![thread_id],
                |row| decode_thread_row(row.get::<_, String>(0)?, row.get::<_, i64>(1)?),
            )
            .optional()?;
        Ok(record)
    }

    pub fn list_thread_index(
        &self,
        directory_prefix: Option<&str>,
        runtime_id: Option<&str>,
        archived: Option<bool>,
        search_term: Option<&str>,
    ) -> anyhow::Result<Vec<ThreadSummary>> {
        let conn = self.connect()?;
        let mut sql = String::from(
            "SELECT raw_json, archived
             FROM thread_index",
        );
        let mut clauses = Vec::new();
        let mut values = Vec::new();

        if let Some(runtime_id) = runtime_id {
            clauses.push("runtime_id = ?");
            values.push(rusqlite::types::Value::from(runtime_id.to_string()));
        }

        if let Some(archived) = archived {
            clauses.push("archived = ?");
            values.push(rusqlite::types::Value::from(if archived {
                1_i64
            } else {
                0_i64
            }));
        }

        if let Some(search_term) = search_term.filter(|value| !value.trim().is_empty()) {
            clauses.push(
                "(LOWER(COALESCE(name, '')) LIKE ? OR LOWER(preview) LIKE ? OR LOWER(cwd) LIKE ?)",
            );
            let pattern = format!("%{}%", search_term.trim().to_lowercase());
            values.push(rusqlite::types::Value::from(pattern.clone()));
            values.push(rusqlite::types::Value::from(pattern.clone()));
            values.push(rusqlite::types::Value::from(pattern));
        }

        if !clauses.is_empty() {
            sql.push_str(" WHERE ");
            sql.push_str(&clauses.join(" AND "));
        }
        sql.push_str(" ORDER BY updated_at_ms DESC");

        let mut stmt = conn.prepare(&sql)?;
        let rows = stmt.query_map(params_from_iter(values), |row| {
            decode_thread_row(row.get::<_, String>(0)?, row.get::<_, i64>(1)?)
        })?;

        let mut threads = rows.collect::<rusqlite::Result<Vec<_>>>()?;

        if let Some(directory_prefix) = directory_prefix {
            let prefix = normalize_absolute_directory(Path::new(directory_prefix))?;
            threads.retain(|thread| directory_contains(&prefix, Path::new(&thread.cwd)));
        }

        Ok(threads)
    }
    pub fn set_thread_archived(&self, thread_id: &str, archived: bool) -> anyhow::Result<()> {
        let conn = self.connect()?;
        conn.execute(
            "UPDATE thread_index
             SET archived = ?2
             WHERE thread_id = ?1",
            params![thread_id, if archived { 1_i64 } else { 0_i64 }],
        )?;
        Ok(())
    }
}