ui: add runtime workdir pages

This commit is contained in:
2026-07-11 00:15:49 +09:00
parent 47e03bd54b
commit 24156947ac
12 changed files with 362 additions and 21 deletions
@@ -2,6 +2,7 @@
import type { PageProps } from './$types';
let { data }: PageProps = $props();
let workspaceId = $derived(data.workspace?.workspace_id ?? data.workspaceId);
</script>
<svelte:head>
@@ -37,6 +38,19 @@
{/if}
</section>
<section class="workspace-actions" aria-label="Workspace sections">
<a class="workspace-action-card" href={`/w/${workspaceId}/runtimes`}>
<span>Runtimes</span>
<strong>Manage runtimes and workdirs</strong>
<small>{data.hosts?.items.length ?? 0} host{(data.hosts?.items.length ?? 0) === 1 ? '' : 's'} visible</small>
</a>
<a class="workspace-action-card" href={`/w/${workspaceId}/workers`}>
<span>Workers</span>
<strong>Open worker list</strong>
<small>Inspect status and attach to consoles</small>
</a>
</section>
<section class="card">
<h2>Hosts</h2>
{#if data.hosts}
@@ -0,0 +1,72 @@
<script lang="ts">
import type { Runtime } from '$lib/workspace-sidebar/types';
import type { PageProps } from './$types';
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}`;
}
</script>
<svelte:head>
<title>Runtimes · Yoi Workspace</title>
<meta name="description" content="Workspace Runtimes" />
</svelte:head>
<section class="runtimes-page" aria-labelledby="runtimes-heading">
<header class="page-header-row">
<div>
<h1 id="runtimes-heading">Runtimes</h1>
<p>Execution backends available to this workspace.</p>
</div>
</header>
{#if data.runtimesError}
<p class="section-state error">{data.runtimesError}</p>
{:else if !data.runtimes}
<p class="section-state">Loading Runtimes…</p>
{:else if data.runtimes.items.length === 0}
<p class="section-state">No Runtimes are visible.</p>
{:else}
<div class="table-wrap">
<table class="runtimes-table">
<thead>
<tr>
<th>Runtime</th>
<th>Kind</th>
<th>Status</th>
<th>Scope</th>
<th>Platform</th>
<th>Capacity</th>
<th>Workdirs</th>
</tr>
</thead>
<tbody>
{#each data.runtimes.items as runtime}
<tr>
<td>
<strong>{runtime.label}</strong>
<small><code>{runtime.runtime_id}</code></small>
</td>
<td>{runtime.kind}</td>
<td>{runtime.status}</td>
<td>{runtimeScope(runtime)}</td>
<td>{runtimePlatform(runtime)}</td>
<td>{runtime.capabilities.max_workers} workers</td>
<td>
<a class="inline-link" href={`/w/${data.workspaceId}/runtimes/${encodeURIComponent(runtime.runtime_id)}/workdirs`}>
Open workdirs
</a>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</section>
@@ -0,0 +1,16 @@
import { loadJson, workspaceApiPath } from "$lib/workspace-api/http";
import type { ListResponse, Runtime } from "$lib/workspace-sidebar/types";
import type { PageLoad } from "./$types";
export const load: PageLoad = async ({ fetch, params }) => {
const runtimes = await loadJson<ListResponse<Runtime>>(
fetch,
workspaceApiPath(params.workspaceId, "/runtimes"),
);
return {
workspaceId: params.workspaceId,
runtimes: runtimes.data,
runtimesError: runtimes.error,
};
};
@@ -0,0 +1,70 @@
<script lang="ts">
import type { WorkingDirectorySummary } from '$lib/workspace-sidebar/types';
import type { PageProps } from './$types';
let { data }: PageProps = $props();
let runtimeLabel = $derived(
data.runtimes?.items.find((runtime) => runtime.runtime_id === data.runtimeId)?.label ?? data.runtimeId,
);
function commitLabel(workdir: WorkingDirectorySummary): string {
return workdir.resolved_commit ? workdir.resolved_commit.slice(0, 12) : '—';
}
function selectorLabel(workdir: WorkingDirectorySummary): string {
return workdir.requested_selector ?? 'HEAD';
}
</script>
<svelte:head>
<title>Workdirs · {runtimeLabel} · Yoi Workspace</title>
<meta name="description" content="Runtime workdirs" />
</svelte:head>
<section class="workdirs-page" aria-labelledby="workdirs-heading">
<header class="page-header-row">
<div>
<p class="breadcrumb"><a href={`/w/${data.workspaceId}/runtimes`}>Runtimes</a> / {runtimeLabel}</p>
<h1 id="workdirs-heading">Workdirs</h1>
<p>Workdirs owned by <code>{data.runtimeId}</code>.</p>
</div>
</header>
{#if data.workdirsError}
<p class="section-state error">{data.workdirsError}</p>
{:else if !data.workdirs}
<p class="section-state">Loading workdirs…</p>
{:else if data.workdirs.items.length === 0}
<p class="section-state">No workdirs are visible for this Runtime.</p>
{:else}
<div class="table-wrap">
<table class="workdirs-table">
<thead>
<tr>
<th>Workdir</th>
<th>Repository</th>
<th>Selector</th>
<th>Commit</th>
<th>Status</th>
<th>Policy</th>
</tr>
</thead>
<tbody>
{#each data.workdirs.items as workdir}
<tr>
<td><code>{workdir.working_directory_id}</code></td>
<td>{workdir.repository_id}</td>
<td>{selectorLabel(workdir)}</td>
<td><code>{commitLabel(workdir)}</code></td>
<td>{workdir.status}</td>
<td>
<span>{workdir.dirty_state_policy}</span>
<small>{workdir.cleanup_policy}</small>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</section>
@@ -0,0 +1,30 @@
import { loadJson, workspaceApiPath } from "$lib/workspace-api/http";
import type {
BrowserWorkingDirectoryListResponse,
ListResponse,
Runtime,
} from "$lib/workspace-sidebar/types";
import type { PageLoad } from "./$types";
export const load: PageLoad = async ({ fetch, params }) => {
const runtimeId = params.runtimeId;
const [runtimes, workdirs] = await Promise.all([
loadJson<ListResponse<Runtime>>(fetch, workspaceApiPath(params.workspaceId, "/runtimes")),
loadJson<BrowserWorkingDirectoryListResponse>(
fetch,
workspaceApiPath(
params.workspaceId,
`/runtimes/${encodeURIComponent(runtimeId)}/working-directories`,
),
),
]);
return {
workspaceId: params.workspaceId,
runtimeId,
runtimes: runtimes.data,
runtimesError: runtimes.error,
workdirs: workdirs.data,
workdirsError: workdirs.error,
};
};
@@ -51,7 +51,7 @@
<th>Runtime</th>
<th>Profile</th>
<th>Status</th>
<th>Working directory</th>
<th>Workdir</th>
<th>Action</th>
</tr>
</thead>
@@ -99,11 +99,11 @@
async function createWorkingDirectory() {
if (!runtimeId) {
submitError = { message: 'select a runtime before creating a working directory', diagnostics: [] };
submitError = { message: 'select a runtime before creating a workdir', diagnostics: [] };
return;
}
if (!workingDirectoryRepositoryId) {
submitError = { message: 'select a repository before creating a working directory', diagnostics: [] };
submitError = { message: 'select a repository before creating a workdir', diagnostics: [] };
return;
}
creatingWorkingDirectory = true;
@@ -122,7 +122,7 @@
},
);
if (!response.ok) {
submitError = await responseDisplayError(response, 'working directory create failed');
submitError = await responseDisplayError(response, 'workdir create failed');
return;
}
const payload = (await response.json()) as BrowserWorkingDirectoryCreateResponse;
@@ -138,7 +138,7 @@
: options;
workingDirectoryId = payload.item.working_directory_id;
} catch (err) {
submitError = exceptionDisplayError(err, 'working directory create failed');
submitError = exceptionDisplayError(err, 'workdir create failed');
} finally {
creatingWorkingDirectory = false;
}
@@ -150,7 +150,7 @@
return;
}
if (isNewWorkingDirectorySelected || !workingDirectoryId) {
submitError = { message: 'select or create a working directory before starting a Worker', diagnostics: [] };
submitError = { message: 'select or create a workdir before starting a Worker', diagnostics: [] };
return;
}
@@ -220,7 +220,7 @@
<header class="worker-new-page-header">
<div>
<h1 id="new-worker-heading">New Worker</h1>
<p>Create a Worker on a selected Runtime and working directory.</p>
<p>Create a Worker on a selected Runtime and workdir.</p>
</div>
<a class="secondary-link" href={`/w/${workspaceId}`}>Back to workspace</a>
</header>
@@ -236,14 +236,14 @@
<div class="worker-launch-sentence">
<span>Run at</span>
<select class="worker-inline-select wd-select" bind:value={workingDirectoryId} aria-label="Working directory">
<option value="">Select working directory</option>
<select class="worker-inline-select wd-select" bind:value={workingDirectoryId} aria-label="Workdir">
<option value="">Select workdir</option>
{#each options?.working_directories ?? [] as directory}
<option value={directory.working_directory_id} disabled={directory.status !== 'active'}>
{directory.repository_id} · {directory.requested_selector ?? 'HEAD'}
</option>
{/each}
<option value={NEW_WORKING_DIRECTORY_VALUE}>New working directory…</option>
<option value={NEW_WORKING_DIRECTORY_VALUE}>New workdir…</option>
</select>
<span>in</span>
<select class="worker-inline-select runtime-select" bind:value={runtimeId} required aria-label="Runtime">
@@ -261,7 +261,7 @@
{#if isNewWorkingDirectorySelected}
<div class="new-working-directory-panel">
<h3>New working directory</h3>
<h3>New workdir</h3>
<div class="new-working-directory-fields">
<label>
<span>Repository</span>
@@ -281,14 +281,14 @@
</label>
</div>
<button type="button" disabled={creatingWorkingDirectory || !runtimeId || !workingDirectoryRepositoryId} onclick={() => void createWorkingDirectory()}>
{creatingWorkingDirectory ? 'Creating…' : 'Create working directory'}
{creatingWorkingDirectory ? 'Creating…' : 'Create workdir'}
</button>
</div>
{/if}
<label class="relative-cwd-field">
<span>Relative cwd inside WD</span>
<input bind:value={relativeCwd} autocomplete="off" placeholder="Optional path inside working directory" />
<span>Relative cwd inside workdir</span>
<input bind:value={relativeCwd} autocomplete="off" placeholder="Optional path inside workdir" />
</label>
</section>