diff --git a/web/workspace/deno.json b/web/workspace/deno.json
index b9bfb471..adda00c7 100644
--- a/web/workspace/deno.json
+++ b/web/workspace/deno.json
@@ -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",
+ "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",
"build": "deno run -A npm:vite@7.2.7 build",
"preview": "deno run -A npm:vite@7.2.7 preview"
},
diff --git a/web/workspace/src/app.css b/web/workspace/src/app.css
index e10c04ae..fda816ee 100644
--- a/web/workspace/src/app.css
+++ b/web/workspace/src/app.css
@@ -988,3 +988,155 @@
text-align: left;
}
}
+
+.settings-button {
+ text-decoration: none;
+ cursor: pointer;
+}
+
+.settings-button:hover,
+.settings-button:focus-visible,
+.settings-button.active {
+ background: var(--interactive-selected);
+ color: var(--accent);
+}
+
+.settings-shell {
+ gap: var(--space-5);
+}
+
+.settings-hero,
+.settings-notice,
+.settings-section-header,
+.settings-patterns {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: var(--space-4);
+}
+
+.hero-copy {
+ max-width: 54rem;
+ margin: 0;
+ color: var(--text-muted);
+}
+
+.badge {
+ flex: 0 0 auto;
+ border-radius: 999px;
+ padding: 0.35rem 0.65rem;
+ background: var(--bg-subtle);
+ color: var(--text-muted);
+ font-size: 0.72rem;
+ font-weight: 800;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+}
+
+.badge.warning {
+ color: var(--warning);
+}
+
+.badge.success {
+ color: var(--success);
+}
+
+.settings-notice {
+ border-left: 4px solid var(--warning);
+ padding-left: var(--space-4);
+}
+
+.settings-diagnostic {
+ display: grid;
+ gap: var(--space-1);
+ max-width: 24rem;
+ padding: var(--space-3) var(--space-4);
+ border: 1px solid var(--line);
+ border-radius: var(--radius-panel);
+ background: var(--bg-raised);
+ color: var(--text-muted);
+}
+
+.settings-diagnostic strong {
+ color: var(--text-strong);
+}
+
+.settings-nav-card {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--space-3);
+}
+
+.settings-nav-link {
+ display: grid;
+ gap: var(--space-1);
+ min-width: min(16rem, 100%);
+ padding: var(--space-3) var(--space-4);
+ border: 1px solid var(--line);
+ border-radius: var(--radius-panel);
+ background: var(--bg-raised);
+ color: inherit;
+ text-decoration: none;
+}
+
+.settings-nav-link:hover,
+.settings-nav-link:focus-visible {
+ background: var(--interactive-hover);
+}
+
+.settings-nav-link span {
+ color: var(--text-strong);
+ font-weight: 800;
+}
+
+.settings-grid,
+.settings-pattern-grid {
+ grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr));
+}
+
+.settings-section,
+.settings-patterns,
+.settings-pattern {
+ display: grid;
+ gap: var(--space-4);
+}
+
+.settings-section ul,
+.settings-patterns ul {
+ margin: 0;
+ padding-left: 1.2rem;
+}
+
+.settings-section li + li {
+ margin-top: var(--space-2);
+}
+
+.settings-identity-list {
+ padding-top: var(--space-3);
+ border-top: 1px solid var(--line);
+}
+
+.settings-pattern {
+ padding: var(--space-4);
+ border-radius: var(--radius-panel);
+ background: var(--bg-raised);
+}
+
+.settings-pattern h3,
+.settings-pattern p {
+ margin: 0;
+}
+
+.status-message {
+ margin: 0;
+ color: var(--text-muted);
+}
+
+@media (max-width: 760px) {
+ .settings-hero,
+ .settings-notice,
+ .settings-section-header,
+ .settings-patterns {
+ display: grid;
+ }
+}
diff --git a/web/workspace/src/lib/workspace-settings/SettingsPage.svelte b/web/workspace/src/lib/workspace-settings/SettingsPage.svelte
new file mode 100644
index 00000000..fbe897b5
--- /dev/null
+++ b/web/workspace/src/lib/workspace-settings/SettingsPage.svelte
@@ -0,0 +1,153 @@
+
+
+
+ Settings · Yoi Workspace
+
+
+
+
+
+
+
+
+
Workspace Browser
+
Settings / Admin
+
+ Read-only shell for future local administration surfaces. This page creates
+ navigation and operator context without adding mutation authority.
+
+
+ shell only
+
+
+
+
+
Authority boundary
+
No browser admin permission model
+
{SETTINGS_PERMISSION_NOTICE}
+
+
+ Diagnostic pattern
+ Future controls must use typed Backend diagnostics and restart-required states.
+
+
+
+
+
+
+ {#each SETTINGS_SECTIONS as section}
+
+
+ {section.summary}
+
+ {#each section.bullets as bullet}
+ - {bullet}
+ {/each}
+
+
+ {#if section.id === "workspace-identity"}
+
+
+
- Workspace id
+ {workspace?.workspace_id ?? "loading"}
+
+
+
- Display name
+ - {workspace?.display_name ?? "loading"}
+
+
+
- Record authority
+ - .yoi tickets/objectives through the Backend projection
+
+
+ {/if}
+
+ {/each}
+
+
+
+
+
Implementation patterns
+
How future settings should appear
+
+
+ {#each SETTINGS_PATTERNS as pattern}
+
+ {pattern.title}
+ {pattern.body}
+
+ {/each}
+
+
+
+ {#if loading}
+ Loading workspace summary…
+ {:else if error}
+ Workspace summary unavailable: {error}
+ {/if}
+
+
diff --git a/web/workspace/src/lib/workspace-settings/model.test.ts b/web/workspace/src/lib/workspace-settings/model.test.ts
new file mode 100644
index 00000000..6c876dc9
--- /dev/null
+++ b/web/workspace/src/lib/workspace-settings/model.test.ts
@@ -0,0 +1,82 @@
+import {
+ SETTINGS_PATTERNS,
+ SETTINGS_PERMISSION_NOTICE,
+ SETTINGS_ROUTE,
+ SETTINGS_SECTIONS,
+ settingsSectionHref,
+} from "./model.ts";
+
+declare const Deno: {
+ test(name: string, fn: () => void): void;
+};
+
+function assert(condition: unknown, message: string): asserts condition {
+ if (!condition) {
+ throw new Error(message);
+ }
+}
+
+Deno.test("settings section navigation stays under the settings route", () => {
+ assert(SETTINGS_ROUTE === "/settings", "settings route should be stable");
+
+ for (const section of SETTINGS_SECTIONS) {
+ const href = settingsSectionHref(section.id);
+ assert(
+ href.startsWith("/settings#"),
+ `${section.id} should link under settings`,
+ );
+ assert(
+ href.endsWith(section.id),
+ `${section.id} href should preserve section id`,
+ );
+ }
+});
+
+Deno.test("settings shell advertises no fake browser admin model", () => {
+ assert(
+ SETTINGS_PERMISSION_NOTICE.includes("no browser user, role, permission"),
+ "notice should explicitly deny a browser permission model",
+ );
+ assert(
+ SETTINGS_PERMISSION_NOTICE.includes("does not create an admin role"),
+ "notice should not imply an admin role exists",
+ );
+});
+
+Deno.test("settings placeholders avoid mutation promises and raw authority leaks", () => {
+ const allText = [
+ SETTINGS_PERMISSION_NOTICE,
+ ...SETTINGS_SECTIONS.flatMap((section) => [
+ section.label,
+ section.summary,
+ ...section.bullets,
+ ]),
+ ...SETTINGS_PATTERNS.flatMap((pattern) => [pattern.title, pattern.body]),
+ ].join("\n");
+
+ assert(
+ allText.includes(
+ "does not add, remove, test, or persist Runtime endpoints",
+ ),
+ "Runtime Connections should remain a placeholder",
+ );
+ assert(
+ allText.includes("Restart-required"),
+ "restart-required pattern should be visible",
+ );
+
+ for (
+ const forbidden of [
+ "/home/",
+ "socket path:",
+ "token:",
+ "secret:",
+ "store root:",
+ ]
+ ) {
+ assert(
+ !allText.includes(forbidden),
+ `settings copy should not expose ${forbidden}`,
+ );
+ }
+});
diff --git a/web/workspace/src/lib/workspace-settings/model.ts b/web/workspace/src/lib/workspace-settings/model.ts
new file mode 100644
index 00000000..f823c162
--- /dev/null
+++ b/web/workspace/src/lib/workspace-settings/model.ts
@@ -0,0 +1,83 @@
+export type SettingsSectionId =
+ | "runtime-connections"
+ | "backend-config"
+ | "workspace-identity";
+
+export type SettingsSection = {
+ readonly id: SettingsSectionId;
+ readonly label: string;
+ readonly status: "placeholder" | "read-only";
+ readonly summary: string;
+ readonly bullets: readonly string[];
+};
+
+export type SettingsPattern = {
+ readonly title: string;
+ readonly body: string;
+};
+
+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.";
+
+export const SETTINGS_SECTIONS: readonly SettingsSection[] = [
+ {
+ id: "runtime-connections",
+ label: "Runtime Connections",
+ status: "placeholder",
+ summary:
+ "Future Runtime connection management will live here. The current view does not add, remove, test, or persist Runtime endpoints.",
+ 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.",
+ ],
+ },
+ {
+ id: "backend-config",
+ label: "Backend Config",
+ status: "placeholder",
+ summary:
+ "Configuration inspection is planned, but editing Backend config or secrets is out of scope for this shell.",
+ 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.",
+ ],
+ },
+ {
+ id: "workspace-identity",
+ label: "Workspace Identity",
+ status: "read-only",
+ summary:
+ "Workspace identity is presented as read-only context so operators can tell which workspace the browser is attached to.",
+ bullets: [
+ "Use opaque workspace ids and display names rather than raw filesystem paths.",
+ "Repository/project-record authority remains backend-side and is not edited here.",
+ "Identity changes need a later explicit migration flow.",
+ ],
+ },
+];
+
+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.",
+ },
+ {
+ 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.",
+ },
+ {
+ title: "Read-only until typed APIs exist",
+ body:
+ "Placeholder sections describe planned surfaces without pretending that user, role, permission, or Runtime mutation APIs already exist.",
+ },
+];
+
+export function settingsSectionHref(id: SettingsSectionId): string {
+ return `${SETTINGS_ROUTE}#${id}`;
+}
diff --git a/web/workspace/src/lib/workspace-sidebar/WorkspaceSidebar.svelte b/web/workspace/src/lib/workspace-sidebar/WorkspaceSidebar.svelte
index ca947577..7279cfaf 100644
--- a/web/workspace/src/lib/workspace-sidebar/WorkspaceSidebar.svelte
+++ b/web/workspace/src/lib/workspace-sidebar/WorkspaceSidebar.svelte
@@ -11,6 +11,7 @@
};
let { workspace, workspaceError = null, currentPath = '/' }: Props = $props();
+ let settingsActive = $derived(currentPath.startsWith("/settings"));
diff --git a/web/workspace/src/routes/settings/+page.svelte b/web/workspace/src/routes/settings/+page.svelte
new file mode 100644
index 00000000..a7831cef
--- /dev/null
+++ b/web/workspace/src/routes/settings/+page.svelte
@@ -0,0 +1,5 @@
+
+
+