ralph-agent-loop 0.4.0

A Rust CLI for managing AI agent loops with a structured JSON task queue
Documentation
//! App-launch runtime helpers.
//!
//! Purpose:
//! - Execute the planned launcher commands for `ralph app open`.
//!
//! Responsibilities:
//! - Spawn the initial app-launch command when no workspace routing is needed.
//! - Retry workspace URL handoff until the app is ready to accept it.
//! - Keep runtime behavior separate from pure planning helpers.
//!
//! Scope:
//! - Command execution for the app-open workflow only.
//!
//! Usage:
//! - Re-exported as `crate::commands::app::open`.
//!
//! Invariants/assumptions:
//! - URL handoff retries preserve the last launch error for diagnostics.

use anyhow::{Context, Result};
use std::thread;
use std::time::Duration;

use crate::cli::app::AppOpenArgs;
use crate::runutil::{ManagedCommand, TimeoutClass, execute_checked_command};

use super::launch_plan::{current_executable_for_gui, plan_open_command};
use super::model::OpenCommandSpec;
use super::url_plan::{plan_url_command, resolve_workspace_path};

pub(super) fn execute_launch_command(spec: &OpenCommandSpec) -> Result<()> {
    execute_checked_command(ManagedCommand::new(
        spec.to_command(),
        "launch macOS app",
        TimeoutClass::AppLaunch,
    ))
    .context("spawn macOS app launch command")?;
    Ok(())
}

/// Open the Ralph macOS app.
///
/// On macOS, workspace-aware launches use a single URL handoff so the app can
/// cold-start directly into the requested workspace. Plain app opens still use
/// the normal launcher path.
pub fn open(args: AppOpenArgs) -> Result<()> {
    let cli_executable = current_executable_for_gui();
    let Some(workspace_path) = resolve_workspace_path(&args)? else {
        let open_spec =
            plan_open_command(cfg!(target_os = "macos"), &args, cli_executable.as_deref())?;
        execute_launch_command(&open_spec)?;
        return Ok(());
    };

    let url_spec = plan_url_command(&workspace_path, &args, cli_executable.as_deref())?;
    let mut last_error = None;
    for attempt in 0..10 {
        match execute_launch_command(&url_spec) {
            Ok(()) => return Ok(()),
            Err(error) => {
                last_error = Some(error);
                if attempt < 9 {
                    thread::sleep(Duration::from_millis(250));
                }
            }
        }
    }

    Err(last_error.expect("url launch attempts should record an error"))
}