codex-mobile-bridge 0.3.3

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

use anyhow::{Context, Result};
use serde_json::{Value, json};

use super::BridgeState;
use crate::bridge_protocol::{
    CreateDirectoryBookmarkRequest, DirectoryEntry, DirectoryListing, ReadDirectoryRequest,
    RemoveDirectoryBookmarkRequest,
};
use crate::config::expand_path;
use crate::directory::{canonicalize_directory, parent_directory};
use crate::storage::Storage;

const DIRECTORY_HISTORY_LIMIT: usize = 20;

pub(super) fn seed_directory_bookmarks(
    storage: &Storage,
    configured_bookmarks: &[PathBuf],
) -> Result<()> {
    for path in configured_bookmarks {
        storage.upsert_directory_bookmark(path, None)?;
    }
    Ok(())
}

impl BridgeState {
    pub(super) fn directory_state_snapshot(
        &self,
    ) -> Result<(
        Vec<crate::bridge_protocol::DirectoryBookmarkRecord>,
        Vec<crate::bridge_protocol::DirectoryHistoryRecord>,
    )> {
        Ok((
            self.storage.list_directory_bookmarks()?,
            self.storage
                .list_directory_history(DIRECTORY_HISTORY_LIMIT)?,
        ))
    }

    pub(super) fn emit_directory_state(&self) -> Result<()> {
        let (directory_bookmarks, directory_history) = self.directory_state_snapshot()?;
        self.emit_event(
            "directory_state",
            None,
            None,
            json!({
                "directoryBookmarks": directory_bookmarks,
                "directoryHistory": directory_history,
            }),
        )
    }

    pub(super) async fn create_directory_bookmark(
        &self,
        request: CreateDirectoryBookmarkRequest,
    ) -> Result<Value> {
        let path = expand_path(Path::new(&request.path))?;
        let bookmark = self
            .storage
            .upsert_directory_bookmark(&path, request.display_name.as_deref())?;
        let (directory_bookmarks, directory_history) = self.directory_state_snapshot()?;
        self.emit_directory_state()?;
        Ok(json!({
            "bookmark": bookmark,
            "directoryBookmarks": directory_bookmarks,
            "directoryHistory": directory_history,
        }))
    }

    pub(super) async fn remove_directory_bookmark(
        &self,
        request: RemoveDirectoryBookmarkRequest,
    ) -> Result<Value> {
        let path = expand_path(Path::new(&request.path))?;
        self.storage.remove_directory_bookmark(&path)?;
        let (directory_bookmarks, directory_history) = self.directory_state_snapshot()?;
        self.emit_directory_state()?;
        Ok(json!({
            "removed": true,
            "directoryBookmarks": directory_bookmarks,
            "directoryHistory": directory_history,
        }))
    }

    pub(super) async fn read_directory(&self, request: ReadDirectoryRequest) -> Result<Value> {
        let runtime = self.require_runtime(request.runtime_id.as_deref()).await?;
        let runtime_id = runtime.record.runtime_id.clone();
        let path = canonicalize_directory(&expand_path(Path::new(&request.path))?)?;
        let response = runtime
            .app_server
            .request(
                "fs/readDirectory",
                json!({
                    "path": path.to_string_lossy().to_string(),
                }),
            )
            .await?;
        let raw_entries = response
            .get("entries")
            .and_then(Value::as_array)
            .context("fs/readDirectory 返回格式不正确")?;

        let mut entries = raw_entries
            .iter()
            .filter(|entry| {
                entry
                    .get("isDirectory")
                    .and_then(Value::as_bool)
                    .unwrap_or(false)
            })
            .filter_map(|entry| {
                let name = entry.get("fileName").and_then(Value::as_str)?.trim();
                if name.is_empty() {
                    return None;
                }
                Some(DirectoryEntry {
                    name: name.to_string(),
                    path: path.join(name).to_string_lossy().to_string(),
                    is_directory: true,
                })
            })
            .collect::<Vec<_>>();
        entries.sort_by(|left, right| {
            left.name
                .to_ascii_lowercase()
                .cmp(&right.name.to_ascii_lowercase())
                .then_with(|| left.name.cmp(&right.name))
        });

        let listing = DirectoryListing {
            path: path.to_string_lossy().to_string(),
            parent_path: parent_directory(&path).map(|parent| parent.to_string_lossy().to_string()),
            entries,
        };
        Ok(json!({
            "runtimeId": runtime_id,
            "listing": listing,
        }))
    }
}