merge: runtime worker controls
This commit is contained in:
commit
4edaa73dde
|
|
@ -0,0 +1,3 @@
|
|||
{"id":"orch-plan-20260702-164730-1","ticket_id":"00001KWHEM8YJ","kind":"waiting_capacity_note","note":"Dashboard 起動時の queue review。対象 Ticket は未指定で、ユーザー指示により explicit follow-up まで role Pod spawn / queued->inprogress acceptance は行わない human gate として待機する。確認済み: Ticket 本文/最近の thread、TicketRelationQuery(0件)、TicketOrchestrationPlanQuery(0件)、workspace git/worktree 状態、visible Pods、TicketDoctor(0 errors)。","author":"orchestrator","at":"2026-07-02T16:47:30Z"}
|
||||
{"id":"orch-plan-20260702-170047-2","ticket_id":"00001KWHEM8YJ","kind":"accepted_plan","accepted_plan":{"summary":"`00001KWHHRTM9` と `00001KWHEM8YJ` は workspace Browser の runtime/worker UI/API surface が重なるため、別 worktree 並列ではなく同一 branch/worktree で順序実装する。human gate は明示 follow-up で解除済み。","branch":"work/00001KWHHRTM9-00001KWHEM8YJ","worktree":"/home/hare/Projects/yoi/.worktree/00001KWHHRTM9-00001KWHEM8YJ","role_plan":"単一 sibling Coder Pod に同一 implementation worktree を委譲し、Runtime connection settings/API を先に実装してから Manual Coding Worker launch 導線を実装する。実装後は別 sibling Reviewer Pod で両 Ticket の recorded IntentPacket / invariants / acceptance criteria に照らしてレビューする。"},"author":"orchestrator","at":"2026-07-02T17:00:47Z"}
|
||||
{"id":"orch-plan-20260702-170054-3","ticket_id":"00001KWHEM8YJ","kind":"after","related_ticket":"00001KWHHRTM9","note":"同一 worktree で順序実装する。Manual Coding Worker launch は Runtime connection settings/API の candidate/projection 境界に合わせて後続実装する。","author":"orchestrator","at":"2026-07-02T17:00:54Z"}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
title: 'Workspace Browserから手動Coding Workerを作成する導線を追加する'
|
||||
state: 'queued'
|
||||
state: 'inprogress'
|
||||
created_at: '2026-07-02T12:59:57Z'
|
||||
updated_at: '2026-07-02T16:13:24Z'
|
||||
updated_at: '2026-07-02T17:01:11Z'
|
||||
assignee: null
|
||||
queued_by: 'workspace-panel'
|
||||
queued_at: '2026-07-02T16:13:24Z'
|
||||
|
|
|
|||
|
|
@ -62,4 +62,97 @@ Marked ready by `yoi ticket state`.
|
|||
Ticket を `workspace-panel` が queued にしました。
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: orchestrator at: 2026-07-02T17:00:32Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
Routing decision: implementation_ready
|
||||
|
||||
Reason:
|
||||
- Ticket は Manual Coding Worker launch の product-level API / Sidebar New form / sanitized payload / success Console navigation / unsupported runtime diagnostic を具体化しており、observable な acceptance criteria と validation がある。
|
||||
- typed relation blocker は 0 件、OrchestrationPlan は prior human-gate waiting note のみで、ユーザーから「2つとも消化して」と明示 follow-up があったため human gate は解除された。
|
||||
- `00001KWHHRTM9` の Runtime connection management と UI/API surface が近く、別 worktree 並列では衝突リスクが高いので、同一 implementation worktree/branch で `00001KWHHRTM9` を先に実装し、その Runtime candidate/projection を利用して本 Ticket を続けて実装する。
|
||||
|
||||
Evidence checked:
|
||||
- Ticket body / thread / artifacts。
|
||||
- `TicketRelationQuery(00001KWHEM8YJ)` は 0 件。
|
||||
- `TicketOrchestrationPlanQuery(00001KWHEM8YJ)` は prior human-gate waiting note のみ。
|
||||
- queued Ticket 一覧では `00001KWHHRTM9` も queued、ready/inprogress は 0 件。
|
||||
- workspace/orchestration git state と worktree 一覧、visible Pods、TicketDoctor(0 errors / 既存 warning のみ)。
|
||||
- Bounded code map: `web/workspace/src/lib/workspace-sidebar/WorkersNavSection.svelte`、console route under `web/workspace/src/routes/runtimes/[runtimeId]/workers/[workerId]/console/`、existing `/api/workers` list projection、`/api/runtimes/{runtime_id}/workers` internal-ish Runtime spawn path、`crates/workspace-server` runtime/backend/config areas。
|
||||
|
||||
IntentPacket:
|
||||
|
||||
Intent:
|
||||
- Workspace Browser sidebar の WORKER heading 横に `New` button と作成 form を追加し、Browser-facing `/api/workers` product-level endpoint から coding Worker を作成し、成功後に Worker Console へ遷移する。
|
||||
|
||||
Binding decisions / invariants:
|
||||
- Browser UI は existing Runtime create payload を直接露出しない。
|
||||
- Browser-facing request fields は `runtime_id` / `display_name` / `profile` / `initial_text` のみ。`kind` は endpoint/backend launch mode として扱い、form input や request field にしない。
|
||||
- Browser-facing payload/response/form に raw workspace path、cwd、tool scope、`ConfigBundleRef`、requested capabilities、secret/token、Runtime endpoint、socket/session/store path を含めない。
|
||||
- `profile` は Backend が公開する候補から選ばせ、自由入力にしない。
|
||||
- v0 は embedded/local Runtime を primary target とし、remote Runtime の workspace provisioning が未対応なら typed diagnostic で拒否してよい。
|
||||
- `/api/runtimes/{runtime_id}/workers` は残してよいが、New Worker UI は新しい product-level `/api/workers` POST を使う。
|
||||
|
||||
Requirements / acceptance criteria:
|
||||
- Sidebar WORKER heading 横に `New` button がある。
|
||||
- `New` で display name / runtime / profile / initial text だけの form が開く。
|
||||
- Form submit は `/api/workers` product-level endpoint を使い、coding Worker を作成できる。
|
||||
- Backend は request の `profile` から Profile / ConfigBundle / execution backend を解決する。
|
||||
- 作成成功後、Worker list を refresh し、`/runtimes/{runtime_id}/workers/{worker_id}/console` へ遷移する。
|
||||
- 作成失敗時、入力値を保持して sanitized diagnostic を form に表示する。
|
||||
- focused backend/UI tests を追加し、指定 validation を可能な範囲で実行する。
|
||||
|
||||
Implementation latitude:
|
||||
- Runtime candidate は `00001KWHHRTM9` の projection を利用する。v0 で embedded-only fallback が必要なら許容されるが、Browser 自由入力にはしない。
|
||||
- Product-level endpoint の internal mapping、response shape、Svelte form placement(inline/sidebar panel/modal)は既存 UX を壊さない範囲で選んでよい。
|
||||
- Profile candidate の v0 source は backend-provided static/builtin candidates でもよい。ただし自由入力化しない。
|
||||
|
||||
Escalate if:
|
||||
- Browser-facing API に raw path/cwd/scope/secret/runtime internal location を含めないと実装できない場合。
|
||||
- Profile/ConfigBundle resolution の既存 boundary を変える必要が出た場合。
|
||||
- remote runtime provisioning を完成させないと acceptance を満たせない場合。
|
||||
|
||||
Validation:
|
||||
- `cd web/workspace && deno task test`
|
||||
- `cd web/workspace && deno task check`
|
||||
- `cargo test -p yoi-workspace-server`
|
||||
- `cargo check -p yoi`
|
||||
- `git diff --check`
|
||||
- `nix build .#yoi --no-link` は時間/依存変更の重さを見て実行、未実行なら理由を report。
|
||||
|
||||
Current code map:
|
||||
- Sidebar UI: `web/workspace/src/lib/workspace-sidebar/WorkersNavSection.svelte`, `WorkspaceSidebar.svelte`。
|
||||
- Worker console/navigation: `web/workspace/src/lib/workspace-console/model`, `web/workspace/src/routes/runtimes/[runtimeId]/workers/[workerId]/console/`。
|
||||
- Existing worker list and runtime spawn APIs: `crates/workspace-server` `/api/workers` and `/api/runtimes/{runtime_id}/workers` handlers, worker-runtime `CreateWorkerRequest` / config bundle resolution paths。
|
||||
- Runtime candidates/settings base: `00001KWHHRTM9` implementation in the same worktree.
|
||||
|
||||
Critical risks / reviewer focus:
|
||||
- Product-level endpoint does not deserialize or expose the internal Runtime create request shape.
|
||||
- Browser-facing surfaces are sanitized and do not leak raw path/cwd/scope/ConfigBundleRef/requested capabilities/secret/runtime endpoint/socket/session/store path.
|
||||
- Profile/runtime selectors are bounded to backend-published choices.
|
||||
- Success navigation and worker list refresh are deterministic.
|
||||
- The combined implementation with `00001KWHHRTM9` keeps runtime connection management and manual worker launch contracts consistent.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: orchestrator at: 2026-07-02T17:01:11Z from: queued to: inprogress reason: routing_acceptance_implementation_ready field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Queued acceptance recorded after explicit user follow-up 「2つとも消化して」。
|
||||
|
||||
Checked context:
|
||||
- Ticket body / thread / artifacts。
|
||||
- `TicketRelationQuery(00001KWHEM8YJ)`: blocking relation 0 件。
|
||||
- `TicketOrchestrationPlanQuery(00001KWHEM8YJ)`: prior human-gate waiting note を確認し、今回 accepted_plan / after ordering を記録済み。
|
||||
- workspace/worktree/visible Pod/TicketDoctor/code-map の bounded check。
|
||||
|
||||
Acceptance basis:
|
||||
- concrete missing decision / information は残っていない。
|
||||
- `00001KWHHRTM9` と surface が重なるため、同一 implementation worktree で `00001KWHHRTM9` の後に実装する。
|
||||
- side effect はこの `queued -> inprogress` acceptance 後に開始する。
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
{"id":"orch-plan-20260702-164730-1","ticket_id":"00001KWHHRTM9","kind":"waiting_capacity_note","note":"Dashboard 起動時の queue review。対象 Ticket は未指定で、ユーザー指示により explicit follow-up まで role Pod spawn / queued->inprogress acceptance は行わない human gate として待機する。確認済み: Ticket 本文/最近の thread、TicketRelationQuery(0件)、TicketOrchestrationPlanQuery(0件)、workspace git/worktree 状態、visible Pods、TicketDoctor(0 errors)。本文上の依存先 00001KWHJ0XH6 は TicketShow で closed を確認済み。","author":"orchestrator","at":"2026-07-02T16:47:30Z"}
|
||||
{"id":"orch-plan-20260702-170047-2","ticket_id":"00001KWHHRTM9","kind":"accepted_plan","accepted_plan":{"summary":"`00001KWHHRTM9` と `00001KWHEM8YJ` は workspace Browser の runtime/worker UI/API surface が重なるため、別 worktree 並列ではなく同一 branch/worktree で順序実装する。human gate は明示 follow-up で解除済み。","branch":"work/00001KWHHRTM9-00001KWHEM8YJ","worktree":"/home/hare/Projects/yoi/.worktree/00001KWHHRTM9-00001KWHEM8YJ","role_plan":"単一 sibling Coder Pod に同一 implementation worktree を委譲し、Runtime connection settings/API を先に実装してから Manual Coding Worker launch 導線を実装する。実装後は別 sibling Reviewer Pod で両 Ticket の recorded IntentPacket / invariants / acceptance criteria に照らしてレビューする。"},"author":"orchestrator","at":"2026-07-02T17:00:47Z"}
|
||||
{"id":"orch-plan-20260702-170054-3","ticket_id":"00001KWHHRTM9","kind":"before","related_ticket":"00001KWHEM8YJ","note":"同一 worktree で順序実装する。Runtime connection settings/API が Manual Coding Worker form の runtime candidate/projection 基盤になるため、まずこちらを実装する。","author":"orchestrator","at":"2026-07-02T17:00:54Z"}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
title: 'Workspace Backend Runtime接続の管理画面と永続configを追加する'
|
||||
state: 'queued'
|
||||
state: 'inprogress'
|
||||
created_at: '2026-07-02T13:54:52Z'
|
||||
updated_at: '2026-07-02T16:45:19Z'
|
||||
updated_at: '2026-07-02T17:01:01Z'
|
||||
assignee: null
|
||||
queued_by: 'workspace-panel'
|
||||
queued_at: '2026-07-02T16:45:19Z'
|
||||
|
|
|
|||
|
|
@ -78,4 +78,101 @@ Marked ready by `yoi ticket state`.
|
|||
Ticket を `workspace-panel` が queued にしました。
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: orchestrator at: 2026-07-02T16:59:57Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
Routing decision: implementation_ready
|
||||
|
||||
Reason:
|
||||
- Ticket は Runtime connection settings API / `.yoi/workspace-backend.local.toml` 永続化 / Settings UI / sanitized diagnostics / restart_required の v0 境界を具体化しており、observable な acceptance criteria と validation がある。
|
||||
- 先行 Settings shell Ticket `00001KWHJ0XH6` は closed を確認済み。
|
||||
- typed relation blocker は 0 件、OrchestrationPlan は human gate の waiting note のみで、ユーザーから「2つとも消化して」と明示 follow-up があったため human gate は解除された。
|
||||
- Manual Coding Worker Ticket `00001KWHEM8YJ` と UI/API surface が近く、並列別 worktree では衝突リスクが高いので、同一 implementation worktree/branch で Runtime connection 管理を先に実装し、続けて manual Worker 作成導線を実装する。
|
||||
|
||||
Evidence checked:
|
||||
- Ticket body / thread / artifacts。
|
||||
- `TicketRelationQuery(00001KWHHRTM9)` は 0 件。
|
||||
- `TicketOrchestrationPlanQuery(00001KWHHRTM9)` は prior human-gate waiting note のみ。
|
||||
- `TicketShow(00001KWHJ0XH6)` は closed。
|
||||
- queued Ticket 一覧では `00001KWHEM8YJ` も queued、ready/inprogress は 0 件。
|
||||
- workspace/orchestration git state と worktree 一覧、visible Pods、TicketDoctor(0 errors / 既存 warning のみ)。
|
||||
- Bounded code map: `crates/workspace-server` の workspace backend / runtime registry / config file 周辺、`web/workspace/src/lib/workspace-settings/SettingsPage.svelte`、`web/workspace/src/routes/settings/+page.svelte`、既存 `/api/workers` / `/api/runtimes/{runtime_id}/workers` projection、`WorkersNavSection.svelte`。
|
||||
|
||||
IntentPacket:
|
||||
|
||||
Intent:
|
||||
- Workspace Browser の Settings / Runtime Connections で embedded Runtime 表示、remote Runtime connection の list/add/delete/test negotiation、`.yoi/workspace-backend.local.toml` 永続化、restart_required diagnostic を提供する。
|
||||
- `00001KWHEM8YJ` が使う Runtime candidate projection の基盤を作る。
|
||||
|
||||
Binding decisions / invariants:
|
||||
- Runtime が Backend に接続するのではなく、Backend が configured Runtime source を `RuntimeRegistry` に登録する現行構造を前提にする。
|
||||
- embedded Runtime は built-in として表示し、config 管理対象や削除対象にしない。
|
||||
- remote Runtime connection は既存 `[[runtimes.remote]]` schema に保存する。
|
||||
- v0 は config persistence 優先で、config 更新後は `restart_required = true`。live register/unregister は対象外。
|
||||
- raw token 値、secret、socket/session/store path、Runtime event cursor、live handle、config file path は UI/API response に出さない。
|
||||
- negotiation/test result、observed capabilities、health result、checked_at は local config に保存しない。
|
||||
- protocol version が無ければ fake version を作らず、compatibility basis として表現する。
|
||||
|
||||
Requirements / acceptance criteria:
|
||||
- Runtime Connections 管理画面が Settings shell 内にある。
|
||||
- embedded Runtime は built-in / delete不可として表示される。
|
||||
- remote connection を add/delete でき、config の `[[runtimes.remote]]` が read-modify-write される。
|
||||
- test negotiation は `GET /v1/runtime` parse と Browser 必要操作に対する compatibility/sanitized diagnostics を返す。
|
||||
- duplicate id / invalid endpoint / incompatible runtime / embedded delete attempt は typed diagnostic。
|
||||
- request/response/config に raw token 値や internal paths を含めない。
|
||||
- focused backend/UI tests を追加し、指定 validation を可能な範囲で実行する。
|
||||
|
||||
Implementation latitude:
|
||||
- TOML comments/format preservation は v0 で typed serialize により失われてもよい。ただし implementation report/docs/test で明記する。
|
||||
- POST 保存前に test を必須にするか、保存は許して test diagnostic を別扱いにするかは、Ticket の v0 境界内で選んでよい。
|
||||
- API response shape / Svelte component分割 / test helper構成は既存 style に合わせて選んでよい。
|
||||
|
||||
Escalate if:
|
||||
- secret store / raw token input / live RuntimeRegistry unregister / remote workspace provisioning / protocol version追加を必須にしないと満たせない場合。
|
||||
- existing config schema を壊す必要がある場合。
|
||||
- Browser-facing API に raw path/secret/runtime internal location を出す誘惑が出た場合。
|
||||
|
||||
Validation:
|
||||
- `cd web/workspace && deno task test`
|
||||
- `cd web/workspace && deno task check`
|
||||
- `cargo test -p yoi-workspace-server`
|
||||
- `cargo check -p yoi`
|
||||
- `git diff --check`
|
||||
- `nix build .#yoi --no-link` は時間/依存変更の重さを見て実行、未実行なら理由を report。
|
||||
|
||||
Current code map:
|
||||
- Settings UI: `web/workspace/src/lib/workspace-settings/SettingsPage.svelte`, `web/workspace/src/routes/settings/+page.svelte`。
|
||||
- Worker/sidebar UI: `web/workspace/src/lib/workspace-sidebar/WorkersNavSection.svelte`, console route under `web/workspace/src/routes/runtimes/[runtimeId]/workers/[workerId]/console/`。
|
||||
- Backend/runtime/config areas: `crates/workspace-server`, existing `/api/workers`, `/api/runtimes/{runtime_id}/workers`, `WorkspaceBackendConfigFile`, `ServerConfig.remote_runtime_sources`, `RuntimeRegistry`, `EmbeddedWorkerRuntime`。
|
||||
|
||||
Critical risks / reviewer focus:
|
||||
- persisted config、live registry state、test/negotiation observation を混同していないか。
|
||||
- Browser-facing API に secrets/internal paths/runtime store/socket/session/config file path が漏れていないか。
|
||||
- embedded delete が fail closed か。
|
||||
- restart_required semantics が UI/API で明確か。
|
||||
- `00001KWHEM8YJ` と同一 worktreeで実装し、runtime candidates の shared contract が破綻していないか。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: orchestrator at: 2026-07-02T17:01:01Z from: queued to: inprogress reason: routing_acceptance_implementation_ready field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Queued acceptance recorded after explicit user follow-up 「2つとも消化して」。
|
||||
|
||||
Checked context:
|
||||
- Ticket body / thread / artifacts。
|
||||
- `TicketRelationQuery(00001KWHHRTM9)`: blocking relation 0 件。
|
||||
- `TicketOrchestrationPlanQuery(00001KWHHRTM9)`: prior human-gate waiting note を確認し、今回 accepted_plan / before ordering を記録済み。
|
||||
- `00001KWHJ0XH6` は closed。
|
||||
- workspace/worktree/visible Pod/TicketDoctor/code-map の bounded check。
|
||||
|
||||
Acceptance basis:
|
||||
- concrete missing decision / information は残っていない。
|
||||
- `00001KWHEM8YJ` と surface が重なるため、同一 implementation worktree でこの Ticket を先に実装する。
|
||||
- side effect はこの `queued -> inprogress` acceptance 後に開始する。
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::net::SocketAddr;
|
|||
use std::path::{Path, PathBuf};
|
||||
use std::{fs, io};
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::hosts::RemoteRuntimeConfig;
|
||||
use crate::identity::WorkspaceIdentity;
|
||||
|
|
@ -16,7 +16,7 @@ const DEFAULT_LISTEN: &str = "127.0.0.1:8787";
|
|||
const DEFAULT_FRONTEND_URL: &str = "http://127.0.0.1:5173";
|
||||
const DEFAULT_MAX_RECORDS: usize = 200;
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct WorkspaceBackendConfigFile {
|
||||
#[serde(default)]
|
||||
|
|
@ -29,7 +29,7 @@ pub struct WorkspaceBackendConfigFile {
|
|||
pub runtimes: WorkspaceBackendRuntimesConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct WorkspaceBackendServerConfig {
|
||||
#[serde(default)]
|
||||
|
|
@ -40,7 +40,7 @@ pub struct WorkspaceBackendServerConfig {
|
|||
pub static_assets_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct WorkspaceBackendDataConfig {
|
||||
#[serde(default)]
|
||||
|
|
@ -51,21 +51,21 @@ pub struct WorkspaceBackendDataConfig {
|
|||
pub embedded_runtime_store_root: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct WorkspaceBackendLimitsConfig {
|
||||
#[serde(default)]
|
||||
pub max_records: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct WorkspaceBackendRuntimesConfig {
|
||||
#[serde(default)]
|
||||
pub remote: Vec<RemoteRuntimeConfigFile>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct RemoteRuntimeConfigFile {
|
||||
pub id: String,
|
||||
|
|
@ -189,6 +189,20 @@ impl WorkspaceBackendConfigFile {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn write_for_workspace(&self, workspace_root: impl AsRef<Path>) -> Result<()> {
|
||||
let path = Self::path_for_workspace(workspace_root);
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
let raw = toml::to_string_pretty(self).map_err(|error| {
|
||||
Error::Config(format!(
|
||||
"failed to serialize workspace backend config: {error}"
|
||||
))
|
||||
})?;
|
||||
fs::write(path, raw)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn parse_str(raw: &str, path: impl AsRef<Path>) -> Result<Self> {
|
||||
toml::from_str(raw).map_err(|error| {
|
||||
Error::Config(format!(
|
||||
|
|
|
|||
|
|
@ -266,6 +266,7 @@ pub struct WorkerSpawnRequest {
|
|||
pub enum WorkerSpawnIntent {
|
||||
WorkspaceCompanion,
|
||||
WorkspaceOrchestrator,
|
||||
WorkspaceCoding,
|
||||
TicketRole {
|
||||
ticket_id: String,
|
||||
role: TicketWorkerRole,
|
||||
|
|
@ -2260,6 +2261,7 @@ fn embedded_profile_selector(intent: &WorkerSpawnIntent) -> ProfileSelector {
|
|||
ProfileSelector::Builtin("builtin:companion".to_string())
|
||||
}
|
||||
WorkerSpawnIntent::WorkspaceOrchestrator => ProfileSelector::RuntimeDefault,
|
||||
WorkerSpawnIntent::WorkspaceCoding => ProfileSelector::Builtin("builtin:coder".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2680,6 +2682,7 @@ fn worker_spawn_intent_label(intent: &WorkerSpawnIntent) -> &'static str {
|
|||
match intent {
|
||||
WorkerSpawnIntent::WorkspaceCompanion => "workspace_companion",
|
||||
WorkerSpawnIntent::WorkspaceOrchestrator => "workspace_orchestrator",
|
||||
WorkerSpawnIntent::WorkspaceCoding => "workspace_coding",
|
||||
WorkerSpawnIntent::TicketRole { role, .. } => match role {
|
||||
TicketWorkerRole::Intake => "ticket_intake",
|
||||
TicketWorkerRole::Orchestrator => "ticket_orchestrator",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -6,7 +6,7 @@
|
|||
"dev": "deno run -A npm:vite@7.2.7 dev",
|
||||
"dev:backend": "cd ../.. && cargo run -p yoi-workspace-server -- serve --workspace . --db .yoi/workspace.db --listen 127.0.0.1:8787",
|
||||
"check": "deno run -A npm:@sveltejs/kit@2.49.4 sync && deno run -A npm:svelte-check@4.3.4 --tsconfig ./tsconfig.json",
|
||||
"test": "deno test --allow-read=src src/lib/workspace-console/model.test.ts src/lib/workspace-console/worker-console.ui.test.ts src/lib/workspace-settings/model.test.ts",
|
||||
"test": "deno test --allow-read=src src/lib/workspace-console/model.test.ts src/lib/workspace-console/worker-console.ui.test.ts src/lib/workspace-settings/model.test.ts src/lib/workspace-sidebar/worker-launch.test.ts",
|
||||
"build": "deno run -A npm:vite@7.2.7 build",
|
||||
"preview": "deno run -A npm:vite@7.2.7 preview"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1140,3 +1140,156 @@
|
|||
display: grid;
|
||||
}
|
||||
}
|
||||
|
||||
.section-heading-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.section-action {
|
||||
margin-left: auto;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
background: var(--bg-subtle);
|
||||
color: var(--text-strong);
|
||||
font-size: 0.72rem;
|
||||
padding: 0.2rem 0.55rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.worker-new-form {
|
||||
display: grid;
|
||||
gap: 0.65rem;
|
||||
margin: 0.6rem 0 0.75rem;
|
||||
padding: 0.65rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 0.75rem;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.worker-new-form label,
|
||||
.settings-runtime-form label {
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.worker-new-form input,
|
||||
.worker-new-form select,
|
||||
.worker-new-form textarea,
|
||||
.settings-runtime-form input {
|
||||
width: 100%;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 0.55rem;
|
||||
background: var(--bg-raised);
|
||||
color: var(--text-strong);
|
||||
padding: 0.45rem 0.55rem;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.worker-new-form textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.worker-new-form button,
|
||||
.settings-runtime-form button,
|
||||
.settings-action-row button {
|
||||
border: 0;
|
||||
border-radius: 0.6rem;
|
||||
background: var(--accent);
|
||||
color: var(--bg);
|
||||
font-weight: 700;
|
||||
padding: 0.5rem 0.75rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.worker-new-form button:disabled,
|
||||
.settings-runtime-form button:disabled,
|
||||
.settings-action-row button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.settings-runtime-form,
|
||||
.settings-runtime-list {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.settings-runtime-form {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 1rem;
|
||||
padding: 1rem;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.settings-runtime-card {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 1rem;
|
||||
padding: 1rem;
|
||||
background: var(--bg-raised);
|
||||
}
|
||||
|
||||
.settings-runtime-card header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.settings-runtime-card.inactive {
|
||||
opacity: 0.86;
|
||||
}
|
||||
|
||||
.settings-identity-list.compact {
|
||||
grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
|
||||
}
|
||||
|
||||
.settings-action-row {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.settings-action-row .danger {
|
||||
background: var(--danger);
|
||||
color: var(--bg);
|
||||
}
|
||||
|
||||
.settings-diagnostics-list {
|
||||
display: grid;
|
||||
gap: 0.4rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.settings-diagnostics-list li {
|
||||
display: grid;
|
||||
gap: 0.15rem;
|
||||
border-radius: 0.6rem;
|
||||
border: 1px solid var(--line);
|
||||
padding: 0.55rem 0.65rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.settings-diagnostics-list li.error {
|
||||
border-color: rgba(255, 99, 99, 0.55);
|
||||
}
|
||||
|
||||
.settings-diagnostics-list li.warning {
|
||||
border-color: rgba(255, 205, 86, 0.55);
|
||||
}
|
||||
|
||||
.settings-test-result {
|
||||
display: grid;
|
||||
gap: 0.3rem;
|
||||
border-radius: 0.75rem;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,39 @@
|
|||
SETTINGS_PATTERNS,
|
||||
SETTINGS_PERMISSION_NOTICE,
|
||||
SETTINGS_SECTIONS,
|
||||
diagnosticLabel,
|
||||
settingsSectionHref,
|
||||
type Diagnostic,
|
||||
type RemoteRuntimeConnectionSummary,
|
||||
type RemoteRuntimeTestResponse,
|
||||
type RuntimeConnectionMutationResponse,
|
||||
type RuntimeConnectionSettingsResponse,
|
||||
type RuntimeConnectionSummary,
|
||||
} from "./model";
|
||||
|
||||
type RemoteAddForm = {
|
||||
runtime_id: string;
|
||||
display_name: string;
|
||||
endpoint: string;
|
||||
};
|
||||
|
||||
let workspace = $state<WorkspaceResponse | null>(null);
|
||||
let runtimeSettings = $state<RuntimeConnectionSettingsResponse | null>(null);
|
||||
let loading = $state(true);
|
||||
let runtimeLoading = $state(true);
|
||||
let error = $state<string | null>(null);
|
||||
let runtimeError = $state<string | null>(null);
|
||||
let mutationMessage = $state<string | null>(null);
|
||||
let mutationDiagnostics = $state<Diagnostic[]>([]);
|
||||
let tests = $state<Record<string, RemoteRuntimeTestResponse>>({});
|
||||
let deleting = $state<string | null>(null);
|
||||
let testing = $state<string | null>(null);
|
||||
let submitting = $state(false);
|
||||
let remoteForm = $state<RemoteAddForm>({
|
||||
runtime_id: "",
|
||||
display_name: "",
|
||||
endpoint: "",
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
let cancelled = false;
|
||||
|
|
@ -39,12 +66,165 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function loadRuntimeSettings() {
|
||||
runtimeLoading = true;
|
||||
runtimeError = null;
|
||||
try {
|
||||
const response = await fetch("/api/settings/runtime-connections");
|
||||
if (!response.ok) {
|
||||
throw new Error(`runtime settings request failed (${response.status})`);
|
||||
}
|
||||
const data = (await response.json()) as RuntimeConnectionSettingsResponse;
|
||||
if (!cancelled) {
|
||||
runtimeSettings = data;
|
||||
}
|
||||
} catch (err) {
|
||||
if (!cancelled) {
|
||||
runtimeError = err instanceof Error ? err.message : "runtime settings request failed";
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
runtimeLoading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadWorkspace();
|
||||
loadRuntimeSettings();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
});
|
||||
|
||||
async function submitRemoteRuntime() {
|
||||
submitting = true;
|
||||
mutationMessage = null;
|
||||
mutationDiagnostics = [];
|
||||
try {
|
||||
const response = await fetch("/api/settings/runtime-connections/remotes", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
runtime_id: remoteForm.runtime_id,
|
||||
display_name: remoteForm.display_name || null,
|
||||
endpoint: remoteForm.endpoint,
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(await responseErrorMessage(response, "add remote Runtime failed"));
|
||||
}
|
||||
const data = (await response.json()) as RuntimeConnectionMutationResponse;
|
||||
applyRuntimeMutation(data);
|
||||
remoteForm = { runtime_id: "", display_name: "", endpoint: "" };
|
||||
} catch (err) {
|
||||
mutationMessage = err instanceof Error ? err.message : "add remote Runtime failed";
|
||||
} finally {
|
||||
submitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRemoteRuntime(runtimeId: string) {
|
||||
deleting = runtimeId;
|
||||
mutationMessage = null;
|
||||
mutationDiagnostics = [];
|
||||
try {
|
||||
const response = await fetch(`/api/settings/runtime-connections/remotes/${encodeURIComponent(runtimeId)}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(await responseErrorMessage(response, "delete remote Runtime failed"));
|
||||
}
|
||||
const data = (await response.json()) as RuntimeConnectionMutationResponse;
|
||||
applyRuntimeMutation(data);
|
||||
const nextTests = { ...tests };
|
||||
delete nextTests[runtimeId];
|
||||
tests = nextTests;
|
||||
} catch (err) {
|
||||
mutationMessage = err instanceof Error ? err.message : "delete remote Runtime failed";
|
||||
} finally {
|
||||
deleting = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function testRemoteRuntime(runtimeId: string) {
|
||||
testing = runtimeId;
|
||||
try {
|
||||
const response = await fetch(`/api/settings/runtime-connections/remotes/${encodeURIComponent(runtimeId)}/test`, {
|
||||
method: "POST",
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(await responseErrorMessage(response, "test remote Runtime failed"));
|
||||
}
|
||||
const data = (await response.json()) as RemoteRuntimeTestResponse;
|
||||
tests = { ...tests, [runtimeId]: data };
|
||||
} catch (err) {
|
||||
tests = {
|
||||
...tests,
|
||||
[runtimeId]: {
|
||||
workspace_id: runtimeSettings?.workspace_id ?? "unknown",
|
||||
runtime_id: runtimeId,
|
||||
checked_at: new Date().toISOString(),
|
||||
state: "failed",
|
||||
protocol_version: null,
|
||||
compatibility_basis: "browser request failed",
|
||||
capabilities: [],
|
||||
health_result: "failed",
|
||||
diagnostics: [
|
||||
{
|
||||
code: "browser_runtime_test_failed",
|
||||
severity: "error",
|
||||
message: err instanceof Error ? err.message : "test remote Runtime failed",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
} finally {
|
||||
testing = null;
|
||||
}
|
||||
}
|
||||
|
||||
function applyRuntimeMutation(data: RuntimeConnectionMutationResponse) {
|
||||
runtimeSettings = runtimeSettings
|
||||
? { ...runtimeSettings, remotes: data.remotes, diagnostics: data.diagnostics }
|
||||
: {
|
||||
workspace_id: data.workspace_id,
|
||||
embedded: {
|
||||
runtime_id: "embedded-worker-runtime",
|
||||
display_name: "Embedded Runtime",
|
||||
kind: "embedded_worker_runtime",
|
||||
built_in: true,
|
||||
config_managed: false,
|
||||
active: false,
|
||||
can_spawn_worker: false,
|
||||
restart_required: false,
|
||||
status: "unknown",
|
||||
diagnostics: [],
|
||||
},
|
||||
remotes: data.remotes,
|
||||
diagnostics: data.diagnostics,
|
||||
};
|
||||
mutationDiagnostics = data.diagnostics;
|
||||
mutationMessage = data.restart_required
|
||||
? "Runtime config saved. Restart the Workspace backend to apply live registry changes."
|
||||
: "Runtime config saved.";
|
||||
}
|
||||
|
||||
async function responseErrorMessage(response: Response, fallback: string): Promise<string> {
|
||||
try {
|
||||
const payload = (await response.json()) as { error?: { message?: string; code?: string } | string; message?: string };
|
||||
if (typeof payload.error === "object" && payload.error?.message) {
|
||||
return `${payload.error.code ?? "request_failed"}: ${payload.error.message}`;
|
||||
}
|
||||
if (payload.message) {
|
||||
const code = typeof payload.error === "string" ? payload.error : "request_failed";
|
||||
return `${code}: ${payload.message}`;
|
||||
}
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
return `${fallback} (${response.status})`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -60,11 +240,10 @@
|
|||
<p class="eyebrow">Workspace Browser</p>
|
||||
<h1 id="settings-title">Settings / Admin</h1>
|
||||
<p class="hero-copy">
|
||||
Read-only shell for future local administration surfaces. This page creates
|
||||
navigation and operator context without adding mutation authority.
|
||||
Local administration surfaces for the Workspace backend. Runtime Connections v0 is editable through typed APIs; broader admin controls remain bounded placeholders.
|
||||
</p>
|
||||
</div>
|
||||
<span class="badge warning">shell only</span>
|
||||
<span class="badge warning">local only</span>
|
||||
</section>
|
||||
|
||||
<section class="card settings-notice" aria-labelledby="settings-boundary-title">
|
||||
|
|
@ -74,8 +253,8 @@
|
|||
<p>{SETTINGS_PERMISSION_NOTICE}</p>
|
||||
</div>
|
||||
<div class="settings-diagnostic" role="note">
|
||||
<strong>Diagnostic pattern</strong>
|
||||
<span>Future controls must use typed Backend diagnostics and restart-required states.</span>
|
||||
<strong>Restart-required</strong>
|
||||
<span>Runtime config changes are persisted, then applied after backend restart.</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
@ -83,13 +262,95 @@
|
|||
{#each SETTINGS_SECTIONS as section}
|
||||
<a class="settings-nav-link" href={settingsSectionHref(section.id)}>
|
||||
<span>{section.label}</span>
|
||||
<small>{section.status === "read-only" ? "Read-only" : "Placeholder"}</small>
|
||||
<small>{section.status === "editable" ? "Editable" : section.status === "read-only" ? "Read-only" : "Placeholder"}</small>
|
||||
</a>
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
<section class="card settings-section" id="runtime-connections" aria-labelledby="runtime-connections-title">
|
||||
<header class="settings-section-header">
|
||||
<div>
|
||||
<p class="eyebrow">editable</p>
|
||||
<h2 id="runtime-connections-title">Runtime Connections</h2>
|
||||
</div>
|
||||
<span class="badge success">typed API</span>
|
||||
</header>
|
||||
<p>{SETTINGS_SECTIONS.find((section) => section.id === "runtime-connections")?.summary}</p>
|
||||
|
||||
{#if runtimeLoading}
|
||||
<p class="status-message">Loading Runtime connections…</p>
|
||||
{:else if runtimeError}
|
||||
<p class="status-message error">Runtime connection settings unavailable: {runtimeError}</p>
|
||||
{:else if runtimeSettings}
|
||||
{@render RuntimeConnectionCard({ connection: runtimeSettings.embedded })}
|
||||
|
||||
<form class="settings-runtime-form" onsubmit={(event) => { event.preventDefault(); void submitRemoteRuntime(); }}>
|
||||
<h3>Add remote Runtime</h3>
|
||||
<p>Endpoint is submitted to the Backend but not echoed back in settings responses.</p>
|
||||
<label>
|
||||
<span>Runtime id</span>
|
||||
<input bind:value={remoteForm.runtime_id} required maxlength="96" pattern="[A-Za-z0-9_.-]+" placeholder="team-runtime" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Display name</span>
|
||||
<input bind:value={remoteForm.display_name} maxlength="80" placeholder="Team Runtime" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Endpoint</span>
|
||||
<input bind:value={remoteForm.endpoint} required inputmode="url" placeholder="https://runtime.example" />
|
||||
</label>
|
||||
<button type="submit" disabled={submitting}>{submitting ? "Saving…" : "Add Runtime"}</button>
|
||||
</form>
|
||||
|
||||
{#if mutationMessage}
|
||||
<p class="status-message" class:error={mutationMessage.includes("failed")}>{mutationMessage}</p>
|
||||
{/if}
|
||||
{@render DiagnosticsList({ diagnostics: mutationDiagnostics })}
|
||||
|
||||
<div class="settings-runtime-list" aria-label="Remote Runtime connections">
|
||||
<h3>Remote Runtimes</h3>
|
||||
{#if runtimeSettings.remotes.length === 0}
|
||||
<p class="status-message">No remote Runtime connections configured.</p>
|
||||
{:else}
|
||||
{#each runtimeSettings.remotes as remote (remote.runtime_id)}
|
||||
<article class="settings-runtime-card">
|
||||
{@render RuntimeConnectionCard({ connection: remote })}
|
||||
<dl class="settings-identity-list compact">
|
||||
<div>
|
||||
<dt>Endpoint</dt>
|
||||
<dd>{remote.endpoint_configured ? "configured (hidden)" : "not configured"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Token ref</dt>
|
||||
<dd>{remote.token_ref_configured ? "configured (hidden)" : "not configured"}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div class="settings-action-row">
|
||||
<button type="button" onclick={() => void testRemoteRuntime(remote.runtime_id)} disabled={testing === remote.runtime_id}>
|
||||
{testing === remote.runtime_id ? "Testing…" : "Test"}
|
||||
</button>
|
||||
<button type="button" class="danger" onclick={() => void deleteRemoteRuntime(remote.runtime_id)} disabled={deleting === remote.runtime_id}>
|
||||
{deleting === remote.runtime_id ? "Deleting…" : "Delete"}
|
||||
</button>
|
||||
</div>
|
||||
{#if tests[remote.runtime_id]}
|
||||
{@const test = tests[remote.runtime_id]}
|
||||
<div class="settings-test-result">
|
||||
<strong>Test: {test.state}</strong>
|
||||
<span>{test.health_result} · {test.checked_at}</span>
|
||||
<p>{test.compatibility_basis}</p>
|
||||
{@render DiagnosticsList({ diagnostics: test.diagnostics })}
|
||||
</div>
|
||||
{/if}
|
||||
</article>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<div class="grid settings-grid">
|
||||
{#each SETTINGS_SECTIONS as section}
|
||||
{#each SETTINGS_SECTIONS.filter((section) => section.id !== "runtime-connections") as section}
|
||||
<section class="card settings-section" id={section.id} aria-labelledby={`${section.id}-title`}>
|
||||
<header class="settings-section-header">
|
||||
<div>
|
||||
|
|
@ -132,7 +393,7 @@
|
|||
<section class="card settings-patterns" aria-labelledby="settings-patterns-title">
|
||||
<div>
|
||||
<p class="eyebrow">Implementation patterns</p>
|
||||
<h2 id="settings-patterns-title">How future settings should appear</h2>
|
||||
<h2 id="settings-patterns-title">How settings should appear</h2>
|
||||
</div>
|
||||
<div class="grid settings-pattern-grid">
|
||||
{#each SETTINGS_PATTERNS as pattern}
|
||||
|
|
@ -151,3 +412,51 @@
|
|||
{/if}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{#snippet RuntimeConnectionCard({ connection }: { connection: RuntimeConnectionSummary | RemoteRuntimeConnectionSummary })}
|
||||
<article class="settings-runtime-card embedded" class:inactive={!connection.active}>
|
||||
<header>
|
||||
<div>
|
||||
<h3>{connection.display_name}</h3>
|
||||
<p><code>{connection.runtime_id}</code></p>
|
||||
</div>
|
||||
<span class="badge" class:success={connection.active} class:warning={!connection.active}>{connection.status}</span>
|
||||
</header>
|
||||
<dl class="settings-identity-list compact">
|
||||
<div>
|
||||
<dt>Kind</dt>
|
||||
<dd>{connection.kind}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Built in</dt>
|
||||
<dd>{connection.built_in ? "yes" : "no"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Config managed</dt>
|
||||
<dd>{connection.config_managed ? "yes" : "no"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Spawn</dt>
|
||||
<dd>{connection.can_spawn_worker ? "available" : "unavailable"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Restart required</dt>
|
||||
<dd>{connection.restart_required ? "yes" : "no"}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
{@render DiagnosticsList({ diagnostics: connection.diagnostics })}
|
||||
</article>
|
||||
{/snippet}
|
||||
|
||||
{#snippet DiagnosticsList({ diagnostics }: { diagnostics: Diagnostic[] })}
|
||||
{#if diagnostics.length > 0}
|
||||
<ul class="settings-diagnostics-list">
|
||||
{#each diagnostics as diagnostic}
|
||||
<li class={diagnostic.severity}>
|
||||
<strong>{diagnosticLabel(diagnostic)}</strong>
|
||||
<span>{diagnostic.message}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {
|
|||
SETTINGS_PERMISSION_NOTICE,
|
||||
SETTINGS_ROUTE,
|
||||
SETTINGS_SECTIONS,
|
||||
diagnosticLabel,
|
||||
settingsSectionHref,
|
||||
} from "./model.ts";
|
||||
|
||||
|
|
@ -43,7 +44,12 @@ Deno.test("settings shell advertises no fake browser admin model", () => {
|
|||
);
|
||||
});
|
||||
|
||||
Deno.test("settings placeholders avoid mutation promises and raw authority leaks", () => {
|
||||
Deno.test("runtime connections are editable without advertising raw authority leaks", () => {
|
||||
const runtimeSection = SETTINGS_SECTIONS.find((section) =>
|
||||
section.id === "runtime-connections"
|
||||
);
|
||||
assert(runtimeSection?.status === "editable", "Runtime Connections should be editable");
|
||||
|
||||
const allText = [
|
||||
SETTINGS_PERMISSION_NOTICE,
|
||||
...SETTINGS_SECTIONS.flatMap((section) => [
|
||||
|
|
@ -55,14 +61,12 @@ Deno.test("settings placeholders avoid mutation promises and raw authority leaks
|
|||
].join("\n");
|
||||
|
||||
assert(
|
||||
allText.includes(
|
||||
"does not add, remove, test, or persist Runtime endpoints",
|
||||
),
|
||||
"Runtime Connections should remain a placeholder",
|
||||
allText.includes("restart_required=true") || allText.includes("Restart-required"),
|
||||
"restart-required pattern should be visible",
|
||||
);
|
||||
assert(
|
||||
allText.includes("Restart-required"),
|
||||
"restart-required pattern should be visible",
|
||||
allText.includes("not echoed back") || allText.includes("not echoed"),
|
||||
"endpoint submission should not imply endpoint echoing",
|
||||
);
|
||||
|
||||
for (
|
||||
|
|
@ -72,6 +76,7 @@ Deno.test("settings placeholders avoid mutation promises and raw authority leaks
|
|||
"token:",
|
||||
"secret:",
|
||||
"store root:",
|
||||
"config file path:",
|
||||
]
|
||||
) {
|
||||
assert(
|
||||
|
|
@ -80,3 +85,15 @@ Deno.test("settings placeholders avoid mutation promises and raw authority leaks
|
|||
);
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("diagnostic labels preserve severity and code", () => {
|
||||
const diagnostic = {
|
||||
severity: "warning",
|
||||
code: "runtime_registry_restart_required",
|
||||
message: "Restart required.",
|
||||
} as const;
|
||||
assert(
|
||||
diagnosticLabel(diagnostic) === "warning: runtime_registry_restart_required",
|
||||
"diagnostic label should be bounded and stable",
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
export type Diagnostic = {
|
||||
severity: "info" | "warning" | "error";
|
||||
code: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type SettingsSectionId =
|
||||
| "runtime-connections"
|
||||
| "backend-config"
|
||||
|
|
@ -6,7 +12,7 @@ export type SettingsSectionId =
|
|||
export type SettingsSection = {
|
||||
readonly id: SettingsSectionId;
|
||||
readonly label: string;
|
||||
readonly status: "placeholder" | "read-only";
|
||||
readonly status: "editable" | "placeholder" | "read-only";
|
||||
readonly summary: string;
|
||||
readonly bullets: readonly string[];
|
||||
};
|
||||
|
|
@ -16,22 +22,66 @@ export type SettingsPattern = {
|
|||
readonly body: string;
|
||||
};
|
||||
|
||||
export type RuntimeConnectionSummary = {
|
||||
runtime_id: string;
|
||||
display_name: string;
|
||||
kind: string;
|
||||
built_in: boolean;
|
||||
config_managed: boolean;
|
||||
active: boolean;
|
||||
can_spawn_worker: boolean;
|
||||
restart_required: boolean;
|
||||
status: string;
|
||||
diagnostics: Diagnostic[];
|
||||
};
|
||||
|
||||
export type RemoteRuntimeConnectionSummary = RuntimeConnectionSummary & {
|
||||
endpoint_configured: boolean;
|
||||
token_ref_configured: boolean;
|
||||
};
|
||||
|
||||
export type RuntimeConnectionSettingsResponse = {
|
||||
workspace_id: string;
|
||||
embedded: RuntimeConnectionSummary;
|
||||
remotes: RemoteRuntimeConnectionSummary[];
|
||||
diagnostics: Diagnostic[];
|
||||
};
|
||||
|
||||
export type RuntimeConnectionMutationResponse = {
|
||||
workspace_id: string;
|
||||
restart_required: boolean;
|
||||
remotes: RemoteRuntimeConnectionSummary[];
|
||||
diagnostics: Diagnostic[];
|
||||
};
|
||||
|
||||
export type RemoteRuntimeTestResponse = {
|
||||
workspace_id: string;
|
||||
runtime_id: string;
|
||||
checked_at: string;
|
||||
state: string;
|
||||
protocol_version?: string | null;
|
||||
compatibility_basis: string;
|
||||
capabilities: string[];
|
||||
health_result: string;
|
||||
diagnostics: Diagnostic[];
|
||||
};
|
||||
|
||||
export const SETTINGS_ROUTE = "/settings";
|
||||
|
||||
export const SETTINGS_PERMISSION_NOTICE =
|
||||
"Yoi currently has no browser user, role, permission, or multi-user authorization model. This shell is intentionally local and descriptive; it does not create an admin role or grant mutation authority.";
|
||||
"Yoi currently has no browser user, role, permission, or multi-user authorization model. This local settings surface uses typed Backend APIs only; it does not create an admin role or grant broad mutation authority.";
|
||||
|
||||
export const SETTINGS_SECTIONS: readonly SettingsSection[] = [
|
||||
{
|
||||
id: "runtime-connections",
|
||||
label: "Runtime Connections",
|
||||
status: "placeholder",
|
||||
status: "editable",
|
||||
summary:
|
||||
"Future Runtime connection management will live here. The current view does not add, remove, test, or persist Runtime endpoints.",
|
||||
"Manage remote Runtime connection records stored in the workspace-local Backend config. The embedded Runtime is built in and shown separately.",
|
||||
bullets: [
|
||||
"Shows where connection diagnostics will surface without exposing tokens, sockets, store roots, or raw endpoint secrets.",
|
||||
"Connection changes require a later typed Backend API and are not performed by this shell.",
|
||||
"Restart-required states should be shown as bounded diagnostics rather than live mutation controls.",
|
||||
"Remote connection changes are persisted through typed read-modify-write config updates and require a Backend restart before the live registry changes.",
|
||||
"The browser may submit a new endpoint, but Runtime endpoints, tokens, sockets, store roots, and config paths are not echoed back in API responses.",
|
||||
"Test negotiation is an observation only; checked_at, health, compatibility, and capability results are not persisted to local config.",
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -39,11 +89,11 @@ export const SETTINGS_SECTIONS: readonly SettingsSection[] = [
|
|||
label: "Backend Config",
|
||||
status: "placeholder",
|
||||
summary:
|
||||
"Configuration inspection is planned, but editing Backend config or secrets is out of scope for this shell.",
|
||||
"General Backend config editing remains out of scope; this page only exposes the Runtime Connections v0 typed surface.",
|
||||
bullets: [
|
||||
"Only sanitized summaries belong in the browser; raw config paths, secret refs, tokens, and store roots stay backend-side.",
|
||||
"Missing-provider or invalid-config states should be displayed as typed diagnostics.",
|
||||
"No fake permission model is created to make config editing appear available.",
|
||||
"No fake permission model is created to make unrelated config editing appear available.",
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -64,20 +114,24 @@ export const SETTINGS_PATTERNS: readonly SettingsPattern[] = [
|
|||
{
|
||||
title: "Sanitized diagnostics",
|
||||
body:
|
||||
"Settings cards should show bounded codes and operator-facing messages, not raw socket paths, credentials, secret refs, token values, or Runtime store paths.",
|
||||
"Settings cards show bounded codes and operator-facing messages, not raw socket paths, credentials, token values, Runtime endpoints, or Runtime store paths.",
|
||||
},
|
||||
{
|
||||
title: "Restart-required changes",
|
||||
body:
|
||||
"When a future setting cannot apply live, the browser should say restart required and leave the mutation to a typed Backend workflow.",
|
||||
"Remote Runtime config updates return restart_required=true because v0 does not unregister/register live Runtime handles.",
|
||||
},
|
||||
{
|
||||
title: "Read-only until typed APIs exist",
|
||||
title: "Typed Runtime surface only",
|
||||
body:
|
||||
"Placeholder sections describe planned surfaces without pretending that user, role, permission, or Runtime mutation APIs already exist.",
|
||||
"Runtime Connections v0 is intentionally narrow: embedded is built in, remote config is add/delete/test, and broader Backend admin controls stay unavailable.",
|
||||
},
|
||||
];
|
||||
|
||||
export function settingsSectionHref(id: SettingsSectionId): string {
|
||||
return `${SETTINGS_ROUTE}#${id}`;
|
||||
}
|
||||
|
||||
export function diagnosticLabel(diagnostic: Diagnostic): string {
|
||||
return `${diagnostic.severity}: ${diagnostic.code}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { workerConsoleHref } from '$lib/workspace-console/model';
|
||||
import type { ListResponse, Worker } from './types';
|
||||
import { buildBrowserCreateWorkerRequest, defaultWorkerLaunchForm } from './worker-launch';
|
||||
import type {
|
||||
BrowserCreateWorkerResponse,
|
||||
ListResponse,
|
||||
Worker,
|
||||
WorkerLaunchOptionsResponse,
|
||||
} from './types';
|
||||
|
||||
const MAX_VISIBLE_WORKERS = 6;
|
||||
|
||||
|
|
@ -14,14 +20,24 @@
|
|||
let error = $state<string | null>(null);
|
||||
let workers = $state<Worker[]>([]);
|
||||
let placeholder = $state<string | null>(null);
|
||||
let options = $state<WorkerLaunchOptionsResponse | null>(null);
|
||||
let optionsError = $state<string | null>(null);
|
||||
let showNewWorker = $state(false);
|
||||
let submitting = $state(false);
|
||||
let submitError = $state<string | null>(null);
|
||||
let displayName = $state('Coding Worker');
|
||||
let runtimeId = $state('');
|
||||
let profile = $state('builtin:coder');
|
||||
let initialText = $state('');
|
||||
|
||||
$effect(() => {
|
||||
const controller = new AbortController();
|
||||
void loadWorkers(controller.signal);
|
||||
void loadLaunchOptions(controller.signal);
|
||||
return () => controller.abort();
|
||||
});
|
||||
|
||||
async function loadWorkers(signal: AbortSignal) {
|
||||
async function loadWorkers(signal?: AbortSignal) {
|
||||
loading = true;
|
||||
error = null;
|
||||
placeholder = null;
|
||||
|
|
@ -47,21 +63,141 @@
|
|||
error = err instanceof Error ? err.message : 'workers request failed';
|
||||
workers = [];
|
||||
} finally {
|
||||
if (!signal.aborted) {
|
||||
if (!signal?.aborted) {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLaunchOptions(signal?: AbortSignal) {
|
||||
optionsError = null;
|
||||
try {
|
||||
const response = await fetch('/api/workers/launch-options', { signal });
|
||||
if (!response.ok) {
|
||||
throw new Error(`worker launch options failed (${response.status})`);
|
||||
}
|
||||
const payload = (await response.json()) as WorkerLaunchOptionsResponse;
|
||||
options = payload;
|
||||
const form = defaultWorkerLaunchForm(payload, {
|
||||
runtime_id: runtimeId,
|
||||
display_name: displayName,
|
||||
profile,
|
||||
initial_text: initialText,
|
||||
});
|
||||
runtimeId = form.runtime_id;
|
||||
displayName = form.display_name;
|
||||
profile = form.profile;
|
||||
} catch (err) {
|
||||
if (err instanceof DOMException && err.name === 'AbortError') {
|
||||
return;
|
||||
}
|
||||
optionsError = err instanceof Error ? err.message : 'worker launch options failed';
|
||||
}
|
||||
}
|
||||
|
||||
async function createWorker() {
|
||||
submitError = null;
|
||||
submitting = true;
|
||||
try {
|
||||
const response = await fetch('/api/workers', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(buildBrowserCreateWorkerRequest({
|
||||
runtime_id: runtimeId,
|
||||
display_name: displayName,
|
||||
profile,
|
||||
initial_text: initialText,
|
||||
})),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(await responseErrorMessage(response, 'worker create failed'));
|
||||
}
|
||||
const payload = (await response.json()) as BrowserCreateWorkerResponse;
|
||||
await loadWorkers();
|
||||
window.location.href = payload.console_href;
|
||||
} catch (err) {
|
||||
submitError = err instanceof Error ? err.message : 'worker create failed';
|
||||
} finally {
|
||||
submitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function responseErrorMessage(response: Response, fallback: string): Promise<string> {
|
||||
try {
|
||||
const payload = (await response.json()) as { error?: { message?: string; code?: string } | string; message?: string };
|
||||
if (typeof payload.error === 'object' && payload.error?.message) {
|
||||
return `${payload.error.code ?? 'request_failed'}: ${payload.error.message}`;
|
||||
}
|
||||
if (payload.message) {
|
||||
const code = typeof payload.error === 'string' ? payload.error : 'request_failed';
|
||||
return `${code}: ${payload.message}`;
|
||||
}
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
return `${fallback} (${response.status})`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="nav-section" aria-labelledby="workers-heading">
|
||||
<div class="section-heading-row">
|
||||
<h2 id="workers-heading">workers</h2>
|
||||
<button type="button" class="section-action" onclick={() => (showNewWorker = !showNewWorker)}>
|
||||
{showNewWorker ? 'Close' : 'New'}
|
||||
</button>
|
||||
{#if !loading && !error && workers.length > 0}
|
||||
<span class="section-count">{workers.length}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if showNewWorker}
|
||||
<form class="worker-new-form" onsubmit={(event) => { event.preventDefault(); void createWorker(); }}>
|
||||
<label>
|
||||
<span>Display name</span>
|
||||
<input bind:value={displayName} required maxlength="80" autocomplete="off" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Runtime</span>
|
||||
<select bind:value={runtimeId} required>
|
||||
{#if options?.runtimes.length}
|
||||
{#each options.runtimes as runtime}
|
||||
<option value={runtime.runtime_id} disabled={!runtime.can_spawn_worker}>
|
||||
{runtime.display_name} · {runtime.status}{runtime.built_in ? ' · embedded' : ''}
|
||||
</option>
|
||||
{/each}
|
||||
{:else}
|
||||
<option value="" disabled>No Runtime options</option>
|
||||
{/if}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Profile</span>
|
||||
<select bind:value={profile} required>
|
||||
{#if options?.profiles.length}
|
||||
{#each options.profiles as candidate}
|
||||
<option value={candidate.id}>{candidate.label}</option>
|
||||
{/each}
|
||||
{:else}
|
||||
<option value="" disabled>No profile candidates</option>
|
||||
{/if}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Initial text</span>
|
||||
<textarea bind:value={initialText} rows="3" placeholder="Optional first instruction"></textarea>
|
||||
</label>
|
||||
{#if optionsError}
|
||||
<p class="section-state error">{optionsError}</p>
|
||||
{/if}
|
||||
{#if submitError}
|
||||
<p class="section-state error">{submitError}</p>
|
||||
{/if}
|
||||
<button type="submit" disabled={submitting || !runtimeId || !profile}>
|
||||
{submitting ? 'Starting…' : 'Start Coding Worker'}
|
||||
</button>
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
{#if loading}
|
||||
<p class="section-state">Checking workers…</p>
|
||||
{:else if error}
|
||||
|
|
|
|||
|
|
@ -93,6 +93,37 @@ export type Worker = {
|
|||
|
||||
export type WorkerOperationState = 'accepted' | 'unsupported' | 'rejected';
|
||||
|
||||
export type WorkerLaunchRuntimeOption = {
|
||||
runtime_id: string;
|
||||
display_name: string;
|
||||
built_in: boolean;
|
||||
can_spawn_worker: boolean;
|
||||
status: string;
|
||||
diagnostics: Diagnostic[];
|
||||
};
|
||||
|
||||
export type WorkerLaunchProfileCandidate = {
|
||||
id: string;
|
||||
label: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type WorkerLaunchOptionsResponse = {
|
||||
workspace_id: string;
|
||||
runtimes: WorkerLaunchRuntimeOption[];
|
||||
profiles: WorkerLaunchProfileCandidate[];
|
||||
diagnostics: Diagnostic[];
|
||||
};
|
||||
|
||||
export type BrowserCreateWorkerResponse = {
|
||||
workspace_id: string;
|
||||
runtime_id: string;
|
||||
worker_id: string;
|
||||
console_href: string;
|
||||
worker: Worker;
|
||||
diagnostics: Diagnostic[];
|
||||
};
|
||||
|
||||
export type WorkerInputResult = {
|
||||
state: WorkerOperationState;
|
||||
runtime_id: string;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
import {
|
||||
buildBrowserCreateWorkerRequest,
|
||||
defaultWorkerLaunchForm,
|
||||
type WorkerLaunchFormState,
|
||||
} from './worker-launch.ts';
|
||||
|
||||
import type { WorkerLaunchOptionsResponse } from './types.ts';
|
||||
|
||||
declare const Deno: {
|
||||
test(name: string, fn: () => void): void;
|
||||
};
|
||||
|
||||
function assert(condition: unknown, message: string): asserts condition {
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
const options: WorkerLaunchOptionsResponse = {
|
||||
workspace_id: 'workspace',
|
||||
runtimes: [
|
||||
{
|
||||
runtime_id: 'remote-runtime',
|
||||
display_name: 'Remote Runtime',
|
||||
built_in: false,
|
||||
can_spawn_worker: false,
|
||||
status: 'active',
|
||||
diagnostics: [],
|
||||
},
|
||||
{
|
||||
runtime_id: 'embedded-worker-runtime',
|
||||
display_name: 'Embedded Runtime',
|
||||
built_in: true,
|
||||
can_spawn_worker: true,
|
||||
status: 'active',
|
||||
diagnostics: [],
|
||||
},
|
||||
],
|
||||
profiles: [
|
||||
{
|
||||
id: 'runtime_default',
|
||||
label: 'Runtime default',
|
||||
description: 'Runtime default profile.',
|
||||
},
|
||||
{
|
||||
id: 'builtin:coder',
|
||||
label: 'Coding Worker',
|
||||
description: 'Coding role.',
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
};
|
||||
|
||||
Deno.test('new worker form defaults to backend-published runtime and profile candidates', () => {
|
||||
const current: WorkerLaunchFormState = {
|
||||
runtime_id: '',
|
||||
display_name: '',
|
||||
profile: 'free-text-profile',
|
||||
initial_text: 'start here',
|
||||
};
|
||||
|
||||
const form = defaultWorkerLaunchForm(options, current);
|
||||
assert(form.runtime_id === 'embedded-worker-runtime', 'should choose spawn-capable runtime');
|
||||
assert(form.profile === 'builtin:coder', 'should choose backend-published coder profile');
|
||||
assert(form.display_name === 'Coding Worker', 'should derive default display name');
|
||||
assert(form.initial_text === 'start here', 'should preserve initial text');
|
||||
});
|
||||
|
||||
Deno.test('new worker submit payload exposes only browser contract fields', () => {
|
||||
const request = buildBrowserCreateWorkerRequest({
|
||||
runtime_id: 'embedded-worker-runtime',
|
||||
display_name: 'Coding Worker',
|
||||
profile: 'builtin:coder',
|
||||
initial_text: 'implement ticket',
|
||||
});
|
||||
|
||||
assert(
|
||||
JSON.stringify(Object.keys(request).sort()) ===
|
||||
JSON.stringify(['display_name', 'initial_text', 'profile', 'runtime_id'].sort()),
|
||||
'submit payload should contain only Browser-facing worker create fields',
|
||||
);
|
||||
assert(!('kind' in request), 'kind must not be exposed as a Browser request field');
|
||||
});
|
||||
41
web/workspace/src/lib/workspace-sidebar/worker-launch.ts
Normal file
41
web/workspace/src/lib/workspace-sidebar/worker-launch.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import type { WorkerLaunchOptionsResponse } from './types';
|
||||
|
||||
export type WorkerLaunchFormState = {
|
||||
runtime_id: string;
|
||||
display_name: string;
|
||||
profile: string;
|
||||
initial_text: string;
|
||||
};
|
||||
|
||||
export type BrowserCreateWorkerRequest = WorkerLaunchFormState;
|
||||
|
||||
export function defaultWorkerLaunchForm(
|
||||
options: WorkerLaunchOptionsResponse | null,
|
||||
current: WorkerLaunchFormState,
|
||||
): WorkerLaunchFormState {
|
||||
const preferredRuntime = options?.runtimes.find((runtime) => runtime.can_spawn_worker && runtime.status === 'active')
|
||||
?? options?.runtimes.find((runtime) => runtime.can_spawn_worker)
|
||||
?? options?.runtimes[0];
|
||||
const preferredProfile = options?.profiles.find((candidate) => candidate.id === 'builtin:coder')
|
||||
?? options?.profiles[0];
|
||||
|
||||
return {
|
||||
runtime_id: current.runtime_id || preferredRuntime?.runtime_id || '',
|
||||
display_name: current.display_name || 'Coding Worker',
|
||||
profile: options?.profiles.some((candidate) => candidate.id === current.profile)
|
||||
? current.profile
|
||||
: preferredProfile?.id || '',
|
||||
initial_text: current.initial_text,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildBrowserCreateWorkerRequest(
|
||||
form: WorkerLaunchFormState,
|
||||
): BrowserCreateWorkerRequest {
|
||||
return {
|
||||
runtime_id: form.runtime_id,
|
||||
display_name: form.display_name,
|
||||
profile: form.profile,
|
||||
initial_text: form.initial_text,
|
||||
};
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@
|
|||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user