diff --git a/docs/design/workspace-web-design-system.md b/docs/design/workspace-web-design-system.md
new file mode 100644
index 00000000..192c86f9
--- /dev/null
+++ b/docs/design/workspace-web-design-system.md
@@ -0,0 +1,78 @@
+# Workspace web design system
+
+This document defines the visual rules for `web/workspace`. The current authority for tokens and reusable page/sidebar styling is `web/workspace/src/app.css`.
+
+## Design position
+
+Workspace web should read as a control surface, not a set of detached widgets. Group information primarily through spacing, typography, and text contrast. Use borders, rounded rectangles, shadows, and filled panels only when they clarify hierarchy that spacing cannot express.
+
+## Palette
+
+Colors are defined as CSS custom properties in OKLCH. The palette supports light and dark modes through `prefers-color-scheme`.
+
+Rules:
+
+- Background and layout surfaces use zero chroma: `oklch(... 0 0)`.
+- Primary text and code text are near-neutral warm colors. Because CSS OKLCH exposes chroma (`C`) rather than saturation directly, encode the “about 5% saturation” intent as very low warm chroma, around `C = 0.01` to `0.012`.
+- Muted text reduces lightness/chroma before introducing new hues.
+- Accent/status colors are semantic exceptions. They should mark state, focus, or navigation, not decorate containers.
+- Do not introduce raw hex/rgb colors in Workspace web components. Add or reuse a token in `app.css`.
+
+Core tokens:
+
+```css
+--bg
+--bg-raised
+--bg-subtle
+--line
+--line-strong
+--text
+--text-strong
+--text-muted
+--text-faint
+--code
+--accent
+--success
+--warning
+--danger
+```
+
+## Layout and grouping
+
+Prefer vertical rhythm and text hierarchy over card chrome.
+
+- Page sections are separated by whitespace and a light top rule.
+- Navigation selection uses a left rule rather than filled pills.
+- Nested records use indentation or top rules, not repeated rounded containers.
+- Shadows are avoided in the base system.
+- Rounded corners are reserved for small controls where hit area shape matters.
+
+## Typography
+
+- Headings and primary labels use `--text-strong`.
+- Body text uses `--text`.
+- Metadata, helper text, timestamps, and table headings use `--text-muted` or `--text-faint`.
+- Uppercase labels are acceptable for small metadata labels only; avoid large all-caps UI blocks.
+
+## Component styling boundary
+
+`app.css` owns the shared visual language:
+
+- reset/base body styles
+- OKLCH tokens
+- layout primitives
+- page cards/sections
+- sidebar/navigation sections
+- tables, kanban lists, diagnostics, record bodies
+
+Svelte components should keep local styles only when a behavior is truly component-specific. If a style affects color, spacing, borders, text hierarchy, or repeated record layout, it belongs in `app.css`.
+
+## Adding new UI
+
+When adding Workspace web UI:
+
+1. Start with semantic HTML and existing classes from `app.css`.
+2. Use spacing and text contrast first.
+3. Use a border only when the boundary carries meaning.
+4. Use background fills only for page-level surfaces or read-only code/record bodies.
+5. If a new color is needed, define it as an OKLCH token and document why the existing semantic tokens are insufficient.
diff --git a/web/workspace/src/app.css b/web/workspace/src/app.css
new file mode 100644
index 00000000..d1108aab
--- /dev/null
+++ b/web/workspace/src/app.css
@@ -0,0 +1,473 @@
+@layer reset, tokens, base, layout, components;
+
+@layer reset {
+ *,
+ *::before,
+ *::after {
+ box-sizing: border-box;
+ }
+
+ body {
+ margin: 0;
+ }
+
+ button,
+ input,
+ textarea,
+ select {
+ font: inherit;
+ }
+}
+
+@layer tokens {
+ :root {
+ color-scheme: light dark;
+
+ /* Palette rule: layout/background neutrals use zero chroma. */
+ --bg: oklch(98.5% 0 0);
+ --bg-raised: oklch(96% 0 0);
+ --bg-subtle: oklch(93% 0 0);
+ --line: oklch(82% 0 0);
+ --line-strong: oklch(70% 0 0);
+
+ /* Text is near-neutral warm: OKLCH C is kept around 0.01, roughly a 5% saturation intent. */
+ --text: oklch(30% 0.012 75);
+ --text-strong: oklch(22% 0.012 75);
+ --text-muted: oklch(50% 0.01 75);
+ --text-faint: oklch(62% 0.008 75);
+ --code: oklch(34% 0.012 75);
+
+ --accent: oklch(54% 0.13 230);
+ --accent-muted: oklch(54% 0.08 230);
+ --success: oklch(48% 0.11 145);
+ --warning: oklch(62% 0.12 85);
+ --danger: oklch(54% 0.14 25);
+
+ --space-1: 4px;
+ --space-2: 8px;
+ --space-3: 12px;
+ --space-4: 16px;
+ --space-5: 24px;
+ --space-6: 32px;
+
+ --radius-soft: 8px;
+ --radius-panel: 12px;
+ --font-sans:
+ Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
+ "Segoe UI", sans-serif;
+ --font-mono:
+ ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
+ "Liberation Mono", monospace;
+ }
+
+ @media (prefers-color-scheme: dark) {
+ :root {
+ --bg: oklch(16% 0 0);
+ --bg-raised: oklch(21% 0 0);
+ --bg-subtle: oklch(26% 0 0);
+ --line: oklch(36% 0 0);
+ --line-strong: oklch(48% 0 0);
+
+ --text: oklch(87% 0.012 75);
+ --text-strong: oklch(95% 0.01 75);
+ --text-muted: oklch(70% 0.01 75);
+ --text-faint: oklch(58% 0.008 75);
+ --code: oklch(84% 0.012 75);
+
+ --accent: oklch(76% 0.12 230);
+ --accent-muted: oklch(71% 0.08 230);
+ --success: oklch(77% 0.12 145);
+ --warning: oklch(82% 0.13 85);
+ --danger: oklch(76% 0.14 25);
+ }
+ }
+}
+
+@layer base {
+ body {
+ background: var(--bg);
+ color: var(--text);
+ font-family: var(--font-sans);
+ }
+
+ a {
+ color: var(--accent);
+ }
+
+ h1,
+ h2,
+ h3,
+ p,
+ li,
+ dd {
+ overflow-wrap: anywhere;
+ }
+
+ h1,
+ h2,
+ h3 {
+ color: var(--text-strong);
+ margin-top: 0;
+ }
+
+ p {
+ line-height: 1.55;
+ }
+
+ code,
+ pre {
+ font-family: var(--font-mono);
+ }
+
+ code {
+ color: var(--code);
+ }
+
+ small {
+ color: var(--text-muted);
+ display: block;
+ margin-top: var(--space-1);
+ }
+}
+
+@layer layout {
+ .workspace-layout {
+ display: grid;
+ grid-template-columns: minmax(220px, 280px) minmax(0, 1fr);
+ width: 100vw;
+ height: 100dvh;
+ margin: 0;
+ padding: 0;
+ overflow: hidden;
+ min-width: 0;
+ }
+
+ .shell {
+ display: grid;
+ gap: var(--space-6);
+ min-width: 0;
+ min-height: 0;
+ overflow-y: auto;
+ padding: var(--space-6);
+ }
+
+ .grid {
+ display: grid;
+ gap: var(--space-5);
+ grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
+ min-width: 0;
+ }
+
+ .runtime {
+ grid-template-columns: repeat(auto-fit, minmax(min(360px, 100%), 1fr));
+ }
+
+ .stack {
+ display: grid;
+ gap: var(--space-4);
+ }
+
+ @media (max-width: 760px) {
+ .workspace-layout {
+ grid-template-columns: 1fr;
+ width: 100vw;
+ height: auto;
+ min-height: 100dvh;
+ overflow: visible;
+ }
+
+ .shell {
+ overflow: visible;
+ padding: var(--space-5) var(--space-4);
+ }
+ }
+}
+
+@layer components {
+ .workspace-sidebar {
+ align-self: stretch;
+ min-width: 0;
+ min-height: 0;
+ overflow-y: auto;
+ padding: var(--space-6) var(--space-5);
+ }
+
+ .sidebar-header {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: var(--space-4);
+ margin-bottom: var(--space-6);
+ min-width: 0;
+ }
+
+ .workspace-label {
+ display: grid;
+ gap: var(--space-1);
+ min-width: 0;
+ }
+
+ .workspace-sidebar h1 {
+ margin: 0;
+ font-size: 1.05rem;
+ line-height: 1.25;
+ }
+
+ .workspace-status {
+ margin: 0;
+ color: var(--text-muted);
+ font-size: 0.78rem;
+ line-height: 1.35;
+ }
+
+ .workspace-status.error,
+ .section-state.error,
+ .error {
+ color: var(--danger);
+ }
+
+ .settings-button {
+ flex: 0 0 auto;
+ display: grid;
+ place-items: center;
+ width: 32px;
+ height: 32px;
+ border: 0;
+ border-radius: var(--radius-soft);
+ background: var(--bg-subtle);
+ color: var(--text-muted);
+ cursor: not-allowed;
+ }
+
+ .sidebar-sections {
+ display: grid;
+ gap: var(--space-5);
+ min-width: 0;
+ }
+
+ .nav-section {
+ display: grid;
+ gap: var(--space-2);
+ }
+
+ .section-heading-row,
+ .section-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--space-3);
+ }
+
+ .section-heading-row h2,
+ .section-header,
+ .eyebrow {
+ color: var(--text-faint);
+ font-size: 0.72rem;
+ font-weight: 750;
+ letter-spacing: 0.11em;
+ text-transform: uppercase;
+ }
+
+ .section-heading-row h2 {
+ margin: 0;
+ }
+
+ .eyebrow {
+ color: var(--accent-muted);
+ margin: 0;
+ }
+
+ .section-count {
+ color: var(--text-muted);
+ font-size: 0.72rem;
+ line-height: 1;
+ }
+
+ .nav-list {
+ display: grid;
+ gap: var(--space-1);
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ }
+
+ .nav-item,
+ .objective-link {
+ display: grid;
+ gap: 3px;
+ min-width: 0;
+ padding: var(--space-2) 0 var(--space-2) var(--space-3);
+ border-left: 2px solid transparent;
+ color: inherit;
+ text-align: left;
+ text-decoration: none;
+ }
+
+ .nav-item.active,
+ .objective-link.active,
+ .nav-item:hover,
+ .objective-link:hover {
+ border-left-color: var(--accent);
+ }
+
+ .item-title,
+ .item-meta {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .item-title {
+ color: var(--text-strong);
+ font-weight: 650;
+ }
+
+ .item-meta,
+ .section-note,
+ .section-state,
+ .muted {
+ color: var(--text-muted);
+ font-size: 0.82rem;
+ }
+
+ .section-note,
+ .section-state {
+ margin: 0;
+ line-height: 1.45;
+ }
+
+ .card {
+ min-width: 0;
+ padding: var(--space-5) 0 0;
+ border-top: 1px solid var(--line);
+ }
+
+ .card:first-child {
+ padding-top: 0;
+ border-top: 0;
+ }
+
+ .runtime-card,
+ .kanban-column {
+ padding: var(--space-4) 0 0;
+ border-top: 1px solid var(--line);
+ }
+
+ .selected-card.selected {
+ border-top-color: var(--accent);
+ }
+
+ .runtime-heading,
+ .detail-heading {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: var(--space-3);
+ margin-bottom: var(--space-3);
+ }
+
+ .runtime-heading span {
+ color: var(--success);
+ }
+
+ .runtime-heading span.warn {
+ color: var(--warning);
+ }
+
+ .detail-heading h3 {
+ margin: 0;
+ }
+
+ .detail-heading span {
+ color: var(--text-muted);
+ font-size: 0.8rem;
+ }
+
+ dl {
+ display: grid;
+ gap: var(--space-3);
+ }
+
+ dt {
+ color: var(--text-faint);
+ font-size: 0.78rem;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ }
+
+ dd {
+ margin: 0;
+ }
+
+ .table-wrap {
+ overflow-x: auto;
+ }
+
+ table {
+ width: 100%;
+ border-collapse: collapse;
+ }
+
+ th,
+ td {
+ border-bottom: 1px solid var(--line);
+ padding: 10px 8px;
+ text-align: left;
+ vertical-align: top;
+ }
+
+ th {
+ color: var(--text-faint);
+ font-size: 0.78rem;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ }
+
+ .kanban {
+ display: grid;
+ gap: var(--space-5);
+ grid-template-columns: repeat(auto-fit, minmax(min(190px, 100%), 1fr));
+ margin-top: var(--space-4);
+ }
+
+ .kanban-column h3 {
+ display: flex;
+ justify-content: space-between;
+ gap: var(--space-3);
+ color: var(--text-muted);
+ font-size: 0.9rem;
+ letter-spacing: 0.07em;
+ text-transform: uppercase;
+ }
+
+ .kanban-column ul {
+ display: grid;
+ gap: var(--space-3);
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ }
+
+ .kanban-column li {
+ padding-left: var(--space-3);
+ border-left: 2px solid var(--line);
+ }
+
+ .diagnostics {
+ margin-top: var(--space-4);
+ }
+
+ .diagnostics li {
+ display: grid;
+ gap: var(--space-1);
+ margin-bottom: var(--space-3);
+ }
+
+ .record-body {
+ overflow-x: auto;
+ padding: var(--space-4) 0 0;
+ border-top: 1px solid var(--line);
+ color: var(--text);
+ white-space: pre-wrap;
+ }
+}
diff --git a/web/workspace/src/lib/workspace-pages/WorkspacePage.svelte b/web/workspace/src/lib/workspace-pages/WorkspacePage.svelte
new file mode 100644
index 00000000..232f579b
--- /dev/null
+++ b/web/workspace/src/lib/workspace-pages/WorkspacePage.svelte
@@ -0,0 +1,645 @@
+
+
+
+ Yoi Workspace Control Plane
+
+
+
+
+
+
+
+ {#if route.page === 'repository'}
+
+
+
Repository summary
+ {#if repository}
+
+
+
- ID
+ {repository.id}
+
+
+
- Kind
+ - {repository.kind}
+
+
+
- Workspace root
+ {repository.workspace_root}
+
+
+
- Record authority
+ - {repository.record_authority}
+
+
+
- Git
+ - {repository.git.status}
+
+
+ {:else if repositoryError}
+
{repositoryError}
+ {:else}
+
Waiting for /api/repositories/local…
+ {/if}
+
+
+
+
Git summary
+ {#if repository}
+ {#if repository.git.status === 'available'}
+
+
+
- Root
+ {repository.git.root ?? 'unknown'}
+
+
+
- Branch
+ - {repository.git.branch ?? 'unknown'}
+
+
+
- HEAD
+ {shortHash(repository.git.head)}
+
+
+
- Dirty
+ - {repository.git.dirty === null || repository.git.dirty === undefined ? 'unknown' : repository.git.dirty ? 'yes' : 'no'} {repository.git.dirty_scope}
+
+
+
- Remote
+ -
+ {#if repository.git.remote}
+
{repository.git.remote.name} · {repository.git.remote.url}
+ {#if repository.git.remote.redacted}credentials redacted{/if}
+ {:else}
+ not configured
+ {/if}
+
+
+
+ {:else}
+
Git metadata is unavailable for this local Repository.
+ {/if}
+ {:else if repositoryError}
+
{repositoryError}
+ {:else}
+
Waiting for Git summary…
+ {/if}
+
+
+
+
+ Recent Git log
+ {#if repositoryLog}
+ {#if repositoryLog.items.length === 0}
+ No recent commits are available from the bounded Git log API.
+ {:else}
+
+
+
+
+ | Commit |
+ Subject |
+ Author |
+ Timestamp |
+
+
+
+ {#each repositoryLog.items as commit (commit.hash)}
+
+ {shortHash(commit.hash)} |
+ {commit.subject} |
+ {commit.author_name} {commit.author_email} |
+ {commit.timestamp} |
+
+ {/each}
+
+
+
+ {/if}
+ {:else if repositoryLogError}
+ {repositoryLogError}
+ {:else}
+ Waiting for /api/repositories/local/log…
+ {/if}
+
+
+
+ Repository Ticket Kanban
+
+ Read-only grouping of canonical Ticket records. No drag/drop or lifecycle mutation is exposed.
+
+ {#if repositoryTickets}
+
+ {#each repositoryTickets.columns as column (column.state)}
+
+ {column.state} {column.items.length}
+ {#if column.items.length === 0}
+ No tickets.
+ {:else}
+
+ {#each column.items as ticket (ticket.id)}
+ -
+ {ticket.title}
+
{ticket.id} · updated {formatDate(ticket.updated_at)}
+
+ {/each}
+
+ {/if}
+
+ {/each}
+
+ {:else if repositoryTicketsError}
+ {repositoryTicketsError}
+ {:else}
+ Waiting for /api/repositories/local/tickets…
+ {/if}
+
+
+ {@const repositoryDiagnostics = diagnosticsFor(repository?.git.diagnostics, repositoryLog?.diagnostics, repositoryTickets?.diagnostics)}
+ {#if repositoryDiagnostics.length > 0}
+
+ Repository diagnostics
+
+ {#each repositoryDiagnostics as diagnostic}
+ -
+ {diagnostic.severity}
+
{diagnostic.code}
+ {diagnostic.message}
+
+ {/each}
+
+
+ {/if}
+ {:else if route.page === 'objectives' || route.page === 'objective'}
+
+ Objectives
+
+ Objectives are read from canonical filesystem records through /api/objectives.
+
+ {#if objectives}
+ {#if objectives.items.length === 0}
+ No Objective records are present.
+ {:else}
+
+ {#each objectives.items as objective (objective.id)}
+
+
+ {objective.title}
+ {objective.state}
+
+ {objective.summary || 'No summary text is available.'}
+
+
+
- ID
+ {objective.id}
+
+
+
- Updated
+ - {formatDate(objective.updated_at)}
+
+
+
- Linked tickets
+ - {objective.linked_tickets?.length ? objective.linked_tickets.join(', ') : 'none'}
+
+
+ View detail
+
+ {/each}
+
+ {/if}
+ {#if objectives.invalid_records.length > 0}
+ {objectives.invalid_records.length} invalid objective record(s) hidden.
+ {/if}
+ {:else if objectivesError}
+ {objectivesError}
+ {:else}
+ Waiting for /api/objectives…
+ {/if}
+
+
+ {#if route.page === 'objective'}
+
+ Objective detail
+ {#if objectiveDetail}
+
+
{objectiveDetail.title}
+ {objectiveDetail.state}
+
+
+
+
- ID
+ {objectiveDetail.id}
+
+
+
- Updated
+ - {formatDate(objectiveDetail.updated_at)}
+
+
+
- Created
+ - {formatDate(objectiveDetail.created_at)}
+
+
+
- Linked tickets
+ - {objectiveDetail.linked_tickets.length ? objectiveDetail.linked_tickets.join(', ') : 'none'}
+
+
+
- Source
+ - {objectiveDetail.record_source}
+
+
+ {#if objectiveDetail.body_truncated}
+ Objective body was truncated by the backend response limit.
+ {/if}
+ {objectiveDetail.body || 'No Objective body text is available.'}
+ {:else if objectiveDetailError}
+ {objectiveDetailError}
+ {:else if objectiveDetailLoading}
+ Loading Objective {route.objectiveId}…
+ {:else}
+ Waiting for Objective detail…
+ {/if}
+
+ {/if}
+ {:else}
+
+ Workspace
+ {#if workspace}
+
+
+
- ID
+ - {workspace.workspace_id}
+
+
+
- Name
+ - {workspace.display_name}
+
+
+
- Record authority
+ - {workspace.record_authority}
+
+
+
- Host / Worker bridge
+ - {workspace.extension_points.host_worker_bridge.status}
+
+
+ {:else if workspaceError}
+ {workspaceError}
+ {:else}
+ Waiting for /api/workspace…
+ {/if}
+
+
+
+
+
Read API surface
+
+ {#each endpoints as endpoint}
+ {endpoint.path} — {endpoint.label}
+ {/each}
+
+
+
+
+
Reserved seams
+
+ Event streams remain represented as extension-point state in the backend
+ response. Hosts and Workers are read-only local observations; no
+ scheduler, lifecycle control, or hosted multi-tenant behavior is
+ implemented in this slice.
+
+
+
+
+
+
+
Hosts
+ {#if hosts}
+ {#if hosts.items.length === 0}
+
No local Hosts are visible.
+ {:else}
+
+ {#each hosts.items as host}
+
+
+ {host.label}
+ {host.status}
+
+
+
+
- ID
+ {host.host_id}
+
+
+
- Kind
+ - {host.kind}
+
+
+
- Local inspection
+ - {host.capabilities.local_pod_inspection}
+
+
+
- Platform
+ - {host.capabilities.os} / {host.capabilities.arch}
+
+
+
+ {/each}
+
+ {/if}
+ {:else if hostsError}
+
{hostsError}
+ {:else}
+
Waiting for /api/hosts…
+ {/if}
+
+
+
+
Workers
+ {#if workers}
+ {#if workers.items.length === 0}
+
No local Workers are visible.
+ {:else}
+
+
+
+
+ | Worker |
+ Host |
+ State |
+ Workspace |
+ Implementation |
+
+
+
+ {#each workers.items as worker}
+
+ |
+ {worker.label}
+ {#if worker.role || worker.profile}
+ {worker.role ?? 'role unknown'} / {worker.profile ?? 'profile unknown'}
+ {/if}
+ |
+ {worker.host_id} |
+ {worker.state} · {worker.status} |
+ {worker.workspace_root ?? 'unknown'} |
+ {worker.implementation.kind} |
+
+ {/each}
+
+
+
+ {/if}
+ {:else if workersError}
+
{workersError}
+ {:else}
+
Waiting for /api/workers…
+ {/if}
+
+
+
+ {#if hosts || workers}
+ {@const diagnostics = diagnosticsFor(hosts?.diagnostics, workers?.diagnostics)}
+ {#if diagnostics.length > 0}
+
+ Diagnostics
+
+ {#each diagnostics as diagnostic}
+ -
+ {diagnostic.severity}
+
{diagnostic.code}
+ {diagnostic.message}
+
+ {/each}
+
+
+ {/if}
+ {/if}
+ {/if}
+
+
diff --git a/web/workspace/src/lib/workspace-sidebar/ObjectivesNavSection.svelte b/web/workspace/src/lib/workspace-sidebar/ObjectivesNavSection.svelte
index 4d22690f..7052d95b 100644
--- a/web/workspace/src/lib/workspace-sidebar/ObjectivesNavSection.svelte
+++ b/web/workspace/src/lib/workspace-sidebar/ObjectivesNavSection.svelte
@@ -1,167 +1,18 @@
-
-
-
objectives
- {#if !loading && !error}
- {objectives.length}
- {/if}
-
+
+
- {#if loading}
- Loading objectives…
- {:else if error}
- {error}
- {:else if objectives.length === 0}
- No objectives found.
- {:else}
-
- {/if}
-
- {#if invalidRecordCount > 0}
- {invalidRecordCount} invalid objective record(s) hidden.
- {/if}
+
+ Open Objectives
+ workspace objectives
+
-
-
diff --git a/web/workspace/src/lib/workspace-sidebar/RepositoriesNavSection.svelte b/web/workspace/src/lib/workspace-sidebar/RepositoriesNavSection.svelte
index 85223908..f64f44e7 100644
--- a/web/workspace/src/lib/workspace-sidebar/RepositoriesNavSection.svelte
+++ b/web/workspace/src/lib/workspace-sidebar/RepositoriesNavSection.svelte
@@ -3,9 +3,10 @@
type Props = {
workspace: WorkspaceResponse | null;
+ currentPath?: string;
};
- let { workspace }: Props = $props();
+ let { workspace, currentPath = '/' }: Props = $props();
-
-
diff --git a/web/workspace/src/lib/workspace-sidebar/WorkersNavSection.svelte b/web/workspace/src/lib/workspace-sidebar/WorkersNavSection.svelte
index 0b81f0b6..8cd50e3f 100644
--- a/web/workspace/src/lib/workspace-sidebar/WorkersNavSection.svelte
+++ b/web/workspace/src/lib/workspace-sidebar/WorkersNavSection.svelte
@@ -74,84 +74,3 @@
{/if}
-
-
diff --git a/web/workspace/src/lib/workspace-sidebar/WorkspaceSidebar.svelte b/web/workspace/src/lib/workspace-sidebar/WorkspaceSidebar.svelte
index ecc85e69..4ab90734 100644
--- a/web/workspace/src/lib/workspace-sidebar/WorkspaceSidebar.svelte
+++ b/web/workspace/src/lib/workspace-sidebar/WorkspaceSidebar.svelte
@@ -7,9 +7,10 @@
type Props = {
workspace: WorkspaceResponse | null;
workspaceError?: string | null;
+ currentPath?: string;
};
- let { workspace, workspaceError = null }: Props = $props();
+ let { workspace, workspaceError = null, currentPath = '/' }: Props = $props();
-
-
diff --git a/web/workspace/src/lib/workspace-sidebar/types.ts b/web/workspace/src/lib/workspace-sidebar/types.ts
index f461829c..28e8fec9 100644
--- a/web/workspace/src/lib/workspace-sidebar/types.ts
+++ b/web/workspace/src/lib/workspace-sidebar/types.ts
@@ -144,6 +144,18 @@ export type ObjectiveSummary = {
record_source?: string;
};
+export type ObjectiveDetail = {
+ id: string;
+ title: string;
+ state: string;
+ created_at?: string | null;
+ updated_at?: string | null;
+ linked_tickets: string[];
+ body: string;
+ body_truncated: boolean;
+ record_source: string;
+};
+
export type InvalidProjectRecord = {
label: string;
reason: string;
diff --git a/web/workspace/src/routes/+layout.svelte b/web/workspace/src/routes/+layout.svelte
new file mode 100644
index 00000000..5d83456e
--- /dev/null
+++ b/web/workspace/src/routes/+layout.svelte
@@ -0,0 +1,7 @@
+
+
+{@render children()}
diff --git a/web/workspace/src/routes/+layout.ts b/web/workspace/src/routes/+layout.ts
index 89da957b..83addb7e 100644
--- a/web/workspace/src/routes/+layout.ts
+++ b/web/workspace/src/routes/+layout.ts
@@ -1,2 +1,2 @@
export const ssr = false;
-export const prerender = true;
+export const prerender = false;
diff --git a/web/workspace/src/routes/+page.svelte b/web/workspace/src/routes/+page.svelte
index 32f89cbb..309e087a 100644
--- a/web/workspace/src/routes/+page.svelte
+++ b/web/workspace/src/routes/+page.svelte
@@ -1,827 +1,5 @@
-
- Yoi Workspace Control Plane
-
-
-
-
-
-
-
-
- Local / single-workspace bootstrap
- Yoi Workspace Control Plane
-
- Static SPA shell for reading canonical .yoi project records,
- bounded local Repository summaries, and the local Host / Worker execution
- view. Ticket and Objective lifecycle authority stays in the existing local
- record workflow.
-
-
- Overview
- Repository
- Objectives
-
-
-
- {#if route.page === 'repository'}
-
-
-
Repository summary
- {#if repository}
-
-
-
- ID
- {repository.id}
-
-
-
- Kind
- - {repository.kind}
-
-
-
- Workspace root
- {repository.workspace_root}
-
-
-
- Record authority
- - {repository.record_authority}
-
-
-
- Git
- - {repository.git.status}
-
-
- {:else if repositoryError}
-
{repositoryError}
- {:else}
-
Waiting for /api/repositories/local…
- {/if}
-
-
-
-
Git summary
- {#if repository}
- {#if repository.git.status === 'available'}
-
-
-
- Root
- {repository.git.root ?? 'unknown'}
-
-
-
- Branch
- - {repository.git.branch ?? 'unknown'}
-
-
-
- HEAD
- {shortHash(repository.git.head)}
-
-
-
- Dirty
- - {repository.git.dirty === null || repository.git.dirty === undefined ? 'unknown' : repository.git.dirty ? 'yes' : 'no'} {repository.git.dirty_scope}
-
-
-
- Remote
- -
- {#if repository.git.remote}
-
{repository.git.remote.name} · {repository.git.remote.url}
- {#if repository.git.remote.redacted}credentials redacted{/if}
- {:else}
- not configured
- {/if}
-
-
-
- {:else}
-
Git metadata is unavailable for this local Repository.
- {/if}
- {:else if repositoryError}
-
{repositoryError}
- {:else}
-
Waiting for Git summary…
- {/if}
-
-
-
-
- Recent Git log
- {#if repositoryLog}
- {#if repositoryLog.items.length === 0}
- No recent commits are available from the bounded Git log API.
- {:else}
-
-
-
-
- | Commit |
- Subject |
- Author |
- Timestamp |
-
-
-
- {#each repositoryLog.items as commit (commit.hash)}
-
- {shortHash(commit.hash)} |
- {commit.subject} |
- {commit.author_name} {commit.author_email} |
- {commit.timestamp} |
-
- {/each}
-
-
-
- {/if}
- {:else if repositoryLogError}
- {repositoryLogError}
- {:else}
- Waiting for /api/repositories/local/log…
- {/if}
-
-
-
- Repository Ticket Kanban
-
- Read-only grouping of canonical Ticket records. No drag/drop or lifecycle mutation is exposed.
-
- {#if repositoryTickets}
-
- {#each repositoryTickets.columns as column (column.state)}
-
- {column.state} {column.items.length}
- {#if column.items.length === 0}
- No tickets.
- {:else}
-
- {#each column.items as ticket (ticket.id)}
- -
- {ticket.title}
-
{ticket.id} · updated {formatDate(ticket.updated_at)}
-
- {/each}
-
- {/if}
-
- {/each}
-
- {:else if repositoryTicketsError}
- {repositoryTicketsError}
- {:else}
- Waiting for /api/repositories/local/tickets…
- {/if}
-
-
- {@const repositoryDiagnostics = diagnosticsFor(repository?.git.diagnostics, repositoryLog?.diagnostics, repositoryTickets?.diagnostics)}
- {#if repositoryDiagnostics.length > 0}
-
- Repository diagnostics
-
- {#each repositoryDiagnostics as diagnostic}
- -
- {diagnostic.severity}
-
{diagnostic.code}
- {diagnostic.message}
-
- {/each}
-
-
- {/if}
- {:else if route.page === 'objectives'}
-
- Objectives
-
- Objectives are read from canonical filesystem records through /api/objectives.
-
- {#if objectives}
- {#if objectives.items.length === 0}
- No Objective records are present.
- {:else}
-
- {#each objectives.items as objective (objective.id)}
-
-
- {objective.title}
- {objective.state}
-
- {objective.summary || 'No summary text is available.'}
-
-
-
- ID
- {objective.id}
-
-
-
- Updated
- - {formatDate(objective.updated_at)}
-
-
-
- Linked tickets
- - {objective.linked_tickets?.length ? objective.linked_tickets.join(', ') : 'none'}
-
-
- Detail placeholder
-
- {/each}
-
- {/if}
- {#if objectives.invalid_records.length > 0}
- {objectives.invalid_records.length} invalid objective record(s) hidden.
- {/if}
- {:else if objectivesError}
- {objectivesError}
- {:else}
- Waiting for /api/objectives…
- {/if}
-
-
- {#if route.objectiveId}
-
- Objective detail
-
- Selected Objective {route.objectiveId}. This slice keeps detail navigation as a
- static SPA placeholder; canonical Objective content remains in the filesystem record.
-
-
- {/if}
- {:else}
-
- Workspace
- {#if workspace}
-
-
-
- ID
- - {workspace.workspace_id}
-
-
-
- Name
- - {workspace.display_name}
-
-
-
- Record authority
- - {workspace.record_authority}
-
-
-
- Host / Worker bridge
- - {workspace.extension_points.host_worker_bridge.status}
-
-
- {:else if workspaceError}
- {workspaceError}
- {:else}
- Waiting for /api/workspace…
- {/if}
-
-
-
-
-
Read API surface
-
- {#each endpoints as endpoint}
- {endpoint.path} — {endpoint.label}
- {/each}
-
-
-
-
-
Reserved seams
-
- Event streams remain represented as extension-point state in the backend
- response. Hosts and Workers are read-only local observations; no
- scheduler, lifecycle control, or hosted multi-tenant behavior is
- implemented in this slice.
-
-
-
-
-
-
-
Hosts
- {#if hosts}
- {#if hosts.items.length === 0}
-
No local Hosts are visible.
- {:else}
-
- {#each hosts.items as host}
-
-
- {host.label}
- {host.status}
-
-
-
-
- ID
- {host.host_id}
-
-
-
- Kind
- - {host.kind}
-
-
-
- Local inspection
- - {host.capabilities.local_pod_inspection}
-
-
-
- Platform
- - {host.capabilities.os} / {host.capabilities.arch}
-
-
-
- {/each}
-
- {/if}
- {:else if hostsError}
-
{hostsError}
- {:else}
-
Waiting for /api/hosts…
- {/if}
-
-
-
-
Workers
- {#if workers}
- {#if workers.items.length === 0}
-
No local Workers are visible.
- {:else}
-
-
-
-
- | Worker |
- Host |
- State |
- Workspace |
- Implementation |
-
-
-
- {#each workers.items as worker}
-
- |
- {worker.label}
- {#if worker.role || worker.profile}
- {worker.role ?? 'role unknown'} / {worker.profile ?? 'profile unknown'}
- {/if}
- |
- {worker.host_id} |
- {worker.state} · {worker.status} |
- {worker.workspace_root ?? 'unknown'} |
- {worker.implementation.kind} |
-
- {/each}
-
-
-
- {/if}
- {:else if workersError}
-
{workersError}
- {:else}
-
Waiting for /api/workers…
- {/if}
-
-
-
- {#if hosts || workers}
- {@const diagnostics = diagnosticsFor(hosts?.diagnostics, workers?.diagnostics)}
- {#if diagnostics.length > 0}
-
- Diagnostics
-
- {#each diagnostics as diagnostic}
- -
- {diagnostic.severity}
-
{diagnostic.code}
- {diagnostic.message}
-
- {/each}
-
-
- {/if}
- {/if}
- {/if}
-
-
-
-
+
diff --git a/web/workspace/src/routes/objectives/+page.svelte b/web/workspace/src/routes/objectives/+page.svelte
new file mode 100644
index 00000000..750a969f
--- /dev/null
+++ b/web/workspace/src/routes/objectives/+page.svelte
@@ -0,0 +1,5 @@
+
+
+
diff --git a/web/workspace/src/routes/objectives/[objectiveId]/+page.svelte b/web/workspace/src/routes/objectives/[objectiveId]/+page.svelte
new file mode 100644
index 00000000..f7b3689a
--- /dev/null
+++ b/web/workspace/src/routes/objectives/[objectiveId]/+page.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/web/workspace/src/routes/objectives/[objectiveId]/+page.ts b/web/workspace/src/routes/objectives/[objectiveId]/+page.ts
new file mode 100644
index 00000000..250f4d4e
--- /dev/null
+++ b/web/workspace/src/routes/objectives/[objectiveId]/+page.ts
@@ -0,0 +1,5 @@
+import type { PageLoad } from './$types';
+
+export const load: PageLoad = ({ params }) => ({
+ objectiveId: params.objectiveId
+});
diff --git a/web/workspace/src/routes/repositories/[repositoryId]/+page.svelte b/web/workspace/src/routes/repositories/[repositoryId]/+page.svelte
new file mode 100644
index 00000000..e15fdf2b
--- /dev/null
+++ b/web/workspace/src/routes/repositories/[repositoryId]/+page.svelte
@@ -0,0 +1,5 @@
+
+
+