web: retire profile-specific editor

This commit is contained in:
2026-08-14 09:19:13 +09:00
parent c2510495ef
commit 4857910410
8 changed files with 91 additions and 619 deletions
@@ -6,10 +6,6 @@ import {
SETTINGS_SECTIONS,
settingsSectionHref,
} from "./model.ts";
import {
profileSourceTreeSettingsHref,
virtualProfilePathForCreate,
} from "./profile-routes.ts";
declare const Deno: {
test(name: string, fn: () => void): void;
@@ -106,29 +102,3 @@ Deno.test("diagnostic labels preserve severity and code", () => {
"diagnostic label should be bounded and stable",
);
});
Deno.test("profile source tree routes use encoded scoped ids", () => {
const href = profileSourceTreeSettingsHref("workspace a", "project/tree");
assert(
href === "/w/workspace%20a/settings/profiles/trees/project%2Ftree",
`unexpected href ${href}`,
);
assert(!href.includes("/home/"), "route must not contain host paths");
});
Deno.test("profile source create paths are normalized to virtual profile paths", () => {
assert(
virtualProfilePathForCreate("alpha.dcdl") === "profiles/alpha.dcdl",
"bare file names are scoped",
);
assert(
virtualProfilePathForCreate("profiles/alpha.dcdl") ===
"profiles/alpha.dcdl",
"virtual paths are preserved",
);
assert(
virtualProfilePathForCreate("project:profiles/alpha.dcdl") ===
"project:profiles/alpha.dcdl",
"safe virtual namespaces are preserved",
);
});
@@ -108,7 +108,7 @@ export const SETTINGS_SECTIONS: readonly SettingsSection[] = [
bullets: [
"Virtual paths and imports resolve inside the committed Workspace tree, never from browser or Server host paths.",
"Browser analysis is advisory; Server evaluation is required before an atomic revision commit.",
"Profile, Skill, Prompt, and Plugin consumers remain on their existing authorities until their follow-up cutovers.",
"Profile launch data is projected from this active revision; remaining Skill, Prompt, and Plugin consumers migrate in their follow-up cutovers.",
],
},
{
@@ -116,11 +116,11 @@ export const SETTINGS_SECTIONS: readonly SettingsSection[] = [
label: "Profile Sources",
status: "editable",
summary:
"Manage the workspace-scoped Decodal Profile registry and source files used by Backend-published launch profile discovery.",
"Inspect the Profile launch projection derived from the active Workspace configuration revision.",
bullets: [
"Selectors are source-qualified (builtin:* or project:*); raw profile source paths, archive content, archive digests, resource handles, and runtime tokens are not exposed.",
"Profile source edits are validated through the Backend ProfileSourceArchive/Decodal boundary before they are persisted.",
"Launch profile candidates refresh from the same Backend projection after registry or source updates.",
"Profile declarations and sources are edited only through the shared Workspace configuration editor and evaluate-before-commit contract.",
"Launch candidates and Profile archives carry the same active config revision, tree digest, and projection digest.",
],
},
{
@@ -1,166 +1,72 @@
import { workspaceApiJson, workspaceApiJsonWithBody } from "../api/http";
import type {
ProfileSettingsMutationResponse,
ProfileSettingsResponse,
WorkspaceMetadataMutationResponse,
WorkspaceMetadataSettingsResponse,
WorkspaceProfileSourceDetailResponse,
WorkspaceProfileSourceTreeFileResponse,
WorkspaceProfileSourceTreeResponse,
} from "./profile-types";
export function fetchWorkspaceMetadataSettings(
export type WorkspaceProfileApi = {
getMetadata(workspaceId: string): Promise<WorkspaceMetadataSettingsResponse>;
updateMetadata(
workspaceId: string,
displayName: string,
expectedRevision: string,
): Promise<WorkspaceMetadataMutationResponse>;
getProfiles(workspaceId: string): Promise<ProfileSettingsResponse>;
};
async function requestJson<T>(input: RequestInfo | URL, init?: RequestInit): Promise<T> {
const response = await fetch(input, init);
if (!response.ok) {
throw new Error(`request failed: ${response.status}`);
}
return (await response.json()) as T;
}
export async function fetchWorkspaceMetadataSettings(
workspaceId: string,
): Promise<WorkspaceMetadataSettingsResponse> {
return workspaceApiJson(
return await requestJson<WorkspaceMetadataSettingsResponse>(
`/api/w/${encodeURIComponent(workspaceId)}/settings/workspace`,
);
}
export function updateWorkspaceMetadataSettings(
export async function updateWorkspaceMetadataSettings(
workspaceId: string,
request: { display_name: string; revision: string },
): Promise<WorkspaceMetadataMutationResponse> {
return workspaceApiJsonWithBody(
return await requestJson<WorkspaceMetadataMutationResponse>(
`/api/w/${encodeURIComponent(workspaceId)}/settings/workspace`,
{
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify(request),
},
);
}
export function fetchProfileSettings(
workspaceId: string,
): Promise<ProfileSettingsResponse> {
return workspaceApiJson(
export async function fetchProfileSettings(workspaceId: string): Promise<ProfileSettingsResponse> {
return await requestJson<ProfileSettingsResponse>(
`/api/w/${encodeURIComponent(workspaceId)}/settings/profiles`,
);
}
export function createProfileSource(
workspaceId: string,
request: {
name: string;
description?: string;
content: string;
registry_revision: string;
},
): Promise<ProfileSettingsMutationResponse> {
return workspaceApiJsonWithBody(
`/api/w/${encodeURIComponent(workspaceId)}/settings/profiles`,
{
method: "POST",
body: JSON.stringify(request),
export function createWorkspaceProfileApi(): WorkspaceProfileApi {
return {
async getMetadata(workspaceId) {
return await requestJson<WorkspaceMetadataSettingsResponse>(
`/api/w/${encodeURIComponent(workspaceId)}/settings/metadata`,
);
},
);
}
export function updateProfileRegistry(
workspaceId: string,
request: {
registry_revision: string;
default_profile?: string | null;
profiles: Array<
{
name: string;
description?: string | null;
profile_source_id?: string | null;
}
>;
},
): Promise<ProfileSettingsMutationResponse> {
return workspaceApiJsonWithBody(
`/api/w/${encodeURIComponent(workspaceId)}/settings/profiles/registry`,
{
method: "PUT",
body: JSON.stringify(request),
async updateMetadata(workspaceId, displayName, expectedRevision) {
return await requestJson<WorkspaceMetadataMutationResponse>(
`/api/w/${encodeURIComponent(workspaceId)}/settings/metadata`,
{
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({ display_name: displayName, expected_revision: expectedRevision }),
},
);
},
);
}
export function fetchProfileSource(
workspaceId: string,
sourceId: string,
): Promise<WorkspaceProfileSourceDetailResponse> {
return workspaceApiJson(
`/api/w/${encodeURIComponent(workspaceId)}/settings/profiles/${
encodeURIComponent(sourceId)
}`,
);
}
export function updateProfileSource(
workspaceId: string,
sourceId: string,
request: { content: string; revision: string },
): Promise<ProfileSettingsMutationResponse> {
return workspaceApiJsonWithBody(
`/api/w/${encodeURIComponent(workspaceId)}/settings/profiles/${
encodeURIComponent(sourceId)
}`,
{ method: "PUT", body: JSON.stringify(request) },
);
}
export function deleteProfileSource(
workspaceId: string,
sourceId: string,
request: { registry_revision: string; source_revision: string },
): Promise<ProfileSettingsMutationResponse> {
return workspaceApiJsonWithBody(
`/api/w/${encodeURIComponent(workspaceId)}/settings/profiles/${
encodeURIComponent(sourceId)
}`,
{ method: "DELETE", body: JSON.stringify(request) },
);
}
export function fetchProfileSourceTree(
workspaceId: string,
sourceTreeId: string,
): Promise<WorkspaceProfileSourceTreeResponse> {
return workspaceApiJson(
`/api/w/${encodeURIComponent(workspaceId)}/settings/profiles/trees/${
encodeURIComponent(sourceTreeId)
}`,
);
}
export function fetchProfileTreeFile(
workspaceId: string,
sourceTreeId: string,
path: string,
): Promise<WorkspaceProfileSourceTreeFileResponse> {
return workspaceApiJson(
`/api/w/${encodeURIComponent(workspaceId)}/settings/profiles/trees/${
encodeURIComponent(sourceTreeId)
}/file?path=${encodeURIComponent(path)}`,
);
}
export function writeProfileTreeFile(
workspaceId: string,
sourceTreeId: string,
request: { path: string; content: string; revision?: string | null },
): Promise<WorkspaceProfileSourceTreeFileResponse> {
return workspaceApiJsonWithBody(
`/api/w/${encodeURIComponent(workspaceId)}/settings/profiles/trees/${
encodeURIComponent(sourceTreeId)
}/file`,
{ method: "PUT", body: JSON.stringify(request) },
);
}
export function deleteProfileTreeFile(
workspaceId: string,
sourceTreeId: string,
request: { path: string; revision: string },
): Promise<WorkspaceProfileSourceTreeResponse> {
return workspaceApiJsonWithBody(
`/api/w/${encodeURIComponent(workspaceId)}/settings/profiles/trees/${
encodeURIComponent(sourceTreeId)
}/file`,
{ method: "DELETE", body: JSON.stringify(request) },
);
getProfiles: fetchProfileSettings,
};
}
@@ -1,22 +0,0 @@
export function profileSettingsHref(workspaceId: string): string {
return `/w/${encodeURIComponent(workspaceId)}/settings/profiles`;
}
export function profileSourceTreeSettingsHref(
workspaceId: string,
sourceTreeId: string,
): string {
return `${profileSettingsHref(workspaceId)}/trees/${
encodeURIComponent(sourceTreeId)
}`;
}
export function virtualProfilePathForCreate(input: string): string {
const trimmed = input.trim();
if (!trimmed) return "";
if (trimmed.startsWith("project:") || trimmed.startsWith("workspace:")) {
return trimmed;
}
if (trimmed.startsWith("profiles/")) return trimmed;
return `profiles/${trimmed}`;
}
@@ -29,7 +29,7 @@ export type WorkspaceProfileSummary = {
export type WorkspaceProfileSourceSummary = {
profile_source_id: string;
display_path: string;
kind: "decodal" | string;
kind: "virtual_config" | string;
content_type: string;
content_digest: string;
provenance: "project_profile_source_tree" | string;
@@ -42,64 +42,11 @@ export type WorkspaceProfileSourceSummary = {
export type ProfileSettingsResponse = {
workspace_id: string;
registry_revision: string;
config_revision?: number | null;
tree_digest?: string | null;
projection_digest?: string | null;
default_profile?: string | null;
profiles: WorkspaceProfileSummary[];
sources: WorkspaceProfileSourceSummary[];
source_trees: WorkspaceProfileSourceTreeSummary[];
diagnostics: Diagnostic[];
};
export type WorkspaceProfileSourceDetailResponse = {
workspace_id: string;
profile: WorkspaceProfileSummary;
source: WorkspaceProfileSourceSummary;
content: string;
diagnostics: Diagnostic[];
};
export type ProfileSettingsMutationResponse = {
workspace_id: string;
settings: ProfileSettingsResponse;
diagnostics: Diagnostic[];
};
export type WorkspaceProfileSourceTreeSummary = {
source_tree_id: string;
label: string;
root_path: string;
kind: "decodal_source_tree" | string;
content_type: string;
content_digest: string;
provenance: "project_profile_source_tree" | string;
editable: boolean;
revision: string;
file_count: number;
diagnostics: Diagnostic[];
};
export type WorkspaceProfileSourceTreeFileSummary = {
path: string;
kind: "decodal" | string;
content_type: string;
content_digest: string;
provenance: "project_profile_source_tree" | string;
editable: boolean;
revision: string;
size_bytes: number;
diagnostics: Diagnostic[];
};
export type WorkspaceProfileSourceTreeResponse = {
workspace_id: string;
tree: WorkspaceProfileSourceTreeSummary;
files: WorkspaceProfileSourceTreeFileSummary[];
diagnostics: Diagnostic[];
};
export type WorkspaceProfileSourceTreeFileResponse = {
workspace_id: string;
source_tree_id: string;
file: WorkspaceProfileSourceTreeFileSummary;
content: string;
diagnostics: Diagnostic[];
};
@@ -1,130 +1,30 @@
<script lang="ts">
import DiagnosticsList from '$lib/workspace/settings/DiagnosticsList.svelte';
import DecodalSourceEditor from '$lib/workspace/settings/DecodalSourceEditor.svelte';
import {
fetchProfileSettings,
fetchProfileSourceTree,
deleteProfileTreeFile,
fetchProfileTreeFile,
writeProfileTreeFile,
} from '$lib/workspace/settings/profile-api';
import type { Diagnostic } from '$lib/workspace/settings/model';
import { profileSourceTreeSettingsHref, virtualProfilePathForCreate } from '$lib/workspace/settings/profile-routes';
import type {
ProfileSettingsResponse,
WorkspaceProfileSourceTreeFileResponse,
WorkspaceProfileSourceTreeResponse,
} from '$lib/workspace/settings/profile-types';
import type { PageProps } from './$types';
import DiagnosticsList from "$lib/workspace/settings/DiagnosticsList.svelte";
import { fetchProfileSettings } from "$lib/workspace/settings/profile-api";
import type { ProfileSettingsResponse } from "$lib/workspace/settings/profile-types";
import type { PageProps } from "./$types";
let { data }: PageProps = $props();
let workspaceId = $derived(data.workspace?.workspace_id ?? '');
let workspaceId = $derived(data.workspace?.workspace_id ?? "");
let profileSettings = $state<ProfileSettingsResponse | null>(null);
let sourceTree = $state<WorkspaceProfileSourceTreeResponse | null>(null);
let selectedFile = $state<WorkspaceProfileSourceTreeFileResponse | null>(null);
let draftContent = $state('');
let loading = $state(true);
let saving = $state(false);
let creating = $state(false);
let deleting = $state(false);
let newFilePath = $state('new-profile.dcdl');
let message = $state<string | null>(null);
let diagnostics = $state<Diagnostic[]>([]);
async function loadSettings() {
if (!workspaceId) return;
loading = true;
message = null;
try {
const response = await fetchProfileSettings(workspaceId);
profileSettings = response;
diagnostics = response.diagnostics;
const treeId = response.source_trees[0]?.source_tree_id;
if (treeId) {
sourceTree = await fetchProfileSourceTree(workspaceId, treeId);
const firstPath = sourceTree.files[0]?.path;
if (firstPath) await selectTreeFile(treeId, firstPath);
}
} catch (err) {
message = err instanceof Error ? err.message : 'profile settings request failed';
} finally {
loading = false;
}
}
async function selectTreeFile(sourceTreeId: string, path: string) {
selectedFile = await fetchProfileTreeFile(workspaceId, sourceTreeId, path);
draftContent = selectedFile.content;
diagnostics = selectedFile.diagnostics;
}
async function createTreeFile(sourceTreeId: string) {
creating = true;
message = null;
try {
const path = virtualProfilePathForCreate(newFilePath);
selectedFile = await writeProfileTreeFile(workspaceId, sourceTreeId, {
path,
content: '{\n slug = "new-profile";\n description = "New profile";\n scope = "workspace_read";\n}',
});
draftContent = selectedFile.content;
sourceTree = await fetchProfileSourceTree(workspaceId, sourceTreeId);
diagnostics = selectedFile.diagnostics;
message = 'Created Decodal profile source.';
} catch (err) {
message = err instanceof Error ? err.message : 'profile source create failed';
} finally {
creating = false;
}
}
async function deleteSelectedFile() {
if (!selectedFile) return;
deleting = true;
message = null;
try {
sourceTree = await deleteProfileTreeFile(workspaceId, selectedFile.source_tree_id, {
path: selectedFile.file.path,
revision: selectedFile.file.revision,
});
selectedFile = null;
draftContent = '';
diagnostics = sourceTree.diagnostics;
message = 'Deleted Decodal profile source.';
} catch (err) {
message = err instanceof Error ? err.message : 'profile source delete failed';
} finally {
deleting = false;
}
}
async function saveSelectedFile() {
if (!selectedFile) return;
saving = true;
message = null;
try {
selectedFile = await writeProfileTreeFile(workspaceId, selectedFile.source_tree_id, {
path: selectedFile.file.path,
revision: selectedFile.file.revision,
content: draftContent,
});
draftContent = selectedFile.content;
sourceTree = await fetchProfileSourceTree(workspaceId, selectedFile.source_tree_id);
diagnostics = selectedFile.diagnostics;
message = 'Saved Decodal profile source.';
} catch (err) {
message = err instanceof Error ? err.message : 'profile source save failed';
} finally {
saving = false;
}
}
$effect(() => {
let cancelled = false;
async function load() {
await loadSettings();
if (cancelled) return;
loading = true;
message = null;
try {
const response = await fetchProfileSettings(workspaceId);
if (!cancelled) profileSettings = response;
} catch (error) {
if (!cancelled) {
message = error instanceof Error ? error.message : "profile settings request failed";
}
} finally {
if (!cancelled) loading = false;
}
}
if (workspaceId) load();
else loading = false;
@@ -138,90 +38,39 @@
<title>Profiles · Yoi Workspace</title>
</svelte:head>
<section class="card settings-section" aria-labelledby="profile-sources-title">
<section class="card settings-section" aria-labelledby="profiles-title">
<header class="settings-section-header">
<div>
<p class="eyebrow">Backend-owned source tree</p>
<h2 id="profile-sources-title">Profiles</h2>
<p class="eyebrow">Workspace configuration projection</p>
<h2 id="profiles-title">Profiles</h2>
</div>
<span class="badge">Decodal editor</span>
<a class="badge" href={`/w/${encodeURIComponent(workspaceId)}/settings/configuration-sources`}>
Edit configuration
</a>
</header>
<p>
Review effective launch profiles and edit Decodal source files through virtual profile-tree paths. The browser receives only safe relative paths and revision tokens.
These launch profiles are derived from the active Workspace configuration revision. Edit Profile declarations and Decodal sources in the shared configuration editor.
</p>
{#if loading}
<p class="status-message">Loading profiles…</p>
{:else if message}
<p class="status-message error">{message}</p>
{:else if profileSettings}
<div class="settings-profile-grid">
<article>
<h3>Available profiles</h3>
<p class="settings-note">Profiles that can be selected when launching a Worker.</p>
<ul class="settings-profile-list">
{#each profileSettings.profiles as profile (profile.profile_id)}
<li>
<strong>{profile.selector}</strong>
<span>{profile.label}</span>
<small>{profile.source_kind}{profile.is_default ? ' · default' : ''}</small>
{#if profile.description}<p>{profile.description}</p>{/if}
<DiagnosticsList diagnostics={profile.diagnostics} />
</li>
{/each}
</ul>
</article>
<article>
<h3>Profile source tree</h3>
<p class="settings-note">Virtual Decodal paths exposed by the Backend-owned source tree.</p>
{#if sourceTree}
<small>{sourceTree.tree.root_path} · {sourceTree.tree.file_count} files · {sourceTree.tree.content_type} · rev {sourceTree.tree.revision}</small>
<p><a href={profileSourceTreeSettingsHref(workspaceId, sourceTree.tree.source_tree_id)}>Open tree route</a></p>
<div class="settings-inline-form">
<input bind:value={newFilePath} aria-label="New profile source virtual path" placeholder="profiles/new-profile.dcdl" />
<button type="button" disabled={creating} onclick={() => createTreeFile(sourceTree!.tree.source_tree_id)}>
{creating ? 'Creating…' : 'Create source'}
</button>
</div>
<ul class="settings-profile-list">
{#each sourceTree.files as file (file.path)}
<li>
<button class="link-button" type="button" onclick={() => selectTreeFile(sourceTree!.tree.source_tree_id, file.path)}>
<strong>{file.path}</strong>
</button>
<span>{file.kind} · {file.content_type} · {file.size_bytes} bytes</span>
<small>{file.editable ? 'editable' : 'read-only'} · rev {file.revision}</small>
<DiagnosticsList diagnostics={file.diagnostics} />
</li>
{/each}
</ul>
{:else}
<p class="settings-note">No project profile source tree is available.</p>
{/if}
</article>
</div>
{#if selectedFile}
<article class="settings-editor-panel">
<header class="settings-section-header">
<div>
<p class="eyebrow">{selectedFile.source_tree_id}</p>
<h3>{selectedFile.file.path}</h3>
</div>
<div class="settings-editor-actions">
<button type="button" disabled={saving || draftContent === selectedFile.content} onclick={saveSelectedFile}>
{saving ? 'Saving…' : 'Save'}
</button>
<button type="button" disabled={deleting} onclick={deleteSelectedFile}>
{deleting ? 'Deleting…' : 'Delete'}
</button>
</div>
</header>
<DecodalSourceEditor value={draftContent} onChange={(value) => (draftContent = value)} ariaLabel={`Decodal source ${selectedFile.file.path}`} />
</article>
{/if}
<p class="settings-note">
Revision {profileSettings.config_revision ?? "unknown"} · tree {profileSettings.tree_digest ?? "unknown"} · projection {profileSettings.projection_digest ?? "unknown"}
</p>
<ul class="settings-profile-list">
{#each profileSettings.profiles as profile (profile.profile_id)}
<li>
<strong>{profile.selector}</strong>
<span>{profile.label}</span>
<small>{profile.source_kind}{profile.is_default ? " · default" : ""}</small>
{#if profile.description}<p>{profile.description}</p>{/if}
<DiagnosticsList diagnostics={profile.diagnostics} />
</li>
{/each}
</ul>
<DiagnosticsList diagnostics={profileSettings.diagnostics} />
{/if}
{#if message}
<p class="status-message" class:error={message.includes('failed') || message.includes('error')}>{message}</p>
{/if}
<DiagnosticsList {diagnostics} />
</section>
@@ -1,173 +0,0 @@
<script lang="ts">
import DiagnosticsList from '$lib/workspace/settings/DiagnosticsList.svelte';
import DecodalSourceEditor from '$lib/workspace/settings/DecodalSourceEditor.svelte';
import {
deleteProfileTreeFile,
fetchProfileSourceTree,
fetchProfileTreeFile,
writeProfileTreeFile,
} from '$lib/workspace/settings/profile-api';
import { virtualProfilePathForCreate } from '$lib/workspace/settings/profile-routes';
import type { Diagnostic } from '$lib/workspace/settings/model';
import type {
WorkspaceProfileSourceTreeFileResponse,
WorkspaceProfileSourceTreeResponse,
} from '$lib/workspace/settings/profile-types';
import type { PageProps } from './$types';
let { data }: PageProps = $props();
let workspaceId = $derived(data.workspace?.workspace_id ?? '');
let sourceTreeId = $derived(data.sourceTreeId);
let tree = $state<WorkspaceProfileSourceTreeResponse | null>(null);
let selectedFile = $state<WorkspaceProfileSourceTreeFileResponse | null>(null);
let draftContent = $state('');
let newFilePath = $state('new-profile.dcdl');
let loading = $state(true);
let saving = $state(false);
let creating = $state(false);
let deleting = $state(false);
let message = $state<string | null>(null);
let diagnostics = $state<Diagnostic[]>([]);
async function reloadTree() {
tree = await fetchProfileSourceTree(workspaceId, sourceTreeId);
diagnostics = tree.diagnostics;
}
async function selectFile(path: string) {
selectedFile = await fetchProfileTreeFile(workspaceId, sourceTreeId, path);
draftContent = selectedFile.content;
diagnostics = selectedFile.diagnostics;
}
async function createFile() {
creating = true;
message = null;
try {
selectedFile = await writeProfileTreeFile(workspaceId, sourceTreeId, {
path: virtualProfilePathForCreate(newFilePath),
content: '{\n slug = "new-profile";\n description = "New profile";\n scope = "workspace_read";\n}',
});
draftContent = selectedFile.content;
await reloadTree();
message = 'Created Decodal profile source.';
} catch (err) {
message = err instanceof Error ? err.message : 'profile source create failed';
} finally {
creating = false;
}
}
async function saveFile() {
if (!selectedFile) return;
saving = true;
message = null;
try {
selectedFile = await writeProfileTreeFile(workspaceId, sourceTreeId, {
path: selectedFile.file.path,
revision: selectedFile.file.revision,
content: draftContent,
});
draftContent = selectedFile.content;
await reloadTree();
message = 'Saved Decodal profile source.';
} catch (err) {
message = err instanceof Error ? err.message : 'profile source save failed';
} finally {
saving = false;
}
}
async function deleteFile() {
if (!selectedFile) return;
deleting = true;
message = null;
try {
tree = await deleteProfileTreeFile(workspaceId, sourceTreeId, {
path: selectedFile.file.path,
revision: selectedFile.file.revision,
});
selectedFile = null;
draftContent = '';
diagnostics = tree.diagnostics;
message = 'Deleted Decodal profile source.';
} catch (err) {
message = err instanceof Error ? err.message : 'profile source delete failed';
} finally {
deleting = false;
}
}
$effect(() => {
if (!workspaceId || !sourceTreeId) {
loading = false;
return;
}
loading = true;
reloadTree()
.catch((err) => {
message = err instanceof Error ? err.message : 'profile source tree load failed';
})
.finally(() => {
loading = false;
});
});
</script>
<svelte:head>
<title>Profile source tree · Yoi Workspace</title>
</svelte:head>
<section class="card settings-section" aria-labelledby="profile-tree-title">
<header class="settings-section-header">
<div>
<h2 id="profile-tree-title">Profile source tree</h2>
</div>
</header>
{#if loading}
<p class="status-message">Loading source tree…</p>
{:else if tree}
<p class="settings-note">
{tree.tree.root_path} · {tree.tree.file_count} files · {tree.tree.content_type} · {tree.tree.content_digest}
</p>
<div class="settings-inline-form">
<input bind:value={newFilePath} aria-label="New profile source virtual path" placeholder="profiles/new-profile.dcdl" />
<button type="button" disabled={creating} onclick={createFile}>{creating ? 'Creating…' : 'Create source'}</button>
</div>
<div class="settings-profile-grid">
<article>
<h3>Files</h3>
<ul class="settings-profile-list">
{#each tree.files as file (file.path)}
<li>
<button class="link-button" type="button" onclick={() => selectFile(file.path)}><strong>{file.path}</strong></button>
<span>{file.content_type} · {file.content_digest}</span>
<small>{file.size_bytes} bytes · rev {file.revision}</small>
<DiagnosticsList diagnostics={file.diagnostics} />
</li>
{/each}
</ul>
</article>
{#if selectedFile}
<article>
<header class="settings-section-header">
<div>
<p class="eyebrow">{selectedFile.file.content_type}</p>
<h3>{selectedFile.file.path}</h3>
</div>
<div class="settings-editor-actions">
<button type="button" disabled={saving || draftContent === selectedFile.content} onclick={saveFile}>{saving ? 'Saving…' : 'Save'}</button>
<button type="button" disabled={deleting} onclick={deleteFile}>{deleting ? 'Deleting…' : 'Delete'}</button>
</div>
</header>
<DecodalSourceEditor value={draftContent} onChange={(value) => (draftContent = value)} ariaLabel={`Decodal source ${selectedFile.file.path}`} />
</article>
{/if}
</div>
{/if}
{#if message}<p class="status-message">{message}</p>{/if}
<DiagnosticsList {diagnostics} />
</section>
@@ -1,5 +0,0 @@
import type { PageLoad } from "./$types";
export const load: PageLoad = ({ params }) => ({
sourceTreeId: params.sourceTreeId,
});