ui: add runtime workdir pages
This commit is contained in:
parent
47e03bd54b
commit
24156947ac
|
|
@ -2,7 +2,7 @@
|
||||||
title: "Runtime working directory materialization and sandboxed agent environments"
|
title: "Runtime working directory materialization and sandboxed agent environments"
|
||||||
state: "active"
|
state: "active"
|
||||||
created_at: "2026-07-06T16:28:12Z"
|
created_at: "2026-07-06T16:28:12Z"
|
||||||
updated_at: "2026-07-07T12:40:00Z"
|
updated_at: "2026-07-10T00:00:00Z"
|
||||||
linked_tickets: ["00001KWPC13WQ", "00001KWMBAA6V"]
|
linked_tickets: ["00001KWPC13WQ", "00001KWMBAA6V"]
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -34,7 +34,8 @@ Yoi は Workspace / Repository / Runtime を分ける方針になっている。
|
||||||
|
|
||||||
- Runtime root: Runtime が自身の store、cache、Worker metadata、working directory allocation を管理する root。長期的には `~/.yoi/runtimes/<runtime-id>/` 配下など、Workspace backend store とは別に置く。
|
- Runtime root: Runtime が自身の store、cache、Worker metadata、working directory allocation を管理する root。長期的には `~/.yoi/runtimes/<runtime-id>/` 配下など、Workspace backend store とは別に置く。
|
||||||
- Repository cache: Runtime-local の共有 source/cache。Git なら bare mirror / object cache / packfile cache など。重く、長寿命で、複数 Worker allocation から共有される。
|
- Repository cache: Runtime-local の共有 source/cache。Git なら bare mirror / object cache / packfile cache など。重く、長寿命で、複数 Worker allocation から共有される。
|
||||||
- working directory: Worker ごとの作業環境。短寿命で、Worker が読み書きする root / mounts / scratch / overlay を含む。Browser-facing UI/API では `Workspace` と混同しないよう、この呼称に寄せる。`Volume` は storage backing の候補名であり、この作業領域そのものの呼称にはしない。
|
- working directory: Worker ごとの作業環境。短寿命で、Worker が読み書きする root / mounts / scratch / overlay を含む。Browser-facing UI では `workspace` と混同しないよう `workdir` と表示してよい。`Volume` は storage backing の候補名であり、この作業領域そのものの呼称にはしない。
|
||||||
|
- Worker archive: 作業単位として保存する Worker record。profile、Runtime、RepositoryPoint / workdir evidence、session/transcript refs、status、diagnostics、summary、retention policy を束ねる。Session 単体ではなく Worker archive を保存単位にする。
|
||||||
- Materialization strategy: RepositoryPoint から working directory を作る実装戦略。Git detached worktree、sparse checkout、CoW snapshot、reflink copy、overlay、container filesystem など。
|
- Materialization strategy: RepositoryPoint から working directory を作る実装戦略。Git detached worktree、sparse checkout、CoW snapshot、reflink copy、overlay、container filesystem など。
|
||||||
- WorkingDirectoryAllocation: Runtime が払い出した作業環境の record。allocation id、worker id、workspace id、repository point、materializer kind、root、mounts、cleanup policy、status を持つ。
|
- WorkingDirectoryAllocation: Runtime が払い出した作業環境の record。allocation id、worker id、workspace id、repository point、materializer kind、root、mounts、cleanup policy、status を持つ。
|
||||||
- Sandbox policy: Worker が見られる filesystem、network、process、secret、tool authority の境界を表す policy。
|
- Sandbox policy: Worker が見られる filesystem、network、process、secret、tool authority の境界を表す policy。
|
||||||
|
|
@ -286,6 +287,17 @@ Rift の filtered CoW creation のように、重い artifacts を除外しな
|
||||||
- Runtime が複数 Workspace / Repository の Worker を同時に抱える場合の namespace、quota、cleanup、audit boundary を固める。
|
- Runtime が複数 Workspace / Repository の Worker を同時に抱える場合の namespace、quota、cleanup、audit boundary を固める。
|
||||||
- remote/self-hosted/hosted Runtime fleet で repository cache と working directory storage をどう扱うかを設計する。
|
- remote/self-hosted/hosted Runtime fleet で repository cache と working directory storage をどう扱うかを設計する。
|
||||||
|
|
||||||
|
### 7. Worker / Session / workdir retention policy
|
||||||
|
|
||||||
|
- Worker を保存単位にする。Session / transcript は Worker archive に内包または参照される履歴として扱い、Session 単体を長期保存 authority にしない。
|
||||||
|
- Worker archive には retention policy を持たせる。少なくとも `active` / `archived` / `pinned` / `prunable` 相当の状態を表現できること。`pinned` は必要であり、明示的に残す作業記録を cleanup / prune から守る。
|
||||||
|
- Session / transcript は削除可能にする。Worker が retained/pinned なら full history を残せるが、Worker prune 時には session files を削除、または summary だけ残して full transcript を捨てられるようにする。
|
||||||
|
- Workdir 実ファイルは durable history ではなく再現可能 cache として扱う。RepositoryPoint / resolved commit から再現でき、dirty/uncommitted state が無いなら、停止済み Worker に紐づく workdir files は cleanup eligible とする。
|
||||||
|
- Workdir record は Runtime-owned materialization evidence として扱う。実ファイルが削除済みなら `removed`、実ファイルが外部要因で欠落しているなら stale/missing materialization として診断可能にする。
|
||||||
|
- Workdir と Worker の関係は、Worker archive 側に workdir binding summary / RepositoryPoint / resolved commit を保存する。Workdir list で linked Workers を見せる場合は、まず Worker records から derived view を作る。
|
||||||
|
- Prune は plan-first にする。削除前に対象、理由、削除される bytes、残る archive/summary、blocking conditions(running Worker、pinned Worker、dirty state、unreachable commit など)を提示する。
|
||||||
|
- UI 表示は `workdir` に寄せる。内部型/API の互換名 `working_directory` は移行中に残ってよいが、Browser-facing navigation では Runtime 管理配下の `Workdirs` として扱う。
|
||||||
|
|
||||||
## Non-goals
|
## Non-goals
|
||||||
|
|
||||||
- v0 で完全な container sandbox を実装すること。
|
- v0 で完全な container sandbox を実装すること。
|
||||||
|
|
@ -307,6 +319,8 @@ Rift の filtered CoW creation のように、重い artifacts を除外しな
|
||||||
- Runtime process 起動時の `--workspace` は legacy bootstrap input として隔離され、Runtime identity や single workspace binding とみなされない。
|
- Runtime process 起動時の `--workspace` は legacy bootstrap input として隔離され、Runtime identity や single workspace binding とみなされない。
|
||||||
- Worker ごとの scope allocation conflict が、同一 source root を直接渡す設計ではなく materialized workspace allocation によって解消される。
|
- Worker ごとの scope allocation conflict が、同一 source root を直接渡す設計ではなく materialized workspace allocation によって解消される。
|
||||||
- Sandbox / mount / cache / secret boundary を後続実装で強化できる model になっている。
|
- Sandbox / mount / cache / secret boundary を後続実装で強化できる model になっている。
|
||||||
|
- Worker archive が Session / transcript refs と retention policy を束ね、`pinned` Worker を prune から守れる。
|
||||||
|
- Workdir 実ファイルは再現可能 cache として cleanup/prune でき、削除前に plan-first で linked Worker、session retention、commit reachability、dirty/missing 状態を確認できる。
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -463,6 +463,47 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workspace-actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-action-card {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-1);
|
||||||
|
padding: var(--space-4);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius-card);
|
||||||
|
background: var(--bg-raised);
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-action-card:hover,
|
||||||
|
.workspace-action-card:focus-visible {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-action-card span,
|
||||||
|
.workspace-action-card small {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-action-card span {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-action-card strong {
|
||||||
|
color: var(--text-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.workspace-actions {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.runtime-card {
|
.runtime-card {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
@ -646,11 +687,7 @@
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workers-page {
|
.page-header-row,
|
||||||
display: grid;
|
|
||||||
gap: var(--space-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.workers-page-header {
|
.workers-page-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
@ -658,22 +695,43 @@
|
||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-header-row h1,
|
||||||
|
.page-header-row p,
|
||||||
.workers-page-header h1,
|
.workers-page-header h1,
|
||||||
.workers-page-header p {
|
.workers-page-header p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-header-row h1,
|
||||||
.workers-page-header h1 {
|
.workers-page-header h1 {
|
||||||
color: var(--text-strong);
|
color: var(--text-strong);
|
||||||
font-size: clamp(1.6rem, 4vw, 2.4rem);
|
font-size: clamp(1.6rem, 4vw, 2.4rem);
|
||||||
line-height: 1.05;
|
line-height: 1.05;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-header-row p:not(.breadcrumb),
|
||||||
.workers-page-header p {
|
.workers-page-header p {
|
||||||
margin-top: var(--space-2);
|
margin-top: var(--space-2);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.breadcrumb {
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb a {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtimes-page,
|
||||||
|
.workdirs-page,
|
||||||
|
.workers-page {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
.workers-table-wrap {
|
.workers-table-wrap {
|
||||||
border-top: 1px solid var(--line);
|
border-top: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,11 @@ Deno.test("workspace Worker list lives on the dedicated Workers page", async ()
|
||||||
new URL("../workspace-sidebar/WorkspaceSidebar.svelte", import.meta.url),
|
new URL("../workspace-sidebar/WorkspaceSidebar.svelte", import.meta.url),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assert(
|
||||||
|
workspacePage.includes('href={`/w/${workspaceId}/runtimes`}') &&
|
||||||
|
workspacePage.includes('href={`/w/${workspaceId}/workers`}'),
|
||||||
|
"top workspace page should link to Runtimes and Workers pages",
|
||||||
|
);
|
||||||
assert(
|
assert(
|
||||||
!workspacePage.includes("workerConsoleHref") &&
|
!workspacePage.includes("workerConsoleHref") &&
|
||||||
!workspacePage.includes("Open Console"),
|
!workspacePage.includes("Open Console"),
|
||||||
|
|
@ -47,6 +52,43 @@ Deno.test("workspace Worker list lives on the dedicated Workers page", async ()
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Deno.test("workspace Runtime management pages expose Runtimes and Runtime-owned workdirs", async () => {
|
||||||
|
const sidebar = await Deno.readTextFile(
|
||||||
|
new URL("../workspace-sidebar/WorkspaceSidebar.svelte", import.meta.url),
|
||||||
|
);
|
||||||
|
const runtimesNav = await Deno.readTextFile(
|
||||||
|
new URL("../workspace-sidebar/RuntimesNavSection.svelte", import.meta.url),
|
||||||
|
);
|
||||||
|
const runtimesPage = await Deno.readTextFile(
|
||||||
|
new URL("./../../routes/w/[workspaceId]/runtimes/+page.svelte", import.meta.url),
|
||||||
|
);
|
||||||
|
const workdirsPage = await Deno.readTextFile(
|
||||||
|
new URL("./../../routes/w/[workspaceId]/runtimes/[runtimeId]/workdirs/+page.svelte", import.meta.url),
|
||||||
|
);
|
||||||
|
const workdirsLoad = await Deno.readTextFile(
|
||||||
|
new URL("./../../routes/w/[workspaceId]/runtimes/[runtimeId]/workdirs/+page.ts", import.meta.url),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert(
|
||||||
|
sidebar.includes("RuntimesNavSection") &&
|
||||||
|
runtimesNav.includes('href={runtimesHref}') &&
|
||||||
|
runtimesNav.includes("/runtimes"),
|
||||||
|
"sidebar should expose Runtime management navigation",
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
runtimesPage.includes("Open workdirs") &&
|
||||||
|
runtimesPage.includes("runtimes-table") &&
|
||||||
|
runtimesPage.includes("/workdirs"),
|
||||||
|
"Runtimes page should table Runtimes and link to each Runtime's workdirs",
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
workdirsPage.includes("Workdirs") &&
|
||||||
|
workdirsPage.includes("workdirs-table") &&
|
||||||
|
workdirsLoad.includes("/working-directories"),
|
||||||
|
"Workdirs page should read Runtime-owned working-directory API while using workdir UI language",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
Deno.test("workspace Worker sidebar links New to the dedicated create page", async () => {
|
Deno.test("workspace Worker sidebar links New to the dedicated create page", async () => {
|
||||||
const workersNav = await Deno.readTextFile(
|
const workersNav = await Deno.readTextFile(
|
||||||
new URL("../workspace-sidebar/WorkersNavSection.svelte", import.meta.url),
|
new URL("../workspace-sidebar/WorkersNavSection.svelte", import.meta.url),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<script lang="ts">
|
||||||
|
type Props = {
|
||||||
|
currentPath?: string;
|
||||||
|
workspaceId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
let { currentPath = '/', workspaceId }: Props = $props();
|
||||||
|
let runtimesHref = $derived(workspaceId ? `/w/${workspaceId}/runtimes` : '/runtimes');
|
||||||
|
let active = $derived(currentPath === runtimesHref || currentPath.startsWith(`${runtimesHref}/`));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="sidebar-section" aria-labelledby="runtimes-heading">
|
||||||
|
<div class="section-heading-row">
|
||||||
|
<h2 id="runtimes-heading">
|
||||||
|
<a
|
||||||
|
class="section-heading-link"
|
||||||
|
class:active
|
||||||
|
href={runtimesHref}
|
||||||
|
aria-current={active ? 'page' : undefined}
|
||||||
|
>runtimes</a>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import { workspaceRoute } from '$lib/workspace-api/http';
|
import { workspaceRoute } from '$lib/workspace-api/http';
|
||||||
import ObjectivesNavSection from './ObjectivesNavSection.svelte';
|
import ObjectivesNavSection from './ObjectivesNavSection.svelte';
|
||||||
import RepositoriesNavSection from './RepositoriesNavSection.svelte';
|
import RepositoriesNavSection from './RepositoriesNavSection.svelte';
|
||||||
|
import RuntimesNavSection from './RuntimesNavSection.svelte';
|
||||||
import WorkersNavSection from './WorkersNavSection.svelte';
|
import WorkersNavSection from './WorkersNavSection.svelte';
|
||||||
import type { RepositoryListResponse, WorkspaceResponse } from './types';
|
import type { RepositoryListResponse, WorkspaceResponse } from './types';
|
||||||
|
|
||||||
|
|
@ -106,6 +107,7 @@
|
||||||
<nav class="sidebar-sections" aria-label="Workspace sections">
|
<nav class="sidebar-sections" aria-label="Workspace sections">
|
||||||
<RepositoriesNavSection {repositories} {repositoriesError} {currentPath} {workspaceId} />
|
<RepositoriesNavSection {repositories} {repositoriesError} {currentPath} {workspaceId} />
|
||||||
<ObjectivesNavSection {currentPath} {workspaceId} />
|
<ObjectivesNavSection {currentPath} {workspaceId} />
|
||||||
|
<RuntimesNavSection {currentPath} {workspaceId} />
|
||||||
<WorkersNavSection {currentPath} {workspaceId} />
|
<WorkersNavSection {currentPath} {workspaceId} />
|
||||||
</nav>
|
</nav>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import type { PageProps } from './$types';
|
import type { PageProps } from './$types';
|
||||||
|
|
||||||
let { data }: PageProps = $props();
|
let { data }: PageProps = $props();
|
||||||
|
let workspaceId = $derived(data.workspace?.workspace_id ?? data.workspaceId);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
|
@ -37,6 +38,19 @@
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</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">
|
<section class="card">
|
||||||
<h2>Hosts</h2>
|
<h2>Hosts</h2>
|
||||||
{#if data.hosts}
|
{#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>
|
||||||
16
web/workspace/src/routes/w/[workspaceId]/runtimes/+page.ts
Normal file
16
web/workspace/src/routes/w/[workspaceId]/runtimes/+page.ts
Normal file
|
|
@ -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>Runtime</th>
|
||||||
<th>Profile</th>
|
<th>Profile</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Working directory</th>
|
<th>Workdir</th>
|
||||||
<th>Action</th>
|
<th>Action</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
|
||||||
|
|
@ -99,11 +99,11 @@
|
||||||
|
|
||||||
async function createWorkingDirectory() {
|
async function createWorkingDirectory() {
|
||||||
if (!runtimeId) {
|
if (!runtimeId) {
|
||||||
submitError = { message: 'select a runtime before creating a working directory', diagnostics: [] };
|
submitError = { message: 'select a runtime before creating a workdir', diagnostics: [] };
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!workingDirectoryRepositoryId) {
|
if (!workingDirectoryRepositoryId) {
|
||||||
submitError = { message: 'select a repository before creating a working directory', diagnostics: [] };
|
submitError = { message: 'select a repository before creating a workdir', diagnostics: [] };
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
creatingWorkingDirectory = true;
|
creatingWorkingDirectory = true;
|
||||||
|
|
@ -122,7 +122,7 @@
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
submitError = await responseDisplayError(response, 'working directory create failed');
|
submitError = await responseDisplayError(response, 'workdir create failed');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const payload = (await response.json()) as BrowserWorkingDirectoryCreateResponse;
|
const payload = (await response.json()) as BrowserWorkingDirectoryCreateResponse;
|
||||||
|
|
@ -138,7 +138,7 @@
|
||||||
: options;
|
: options;
|
||||||
workingDirectoryId = payload.item.working_directory_id;
|
workingDirectoryId = payload.item.working_directory_id;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
submitError = exceptionDisplayError(err, 'working directory create failed');
|
submitError = exceptionDisplayError(err, 'workdir create failed');
|
||||||
} finally {
|
} finally {
|
||||||
creatingWorkingDirectory = false;
|
creatingWorkingDirectory = false;
|
||||||
}
|
}
|
||||||
|
|
@ -150,7 +150,7 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isNewWorkingDirectorySelected || !workingDirectoryId) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -220,7 +220,7 @@
|
||||||
<header class="worker-new-page-header">
|
<header class="worker-new-page-header">
|
||||||
<div>
|
<div>
|
||||||
<h1 id="new-worker-heading">New Worker</h1>
|
<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>
|
</div>
|
||||||
<a class="secondary-link" href={`/w/${workspaceId}`}>Back to workspace</a>
|
<a class="secondary-link" href={`/w/${workspaceId}`}>Back to workspace</a>
|
||||||
</header>
|
</header>
|
||||||
|
|
@ -236,14 +236,14 @@
|
||||||
|
|
||||||
<div class="worker-launch-sentence">
|
<div class="worker-launch-sentence">
|
||||||
<span>Run at</span>
|
<span>Run at</span>
|
||||||
<select class="worker-inline-select wd-select" bind:value={workingDirectoryId} aria-label="Working directory">
|
<select class="worker-inline-select wd-select" bind:value={workingDirectoryId} aria-label="Workdir">
|
||||||
<option value="">Select working directory</option>
|
<option value="">Select workdir</option>
|
||||||
{#each options?.working_directories ?? [] as directory}
|
{#each options?.working_directories ?? [] as directory}
|
||||||
<option value={directory.working_directory_id} disabled={directory.status !== 'active'}>
|
<option value={directory.working_directory_id} disabled={directory.status !== 'active'}>
|
||||||
{directory.repository_id} · {directory.requested_selector ?? 'HEAD'}
|
{directory.repository_id} · {directory.requested_selector ?? 'HEAD'}
|
||||||
</option>
|
</option>
|
||||||
{/each}
|
{/each}
|
||||||
<option value={NEW_WORKING_DIRECTORY_VALUE}>New working directory…</option>
|
<option value={NEW_WORKING_DIRECTORY_VALUE}>New workdir…</option>
|
||||||
</select>
|
</select>
|
||||||
<span>in</span>
|
<span>in</span>
|
||||||
<select class="worker-inline-select runtime-select" bind:value={runtimeId} required aria-label="Runtime">
|
<select class="worker-inline-select runtime-select" bind:value={runtimeId} required aria-label="Runtime">
|
||||||
|
|
@ -261,7 +261,7 @@
|
||||||
|
|
||||||
{#if isNewWorkingDirectorySelected}
|
{#if isNewWorkingDirectorySelected}
|
||||||
<div class="new-working-directory-panel">
|
<div class="new-working-directory-panel">
|
||||||
<h3>New working directory</h3>
|
<h3>New workdir</h3>
|
||||||
<div class="new-working-directory-fields">
|
<div class="new-working-directory-fields">
|
||||||
<label>
|
<label>
|
||||||
<span>Repository</span>
|
<span>Repository</span>
|
||||||
|
|
@ -281,14 +281,14 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" disabled={creatingWorkingDirectory || !runtimeId || !workingDirectoryRepositoryId} onclick={() => void createWorkingDirectory()}>
|
<button type="button" disabled={creatingWorkingDirectory || !runtimeId || !workingDirectoryRepositoryId} onclick={() => void createWorkingDirectory()}>
|
||||||
{creatingWorkingDirectory ? 'Creating…' : 'Create working directory'}
|
{creatingWorkingDirectory ? 'Creating…' : 'Create workdir'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<label class="relative-cwd-field">
|
<label class="relative-cwd-field">
|
||||||
<span>Relative cwd inside WD</span>
|
<span>Relative cwd inside workdir</span>
|
||||||
<input bind:value={relativeCwd} autocomplete="off" placeholder="Optional path inside working directory" />
|
<input bind:value={relativeCwd} autocomplete="off" placeholder="Optional path inside workdir" />
|
||||||
</label>
|
</label>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user