From d8c0853d55aba68ff588b93d8254462d803cfa0a Mon Sep 17 00:00:00 2001 From: Hare Date: Sun, 12 Jul 2026 06:57:10 +0900 Subject: [PATCH] workspace: refine runtime and cleanup lists --- crates/worker-runtime/src/management.rs | 8 + crates/worker-runtime/src/runtime.rs | 2 + crates/workspace-server/src/hosts.rs | 14 +- .../src/routes/w/[workspaceId]/+page.svelte | 4 - .../w/[workspaceId]/runtimes/+page.svelte | 6 - .../[runtimeId]/workdirs/+page.svelte | 162 ++++++------------ .../w/[workspaceId]/workers/+page.svelte | 73 +++++++- .../routes/w/[workspaceId]/workers/+page.ts | 20 ++- 8 files changed, 161 insertions(+), 128 deletions(-) diff --git a/crates/worker-runtime/src/management.rs b/crates/worker-runtime/src/management.rs index f1a3549e..697492fa 100644 --- a/crates/worker-runtime/src/management.rs +++ b/crates/worker-runtime/src/management.rs @@ -50,6 +50,10 @@ impl Default for RuntimeOptions { } } +fn unknown_platform_component() -> String { + "unknown".to_string() +} + /// Management-plane summary for a Runtime. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct RuntimeSummary { @@ -63,6 +67,10 @@ pub struct RuntimeSummary { pub cancelled_worker_count: usize, pub diagnostic_count: usize, pub limits: RuntimeLimits, + #[serde(default = "unknown_platform_component")] + pub os: String, + #[serde(default = "unknown_platform_component")] + pub arch: String, #[serde(default)] pub worker_creation_available: bool, } diff --git a/crates/worker-runtime/src/runtime.rs b/crates/worker-runtime/src/runtime.rs index 4b6665ec..81737297 100644 --- a/crates/worker-runtime/src/runtime.rs +++ b/crates/worker-runtime/src/runtime.rs @@ -162,6 +162,8 @@ impl Runtime { cancelled_worker_count, diagnostic_count: state.diagnostics.len(), limits: state.limits.clone(), + os: std::env::consts::OS.to_string(), + arch: std::env::consts::ARCH.to_string(), worker_creation_available: state.execution_backend.is_some(), }) } diff --git a/crates/workspace-server/src/hosts.rs b/crates/workspace-server/src/hosts.rs index 351c4e52..06d91076 100644 --- a/crates/workspace-server/src/hosts.rs +++ b/crates/workspace-server/src/hosts.rs @@ -1765,7 +1765,9 @@ impl RemoteRuntimeConfig { display_name: display_name.into(), base_url: base_url.into(), bearer_token, - cached_capabilities: remote_runtime_capabilities(200, false, false), + cached_capabilities: remote_runtime_capabilities( + 200, false, false, "unknown", "unknown", + ), cached_status: "configured".to_string(), timeout: Duration::from_secs(10), } @@ -2033,6 +2035,8 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime { limit, true, response.runtime.worker_creation_available, + response.runtime.os, + response.runtime.arch, ), diagnostics: Vec::new(), }, @@ -2066,7 +2070,7 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime { status: "configured".to_string(), observed_at: Utc::now().to_rfc3339(), last_seen_at: None, - capabilities: remote_runtime_capabilities(limit, true, false), + capabilities: remote_runtime_capabilities(limit, true, false, "unknown", "unknown"), diagnostics: Vec::new(), }], Vec::new(), @@ -2872,6 +2876,8 @@ fn remote_runtime_capabilities( limit: usize, available: bool, worker_creation_available: bool, + os: impl Into, + arch: impl Into, ) -> RuntimeCapabilitySummary { RuntimeCapabilitySummary { can_list_hosts: true, @@ -2887,8 +2893,8 @@ fn remote_runtime_capabilities( supports_backend_internal_tools: false, workspace_scope: "remote_runtime_backend_private".to_string(), max_workers: limit, - os: "remote".to_string(), - arch: "remote".to_string(), + os: os.into(), + arch: arch.into(), } } diff --git a/web/workspace/src/routes/w/[workspaceId]/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/+page.svelte index 016f9a4d..ce57122c 100644 --- a/web/workspace/src/routes/w/[workspaceId]/+page.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/+page.svelte @@ -77,10 +77,6 @@
Runtime
{host.runtime_id}
-
-
Scope
-
{host.capabilities.workspace_scope}
-
Platform
{host.capabilities.os} / {host.capabilities.arch}
diff --git a/web/workspace/src/routes/w/[workspaceId]/runtimes/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/runtimes/+page.svelte index f4bd641f..a71c6c07 100644 --- a/web/workspace/src/routes/w/[workspaceId]/runtimes/+page.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/runtimes/+page.svelte @@ -4,10 +4,6 @@ let { data }: PageProps = $props(); - function runtimeScope(runtime: Runtime): string { - return runtime.capabilities.workspace_scope; - } - function runtimePlatform(runtime: Runtime): string { return `${runtime.capabilities.os} / ${runtime.capabilities.arch}`; } @@ -40,7 +36,6 @@ Runtime Kind Status - Scope Platform Capacity Workdirs @@ -55,7 +50,6 @@ {runtime.kind} {runtime.status} - {runtimeScope(runtime)} {runtimePlatform(runtime)} {runtime.capabilities.max_workers} workers diff --git a/web/workspace/src/routes/w/[workspaceId]/runtimes/[runtimeId]/workdirs/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/runtimes/[runtimeId]/workdirs/+page.svelte index 17f20b4c..a3741961 100644 --- a/web/workspace/src/routes/w/[workspaceId]/runtimes/[runtimeId]/workdirs/+page.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/runtimes/[runtimeId]/workdirs/+page.svelte @@ -1,19 +1,25 @@ @@ -96,6 +87,8 @@

Workdirs

Workdirs owned by {data.runtimeId}.

+ {#if data.cleanupPlanError}

{data.cleanupPlanError}

{/if} + {#if cleanupStatus}

{cleanupStatus}

{/if}
@@ -106,68 +99,6 @@ {:else if data.workdirs.items.length === 0}

No workdirs are visible for this Runtime.

{:else} -
-
-

Manual cleanup preview

-

Select explicit Workdir targets. Raw Runtime paths are intentionally not shown.

-
- {#if data.cleanupPlanError} -

{data.cleanupPlanError}

- {:else if cleanupCandidates.length === 0 && workerCleanupCandidates.length === 0} -

No cleanup candidates.

- {:else} -
- {#each workerCleanupCandidates as candidate (candidate.target_id)} - - {/each} - {#each cleanupCandidates as candidate (candidate.target_id)} - - {/each} -
- - {#if cleanupStatus}

{cleanupStatus}

{/if} - {/if} -
-
@@ -178,10 +109,12 @@ + {#each data.workdirs.items as workdir} + {@const cleanup = cleanupCandidate(workdir)} @@ -192,6 +125,21 @@ {workdir.dirty_state_policy}{workdir.cleanup_policy} + {/each} diff --git a/web/workspace/src/routes/w/[workspaceId]/workers/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/workers/+page.svelte index 3580cc67..e5e49fbc 100644 --- a/web/workspace/src/routes/w/[workspaceId]/workers/+page.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/workers/+page.svelte @@ -2,14 +2,20 @@ import { workspaceApiPath } from '$lib/workspace-api/http'; import { workerConsoleHref } from '$lib/workspace-console/model'; import { canOpenWorkerConsole } from '$lib/workspace-sidebar/workers'; - import type { Worker } from '$lib/workspace-sidebar/types'; + import type { CleanupWorkerCandidate, RuntimeCleanupExecutionResponse, RuntimeCleanupPlanResponse, Worker } from '$lib/workspace-sidebar/types'; import type { PageProps } from './$types'; let { data }: PageProps = $props(); - let retentionStatus = $state(null); + let statusMessage = $state(null); + let cleanupPlans = $state>({}); + let busyCleanupTarget = $state(null); + + $effect(() => { + cleanupPlans = data.cleanupPlans; + }); async function setPinned(worker: Worker, pinned: boolean): Promise { - retentionStatus = null; + statusMessage = null; const response = await fetch( workspaceApiPath( data.workspaceId, @@ -19,12 +25,56 @@ ); const payload = await response.json().catch(() => null); if (!response.ok) { - retentionStatus = payload?.message ?? payload?.error ?? response.statusText; + statusMessage = payload?.message ?? payload?.error ?? response.statusText; return; } worker.pinned = Boolean(payload?.pinned); worker.retention_state = payload?.retention_state ?? (worker.pinned ? 'pinned' : 'normal'); - retentionStatus = `${worker.label} ${worker.pinned ? 'pinned' : 'unpinned'}.`; + statusMessage = `${worker.label} ${worker.pinned ? 'pinned' : 'unpinned'}.`; + } + + function cleanupCandidate(worker: Worker): CleanupWorkerCandidate | undefined { + return cleanupPlans?.[worker.runtime_id]?.workers.find( + (candidate) => candidate.runtime_id === worker.runtime_id && candidate.runtime_worker_id === worker.worker_id, + ); + } + + async function deleteWorkerRegistryRow(worker: Worker, candidate: CleanupWorkerCandidate): Promise { + if (!cleanupPlans?.[worker.runtime_id]) return; + statusMessage = null; + busyCleanupTarget = candidate.target_id; + try { + const plan = cleanupPlans[worker.runtime_id]; + const response = await fetch( + workspaceApiPath(data.workspaceId, `/runtimes/${encodeURIComponent(worker.runtime_id)}/cleanup-executions`), + { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + expected_plan_revision: plan.revision, + expected_plan_digest: plan.digest, + worker_target_ids: [candidate.target_id], + workdir_target_ids: [], + confirm_dirty_discard_target_ids: [], + }), + }, + ); + const payload = (await response.json().catch(() => null)) as RuntimeCleanupExecutionResponse | { message?: string; error?: string } | null; + if (!response.ok) throw new Error(payload && 'message' in payload ? (payload.message ?? payload.error) : response.statusText); + if (payload && 'plan_after' in payload) { + cleanupPlans = { ...cleanupPlans, [worker.runtime_id]: payload.plan_after }; + } + if (data.workers) { + data.workers.items = data.workers.items.filter( + (item) => !(item.runtime_id === worker.runtime_id && item.worker_id === worker.worker_id), + ); + } + statusMessage = `Deleted Worker registry row for ${worker.label}.`; + } catch (error) { + statusMessage = error instanceof Error ? error.message : 'Worker cleanup failed'; + } finally { + busyCleanupTarget = null; + } } function workerStatus(worker: Worker): string { @@ -54,7 +104,7 @@

Workers

Workers running or persisted for this workspace. Pinning only updates Backend retention.

- {#if retentionStatus}

{retentionStatus}

{/if} + {#if statusMessage}

{statusMessage}

{/if}
New Worker @@ -81,6 +131,7 @@
{#each data.workers.items as worker} + {@const cleanup = cleanupCandidate(worker)} {/each} diff --git a/web/workspace/src/routes/w/[workspaceId]/workers/+page.ts b/web/workspace/src/routes/w/[workspaceId]/workers/+page.ts index 5fb0945f..d98ea12a 100644 --- a/web/workspace/src/routes/w/[workspaceId]/workers/+page.ts +++ b/web/workspace/src/routes/w/[workspaceId]/workers/+page.ts @@ -1,5 +1,5 @@ import { loadJson, workspaceApiPath } from "$lib/workspace-api/http"; -import type { ListResponse, Worker } from "$lib/workspace-sidebar/types"; +import type { ListResponse, RuntimeCleanupPlanResponse, Worker } from "$lib/workspace-sidebar/types"; import type { PageLoad } from "./$types"; export const load: PageLoad = async ({ fetch, params }) => { @@ -7,10 +7,28 @@ export const load: PageLoad = async ({ fetch, params }) => { fetch, workspaceApiPath(params.workspaceId, "/workers"), ); + const runtimeIds = Array.from(new Set(workers.data?.items.map((worker) => worker.runtime_id) ?? [])); + const cleanupPlanEntries = await Promise.all( + runtimeIds.map(async (runtimeId) => { + const cleanupPlan = await loadJson( + fetch, + workspaceApiPath(params.workspaceId, `/runtimes/${encodeURIComponent(runtimeId)}/cleanup-plan`), + ); + return [runtimeId, cleanupPlan] as const; + }), + ); + const cleanupPlans: Record = {}; + const cleanupPlanErrors: Record = {}; + for (const [runtimeId, cleanupPlan] of cleanupPlanEntries) { + if (cleanupPlan.data) cleanupPlans[runtimeId] = cleanupPlan.data; + if (cleanupPlan.error) cleanupPlanErrors[runtimeId] = cleanupPlan.error; + } return { workspaceId: params.workspaceId, workers: workers.data, workersError: workers.error, + cleanupPlans, + cleanupPlanErrors, }; };
Commit Status PolicyAction
{workdir.working_directory_id} {workdir.repository_id} + {#if cleanup} + + {#if cleanup.blocking_reason}{cleanup.blocking_reason}{/if} + {:else} + + {/if} +
{worker.label} @@ -100,6 +151,16 @@ + {#if cleanup} + + {/if}