From 2e2d7b729090bcb591b446e4fbbde253309221f8 Mon Sep 17 00:00:00 2001 From: Hare Date: Tue, 21 Jul 2026 18:04:14 +0900 Subject: [PATCH] fix: expose runtime worker list endpoint --- crates/tui/src/backend_worker_picker.rs | 13 +++++++- crates/workspace-server/src/hosts.rs | 44 +++++++++++++++++++++++++ crates/workspace-server/src/server.rs | 30 +++++++++++++++-- 3 files changed, 84 insertions(+), 3 deletions(-) diff --git a/crates/tui/src/backend_worker_picker.rs b/crates/tui/src/backend_worker_picker.rs index 394825df..ba072018 100644 --- a/crates/tui/src/backend_worker_picker.rs +++ b/crates/tui/src/backend_worker_picker.rs @@ -22,8 +22,19 @@ pub(crate) async fn run(target: BackendRuntimeListTarget) -> Result<(), Box>() + .join("; "); + let detail = if diagnostics.is_empty() { + "no backend diagnostics".to_string() + } else { + diagnostics + }; return Err(Box::new(io::Error::other(format!( - "Backend returned no runtime workers for workspace {}", + "Backend returned no runtime workers for workspace {} ({detail})", response.workspace_id )))); } diff --git a/crates/workspace-server/src/hosts.rs b/crates/workspace-server/src/hosts.rs index bc1164b5..b62231ff 100644 --- a/crates/workspace-server/src/hosts.rs +++ b/crates/workspace-server/src/hosts.rs @@ -902,6 +902,26 @@ impl RuntimeRegistry { RuntimeList::new(items, diagnostics) } + pub fn list_workers_for_runtime( + &self, + runtime_id: &str, + limit: usize, + ) -> Result, RuntimeRegistryError> { + validate_backend_identifier("runtime_id", runtime_id)?; + let runtime = self.runtime(runtime_id)?; + let worker_list = runtime.list_workers(limit); + let mut items: Vec<_> = worker_list + .items + .into_iter() + .filter(|worker| !is_retired_companion_worker(worker)) + .take(limit) + .collect(); + items.truncate(limit); + let mut diagnostics = worker_list.diagnostics; + diagnostics.truncate(MAX_DIAGNOSTICS); + Ok(RuntimeList::new(items, diagnostics)) + } + pub fn list_workers_for_host( &self, host_id: &str, @@ -3774,6 +3794,30 @@ mod tests { assert_eq!(from_runtime_a.label, "worker from runtime a"); } + #[test] + fn registry_worker_list_can_be_scoped_by_runtime_id() { + let registry = RuntimeRegistry::new(vec![ + Arc::new(FixtureRuntime::with_worker( + "runtime-a", + "host-a", + "shared-worker", + "worker from runtime a", + )), + Arc::new(FixtureRuntime::with_worker( + "runtime-b", + "host-b", + "shared-worker", + "worker from runtime b", + )), + ]); + + let listed = registry.list_workers_for_runtime("runtime-b", 10).unwrap(); + assert_eq!(listed.items.len(), 1); + assert_eq!(listed.items[0].runtime_id, "runtime-b"); + assert_eq!(listed.items[0].host_id, "host-b"); + assert_eq!(listed.items[0].label, "worker from runtime b"); + } + #[test] fn registry_worker_lookup_reports_unknown_runtime_and_worker_separately() { let registry = RuntimeRegistry::new(vec![Arc::new(FixtureRuntime::with_worker( diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index 92511061..f67c8486 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -473,11 +473,11 @@ pub fn build_router(api: WorkspaceApi) -> Router { ) .route( "/api/runtimes/{runtime_id}/workers", - post(create_runtime_worker), + get(list_runtime_workers).post(create_runtime_worker), ) .route( "/api/w/{workspace_id}/runtimes/{runtime_id}/workers", - post(scoped_create_runtime_worker), + get(scoped_list_runtime_workers).post(scoped_create_runtime_worker), ) .route( "/api/runtimes/{runtime_id}/config-bundles", @@ -2305,6 +2305,14 @@ async fn scoped_post_companion_cancel( post_companion_cancel(State(api), Json(request)).await } +async fn scoped_list_runtime_workers( + State(api): State, + AxumPath(path): AxumPath, +) -> ApiResult>> { + validate_workspace_scope(&api, &path.workspace_id)?; + list_runtime_workers(State(api), AxumPath(path.runtime_id)).await +} + async fn scoped_create_runtime_worker( State(api): State, AxumPath(path): AxumPath, @@ -3202,6 +3210,24 @@ fn reject_no_workdir_for_non_embedded_runtime( )) } +async fn list_runtime_workers( + State(api): State, + AxumPath(runtime_id): AxumPath, +) -> ApiResult>> { + let limit = api.config.max_records.min(200); + let worker_list = api + .runtime + .list_workers_for_runtime(&runtime_id, limit) + .map_err(|err| err.into_error())?; + Ok(Json(RuntimeListResponse { + workspace_id: api.workspace_id().to_string(), + limit, + items: worker_list.items, + source: "runtime_registry".to_string(), + diagnostics: worker_list.diagnostics, + })) +} + async fn create_runtime_worker( State(api): State, AxumPath(runtime_id): AxumPath,