spool-memory 0.2.3

Local-first developer memory system — persistent, structured knowledge for AI coding tools
Documentation
//! 桌面 facade 里用到的小工具:csv/filepath 解析、session 索引加载、continue/delete 命令、终端启动。

use super::dto::DesktopSessionItem;
use crate::daemon::LifecycleReadOptions;
use crate::lifecycle_store::{LifecycleStore, lifecycle_root_from_config};
use crate::session_sources::{load_provider_sessions, raw_session_id};
use std::fs;
use std::path::Path;

use super::dto::DesktopDaemonRequest;

pub fn parse_file_list(value: &str) -> Vec<String> {
    value
        .split([',', '\n'])
        .map(str::trim)
        .filter(|item| !item.is_empty())
        .map(ToString::to_string)
        .collect()
}

pub fn parse_csv_items(value: &str) -> Vec<String> {
    value
        .split(',')
        .map(str::trim)
        .filter(|item| !item.is_empty())
        .map(ToString::to_string)
        .collect()
}

pub(super) fn lifecycle_read_options(
    daemon: Option<&DesktopDaemonRequest>,
) -> LifecycleReadOptions {
    match daemon {
        Some(DesktopDaemonRequest {
            enabled: true,
            daemon_bin: Some(daemon_bin),
        }) => LifecycleReadOptions::with_daemon(daemon_bin.as_path()),
        _ => LifecycleReadOptions::default(),
    }
}

pub(super) fn store_from_config_path(config_path: &Path) -> LifecycleStore {
    let config_dir = config_path.parent().unwrap_or_else(|| Path::new("."));
    LifecycleStore::new(lifecycle_root_from_config(config_dir).as_path())
}

pub(super) fn load_session_index(_config_path: &Path) -> anyhow::Result<Vec<DesktopSessionItem>> {
    let mut sessions = load_provider_sessions(None)?;
    sessions.sort_by(|left, right| right.updated_at.cmp(&left.updated_at));
    Ok(sessions)
}

pub(super) fn load_session_index_filtered(
    _config_path: &Path,
    provider_filter: Option<&str>,
) -> anyhow::Result<Vec<DesktopSessionItem>> {
    let mut sessions = load_provider_sessions(provider_filter)?;
    sessions.sort_by(|left, right| right.updated_at.cmp(&left.updated_at));
    Ok(sessions)
}

pub(super) fn build_continue_command(session: &DesktopSessionItem) -> Option<String> {
    let cwd_prefix = session
        .cwd
        .as_deref()
        .map(shell_cd_prefix)
        .unwrap_or_default();
    let raw_id = raw_session_id(&session.session_id);
    match session.provider.as_str() {
        "claude" => Some(format!("{cwd_prefix}claude -r \"{raw_id}\"")),
        "codex" => Some(format!("{cwd_prefix}codex resume \"{raw_id}\"")),
        _ => None,
    }
}

pub(super) fn delete_session_file(session: &DesktopSessionItem) -> anyhow::Result<()> {
    let Some(path) = session.source_path.as_deref() else {
        anyhow::bail!("当前会话没有可删除的本地文件")
    };
    let file_path = Path::new(path);
    if !file_path.exists() {
        anyhow::bail!("本地会话文件不存在:{}", file_path.display())
    }
    if !file_path.is_file() {
        anyhow::bail!("本地会话路径不是文件:{}", file_path.display())
    }
    fs::remove_file(file_path)?;
    Ok(())
}

fn shell_cd_prefix(cwd: &str) -> String {
    format!("cd '{}' && ", cwd.replace('\'', "'\\''"))
}

pub(super) fn launch_terminal_command(command: &str) -> anyhow::Result<()> {
    #[cfg(target_os = "macos")]
    {
        let script = format!(
            "tell application \"Terminal\"\nactivate\ndo script \"{}\"\nend tell",
            command.replace('\\', "\\\\").replace('"', "\\\"")
        );
        let output = std::process::Command::new("osascript")
            .arg("-e")
            .arg(script)
            .output()?;
        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
            let detail = if stderr.is_empty() {
                format!("osascript exited with status {}", output.status)
            } else {
                stderr
            };
            anyhow::bail!("无法通过 Terminal 启动命令 `{command}`:{detail}");
        }
        Ok(())
    }

    #[cfg(not(target_os = "macos"))]
    {
        let _ = command;
        anyhow::bail!("continue session is only implemented for macOS right now")
    }
}