mod app; mod backend_dashboard; mod backend_worker_picker; mod backend_workspace_picker; mod block; mod cache; mod command; mod composer_history; mod composer_keys; mod console; mod dashboard; #[cfg(feature = "e2e-test")] mod e2e_observer; mod input; pub mod keys; mod markdown; mod picker; mod role_session_registry; mod scroll; pub mod setup_model; mod spawn; mod task; mod text_selection; mod tool; mod ui; mod view_mode; mod worker_list; mod workspace_panel; use std::io; use std::path::PathBuf; use std::process::ExitCode; use crossterm::event::{DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste}; use crossterm::execute; use crossterm::terminal::{LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}; use session_store::SegmentId; use client::{Target, WorkerConnectionSelector, WorkerListRequest}; #[derive(Debug)] pub struct LaunchOptions { pub target: Box, pub mode: LaunchMode, pub workspace_root: PathBuf, } #[derive(Debug, Clone)] pub enum LaunchMode { Spawn { worker_name: Option, profile: Option, }, /// `yoi --worker `: attach to a live Worker by name if possible; /// otherwise launch the Worker runtime command with `--worker ` so it /// resumes from name-keyed state or creates a fresh same-name Worker. WorkerName { worker_name: String, socket_override: Option, }, /// `yoi workers` / `yoi --backend `: list workers through the selected /// connection target, then attach to the selected Worker. Workers { runtime_id: Option, include_stopped: bool, all: bool, }, /// `yoi --backend --runtime-id --worker-id `: open one Worker /// through the selected connection target. OpenWorker { runtime_id: String, worker_id: String, }, /// `yoi resume`: open the Worker picker, then attach to the selected live Worker /// or restore the selected stopped Worker by name. Without `--all`, the picker /// is scoped to the current runtime workspace. Resume { all: bool }, /// `yoi --session `: skip the picker, go straight to the /// resume name dialog with `id` baked in. ResumeWithSession { id: SegmentId, worker_name: Option, }, /// `yoi panel`: open the workspace Dashboard from the current workspace. Panel { include_stopped: bool }, } pub async fn launch(options: LaunchOptions) -> ExitCode { let LaunchOptions { target, mode, workspace_root, } = options; if let Err(e) = std::env::set_current_dir(&workspace_root) { eprintln!( "yoi: failed to enter workspace {}: {e}", workspace_root.display() ); return ExitCode::FAILURE; } if let Err(e) = enable_raw_mode() { eprintln!("yoi: failed to enter raw mode: {e}"); return ExitCode::FAILURE; } if let Err(e) = execute!(io::stdout(), EnableBracketedPaste) { let _ = disable_raw_mode(); eprintln!("yoi: {e}"); return ExitCode::FAILURE; } let result = match mode { LaunchMode::Spawn { worker_name, profile, } => match target.spawn_worker() { Ok(spawn) => { console::run_spawn(None, worker_name, profile, spawn.runtime_command).await } Err(e) => Err(Box::new(e) as Box), }, LaunchMode::WorkerName { worker_name, socket_override, } => match target.worker_by_name() { Ok(worker_by_name) => { console::run_worker_name( worker_name, socket_override, worker_by_name.runtime_command, ) .await } Err(e) => Err(Box::new(e) as Box), }, LaunchMode::Workers { runtime_id, include_stopped, all, } => match target.list_workers(if include_stopped { WorkerListRequest::with_stopped(runtime_id) } else { WorkerListRequest::new(runtime_id) }) { Ok(worker_list) => { if let Some(target) = worker_list.backend_target { backend_worker_picker::run(target, worker_list.include_stopped).await } else if let Some(runtime_command) = worker_list.local_runtime_command { console::run_worker_picker( runtime_command, workspace_root.clone(), all, worker_list.include_stopped, ) .await } else { Err(Box::new(io::Error::other( "worker list target did not include a local or backend source", )) as Box) } } Err(e) => Err(Box::new(e) as Box), }, LaunchMode::OpenWorker { runtime_id, worker_id, } => match target.connect_worker(WorkerConnectionSelector::new(runtime_id, worker_id)) { Ok(connection) => console::run_backend_runtime(connection.target).await, Err(e) => Err(Box::new(e) as Box), }, LaunchMode::Resume { all } => match target.resume_worker() { Ok(resume) => { console::run_resume(resume.runtime_command, workspace_root.clone(), all).await } Err(e) => Err(Box::new(e) as Box), }, LaunchMode::ResumeWithSession { id, worker_name } => match target.spawn_worker() { Ok(spawn) => { console::run_spawn(Some(id), worker_name, None, spawn.runtime_command).await } Err(e) => Err(Box::new(e) as Box), }, LaunchMode::Panel { include_stopped } => match target.dashboard() { Ok(client::Dashboard::Local { runtime_command }) => { dashboard::launch(runtime_command, include_stopped).await } Ok(client::Dashboard::Backend { base_url, workspace_id, }) => backend_dashboard::launch(base_url, workspace_id).await, Err(e) => Err(Box::new(e) as Box), }, }; // Always restore the terminal first so any pending eprintln below // shows up cleanly in scrollback rather than inside an active // alternate-screen buffer. #[cfg(feature = "e2e-test")] e2e_observer::emit("tui", "terminal_cleanup_started", serde_json::json!({})); let mut stdout = io::stdout(); let _ = execute!( stdout, DisableMouseCapture, LeaveAlternateScreen, DisableBracketedPaste ); let _ = disable_raw_mode(); let _ = execute!(stdout, crossterm::cursor::Show); #[cfg(feature = "e2e-test")] e2e_observer::emit("tui", "terminal_cleanup_finished", serde_json::json!({})); match result { Ok(()) => { #[cfg(feature = "e2e-test")] e2e_observer::emit("tui", "exit", serde_json::json!({ "status": "success" })); ExitCode::SUCCESS } Err(e) => { // SpawnError has already been painted into the inline // viewport's final frame, so it's already visible in the // user's scrollback — printing it again would be a noisy // duplicate. Other errors (worker-name failures, terminal setup // hiccups, etc.) need surfacing here. if e.downcast_ref::().is_none() { eprintln!("yoi: {e}"); } #[cfg(feature = "e2e-test")] e2e_observer::emit("tui", "exit", serde_json::json!({ "status": "failure" })); ExitCode::FAILURE } } }