ui: add runtime workdir pages

This commit is contained in:
Keisuke Hirata 2026-07-11 00:15:49 +09:00
parent 47e03bd54b
commit 24156947ac
No known key found for this signature in database
12 changed files with 362 additions and 21 deletions

View File

@ -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 conditionsrunning 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

View File

@ -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);
} }

View File

@ -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),

View File

@ -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>

View File

@ -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}

View File

@ -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}

View File

@ -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>

View 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,
};
};

View File

@ -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>

View File

@ -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,
};
};

View File

@ -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>

View File

@ -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>