oh-my-llmwikimode 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,14 @@
1
1
  # Eos Chat Home Information Architecture
2
2
 
3
- **Status:** Active implementation contract for Eos Chat Command Center MVP
3
+ **Status:** Active implementation contract for EOS Workspace and Eos Chat Command Center MVP
4
4
  **Date:** 2026-05-29
5
5
  **Depends on:** `EOS_DESIGN.md`, `EOS_UI_CONTRACT.md`, `EOS_SIDE_REVIEW_IA.md`
6
6
 
7
7
  ## 1. Purpose
8
8
 
9
- `Eos Chat` is the first screen and local command center for Eos.
9
+ `EOS Workspace` at `/workspace/` is the browser-first Home for Eos. `Eos Chat` remains the direct chat capture surface and compatibility route.
10
10
 
11
- The user starts by saying what they want. Eos must capture that intent as a local review-required artifact, then make it visible in `Side Review`. Chat does not execute shell commands, providers, agents, wiki promotion, git actions, or background work by itself.
11
+ The user starts with Chat-first capture: saying what they want so Eos captures that intent as a local review-required artifact, then makes it visible in `Side Review`. Chat does not execute shell commands, providers, agents, wiki promotion, git actions, or background work by itself.
12
12
 
13
13
  ## 2. Primary promise
14
14
 
@@ -20,7 +20,7 @@ Tell Eos what you want to prepare. Eos captures it for local review before anyth
20
20
 
21
21
  ## 3. Required regions
22
22
 
23
- 1. **Header rail** - `Eos Chat`, short local workspace status, and link to `Side Review`.
23
+ 1. **Header rail** - `EOS Workspace`, short local workspace status, and links to `Eos Chat` and `Side Review`.
24
24
  2. **Safety strip** - always-visible copy: `Local review workspace. Nothing is applied automatically.`
25
25
  3. **Welcome state** - explains chat-first workflow without starter chips.
26
26
  4. **Status cards** - wiki status, review queue count, and harness readiness in compact cards.
@@ -46,6 +46,7 @@ Do not add starter chips in this slice. Shortcuts may return later only after re
46
46
  - Button label: `Capture for Review`
47
47
  - Success acknowledgement: `Captured for review.`
48
48
  - Follow-up link/action: `Open Side Review`
49
+ - Compatibility link/action: `Open Chat`
49
50
 
50
51
  The composer must not use copy such as `Run`, `Execute`, `Complete`, `Apply`, or `Push`.
51
52
 
@@ -82,9 +83,15 @@ Do not use these labels in the Eos Chat MVP:
82
83
 
83
84
  - `GET /chat/` serves the Eos Chat local UI.
84
85
  - `GET /chat` redirects to `/chat/`.
86
+ - `GET /workspace/` serves the canonical EOS Workspace Home.
87
+ - `GET /workspace` redirects to `/workspace/`.
85
88
  - `GET /api/home` remains a read-only home data snapshot.
86
89
  - `POST /api/chat/requests` creates a review-required local artifact only.
87
90
 
91
+ ## 12. Browser-first Workspace Slice
92
+
93
+ `/workspace/` centers the first safe user action: Chat-first capture. It shows local Home pressure from `GET /api/home`, posts review-required requests to `POST /api/chat/requests`, and links to `Open Side Review` for inspection. `/chat/` stays available as the compatibility chat surface.
94
+
88
95
  ## 9. Safety contract
89
96
 
90
97
  - Local-first.
@@ -6,14 +6,14 @@
6
6
 
7
7
  ## 1. Simple metaphor
8
8
 
9
- Chat is the inbox. Side Review is the review desk.
9
+ EOS Workspace is the browser-first Home. Chat is the inbox. Side Review is the review desk.
10
10
 
11
- The user writes a request in `Eos Chat`. Eos files it as a local review item, adds a deterministic intent hint, and places it on the `Side Review` desk. Nothing is executed or applied just because the request was captured.
11
+ The user writes a request through Chat-first capture on `/workspace/` or the compatibility `/chat/` surface. Eos files it as a local review item, adds a deterministic intent hint, and places it on the `Side Review` desk. Nothing is executed or applied just because the request was captured.
12
12
 
13
13
  ## 2. Operating loop
14
14
 
15
15
  ```text
16
- User request in Eos Chat
16
+ User request in EOS Workspace Chat-first capture
17
17
  -> local capture endpoint
18
18
  -> chat request artifact
19
19
  -> intent hint metadata
@@ -51,6 +51,9 @@ POST /api/chat/requests
51
51
  writes .system/eos/chat/requests/{id}.json
52
52
  appends .system/eos/chat/audit.jsonl
53
53
 
54
+ GET /workspace/
55
+ serves the canonical browser Home with Chat-first capture and Open Side Review navigation
56
+
54
57
  GET /api/home
55
58
  exposes waiting count, intent counts, and recent request summaries
56
59
 
@@ -99,3 +102,7 @@ This slice must not add:
99
102
  `Home` is the mission-control layer between Chat and Side Review. It should tell the user what is waiting, what was recently captured, and whether any local artifact needs attention before the user opens raw files.
100
103
 
101
104
  `Side Review` adds intent filters so chat-derived requests can be inspected by review hint category: knowledge, search, planning, issue, release log, or unknown. These intent filters are visibility controls, not approval controls. Filtering must not apply, promote, merge, delete, push, publish, or mutate source wiki entries.
105
+
106
+ ## 9. Workspace Home coordination
107
+
108
+ `/workspace/` is the canonical browser Home for this slice. It exposes `EOS Workspace`, `Chat-first capture`, `Capture for Review`, `Open Chat`, and `Open Side Review` while keeping every output local-only, review-required, and Not Applied.
@@ -6,7 +6,7 @@
6
6
 
7
7
  ## 1. Purpose
8
8
 
9
- `Side Review` is the companion window for looking at structured material while `Eos Chat` remains the command center.
9
+ `Side Review` is the companion window for looking at structured material while `EOS Workspace` at `/workspace/` remains the browser-first Home and `Eos Chat` remains the compatibility capture surface.
10
10
 
11
11
  It answers five questions:
12
12
 
@@ -291,6 +291,7 @@ Implementation files:
291
291
  User guidance:
292
292
 
293
293
  1. Open `/side-review/` from the local Eos server.
294
+ 1. Use `/workspace/` as the primary Home when returning to Chat-first capture.
294
295
  2. Use group tabs to inspect Review Required, Decision Artifacts, Drafts, Completion Reports, Issues, and Harness Status.
295
296
  3. Read source, artifact, and evidence paths before taking a decision action.
296
297
  4. Use `Prepare Approval Request`, `Prepare Rejection Note`, or `Defer Review` to create a local review artifact only.
@@ -1,7 +1,7 @@
1
1
  # Eos Visual Screenshot Harness MVP
2
2
 
3
3
  **Status:** MVP harness for local visual evidence
4
- **Scope:** Eos Chat, Mission Control, and Side Review
4
+ **Scope:** EOS Workspace, Eos Chat, Mission Control, and Side Review
5
5
  **Boundary:** local-only screenshot capture; no cloud browser, no hosted sync, no source wiki mutation, no publish, no push
6
6
 
7
7
  ## Purpose
@@ -14,7 +14,7 @@ It is intentionally a release-readiness aid, not a new runtime feature:
14
14
  - seeds a temporary wiki fixture with safe review-required data,
15
15
  - captures PNG screenshots for local surfaces,
16
16
  - can capture desktop, tablet, and mobile viewport presets,
17
- - records a static accessibility snapshot for Eos Chat and Side Review,
17
+ - records a static accessibility snapshot for EOS Workspace, Eos Chat, and Side Review,
18
18
  - writes ignored local artifacts under `.system/eos/visual-qa/screenshots/`,
19
19
  - shuts down and removes temporary wiki/profile data.
20
20
 
@@ -46,6 +46,7 @@ npm run eos:visual-screenshots
46
46
 
47
47
  | Target | URL | Desktop output |
48
48
  | --- | --- | --- |
49
+ | EOS Workspace Home | `/workspace/` | `eos-workspace-home.png` |
49
50
  | Eos Chat + Mission Control | `/chat/` | `eos-chat-command-center.png` |
50
51
  | Eos Side Review | `/side-review/` | `eos-side-review.png` |
51
52
 
@@ -65,7 +66,7 @@ Viewport-specific files append the viewport id, for example `eos-chat-command-ce
65
66
  npm run eos:a11y-snapshot
66
67
  ```
67
68
 
68
- The accessibility snapshot is static and local. It checks Eos Chat and Side Review markup/CSS/scripts for baseline review affordances such as language metadata, viewport metadata, skip links, landmarks, ARIA labels, live status regions, focus-visible styling, reduced-motion support, forced-colors support, no broad `transition: all`, no inline event handlers, and no `innerHTML` assignment.
69
+ The accessibility snapshot is static and local. It checks EOS Workspace, Eos Chat, and Side Review markup/CSS/scripts for baseline review affordances such as language metadata, viewport metadata, skip links, landmarks, ARIA labels, live status regions, focus-visible styling, reduced-motion support, forced-colors support, no broad `transition: all`, no inline event handlers, and no `innerHTML` assignment.
69
70
 
70
71
  ## Review contract
71
72
 
@@ -58,17 +58,48 @@ The following remain intentionally out of scope for this boundary MVP:
58
58
 
59
59
  Run from the repo root:
60
60
 
61
+ ```powershell
62
+ npm run verify:eos-workspace-product
63
+ npm run test:eos
64
+ npm run verify:eos-boundary
65
+ npm run verify:eos-boundary-policy
66
+ ```
67
+
68
+ Expected verifier sentinels include:
69
+
70
+ ```text
71
+ EOS_WORKSPACE_BOUNDARY_VERIFY_OK
72
+ EOS_BOUNDARY_POLICY_VERIFY_OK
73
+ ```
74
+
75
+ For the historical integrated plugin path, run:
76
+
61
77
  ```powershell
62
78
  npm run verify:eos-boundary --prefix plugins/oh-my-llmwikimode
63
79
  node --test plugins/oh-my-llmwikimode/test/eos/workspace-boundary-*.test.js
64
80
  ```
65
81
 
66
- Expected verifier sentinel:
82
+ ## Browser Workspace product slice
83
+
84
+ Start the local daemon from the repo root with an explicit local wiki root:
85
+
86
+ ```powershell
87
+ $env:EOS_WIKI_ROOT = "$HOME\Documents\llm-wiki"
88
+ node src\daemon.js
89
+ ```
90
+
91
+ The default daemon address is `http://127.0.0.1:4825`. Open the browser-first Workspace Home at:
67
92
 
68
93
  ```text
69
- EOS_WORKSPACE_BOUNDARY_VERIFY_OK
94
+ http://127.0.0.1:4825/workspace/
70
95
  ```
71
96
 
97
+ The Workspace Home posts chat-first capture requests to `POST /api/chat/requests`. A successful capture writes a local review-required artifact under `.system/eos/chat/requests/` and does not execute, apply, promote, merge, delete, push, or mutate source wiki entries.
98
+
99
+ Inspect the review queue with `GET /api/side-review` or by opening `http://127.0.0.1:4825/side-review/`. Preparing a Side Review decision writes a local artifact under `.system/eos/side-review/decisions/`; prepared decision artifacts are still `review_required` and not applied.
100
+
101
+ Product-slice evidence for this implementation is captured under `.omo/ulw-loop/evidence/eos-workspace-product-20260601`. The evidence root is local review material only and is safe to clean after review if no longer needed.
102
+
72
103
  ## Static boundary policy
73
104
 
74
105
  Run `npm run verify:eos-boundary-policy` to prevent new unregistered private imports across the Eos/LLM Wiki boundary. The verifier is local-only and does not publish, push, sync, or call external services.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-llmwikimode",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "LLM Wiki plugin for OpenCode ??auto-memory, retrieval, and knowledge promotion",
5
5
  "type": "module",
6
6
  "main": "./src/server.js",
@@ -28,6 +28,7 @@
28
28
  "test:storage": "node --test test/storage.test.js",
29
29
  "test:security": "node --test test/security.test.js",
30
30
  "test:eos": "node --test test/eos/**/*.test.js",
31
+ "verify:eos-workspace-product": "npm run test:eos && npm run verify:eos-boundary-policy && npm run verify:eos-boundary && npm run eos:a11y-snapshot -- --json",
31
32
  "verify:package": "node test/verify-package.test.js",
32
33
  "verify:docs": "node test/verify-docs.test.js",
33
34
  "verify:side-effects": "node scripts/side-effect-audit.js",
package/src/eos/home.js CHANGED
@@ -144,6 +144,46 @@ function buildCommandCenter(reviewQueue, librarian, actions) {
144
144
  };
145
145
  }
146
146
 
147
+ function buildWorkspaceHome() {
148
+ return {
149
+ title: "EOS Workspace",
150
+ route: "/workspace/",
151
+ primary_surface: "chat_capture",
152
+ companion_surface: "side_review",
153
+ capture_endpoint: "/api/chat/requests",
154
+ home_endpoint: "/api/home",
155
+ side_review_endpoint: "/api/side-review",
156
+ safe_copy: {
157
+ primary: "Chat-first capture",
158
+ safety: "Local Only / Review Required / Not Applied",
159
+ capture: "Capture for Review",
160
+ review: "Open Side Review",
161
+ },
162
+ nav: [
163
+ {
164
+ id: "chat",
165
+ label: "Open Chat",
166
+ href: "/chat/",
167
+ target: "compatibility_chat",
168
+ },
169
+ {
170
+ id: "side-review",
171
+ label: "Open Side Review",
172
+ href: "/side-review/",
173
+ target: "side_review",
174
+ },
175
+ ],
176
+ forbidden_capabilities: [
177
+ "hidden_execution",
178
+ "hosted_sync",
179
+ "auto_promote",
180
+ "auto_merge",
181
+ "auto_delete",
182
+ "auto_push",
183
+ ],
184
+ };
185
+ }
186
+
147
187
  function summarizeChatRequestsForHome(wikiRoot) {
148
188
  const counts = {
149
189
  knowledge_request: 0,
@@ -296,6 +336,7 @@ export function buildEosHomeData(wikiRoot, config = {}, options = {}) {
296
336
  recent_entries: Array.isArray(stats.recent_entries) ? stats.recent_entries.slice(0, 8) : [],
297
337
  },
298
338
  review_queue: reviewQueue,
339
+ workspace_home: buildWorkspaceHome(),
299
340
  librarian,
300
341
  command_center: buildCommandCenter(reviewQueue, librarian, nextSafeActions),
301
342
  search: {
@@ -74,7 +74,8 @@ function getPathname(url) {
74
74
  function isLocalStaticUiRoute(url) {
75
75
  const pathname = getPathname(url);
76
76
  return pathname === "/side-review" || pathname.startsWith("/side-review/") ||
77
- pathname === "/chat" || pathname.startsWith("/chat/");
77
+ pathname === "/chat" || pathname.startsWith("/chat/") ||
78
+ pathname === "/workspace" || pathname.startsWith("/workspace/");
78
79
  }
79
80
 
80
81
  function isGuardedLocalMutationRoute(url) {
@@ -246,6 +247,15 @@ export async function buildApp(config, logger) {
246
247
  });
247
248
  }
248
249
 
250
+ const workspaceStaticDir = path.resolve(__dirname, "../workspace/static");
251
+ if (fs.existsSync(workspaceStaticDir)) {
252
+ await app.register(fastifyStatic, {
253
+ root: workspaceStaticDir,
254
+ prefix: "/workspace/",
255
+ decorateReply: false,
256
+ });
257
+ }
258
+
249
259
  // -----------------------------------------------------------------------
250
260
  // Body parser
251
261
  // -----------------------------------------------------------------------
@@ -257,8 +267,9 @@ export async function buildApp(config, logger) {
257
267
  // -----------------------------------------------------------------------
258
268
 
259
269
  app.setErrorHandler((error, _request, reply) => {
260
- const statusCode = error.statusCode || 500;
261
- const code = error.code || "INTERNAL_ERROR";
270
+ const invalidJson = error instanceof SyntaxError && /^Invalid JSON:/.test(error.message || "");
271
+ const statusCode = invalidJson ? 400 : error.statusCode || 500;
272
+ const code = invalidJson ? "BAD_REQUEST" : error.code || "INTERNAL_ERROR";
262
273
  const message =
263
274
  statusCode >= 500 ? "Internal server error" : error.message;
264
275
 
@@ -300,6 +311,8 @@ export async function buildApp(config, logger) {
300
311
  await registerAgentRoutes(app, config);
301
312
  await registerSideReviewRoutes(app, config);
302
313
 
314
+ app.get("/workspace", async (_request, reply) => reply.redirect("/workspace/"));
315
+
303
316
  // -----------------------------------------------------------------------
304
317
  // 404 handler (must be registered after all routes)
305
318
  // -----------------------------------------------------------------------
@@ -2,6 +2,13 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
 
4
4
  const SURFACES = Object.freeze([
5
+ Object.freeze({
6
+ id: "workspace-home",
7
+ label: "EOS Workspace Home",
8
+ html: "src/eos/workspace/static/index.html",
9
+ css: "src/eos/workspace/static/styles.css",
10
+ script: "src/eos/workspace/static/app.js",
11
+ }),
5
12
  Object.freeze({
6
13
  id: "chat-command-center",
7
14
  label: "Eos Chat + Mission Control",
@@ -24,6 +24,13 @@ const VIEWPORTS = Object.freeze([
24
24
  ]);
25
25
 
26
26
  const DEFAULT_TARGETS = [
27
+ {
28
+ id: "workspace-home",
29
+ label: "EOS Workspace Home",
30
+ path: "/workspace/",
31
+ fileName: "eos-workspace-home.png",
32
+ captures: ["workspace_home", "chat_first_capture"],
33
+ },
27
34
  {
28
35
  id: "chat-command-center",
29
36
  label: "Eos Chat + Mission Control",
@@ -0,0 +1,148 @@
1
+ const state = {
2
+ loadingHome: false,
3
+ capturing: false,
4
+ };
5
+
6
+ const elements = {
7
+ form: document.querySelector("#capture-form"),
8
+ input: document.querySelector("#request-input"),
9
+ resultEmpty: document.querySelector("#result-empty"),
10
+ resultList: document.querySelector("#result-list"),
11
+ statusGrid: document.querySelector("#status-grid"),
12
+ wikiCount: document.querySelector("#wiki-count"),
13
+ reviewCount: document.querySelector("#review-count"),
14
+ intentSummary: document.querySelector("#intent-summary"),
15
+ healthSummary: document.querySelector("#health-summary"),
16
+ };
17
+
18
+ function setText(node, value) {
19
+ if (node) node.textContent = value;
20
+ }
21
+
22
+ function syncBusyState() {
23
+ const button = elements.form?.querySelector("button");
24
+ if (button) button.disabled = state.capturing;
25
+ if (elements.statusGrid) {
26
+ const busy = state.loadingHome || state.capturing;
27
+ elements.statusGrid.setAttribute("aria-busy", busy ? "true" : "false");
28
+ }
29
+ }
30
+
31
+ function setLoadingHome(value) {
32
+ state.loadingHome = value;
33
+ syncBusyState();
34
+ }
35
+
36
+ function setCapturing(value) {
37
+ state.capturing = value;
38
+ syncBusyState();
39
+ }
40
+
41
+ function formatIntentSummary(counts = {}) {
42
+ const labels = [
43
+ ["issue_report", "Issues"],
44
+ ["planning_request", "Plans"],
45
+ ["knowledge_request", "Knowledge"],
46
+ ["search_request", "Search"],
47
+ ["release_log", "Release"],
48
+ ];
49
+ const visible = labels
50
+ .map(([key, label]) => [label, Number(counts[key] || 0)])
51
+ .filter(([, count]) => count > 0)
52
+ .slice(0, 3);
53
+ if (visible.length === 0) return "No captured requests";
54
+ return visible.map(([label, count]) => `${label} ${count}`).join(" / ");
55
+ }
56
+
57
+ function appendCaptureResult(result) {
58
+ if (!elements.resultList) return;
59
+ if (elements.resultEmpty) elements.resultEmpty.hidden = true;
60
+
61
+ const item = document.createElement("li");
62
+ item.className = "result-item";
63
+
64
+ const title = document.createElement("strong");
65
+ title.textContent = "Captured for review.";
66
+ item.append(title);
67
+
68
+ const intent = document.createElement("p");
69
+ intent.textContent = `Intent hint: ${result.intent_hint?.label || "Unknown Intent"}. Nothing was applied.`;
70
+ item.append(intent);
71
+
72
+ const artifactPath = result.artifactPath || result.request?.artifact_path || "artifact path unavailable";
73
+ const pathLine = document.createElement("p");
74
+ pathLine.className = "artifact-path";
75
+ pathLine.textContent = `Artifact path: ${artifactPath}`;
76
+ item.append(pathLine);
77
+
78
+ const link = document.createElement("a");
79
+ link.href = "/side-review/";
80
+ link.textContent = "Open Side Review";
81
+ item.append(link);
82
+
83
+ elements.resultList.prepend(item);
84
+ }
85
+
86
+ async function loadHome() {
87
+ setLoadingHome(true);
88
+ try {
89
+ const response = await fetch("/api/home", { headers: { accept: "application/json" } });
90
+ if (!response.ok) throw new Error(`Home API failed: ${response.status}`);
91
+ const payload = await response.json();
92
+ const home = payload.eos_home || {};
93
+ const queue = home.review_queue || {};
94
+ const health = home.command_center?.health?.description || "Local status available.";
95
+ setText(elements.wikiCount, `${home.wiki?.total_entries ?? 0} entries`);
96
+ setText(elements.reviewCount, `${queue.total_review_required ?? 0} waiting`);
97
+ setText(elements.intentSummary, formatIntentSummary(queue.chat_request_intent_counts));
98
+ setText(elements.healthSummary, health);
99
+ } catch {
100
+ setText(elements.wikiCount, "Unavailable");
101
+ setText(elements.reviewCount, "Check local server");
102
+ setText(elements.intentSummary, "Unavailable");
103
+ setText(elements.healthSummary, "Home data is unavailable. Nothing was applied.");
104
+ } finally {
105
+ setLoadingHome(false);
106
+ }
107
+ }
108
+
109
+ async function capture(message) {
110
+ const response = await fetch("/api/chat/requests", {
111
+ method: "POST",
112
+ headers: { "content-type": "application/json", accept: "application/json" },
113
+ body: JSON.stringify({ message, source: "eos_chat" }),
114
+ });
115
+ const payload = await response.json();
116
+ if (!response.ok || payload.success === false) {
117
+ throw new Error(payload.error?.message || payload.error || "Capture request failed.");
118
+ }
119
+ return payload;
120
+ }
121
+
122
+ async function onSubmit(event) {
123
+ event.preventDefault();
124
+ if (state.capturing) return;
125
+ const message = elements.input?.value.trim() || "";
126
+ if (!message) return;
127
+
128
+ setCapturing(true);
129
+ try {
130
+ const result = await capture(message);
131
+ appendCaptureResult(result);
132
+ elements.input.value = "";
133
+ await loadHome();
134
+ } catch (error) {
135
+ appendCaptureResult({
136
+ intent_hint: { label: "Capture needs attention" },
137
+ artifactPath: error instanceof Error ? error.message : String(error),
138
+ });
139
+ } finally {
140
+ setCapturing(false);
141
+ }
142
+ }
143
+
144
+ if (elements.form) {
145
+ elements.form.addEventListener("submit", onSubmit);
146
+ }
147
+
148
+ loadHome();
@@ -0,0 +1,64 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>EOS Workspace</title>
7
+ <link rel="stylesheet" href="/workspace/styles.css">
8
+ </head>
9
+ <body>
10
+ <a class="skip-link" href="#workspace-content">Skip to EOS Workspace content</a>
11
+ <main id="workspace-content" class="workspace-shell" aria-labelledby="workspace-title">
12
+ <header class="top-row">
13
+ <div>
14
+ <p class="eyebrow">Chat-first capture</p>
15
+ <h1 id="workspace-title">EOS Workspace</h1>
16
+ <p class="subtitle">Capture requests to local review artifacts before any source updates.</p>
17
+ </div>
18
+ <nav aria-label="Workspace navigation">
19
+ <a class="nav-link" href="/chat/">Open Chat</a>
20
+ <a class="nav-link" href="/side-review/">Open Side Review</a>
21
+ </nav>
22
+ </header>
23
+
24
+ <section class="safety-strip" aria-label="Safety contract">
25
+ <span>Local Only</span>
26
+ <span>Review Required</span>
27
+ <span>Not Applied</span>
28
+ <strong>Local artifact capture only. Nothing was applied.</strong>
29
+ </section>
30
+
31
+ <section class="status-grid" id="status-grid" aria-busy="true" aria-label="Workspace status">
32
+ <article>
33
+ <p class="label">Wiki Entries</p>
34
+ <strong id="wiki-count">Loading...</strong>
35
+ </article>
36
+ <article>
37
+ <p class="label">Review Queue</p>
38
+ <strong id="review-count">Loading...</strong>
39
+ </article>
40
+ <article>
41
+ <p class="label">Intent Summary</p>
42
+ <strong id="intent-summary">Loading...</strong>
43
+ </article>
44
+ <article>
45
+ <p class="label">Local Health</p>
46
+ <strong id="health-summary">Loading...</strong>
47
+ </article>
48
+ </section>
49
+
50
+ <form id="capture-form" class="capture-form" aria-label="Capture request for local review">
51
+ <label for="request-input">Request</label>
52
+ <textarea id="request-input" name="message" rows="4" maxlength="8000" required placeholder="Tell EOS what to prepare for review."></textarea>
53
+ <button type="submit">Capture for Review</button>
54
+ <p class="help-text">Captured requests remain review-required until you inspect them in Side Review.</p>
55
+ </form>
56
+
57
+ <section class="result-panel" aria-live="polite" aria-label="Capture results">
58
+ <p id="result-empty" class="muted">No captured requests in this browser session.</p>
59
+ <ol id="result-list" class="result-list"></ol>
60
+ </section>
61
+ </main>
62
+ <script src="/workspace/app.js" defer></script>
63
+ </body>
64
+ </html>
@@ -0,0 +1,262 @@
1
+ :root {
2
+ color-scheme: dark light;
3
+ --bg: var(--vscode-editor-background, #0f172a);
4
+ --surface: var(--vscode-editorWidget-background, #15243a);
5
+ --surface-soft: var(--vscode-sideBar-background, #112033);
6
+ --text: var(--vscode-foreground, #e2e8f0);
7
+ --muted: var(--vscode-descriptionForeground, #9fb0c3);
8
+ --border: var(--vscode-panel-border, rgba(148, 163, 184, 0.28));
9
+ --accent: var(--vscode-textLink-foreground, #7dd3fc);
10
+ --focus: var(--vscode-focusBorder, #38bdf8);
11
+ --warning: var(--vscode-editorWarning-foreground, #f59e0b);
12
+ font-family: var(--vscode-font-family, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif);
13
+ }
14
+
15
+ * {
16
+ box-sizing: border-box;
17
+ }
18
+
19
+ body {
20
+ margin: 0;
21
+ min-height: 100vh;
22
+ color: var(--text);
23
+ background: linear-gradient(180deg, color-mix(in srgb, var(--surface-soft) 90%, transparent), var(--bg));
24
+ }
25
+
26
+ .skip-link {
27
+ position: absolute;
28
+ top: 12px;
29
+ left: 12px;
30
+ z-index: 100;
31
+ transform: translateY(-180%);
32
+ padding: 8px 12px;
33
+ border-radius: 8px;
34
+ color: var(--bg);
35
+ background: var(--accent);
36
+ font-weight: 700;
37
+ }
38
+
39
+ .skip-link:focus-visible {
40
+ transform: translateY(0);
41
+ }
42
+
43
+ .workspace-shell {
44
+ width: min(980px, calc(100vw - 28px));
45
+ margin: 0 auto;
46
+ min-height: 100vh;
47
+ padding: 24px 0 32px;
48
+ display: grid;
49
+ gap: 14px;
50
+ }
51
+
52
+ .workspace-shell > * {
53
+ min-width: 0;
54
+ }
55
+
56
+ .top-row,
57
+ .status-grid article,
58
+ .capture-form,
59
+ .result-panel {
60
+ border: 1px solid var(--border);
61
+ border-radius: 8px;
62
+ background: color-mix(in srgb, var(--surface) 92%, transparent);
63
+ }
64
+
65
+ .top-row {
66
+ display: flex;
67
+ justify-content: space-between;
68
+ gap: 16px;
69
+ align-items: flex-start;
70
+ padding: 16px;
71
+ }
72
+
73
+ .eyebrow,
74
+ .label {
75
+ margin: 0 0 6px;
76
+ color: var(--accent);
77
+ text-transform: uppercase;
78
+ font-size: 0.76rem;
79
+ font-weight: 700;
80
+ letter-spacing: 0.06em;
81
+ }
82
+
83
+ h1,
84
+ p {
85
+ margin-top: 0;
86
+ overflow-wrap: anywhere;
87
+ }
88
+
89
+ h1 {
90
+ margin-bottom: 8px;
91
+ font-size: 2rem;
92
+ line-height: 1.06;
93
+ }
94
+
95
+ .subtitle {
96
+ margin-bottom: 0;
97
+ color: var(--muted);
98
+ }
99
+
100
+ .nav-link,
101
+ button {
102
+ display: inline-flex;
103
+ justify-content: center;
104
+ align-items: center;
105
+ border: 1px solid color-mix(in srgb, var(--accent) 52%, var(--border));
106
+ border-radius: 8px;
107
+ padding: 9px 12px;
108
+ color: var(--text);
109
+ background: color-mix(in srgb, var(--accent) 12%, var(--surface));
110
+ font: inherit;
111
+ font-weight: 700;
112
+ text-decoration: none;
113
+ cursor: pointer;
114
+ }
115
+
116
+ .nav-link:focus-visible,
117
+ button:focus-visible,
118
+ textarea:focus-visible {
119
+ outline: 2px solid var(--focus);
120
+ outline-offset: 2px;
121
+ }
122
+
123
+ .safety-strip {
124
+ display: flex;
125
+ flex-wrap: wrap;
126
+ align-items: center;
127
+ gap: 8px;
128
+ padding: 10px 12px;
129
+ border: 1px solid color-mix(in srgb, var(--warning) 45%, var(--border));
130
+ border-radius: 8px;
131
+ background: color-mix(in srgb, var(--warning) 10%, var(--surface-soft));
132
+ }
133
+
134
+ .safety-strip span {
135
+ border: 1px solid var(--border);
136
+ border-radius: 999px;
137
+ padding: 4px 8px;
138
+ color: var(--warning);
139
+ font-size: 0.78rem;
140
+ font-weight: 700;
141
+ }
142
+
143
+ .status-grid {
144
+ display: grid;
145
+ grid-template-columns: repeat(4, minmax(0, 1fr));
146
+ gap: 10px;
147
+ }
148
+
149
+ .status-grid article {
150
+ padding: 12px;
151
+ }
152
+
153
+ .status-grid strong {
154
+ display: block;
155
+ font-size: 1.2rem;
156
+ }
157
+
158
+ .capture-form,
159
+ .result-panel {
160
+ padding: 14px;
161
+ }
162
+
163
+ .capture-form label {
164
+ display: block;
165
+ margin-bottom: 8px;
166
+ font-weight: 700;
167
+ }
168
+
169
+ textarea {
170
+ width: 100%;
171
+ min-height: 96px;
172
+ resize: vertical;
173
+ margin-bottom: 10px;
174
+ padding: 10px;
175
+ border: 1px solid var(--border);
176
+ border-radius: 8px;
177
+ color: var(--text);
178
+ background: var(--surface-soft);
179
+ font: inherit;
180
+ }
181
+
182
+ .help-text,
183
+ .muted,
184
+ .artifact-path {
185
+ color: var(--muted);
186
+ }
187
+
188
+ .result-list {
189
+ margin: 0;
190
+ padding: 0;
191
+ list-style: none;
192
+ display: grid;
193
+ gap: 10px;
194
+ }
195
+
196
+ .result-item {
197
+ border: 1px solid var(--border);
198
+ border-radius: 8px;
199
+ padding: 10px;
200
+ background: var(--surface-soft);
201
+ display: grid;
202
+ gap: 6px;
203
+ }
204
+
205
+ .result-item p {
206
+ margin-bottom: 0;
207
+ }
208
+
209
+ .artifact-path {
210
+ font-family: var(--vscode-editor-font-family, ui-monospace, monospace);
211
+ font-size: 0.85rem;
212
+ }
213
+
214
+ button:disabled {
215
+ opacity: 0.6;
216
+ cursor: wait;
217
+ }
218
+
219
+ @media (max-width: 820px) {
220
+ .top-row {
221
+ display: grid;
222
+ }
223
+
224
+ .status-grid {
225
+ grid-template-columns: repeat(2, minmax(0, 1fr));
226
+ }
227
+ }
228
+
229
+ @media (max-width: 560px) {
230
+ .workspace-shell {
231
+ width: min(100% - 16px, 980px);
232
+ padding: 18px 0 24px;
233
+ }
234
+
235
+ .status-grid {
236
+ grid-template-columns: 1fr;
237
+ }
238
+ }
239
+
240
+ @media (prefers-reduced-motion: reduce) {
241
+ *,
242
+ *::before,
243
+ *::after {
244
+ animation-duration: 0.01ms !important;
245
+ animation-iteration-count: 1 !important;
246
+ scroll-behavior: auto !important;
247
+ }
248
+ }
249
+
250
+ @media (forced-colors: active) {
251
+ .top-row,
252
+ .status-grid article,
253
+ .capture-form,
254
+ .result-panel,
255
+ .result-item,
256
+ .safety-strip,
257
+ textarea,
258
+ button,
259
+ .nav-link {
260
+ border-color: CanvasText;
261
+ }
262
+ }
@@ -180,6 +180,9 @@ export function validateSideReviewDecisionRequest(input = {}) {
180
180
  const errors = [];
181
181
  if (!cardId) errors.push("card_id is required");
182
182
  if (!definition) errors.push(`Unsupported side review decision: ${decision || ""}`);
183
+ if (definition && (definition.side_effect !== "review_artifact_only" || definition.creates_decision_artifact !== true)) {
184
+ errors.push(`Side Review decision endpoint only accepts review artifact actions: ${decision}`);
185
+ }
183
186
  if (definition?.side_effect === "review_artifact_only" && !firstString(input.reason, input.note, input.reviewer_note)) {
184
187
  errors.push("reason is required for review artifact decisions");
185
188
  }