cli: route worker lists through targets

This commit is contained in:
2026-07-29 16:04:24 +09:00
parent acc7281414
commit cbbc860366
8 changed files with 589 additions and 308 deletions
+47 -4
View File
@@ -3,7 +3,8 @@ use std::io;
use std::time::Duration;
use client::{
BackendRuntimeListTarget, BackendRuntimeTarget, BackendWorkerSummary, list_backend_workers,
BackendRuntimeListTarget, BackendRuntimeTarget, BackendWorkerSummary,
list_backend_stopped_workers, list_backend_workers, restore_backend_worker,
};
use crossterm::event::{self, Event as TermEvent, KeyCode, KeyEventKind, KeyModifiers};
use ratatui::backend::CrosstermBackend;
@@ -18,13 +19,30 @@ use crate::console;
const MAX_ROWS: usize = 10;
const VIEWPORT_LINES: u16 = MAX_ROWS as u16 + 4;
pub(crate) async fn run(target: BackendRuntimeListTarget) -> Result<(), Box<dyn Error>> {
let response = list_backend_workers(&target).await.map_err(|error| {
pub(crate) async fn run(
target: BackendRuntimeListTarget,
include_stopped: bool,
) -> Result<(), Box<dyn Error>> {
let mut response = list_backend_workers(&target).await.map_err(|error| {
io::Error::other(format!(
"failed to list Backend runtime workers from {}: {error}",
target.base_url
))
})?;
if include_stopped {
match list_backend_stopped_workers(&target).await {
Ok(stopped) => {
response.items.extend(stopped.items);
response.diagnostics.extend(stopped.diagnostics);
}
Err(error) => response.diagnostics.push(client::BackendDiagnostic {
code: "backend_stopped_workers_list_failed".to_string(),
severity: Some("error".to_string()),
message: error.to_string(),
}),
}
}
dedup_workers(&mut response.items);
if response.items.is_empty() {
let diagnostics = response
.diagnostics
@@ -44,11 +62,36 @@ pub(crate) async fn run(target: BackendRuntimeListTarget) -> Result<(), Box<dyn
}
let selected = pick_worker(target.clone(), response.items)?;
let worker = if selected.state == "stopped" {
let restore_target = BackendRuntimeTarget::new(
target.base_url.clone(),
selected.runtime_id.clone(),
selected.worker_id.clone(),
);
restore_backend_worker(&restore_target)
.await
.map_err(|error| {
io::Error::other(format!(
"failed to restore Backend worker {}/{}: {error}",
selected.runtime_id, selected.worker_id
))
})?
.result
.worker
.unwrap_or(selected)
} else {
selected
};
let attach_target =
BackendRuntimeTarget::new(target.base_url, selected.runtime_id, selected.worker_id);
BackendRuntimeTarget::new(target.base_url, worker.runtime_id, worker.worker_id);
console::run_backend_runtime(attach_target).await
}
fn dedup_workers(workers: &mut Vec<BackendWorkerSummary>) {
let mut seen = std::collections::HashSet::new();
workers.retain(|worker| seen.insert((worker.runtime_id.clone(), worker.worker_id.clone())));
}
fn pick_worker(
target: BackendRuntimeListTarget,
mut workers: Vec<BackendWorkerSummary>,
+11 -1
View File
@@ -331,6 +331,15 @@ pub(crate) async fn run_resume(
runtime_command: WorkerRuntimeCommand,
workspace_root: PathBuf,
all: bool,
) -> Result<(), Box<dyn std::error::Error>> {
run_worker_picker(runtime_command, workspace_root, all, true).await
}
pub(crate) async fn run_worker_picker(
runtime_command: WorkerRuntimeCommand,
workspace_root: PathBuf,
all: bool,
include_stopped: bool,
) -> Result<(), Box<dyn std::error::Error>> {
// Pick a Worker in its own inline viewport, dropping the viewport before
// attaching/restoring so each phase gets fresh vertical room.
@@ -338,7 +347,8 @@ pub(crate) async fn run_resume(
picker::PickerOptions::all()
} else {
picker::PickerOptions::workspace(workspace_root)
};
}
.with_stopped(include_stopped);
let (worker_name, socket_override) = match picker::run(picker_options).await? {
PickerOutcome::Picked {
worker_name,
+34 -8
View File
@@ -58,7 +58,11 @@ pub enum LaunchMode {
},
/// `yoi workers` / `yoi --backend <url>`: list workers through the selected
/// connection target, then attach to the selected Worker.
Workers { runtime_id: Option<String> },
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 {
@@ -76,7 +80,7 @@ pub enum LaunchMode {
worker_name: Option<String>,
},
/// `yoi panel`: open the workspace Dashboard from the current workspace.
Panel,
Panel { include_stopped: bool },
}
pub async fn launch(options: LaunchOptions) -> ExitCode {
@@ -128,12 +132,34 @@ pub async fn launch(options: LaunchOptions) -> ExitCode {
}
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error>),
},
LaunchMode::Workers { runtime_id } => {
match target.list_workers(WorkerListRequest::new(runtime_id)) {
Ok(worker_list) => backend_worker_picker::run(worker_list.target).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,
@@ -153,7 +179,7 @@ pub async fn launch(options: LaunchOptions) -> ExitCode {
}
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error>),
},
LaunchMode::Panel => match target.dashboard() {
LaunchMode::Panel { .. } => match target.dashboard() {
Ok(dashboard) => dashboard::launch(dashboard.runtime_command).await,
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error>),
},
+13
View File
@@ -80,20 +80,28 @@ pub enum PickerOutcome {
#[derive(Debug, Clone)]
pub(crate) struct PickerOptions {
scope: PickerScope,
include_stopped: bool,
}
impl PickerOptions {
pub(crate) fn workspace(workspace_root: PathBuf) -> Self {
Self {
scope: PickerScope::Workspace(workspace_root),
include_stopped: true,
}
}
pub(crate) fn all() -> Self {
Self {
scope: PickerScope::All,
include_stopped: true,
}
}
pub(crate) fn with_stopped(mut self, include_stopped: bool) -> Self {
self.include_stopped = include_stopped;
self
}
}
#[derive(Debug, Clone)]
@@ -134,6 +142,11 @@ fn list_for_options(
stored_workers: Vec<StoredWorkerInfo>,
live_workers: Vec<LiveWorkerInfo>,
) -> WorkerList {
let stored_workers = if options.include_stopped {
stored_workers
} else {
Vec::new()
};
match &options.scope {
PickerScope::Workspace(workspace_root) => WorkerList::from_workspace_sources(
WorkerVisibilitySource::ResumePicker,