feat: surface backend runtime workers in tui

This commit is contained in:
2026-07-21 17:41:50 +09:00
parent 2b307b8040
commit f79892baef
8 changed files with 863 additions and 26 deletions
+248 -4
View File
@@ -41,6 +41,122 @@ impl BackendRuntimeTarget {
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BackendRuntimeListTarget {
pub base_url: String,
pub workspace_id: Option<String>,
pub runtime_id: Option<String>,
}
impl BackendRuntimeListTarget {
pub fn new(
base_url: impl Into<String>,
workspace_id: Option<String>,
runtime_id: Option<String>,
) -> Self {
Self {
base_url: base_url.into(),
workspace_id,
runtime_id,
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct BackendRuntimeListResponse<T> {
pub workspace_id: String,
pub limit: usize,
pub items: Vec<T>,
pub source: String,
#[serde(default)]
pub diagnostics: Vec<BackendDiagnostic>,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct BackendRuntimeSummary {
pub runtime_id: String,
pub label: String,
pub kind: String,
pub status: String,
#[serde(default)]
pub host_ids: Vec<String>,
#[serde(default)]
pub diagnostics: Vec<BackendDiagnostic>,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct BackendWorkerWorkspaceSummary {
pub visibility: String,
pub identity: String,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct BackendWorkerImplementationSummary {
pub kind: String,
pub display_hint: String,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct BackendWorkerCapabilitySummary {
pub can_stop: bool,
pub can_spawn_followup: bool,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct BackendWorkingDirectoryCleanupTarget {
pub kind: String,
pub working_directory_id: String,
pub repository_id: String,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct BackendWorkingDirectorySummary {
pub working_directory_id: String,
pub repository_id: String,
#[serde(default)]
pub requested_selector: Option<String>,
pub materializer_kind: String,
#[serde(default)]
pub resolved_commit: Option<String>,
#[serde(default)]
pub resolved_tree: Option<String>,
#[serde(default)]
pub cleanup_target: Option<BackendWorkingDirectoryCleanupTarget>,
pub status: String,
#[serde(default)]
pub cleanliness: Option<String>,
#[serde(default)]
pub primary_worker_id: Option<String>,
#[serde(default)]
pub management_kind: Option<String>,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct BackendWorkerSummary {
pub runtime_id: String,
pub worker_id: String,
pub host_id: String,
pub label: String,
#[serde(default)]
pub role: Option<String>,
#[serde(default)]
pub profile: Option<String>,
pub workspace: BackendWorkerWorkspaceSummary,
pub state: String,
#[serde(default)]
pub last_seen_at: Option<String>,
#[serde(default)]
pub pinned: bool,
#[serde(default)]
pub retention_state: String,
pub implementation: BackendWorkerImplementationSummary,
pub capabilities: BackendWorkerCapabilitySummary,
#[serde(default)]
pub working_directory: Option<BackendWorkingDirectorySummary>,
#[serde(default)]
pub diagnostics: Vec<BackendDiagnostic>,
}
#[derive(Debug)]
pub struct BackendRuntimeClient {
target: BackendRuntimeTarget,
@@ -73,6 +189,72 @@ impl From<reqwest::Error> for BackendRuntimeClientError {
}
}
pub async fn list_backend_workers(
target: &BackendRuntimeListTarget,
) -> Result<BackendRuntimeListResponse<BackendWorkerSummary>, BackendRuntimeClientError> {
validate_list_target(target)?;
let http = reqwest::Client::new();
if let Some(runtime_id) = target.runtime_id.as_deref() {
let path = backend_runtime_workers_path(target.workspace_id.as_deref(), runtime_id);
let url = join_base_and_path(&target.base_url, &path);
return Ok(http
.get(url)
.send()
.await?
.error_for_status()?
.json::<BackendRuntimeListResponse<BackendWorkerSummary>>()
.await?);
}
let runtime_path = backend_runtimes_path(target.workspace_id.as_deref());
let runtime_url = join_base_and_path(&target.base_url, &runtime_path);
let runtimes = http
.get(runtime_url)
.send()
.await?
.error_for_status()?
.json::<BackendRuntimeListResponse<BackendRuntimeSummary>>()
.await?;
let mut items = Vec::new();
let mut diagnostics = runtimes.diagnostics;
for runtime in runtimes.items {
let path =
backend_runtime_workers_path(target.workspace_id.as_deref(), &runtime.runtime_id);
let url = join_base_and_path(&target.base_url, &path);
match http
.get(url)
.send()
.await
.and_then(|response| response.error_for_status())
{
Ok(response) => {
let response = response
.json::<BackendRuntimeListResponse<BackendWorkerSummary>>()
.await?;
diagnostics.extend(response.diagnostics);
items.extend(response.items);
}
Err(error) => diagnostics.push(BackendDiagnostic {
code: "runtime_worker_list_failed".to_string(),
severity: Some("error".to_string()),
message: format!(
"failed to list workers for runtime {}: {error}",
runtime.runtime_id
),
}),
}
}
Ok(BackendRuntimeListResponse {
workspace_id: runtimes.workspace_id,
limit: runtimes.limit,
items,
source: "backend_runtime_worker_summary".to_string(),
diagnostics,
})
}
impl BackendRuntimeClient {
pub async fn connect(target: BackendRuntimeTarget) -> Result<Self, BackendRuntimeClientError> {
validate_target(&target)?;
@@ -375,6 +557,50 @@ fn validate_target(target: &BackendRuntimeTarget) -> Result<(), BackendRuntimeCl
Ok(())
}
fn validate_list_target(
target: &BackendRuntimeListTarget,
) -> Result<(), BackendRuntimeClientError> {
if target.base_url.trim().is_empty() {
return Err(BackendRuntimeClientError::InvalidTarget(
"Backend API base URL is required".to_string(),
));
}
if !(target.base_url.starts_with("http://")) && !(target.base_url.starts_with("https://")) {
return Err(BackendRuntimeClientError::InvalidTarget(
"Backend API base URL must start with http:// or https://".to_string(),
));
}
if target.workspace_id.as_deref().is_some_and(str::is_empty) {
return Err(BackendRuntimeClientError::InvalidTarget(
"workspace_id must not be empty when provided".to_string(),
));
}
if target.runtime_id.as_deref().is_some_and(str::is_empty) {
return Err(BackendRuntimeClientError::InvalidTarget(
"runtime_id must not be empty when provided".to_string(),
));
}
Ok(())
}
fn backend_runtimes_path(workspace_id: Option<&str>) -> String {
match workspace_id {
Some(workspace_id) => format!("/api/w/{}/runtimes", path_segment_encode(workspace_id)),
None => "/api/runtimes".to_string(),
}
}
fn backend_runtime_workers_path(workspace_id: Option<&str>, runtime_id: &str) -> String {
match workspace_id {
Some(workspace_id) => format!(
"/api/w/{}/runtimes/{}/workers",
path_segment_encode(workspace_id),
path_segment_encode(runtime_id)
),
None => format!("/api/runtimes/{}/workers", path_segment_encode(runtime_id)),
}
}
fn observation_ws_url(target: &BackendRuntimeTarget) -> String {
let path = format!(
"/api/runtimes/{}/workers/{}/events/ws",
@@ -449,10 +675,12 @@ struct WorkerLifecycleResult {
diagnostics: Vec<BackendDiagnostic>,
}
#[derive(Debug, Deserialize)]
struct BackendDiagnostic {
code: String,
message: String,
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct BackendDiagnostic {
pub code: String,
#[serde(default)]
pub severity: Option<String>,
pub message: String,
}
#[derive(Debug, Deserialize)]
@@ -502,6 +730,22 @@ mod tests {
);
}
#[test]
fn backend_worker_list_paths_use_scoped_workspace_when_available() {
assert_eq!(
backend_runtimes_path(Some("workspace/one")),
"/api/w/workspace%2Fone/runtimes"
);
assert_eq!(
backend_runtime_workers_path(Some("workspace/one"), "runtime one"),
"/api/w/workspace%2Fone/runtimes/runtime%20one/workers"
);
assert_eq!(
backend_runtime_workers_path(None, "runtime one"),
"/api/runtimes/runtime%20one/workers"
);
}
#[test]
fn observation_url_uses_backend_runtime_worker_identity() {
let target =
+6 -1
View File
@@ -14,7 +14,12 @@ pub mod spawn;
pub mod ticket_role;
mod worker_client;
pub use backend_runtime::{BackendRuntimeClient, BackendRuntimeClientError, BackendRuntimeTarget};
pub use backend_runtime::{
BackendRuntimeClient, BackendRuntimeClientError, BackendRuntimeListResponse,
BackendRuntimeListTarget, BackendRuntimeSummary, BackendRuntimeTarget,
BackendWorkerCapabilitySummary, BackendWorkerImplementationSummary, BackendWorkerSummary,
BackendWorkerWorkspaceSummary, BackendWorkingDirectorySummary, list_backend_workers,
};
pub use runtime_command::WorkerRuntimeCommand;
pub use spawn::{