234 lines
7.9 KiB
Rust
234 lines
7.9 KiB
Rust
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<dyn Target>,
|
|
pub mode: LaunchMode,
|
|
pub workspace_root: PathBuf,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum LaunchMode {
|
|
Spawn {
|
|
worker_name: Option<String>,
|
|
profile: Option<String>,
|
|
},
|
|
/// `yoi --worker <name>`: attach to a live Worker by name if possible;
|
|
/// otherwise launch the Worker runtime command with `--worker <name>` so it
|
|
/// resumes from name-keyed state or creates a fresh same-name Worker.
|
|
WorkerName {
|
|
worker_name: String,
|
|
socket_override: Option<PathBuf>,
|
|
},
|
|
/// `yoi workers` / `yoi --backend <url>`: list workers through the selected
|
|
/// connection target, then attach to the selected Worker.
|
|
Workers {
|
|
runtime_id: Option<String>,
|
|
include_stopped: bool,
|
|
all: bool,
|
|
},
|
|
/// `yoi --backend <url> --runtime-id <id> --worker-id <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 <UUID>`: skip the picker, go straight to the
|
|
/// resume name dialog with `id` baked in.
|
|
ResumeWithSession {
|
|
id: SegmentId,
|
|
worker_name: Option<String>,
|
|
},
|
|
/// `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<dyn std::error::Error>),
|
|
},
|
|
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<dyn std::error::Error>),
|
|
},
|
|
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<dyn std::error::Error>)
|
|
}
|
|
}
|
|
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error>),
|
|
},
|
|
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<dyn std::error::Error>),
|
|
},
|
|
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<dyn std::error::Error>),
|
|
},
|
|
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<dyn std::error::Error>),
|
|
},
|
|
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<dyn std::error::Error>),
|
|
},
|
|
};
|
|
|
|
// 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::<spawn::SpawnError>().is_none() {
|
|
eprintln!("yoi: {e}");
|
|
}
|
|
#[cfg(feature = "e2e-test")]
|
|
e2e_observer::emit("tui", "exit", serde_json::json!({ "status": "failure" }));
|
|
ExitCode::FAILURE
|
|
}
|
|
}
|
|
}
|