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} +
+ + + + + + + + + + + {#each repositoryLog.items as commit (commit.hash)} + + + + + + + {/each} + +
CommitSubjectAuthorTimestamp
{shortHash(commit.hash)}{commit.subject}{commit.author_name} {commit.author_email}{commit.timestamp}
+
+ {/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} +
+ + + + + + + + + + + + {#each workers.items as worker} + + + + + + + + {/each} + +
WorkerHostStateWorkspaceImplementation
+ {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}
+
+ {/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 @@ - - - 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. -

- -
- - {#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} -
- - - - - - - - - - - {#each repositoryLog.items as commit (commit.hash)} - - - - - - - {/each} - -
CommitSubjectAuthorTimestamp
{shortHash(commit.hash)}{commit.subject}{commit.author_name} {commit.author_email}{commit.timestamp}
-
- {/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} -
- - - - - - - - - - - - {#each workers.items as worker} - - - - - - - - {/each} - -
WorkerHostStateWorkspaceImplementation
- {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}
-
- {/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 @@ + + +