ltcai 1.5.0 → 1.6.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.
package/README.md CHANGED
@@ -155,42 +155,56 @@ See [docs/architecture.md](docs/architecture.md) for request and data-flow detai
155
155
  </tr>
156
156
  </table>
157
157
 
158
- > Screenshots above are the live web UI. The diagrams below map the product
159
- > experience to the current v1.5.0 structure.
158
+ > Every image in this section is a **real screenshot** of the running app
159
+ > (Lattice AI v1.6.0), captured with a headless browser.
160
160
 
161
161
  ---
162
162
 
163
163
  ## Product Experience
164
164
 
165
- ### Local model recommendation
165
+ ### Onboard in minutes
166
166
 
167
- Lattice AI detects your OS, CPU, GPU, RAM, and disk, then rates every local model
168
- **Recommended**, **Compatible**, or **Not Recommended** for your machine — grouped
169
- by family (Gemma, Qwen, Llama, Phi, DeepSeek, and more).
167
+ A first run detects your OS, CPU, GPU, RAM, and disk, then recommends a local
168
+ model and rates every option **Recommended**, **Compatible**, or **Not
169
+ Recommended** for your machine — grouped by family (Gemma, Qwen, Llama, Phi,
170
+ DeepSeek, and more), with estimated RAM and a clear next step.
170
171
 
171
172
  <div align="center">
172
- <img src="docs/images/model-recommendation.png" alt="Tri-state local model recommendation grouped by family" width="100%"/>
173
+ <img src="docs/images/onboarding.png" alt="Onboarding hardware scan: OS, CPU, GPU, RAM, disk, runtime" width="49%"/>
174
+ <img src="docs/images/model-recommendation.png" alt="Local model recommendation with best-pick callout and per-family status" width="49%"/>
173
175
  </div>
174
176
 
175
177
  ### Workspaces & organization
176
178
 
177
- Switch instantly between a **Personal** workspace and shared **Organization**
178
- workspaces. Org data is scoped by `workspace_id`, and `owner / admin / member /
179
- viewer` roles map to a transparent permission matrix.
179
+ A **Current Workspace** card shows exactly where you are; switch instantly
180
+ between a **Personal** workspace and shared **Organization** workspaces. Org data
181
+ is scoped by `workspace_id`, and `owner / admin / member / viewer` roles map to a
182
+ transparent permission matrix with member management.
180
183
 
181
184
  <div align="center">
182
- <img src="docs/images/workspace.png" alt="Personal and Organization workspace model" width="49%"/>
183
- <img src="docs/images/organization.png" alt="Organization roles and permission matrix" width="49%"/>
185
+ <img src="docs/images/workspace.png" alt="Current Workspace summary card with scoped counts" width="100%"/>
186
+ <img src="docs/images/organization.png" alt="Organization workspace with members and roles" width="100%"/>
184
187
  </div>
185
188
 
186
- ### Knowledge graph & skills
189
+ ### Knowledge graph explorer
187
190
 
188
- Your work becomes a typed knowledge graph (built automatically), and skills extend
189
- the workspace through an in-product marketplace.
191
+ Your work becomes a typed knowledge graph automatically. The Entity Explorer
192
+ surfaces the most important entities and, on selection, their inbound/outbound
193
+ relationships, related entities, and a path back to you.
190
194
 
191
195
  <div align="center">
192
- <img src="docs/images/graph.png" alt="Knowledge graph node and edge taxonomy" width="49%"/>
193
- <img src="docs/images/skills.png" alt="Skill marketplace: recommended, popular, installed, updates" width="49%"/>
196
+ <img src="docs/images/graph.png" alt="Knowledge graph entity explorer with relationship detail" width="100%"/>
197
+ </div>
198
+
199
+ ### Skills & editions
200
+
201
+ Browse and install skills from an in-product marketplace; an honest editions
202
+ panel shows that every Enterprise capability is an opt-in extension point,
203
+ disabled in the open-source Community build.
204
+
205
+ <div align="center">
206
+ <img src="docs/images/skills.png" alt="Skill marketplace tabs: recommended, popular, installed, updates" width="49%"/>
207
+ <img src="docs/images/enterprise.png" alt="Enterprise capability status panel — all disabled in Community" width="49%"/>
194
208
  </div>
195
209
 
196
210
  ---
@@ -333,22 +347,27 @@ Supported routes include OpenAI-compatible APIs, OpenRouter, Groq, Together, xAI
333
347
 
334
348
  ## Current release
335
349
 
336
- **1.5.0 — Unified Product Release.** Onboarding, model recommendation, and CI
337
- stabilization in one release:
338
-
339
- - **CI / VSIX recovery** — the stale `@azure/core-tracing` lockfile pin that
340
- broke `npm ci` (ETARGET) is regenerated, so the VSIX build is green again
341
- - **Local model recommendation** — a hardware-aware engine
342
- (`latticeai/services/model_recommendation.py`) classifies the model catalog as
343
- Recommended / Compatible / Not Recommended, exposed at `/models/recommendations`
344
- - **Catalog extraction** the static model catalog moved to
345
- `latticeai/services/model_catalog.py`, simplifying `model_runtime.py`
346
- - **Enterprise PoC seam** — admin policy / audit-export / SIEM-stub / org-settings
347
- surfaces consult the capability registry (Community keeps everything ungated)
348
- - **Documentation & visuals** — README rewritten as a product page with an
349
- up-to-date architecture diagram and structural visuals
350
- - Python package, npm package, VS Code extension, FastAPI app, and `/health`
351
- version metadata are aligned at `1.5.0`
350
+ **1.6.0 — Product Experience Deepening.** A UX release: the screens in this README
351
+ are now real captured UI.
352
+
353
+ - **Knowledge Graph explorer** — entity cards, a relationship/related-entities/
354
+ shortest-path detail panel, recent activity, and a memory feed (additive UI on
355
+ existing endpoints)
356
+ - **Workspace UX** a "Current Workspace" summary card with quick-switch chips
357
+ - **Model Recommendation 2.0** machine summary, a best-pick callout with
358
+ estimated RAM and next step, per-family status, and a cloud caution
359
+ - **Skill Marketplace** — Recommended / Popular / Installed / Updates tabs
360
+ - **Enterprise capability panel** — an honest 12-capability matrix (Community: all
361
+ disabled, nothing gated)
362
+ - **Real screenshots** — `docs/images/*` refreshed from the running app; API,
363
+ schemas, `server:app`, CLI, MCP, and the Knowledge Graph contract unchanged
364
+
365
+ | Version | Theme |
366
+ |---|---|
367
+ | **1.6.0** | Product Experience Deepening (UX + real screenshots) |
368
+ | 1.5.0 | Unified Product Release (CI/VSIX recovery, model recommendation, Enterprise PoC) |
369
+ | 1.4.0 | Server App final decomposition |
370
+ | 1.1.0–1.3.0 | Organization workspaces, modularization, route safety net |
352
371
 
353
372
  See the full [changelog](docs/CHANGELOG.md) and [RELEASE.md](RELEASE.md).
354
373
 
package/docs/CHANGELOG.md CHANGED
@@ -1,5 +1,52 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.6.0] - 2026-06-01
4
+
5
+ > Product Experience Deepening — user-facing UX (Knowledge Graph explorer,
6
+ > workspace summary, model recommendation 2.0, skill marketplace tabs, Enterprise
7
+ > capability panel) and a refresh of `docs/images/*` to **real captured UI**
8
+ > screenshots. Not a refactor: API paths, request/response schemas, `server:app`,
9
+ > CLI, MCP, and the Knowledge Graph contract are unchanged. The only code changes
10
+ > are additive frontend (`static/`) and version metadata.
11
+
12
+ ### Added
13
+
14
+ - **Knowledge Graph Explorer (Workspace OS)** — an Entity Explorer (importance-
15
+ ranked entity cards + search) with a detail panel showing inbound/outbound
16
+ relationships, related entities, and the shortest path back to you; plus a
17
+ Recent Activity feed and a Workspace Memory feed. Built entirely on the existing
18
+ `/knowledge-graph/graph` and `/workspace/relationships/*` endpoints (additive
19
+ UI, no new API, no schema change).
20
+ - **Workspace summary & quick-switch** — a "Current Workspace" card (active
21
+ workspace, role, members, scoped counts) and one-click switch chips, preserving
22
+ `workspace_id` scoping and the owner/admin/member/viewer model.
23
+ - **Model Recommendation 2.0** — the onboarding recommendation panel now shows a
24
+ machine summary (OS/RAM/GPU/engine), a "best for this PC" callout with the
25
+ reason, estimated RAM, and next step, per-family status, and a cloud caution.
26
+ Estimates are labelled and conservative.
27
+ - **Skill Marketplace tabs** — Recommended / Popular / Installed / Updates tabs
28
+ with version, category, and source, plus install / enable / disable actions on
29
+ the existing skill lifecycle API.
30
+ - **Enterprise capability panel** — a 12-capability status matrix in Workspace OS
31
+ (Community reports all disabled; nothing gates a Community feature).
32
+
33
+ ### Changed
34
+
35
+ - **Real UI visuals** — `docs/images/{hero.gif,onboarding,model-recommendation,
36
+ workspace,graph,organization,skills,enterprise}` are now **real screenshots**
37
+ captured from the running app with Playwright + headless Chrome (the v1.5.0
38
+ set was structural diagrams). `architecture.png` remains a structural diagram.
39
+ README references the new real screenshots with no broken links.
40
+ - Python package, npm package, VS Code extension, FastAPI app, and `/health`
41
+ version metadata aligned at `1.6.0`.
42
+
43
+ ### Validation
44
+
45
+ - Unit tests pass; route-compatibility, startup/import, streaming, model-endpoint,
46
+ MCP/KG, and workspace/org permission tests preserved; `npm run check:python`
47
+ green; new UI verified rendering in a real browser via Playwright; VSIX build
48
+ verified. Test/build/packaging artifacts only — no package-store publish.
49
+
3
50
  ## [1.5.0] - 2026-06-01
4
51
 
5
52
  > Unified Product Release — CI/VSIX recovery, hardware-aware local model
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -1,3 +1,3 @@
1
1
  """Lattice AI - modular server package."""
2
2
 
3
- __version__ = "1.5.0"
3
+ __version__ = "1.6.0"
@@ -18,7 +18,7 @@ from pathlib import Path
18
18
  from typing import Any, Callable, Dict, Iterable, List, Optional
19
19
 
20
20
 
21
- WORKSPACE_OS_VERSION = "1.5.0"
21
+ WORKSPACE_OS_VERSION = "1.6.0"
22
22
 
23
23
  # Workspace types separate single-user Personal workspaces from shared
24
24
  # Organization workspaces. Both keep the same local-first JSON store; the type
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ltcai",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Lattice AI Workspace OS for local-first graph, memory, agent, workflow, and skill operations",
5
5
  "homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
6
6
  "repository": {
@@ -1033,6 +1033,7 @@ const chatViewport = document.getElementById('chat-viewport');
1033
1033
  const data = await res.json();
1034
1034
  if (!res.ok) return;
1035
1035
  const rec = (data && data.recommendations) || {};
1036
+ const profile = (data && data.profile) || {};
1036
1037
  const families = rec.families || [];
1037
1038
  if (!families.length) return;
1038
1039
  const counts = rec.counts || {};
@@ -1043,25 +1044,45 @@ const chatViewport = document.getElementById('chat-viewport');
1043
1044
  not_recommended: ['권장 안 함', '#9ca3af'],
1044
1045
  };
1045
1046
  const [label, color] = map[status] || ['', '#9ca3af'];
1046
- return `<span style="display:inline-block;padding:1px 7px;border-radius:999px;font-size:11px;color:#fff;background:${color}">${label}</span>`;
1047
+ return `<span style="display:inline-block;padding:1px 8px;border-radius:999px;font-size:11px;font-weight:700;color:#fff;background:${color}">${label}</span>`;
1047
1048
  };
1049
+ const ram = (m) => (m.required_ram_gb != null) ? `~${m.required_ram_gb}GB RAM (est.)` : '';
1050
+ const nextStep = (engine) => engine === 'ollama'
1051
+ ? 'Next: ollama pull'
1052
+ : engine === 'local_mlx' ? 'Next: download & load' : 'Next: connect engine';
1053
+
1054
+ // Top pick callout
1055
+ const top = rec.top_pick;
1056
+ const topHtml = top ? `
1057
+ <div style="border:1px solid #16a34a;background:#f0fdf4;border-radius:10px;padding:10px 12px;margin:8px 0">
1058
+ <div style="font-weight:700">⭐ Best for this PC — ${escapeHtml(top.name || top.id)} ${badge('recommended')}</div>
1059
+ <div style="font-size:12px;opacity:0.8;margin-top:3px">${escapeHtml(top.reason || '')}</div>
1060
+ <div style="font-size:12px;margin-top:4px">${escapeHtml(top.size || '')} · ${escapeHtml(ram(top))} · ${escapeHtml(nextStep(rec.engine))}</div>
1061
+ </div>` : '';
1062
+
1048
1063
  const rows = families.map((fam) => {
1049
1064
  const best = fam.best;
1050
1065
  const items = (fam.models || []).map((m) => `
1051
- <div style="display:flex;justify-content:space-between;gap:8px;padding:2px 0;font-size:12px;opacity:${m.status === 'not_recommended' ? 0.55 : 1}">
1066
+ <div style="display:flex;justify-content:space-between;gap:8px;padding:3px 0;font-size:12px;opacity:${m.status === 'not_recommended' ? 0.55 : 1}">
1052
1067
  <span>${escapeHtml(m.name || m.id)}</span>
1053
- <span>${escapeHtml(m.size || '')} ${badge(m.status)}</span>
1068
+ <span style="white-space:nowrap">${escapeHtml(m.size || '')} · ${escapeHtml(ram(m))} ${badge(m.status)}</span>
1054
1069
  </div>`).join('');
1055
1070
  return `
1056
1071
  <details style="margin:6px 0;border:1px solid var(--border,#e5e7eb);border-radius:8px;padding:8px 10px">
1057
- <summary style="cursor:pointer;font-weight:600">${escapeHtml(fam.family)} ${best ? badge(best.status) : ''}</summary>
1072
+ <summary style="cursor:pointer;font-weight:600">${escapeHtml(fam.family)} ${best ? badge(best.status) : ''}${best ? ` <span style="font-weight:400;opacity:0.7">${escapeHtml(best.name || '')}</span>` : ''}</summary>
1058
1073
  <div style="margin-top:6px">${items}</div>
1059
1074
  </details>`;
1060
1075
  }).join('');
1076
+
1077
+ const engineLabel = rec.engine === 'local_mlx' ? 'MLX (Apple Silicon)' : rec.engine;
1078
+ const machine = `${profile.os || ''} · RAM ${rec.ram_gb || '?'}GB · ${rec.apple_silicon ? 'Apple Silicon' : (profile.gpu && profile.gpu.vendor) || 'CPU'} · engine ${engineLabel}`;
1061
1079
  container.innerHTML = `
1062
- <h3 style="margin:14px 0 4px">이 PC에서 실행 가능한 로컬 모델</h3>
1063
- <p style="font-size:12px;opacity:0.7;margin:0 0 8px">RAM ${escapeHtml(String(rec.ram_gb || '?'))}GB 기준 · 추천 ${counts.recommended || 0} · 실행 가능 ${counts.compatible || 0} · 권장 안 함 ${counts.not_recommended || 0}</p>
1064
- ${rows}`;
1080
+ <h3 style="margin:14px 0 4px">이 PC 맞는 로컬 모델</h3>
1081
+ <p style="font-size:12px;opacity:0.7;margin:0 0 4px">${escapeHtml(machine)}</p>
1082
+ <p style="font-size:12px;opacity:0.7;margin:0 0 6px">${badge('recommended')} ${counts.recommended || 0} · ${badge('compatible')} ${counts.compatible || 0} · ${badge('not_recommended')} ${counts.not_recommended || 0} · estimates are conservative, verify before loading</p>
1083
+ ${topHtml}
1084
+ ${rows}
1085
+ <p style="font-size:12px;opacity:0.65;margin:8px 0 0">로컬 모델이 부족하면 클라우드 모델(OpenAI·OpenRouter·Groq 등, API 키 필요)을 선택할 수 있습니다.</p>`;
1065
1086
  } catch (e) {
1066
1087
  /* best-effort enhancement; never break onboarding */
1067
1088
  }
@@ -6,8 +6,15 @@ const state = {
6
6
  activeWorkspace: null,
7
7
  registry: null,
8
8
  managingWorkspace: null,
9
+ skillsPayload: null,
10
+ skillTab: "recommended",
11
+ entities: [],
12
+ activeEntity: null,
9
13
  };
10
14
 
15
+ // Skills that match common workspace needs are surfaced under "Recommended".
16
+ const RECOMMENDED_SKILL_HINTS = ["code", "review", "doc", "test", "security", "research", "changelog", "refactor", "debug"];
17
+
11
18
  function $(id) {
12
19
  return document.getElementById(id);
13
20
  }
@@ -194,30 +201,74 @@ function renderWorkflows(payload) {
194
201
  `).join("") : `<div class="list-item"><div class="meta-line">No workflows.</div></div>`;
195
202
  }
196
203
 
197
- function renderSkills(payload) {
204
+ function skillName(skill) {
205
+ return skill.skill || skill.name || "skill";
206
+ }
207
+
208
+ // Compute the four marketplace tabs from the registry payload (machine-global
209
+ // registry + locally-installed state). "Updates" = installed skills whose
210
+ // registry version differs from the installed version.
211
+ function computeSkillTabs(payload) {
198
212
  const installed = payload.installed || [];
199
- const available = (payload.available || []).filter((skill) => !installed.some((item) => item.name === (skill.skill || skill.name))).slice(0, 8);
200
- const rows = [
201
- ...installed.map((skill) => ({ ...skill, marketplace: false })),
202
- ...available.map((skill) => ({ name: skill.skill || skill.name, description: skill.description, version: skill.version || "remote", enabled: skill.enabled, marketplace: true })),
203
- ];
204
- $("skill-list").innerHTML = rows.length ? rows.map((skill) => `
213
+ const available = payload.available || [];
214
+ const installedNames = new Set(installed.map(skillName));
215
+ const notInstalled = available.filter((s) => !installedNames.has(skillName(s)));
216
+ const availByName = new Map(available.map((s) => [skillName(s), s]));
217
+ const updates = installed.filter((s) => {
218
+ const remote = availByName.get(skillName(s));
219
+ return remote && remote.version && s.version && remote.version !== s.version;
220
+ });
221
+ const recommended = notInstalled.filter((s) => {
222
+ const hay = `${skillName(s)} ${s.category || ""} ${s.description || ""}`.toLowerCase();
223
+ return RECOMMENDED_SKILL_HINTS.some((h) => hay.includes(h));
224
+ });
225
+ return { installed, popular: notInstalled, recommended, updates };
226
+ }
227
+
228
+ function renderSkillRow(skill, { installed }) {
229
+ const name = skillName(skill);
230
+ const enabled = skill.enabled !== false;
231
+ const version = skill.version || (installed ? "local" : "registry");
232
+ const source = skill.plugin || skill.source || (installed ? "installed" : "marketplace");
233
+ const actions = installed
234
+ ? `<button class="small-action" data-skill-action="${enabled ? "disable" : "enable"}" data-skill="${escapeHtml(name)}"><i class="ti ti-${enabled ? "toggle-left" : "toggle-right"}"></i>${enabled ? "Disable" : "Enable"}</button>`
235
+ : `<button class="small-action" data-skill-action="install" data-skill="${escapeHtml(name)}"><i class="ti ti-download"></i>Install</button>`;
236
+ return `
205
237
  <div class="list-item">
206
238
  <div class="list-title">
207
- <span>${escapeHtml(skill.name)}</span>
208
- <span class="status-pill ${skill.enabled === false ? "status-failed" : "status-complete"}">${skill.enabled === false ? "disabled" : "enabled"}</span>
239
+ <span>${escapeHtml(name)}</span>
240
+ <span class="status-pill ${installed ? (enabled ? "status-complete" : "status-failed") : ""}">${installed ? (enabled ? "enabled" : "disabled") : "available"}</span>
209
241
  </div>
210
- <div class="meta-line">${escapeHtml(skill.description || "")}</div>
242
+ <div class="meta-line">${escapeHtml(skill.description || "No description")}</div>
211
243
  <div class="tag-row">
212
- <span class="tag">${escapeHtml(skill.version || "local")}</span>
213
- <span class="tag">${skill.marketplace ? "marketplace" : "installed"}</span>
214
- </div>
215
- <div class="item-actions">
216
- <button class="small-action" data-skill-action="enable" data-skill="${escapeHtml(skill.name)}"><i class="ti ti-toggle-right"></i>Enable</button>
217
- <button class="small-action" data-skill-action="disable" data-skill="${escapeHtml(skill.name)}"><i class="ti ti-toggle-left"></i>Disable</button>
244
+ <span class="tag">v${escapeHtml(version)}</span>
245
+ ${skill.category ? `<span class="tag">${escapeHtml(skill.category)}</span>` : ""}
246
+ <span class="tag">${escapeHtml(source)}</span>
218
247
  </div>
219
- </div>
220
- `).join("") : `<div class="list-item"><div class="meta-line">No skills found.</div></div>`;
248
+ <div class="item-actions">${actions}</div>
249
+ </div>`;
250
+ }
251
+
252
+ function renderSkills(payload) {
253
+ if (payload) state.skillsPayload = payload;
254
+ const data = state.skillsPayload || { installed: [], available: [] };
255
+ const tabs = computeSkillTabs(data);
256
+ const updatesCount = $("skill-updates-count");
257
+ if (updatesCount) updatesCount.textContent = tabs.updates.length ? String(tabs.updates.length) : "";
258
+ document.querySelectorAll("[data-skill-tab]").forEach((btn) => {
259
+ btn.classList.toggle("active", btn.dataset.skillTab === state.skillTab);
260
+ });
261
+ const tab = state.skillTab;
262
+ const rows = (tab === "installed" || tab === "updates")
263
+ ? (tabs[tab] || []).map((s) => renderSkillRow(s, { installed: true }))
264
+ : (tabs[tab] || []).slice(0, 24).map((s) => renderSkillRow(s, { installed: false }));
265
+ const empty = {
266
+ recommended: "No recommended skills right now.",
267
+ popular: "Marketplace is empty.",
268
+ installed: "No skills installed yet.",
269
+ updates: "All installed skills are up to date.",
270
+ }[tab];
271
+ $("skill-list").innerHTML = rows.length ? rows.join("") : `<div class="list-item"><div class="meta-line">${escapeHtml(empty)}</div></div>`;
221
272
  }
222
273
 
223
274
  function renderTimeline(payload) {
@@ -327,6 +378,173 @@ async function addMember(workspaceId) {
327
378
  await refreshAll();
328
379
  }
329
380
 
381
+ // ── Workspace summary (Phase 3) ──────────────────────────────────────────────
382
+ function renderWorkspaceSummary(os) {
383
+ const reg = os?.workspace_registry || {};
384
+ const workspaces = reg.workspaces || [];
385
+ const activeId = state.activeWorkspace || reg.active_workspace;
386
+ const active = workspaces.find((w) => w.workspace_id === activeId) || workspaces[0] || { name: "Personal Workspace", type: "personal", your_role: "owner", member_count: 1 };
387
+ const counts = os?.counts || {};
388
+ const scopePill = $("summary-scope-pill");
389
+ if (scopePill) scopePill.textContent = active.type || "personal";
390
+ const summary = $("workspace-summary");
391
+ if (summary) {
392
+ const stats = [["Snapshots", counts.snapshots], ["Memories", counts.memories], ["Agent runs", counts.agent_runs], ["Workflows", counts.workflows], ["Traces", counts.traces], ["Timeline", counts.timeline]];
393
+ summary.innerHTML = `
394
+ <div class="summary-main">
395
+ <div class="summary-icon"><i class="ti ${active.type === "organization" ? "ti-building-community" : "ti-user"}"></i></div>
396
+ <div class="summary-id">
397
+ <div class="summary-name">${escapeHtml(active.name || "Personal Workspace")}</div>
398
+ <div class="meta-line">${escapeHtml(active.type || "personal")} workspace · your role <strong>${escapeHtml(active.your_role || "owner")}</strong> · ${escapeHtml(active.member_count ?? 1)} member(s)</div>
399
+ </div>
400
+ </div>
401
+ <div class="summary-stats">
402
+ ${stats.map(([l, v]) => `<div class="summary-stat"><strong>${escapeHtml(v || 0)}</strong><span>${escapeHtml(l)}</span></div>`).join("")}
403
+ </div>`;
404
+ }
405
+ const quick = $("workspace-quickswitch");
406
+ if (quick) {
407
+ quick.innerHTML = workspaces.map((w) => `
408
+ <button class="switch-chip ${w.workspace_id === activeId ? "active" : ""}" data-ws-action="activate" data-ws="${escapeHtml(w.workspace_id)}">
409
+ <i class="ti ${w.type === "organization" ? "ti-building-community" : "ti-user"}"></i>
410
+ <span>${escapeHtml(w.name)}</span>${w.workspace_id === activeId ? ' <i class="ti ti-check"></i>' : ""}
411
+ </button>`).join("");
412
+ }
413
+ }
414
+
415
+ // ── Knowledge Graph explorer (Phase 2) ───────────────────────────────────────
416
+ const ENTITY_ICONS = { Person: "ti-user", Concept: "ti-bulb", Document: "ti-file-text", File: "ti-file", Code: "ti-code", Chat: "ti-message", Conversation: "ti-messages", Message: "ti-message-dots", Task: "ti-checklist", Decision: "ti-gavel", Error: "ti-alert-triangle", Model: "ti-cpu", Tool: "ti-tool", Project: "ti-folders", Feature: "ti-star", AIResponse: "ti-robot", Chunk: "ti-file-stack" };
417
+ function entityIcon(type) { return ENTITY_ICONS[type] || "ti-point"; }
418
+ function prettyId(id) { return String(id || "").split(":").slice(1).join(":") || String(id || ""); }
419
+
420
+ async function loadGraphExplorer() {
421
+ try {
422
+ const data = await api("/knowledge-graph/graph?limit=150");
423
+ const nodes = (data.nodes || []).slice();
424
+ nodes.sort((a, b) => (b.importance ?? b.metadata?.graph_metrics?.importance_raw ?? 0) - (a.importance ?? a.metadata?.graph_metrics?.importance_raw ?? 0));
425
+ state.entities = nodes;
426
+ renderEntities();
427
+ } catch (e) {
428
+ const el = $("entity-list");
429
+ if (el) el.innerHTML = `<div class="list-item"><div class="meta-line">Knowledge graph unavailable: ${escapeHtml(e.message)}</div></div>`;
430
+ }
431
+ }
432
+
433
+ function renderEntities() {
434
+ const el = $("entity-list");
435
+ if (!el) return;
436
+ const q = ($("entity-search")?.value || "").toLowerCase().trim();
437
+ const filtered = q ? state.entities.filter((n) => `${n.title || ""} ${n.type || ""} ${n.id || ""}`.toLowerCase().includes(q)) : state.entities;
438
+ const list = filtered.slice(0, 40);
439
+ el.innerHTML = list.length ? list.map((n) => {
440
+ const m = n.metadata?.graph_metrics || {};
441
+ const imp = Math.round((n.importance_norm ?? m.importance_norm ?? 0) * 100);
442
+ return `
443
+ <button class="list-item entity-card ${n.id === state.activeEntity ? "selected" : ""}" data-entity="${escapeHtml(n.id)}">
444
+ <div class="list-title"><span><i class="ti ${entityIcon(n.type)}"></i> ${escapeHtml(n.title || prettyId(n.id))}</span><span class="status-pill">${escapeHtml(n.type || "node")}</span></div>
445
+ ${n.summary ? `<div class="meta-line">${escapeHtml(String(n.summary).slice(0, 110))}</div>` : ""}
446
+ <div class="tag-row"><span class="tag">${escapeHtml(m.degree ?? 0)} links</span><span class="tag">importance ${imp}%</span></div>
447
+ <div class="importance-bar"><span style="width:${imp}%"></span></div>
448
+ </button>`;
449
+ }).join("") : `<div class="list-item"><div class="meta-line">No matching entities.</div></div>`;
450
+ }
451
+
452
+ async function selectEntity(id) {
453
+ state.activeEntity = id;
454
+ renderEntities();
455
+ const detail = $("entity-detail");
456
+ const title = $("entity-detail-title");
457
+ if (title) title.textContent = "Loading…";
458
+ try {
459
+ const d = await api(`/workspace/relationships/${encodeURIComponent(id)}`);
460
+ const node = d.node || {};
461
+ const related = d.related_entities || [];
462
+ const relMap = new Map(related.map((r) => [r.id, r]));
463
+ const labelFor = (nodeId) => { const r = relMap.get(nodeId); return r ? (r.title || prettyId(nodeId)) : prettyId(nodeId); };
464
+ const edgeRow = (e, dir) => {
465
+ const other = dir === "out" ? e.to : e.from;
466
+ return `<div class="rel-row"><span class="rel-dir">${dir === "out" ? "→" : "←"}</span><span class="tag">${escapeHtml(e.type || "related")}</span><span class="rel-node">${escapeHtml(labelFor(other))}</span></div>`;
467
+ };
468
+ const inbound = (d.inbound || []).slice(0, 8);
469
+ const outbound = (d.outbound || []).slice(0, 8);
470
+ const path = Array.isArray(d.shortest_path) ? d.shortest_path : [];
471
+ if (title) title.textContent = node.title || prettyId(id);
472
+ detail.innerHTML = `
473
+ <div class="list-item">
474
+ <div class="list-title"><span><i class="ti ${entityIcon(node.type)}"></i> ${escapeHtml(node.title || prettyId(id))}</span><span class="status-pill">${escapeHtml(node.type || "node")}</span></div>
475
+ ${node.summary ? `<div class="meta-line">${escapeHtml(node.summary)}</div>` : ""}
476
+ <div class="tag-row"><span class="tag">importance ${Math.round((node.importance_norm || 0) * 100)}%</span><span class="tag">${inbound.length + outbound.length} relationships</span></div>
477
+ </div>
478
+ <div class="list-item"><div class="list-title"><span>Outbound</span><span class="status-pill">${outbound.length}</span></div>${outbound.map((e) => edgeRow(e, "out")).join("") || '<div class="meta-line">None</div>'}</div>
479
+ <div class="list-item"><div class="list-title"><span>Inbound</span><span class="status-pill">${inbound.length}</span></div>${inbound.map((e) => edgeRow(e, "in")).join("") || '<div class="meta-line">None</div>'}</div>
480
+ ${related.length ? `<div class="list-item"><div class="list-title"><span>Related entities</span><span class="status-pill">${related.length}</span></div><div class="tag-row">${related.slice(0, 10).map((r) => `<span class="tag"><i class="ti ${entityIcon(r.type)}"></i> ${escapeHtml(r.title || prettyId(r.id))}</span>`).join("")}</div></div>` : ""}
481
+ ${path.length ? `<div class="list-item"><div class="list-title"><span>Path to you</span><span class="status-pill">${path.length} hops</span></div><div class="meta-line">${path.map((p) => escapeHtml(typeof p === "string" ? prettyId(p) : (p.title || prettyId(p.id)))).join(" → ")}</div></div>` : ""}
482
+ <div class="item-actions"><a class="small-action" href="/graph?node=${encodeURIComponent(id)}"><i class="ti ti-network"></i>Open in Graph Canvas</a></div>`;
483
+ } catch (e) {
484
+ if (title) title.textContent = "Relationships";
485
+ detail.innerHTML = `<div class="list-item"><div class="meta-line">No relationships available: ${escapeHtml(e.message)}</div></div>`;
486
+ }
487
+ }
488
+
489
+ // ── Recent activity feed (Phase 2), built from already-fetched data ───────────
490
+ function renderActivity({ traces, snapshots, memories, workflows, timeline }) {
491
+ const items = [];
492
+ (traces.traces || []).forEach((t) => items.push({ ts: t.created_at, icon: "ti-search", label: `Answer trace: ${t.question || "query"}`, tag: "graph rag" }));
493
+ (snapshots.snapshots || []).forEach((s) => items.push({ ts: s.created_at, icon: "ti-stack-2", label: `Snapshot: ${s.name}`, tag: "snapshot" }));
494
+ (memories.memories || []).forEach((m) => items.push({ ts: m.updated_at, icon: "ti-book-2", label: `Memory: ${(m.content || m.kind || "").slice(0, 60)}`, tag: m.kind || "memory" }));
495
+ (workflows.workflows || []).forEach((w) => items.push({ ts: w.created_at, icon: "ti-git-branch", label: `Workflow: ${w.name}`, tag: "workflow" }));
496
+ (timeline.events || []).forEach((e) => items.push({ ts: e.timestamp, icon: "ti-timeline-event", label: e.event_type || "event", tag: e.area || "workspace" }));
497
+ items.sort((a, b) => String(b.ts || "").localeCompare(String(a.ts || "")));
498
+ const el = $("activity-list");
499
+ if (!el) return;
500
+ el.innerHTML = items.length ? items.slice(0, 18).map((it) => `
501
+ <div class="list-item activity-item">
502
+ <div class="list-title"><span><i class="ti ${it.icon}"></i> ${escapeHtml(it.label)}</span><span class="status-pill">${escapeHtml(it.tag)}</span></div>
503
+ <div class="meta-line">${escapeHtml(it.ts || "")}</div>
504
+ </div>`).join("") : `<div class="list-item"><div class="meta-line">No recent activity yet — index a folder or ask a question to get started.</div></div>`;
505
+ }
506
+
507
+ function renderMemoryFeed(payload) {
508
+ const memories = payload.memories || [];
509
+ const el = $("memory-feed");
510
+ if (!el) return;
511
+ el.innerHTML = memories.length ? memories.slice(0, 8).map((m) => `
512
+ <div class="list-item">
513
+ <div class="list-title"><span><i class="ti ti-book-2"></i> ${escapeHtml(m.kind || "memory")}</span><span class="status-pill">${escapeHtml(m.updated_at || "")}</span></div>
514
+ <div class="meta-line">${escapeHtml(String(m.content || "").slice(0, 140))}</div>
515
+ </div>`).join("") : `<div class="list-item"><div class="meta-line">No workspace memory yet.</div></div>`;
516
+ }
517
+
518
+ // ── Enterprise capability panel (Phase 6) ─────────────────────────────────────
519
+ const CAPABILITY_LABELS = {
520
+ sso_advanced: "Advanced SSO", idp_provisioning: "IdP Provisioning", scim: "SCIM",
521
+ rbac_abac_advanced: "Advanced RBAC/ABAC", tenant_isolation: "Tenant Isolation",
522
+ compliance_retention: "Compliance Retention", siem_export: "SIEM Export",
523
+ private_vpc: "Private VPC", air_gapped_deployment: "Air-gapped Deploy",
524
+ dlp_policy: "DLP Policy", ediscovery: "eDiscovery", admin_policy_packs: "Admin Policy Packs",
525
+ };
526
+ function renderEnterprise(edition) {
527
+ edition = edition || {};
528
+ const caps = edition.capabilities || {};
529
+ const editionName = edition.edition || "community";
530
+ const pill = $("enterprise-edition-pill");
531
+ if (pill) { pill.textContent = editionName; pill.className = `status-pill ${edition.is_enterprise ? "status-complete" : ""}`; }
532
+ const note = $("enterprise-note");
533
+ if (note) note.textContent = edition.community_notice || "Community edition: every Enterprise capability below is an extension point and is disabled. Nothing here gates a Community feature.";
534
+ const grid = $("capability-grid");
535
+ if (!grid) return;
536
+ const keys = Object.keys(caps).length ? Object.keys(caps) : Object.keys(CAPABILITY_LABELS);
537
+ grid.innerHTML = keys.map((k) => {
538
+ const on = Boolean(caps[k]);
539
+ return `
540
+ <div class="capability-card ${on ? "on" : "off"}">
541
+ <i class="ti ${on ? "ti-circle-check" : "ti-lock"}"></i>
542
+ <span class="cap-name">${escapeHtml(CAPABILITY_LABELS[k] || k)}</span>
543
+ <span class="status-pill ${on ? "status-complete" : "status-failed"}">${on ? "enabled" : "disabled"}</span>
544
+ </div>`;
545
+ }).join("");
546
+ }
547
+
330
548
  async function refreshAll() {
331
549
  const [os, onboarding, traces, indexing, snapshots, memories, computerMemory, agents, workflows, skills, timeline] = await Promise.all([
332
550
  api("/workspace/os"),
@@ -354,6 +572,11 @@ async function refreshAll() {
354
572
  renderWorkflows(workflows);
355
573
  renderSkills(skills);
356
574
  renderTimeline(timeline);
575
+ renderWorkspaceSummary(os);
576
+ renderEnterprise(os.edition);
577
+ renderActivity({ traces, snapshots, memories, workflows, timeline });
578
+ renderMemoryFeed(memories);
579
+ loadGraphExplorer();
357
580
  }
358
581
 
359
582
  async function createSnapshot() {
@@ -423,6 +646,19 @@ async function configureComputerMemory(enabled) {
423
646
  }
424
647
 
425
648
  document.addEventListener("click", async (event) => {
649
+ const entityBtn = event.target.closest("[data-entity]");
650
+ if (entityBtn) {
651
+ selectEntity(entityBtn.dataset.entity).catch((err) => toast(err.message));
652
+ return;
653
+ }
654
+
655
+ const skillTab = event.target.closest("[data-skill-tab]");
656
+ if (skillTab) {
657
+ state.skillTab = skillTab.dataset.skillTab;
658
+ renderSkills();
659
+ return;
660
+ }
661
+
426
662
  const step = event.target.closest("[data-step]");
427
663
  if (step) {
428
664
  await api("/workspace/onboarding/step", {
@@ -521,6 +757,12 @@ document.addEventListener("DOMContentLoaded", () => {
521
757
  }));
522
758
  $("create-demo-workflow").addEventListener("click", () => createDemoWorkflow().catch((err) => toast(err.message)));
523
759
  $("reload-skills").addEventListener("click", () => refreshAll().catch((err) => toast(err.message)));
760
+ const entitySearch = $("entity-search");
761
+ if (entitySearch) entitySearch.addEventListener("input", () => renderEntities());
762
+ const reloadEntities = $("reload-entities");
763
+ if (reloadEntities) reloadEntities.addEventListener("click", () => loadGraphExplorer().catch((err) => toast(err.message)));
764
+ const reloadActivity = $("reload-activity");
765
+ if (reloadActivity) reloadActivity.addEventListener("click", () => refreshAll().catch((err) => toast(err.message)));
524
766
  $("workspace-select").addEventListener("change", (event) => activateWorkspace(event.target.value).catch((err) => toast(err.message)));
525
767
  $("create-org").addEventListener("click", () => createOrg().catch((err) => toast(err.message)));
526
768
  $("new-org-btn").addEventListener("click", () => $("org-name").focus());
@@ -544,3 +544,70 @@ textarea {
544
544
  #org-create-form {
545
545
  margin-bottom: 12px;
546
546
  }
547
+
548
+ /* ── Product Experience Deepening (v1.6.0) ──────────────────────────────── */
549
+
550
+ /* Workspace summary */
551
+ .summary-card {
552
+ display: flex;
553
+ flex-wrap: wrap;
554
+ justify-content: space-between;
555
+ gap: 16px;
556
+ align-items: center;
557
+ }
558
+ .summary-main { display: flex; align-items: center; gap: 14px; }
559
+ .summary-icon {
560
+ width: 48px; height: 48px; border-radius: 12px;
561
+ display: grid; place-items: center;
562
+ background: #eef2ff; color: var(--blue); font-size: 24px;
563
+ }
564
+ .summary-name { font-weight: 800; font-size: 18px; color: var(--ink); }
565
+ .summary-stats { display: flex; flex-wrap: wrap; gap: 18px; }
566
+ .summary-stat { display: grid; text-align: center; }
567
+ .summary-stat strong { font-size: 20px; color: var(--ink); }
568
+ .summary-stat span { font-size: 11px; color: var(--muted); font-weight: 700; }
569
+ .quickswitch-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
570
+ .switch-chip {
571
+ display: inline-flex; align-items: center; gap: 6px;
572
+ border: 1px solid var(--line); background: #fbfcfe; color: var(--ink);
573
+ border-radius: 999px; padding: 6px 12px; font-weight: 700; font-size: 13px; cursor: pointer;
574
+ }
575
+ .switch-chip.active { border-color: var(--blue); background: #eef2ff; color: var(--blue); }
576
+
577
+ /* Entity explorer */
578
+ .entity-card { text-align: left; cursor: pointer; width: 100%; font: inherit; }
579
+ .entity-card.selected { border-color: var(--blue); box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.15); }
580
+ .importance-bar { height: 4px; border-radius: 999px; background: #edf2f7; overflow: hidden; }
581
+ .importance-bar span { display: block; height: 100%; background: linear-gradient(90deg, #2563eb, #7c3aed); }
582
+ .rel-row { display: flex; align-items: center; gap: 8px; font-size: 13px; padding: 2px 0; }
583
+ .rel-dir { color: var(--muted); font-weight: 800; width: 16px; }
584
+ .rel-node { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
585
+ .activity-item .list-title span { font-weight: 700; }
586
+
587
+ /* Skill marketplace tabs */
588
+ .tab-bar { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
589
+ .tab {
590
+ border: 1px solid var(--line); background: #fbfcfe; color: var(--muted);
591
+ border-radius: 999px; padding: 6px 14px; font-weight: 700; font-size: 13px; cursor: pointer;
592
+ }
593
+ .tab.active { border-color: var(--blue); background: #eef2ff; color: var(--blue); }
594
+ .tab-count {
595
+ display: inline-block; min-width: 16px; padding: 0 5px; margin-left: 4px;
596
+ border-radius: 999px; background: var(--red); color: #fff; font-size: 11px;
597
+ }
598
+ .tab-count:empty { display: none; }
599
+
600
+ /* Enterprise capability grid */
601
+ .capability-grid {
602
+ display: grid; gap: 10px;
603
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
604
+ margin-top: 12px;
605
+ }
606
+ .capability-card {
607
+ display: flex; align-items: center; gap: 10px;
608
+ border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; background: #fbfcfe;
609
+ }
610
+ .capability-card i { font-size: 18px; }
611
+ .capability-card.off i { color: var(--muted); }
612
+ .capability-card.on i { color: var(--green); }
613
+ .capability-card .cap-name { flex: 1; font-weight: 700; font-size: 13px; color: var(--ink); }
@@ -8,7 +8,7 @@
8
8
  <link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32.png">
9
9
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap">
10
10
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/tabler-icons.min.css">
11
- <link rel="stylesheet" href="/static/workspace.css?v=1.1.0">
11
+ <link rel="stylesheet" href="/static/workspace.css?v=1.6.0">
12
12
  </head>
13
13
  <body>
14
14
  <div class="workspace-shell">
@@ -20,12 +20,14 @@
20
20
  <nav>
21
21
  <a class="active" href="#overview"><i class="ti ti-layout-dashboard"></i><span>Overview</span></a>
22
22
  <a href="#graph"><i class="ti ti-chart-dots-3"></i><span>Graph</span></a>
23
+ <a href="#graph-explorer"><i class="ti ti-affiliate"></i><span>Explorer</span></a>
23
24
  <a href="#snapshots"><i class="ti ti-stack-2"></i><span>Snapshots</span></a>
24
25
  <a href="#memory"><i class="ti ti-book-2"></i><span>Memory</span></a>
25
26
  <a href="#agents"><i class="ti ti-route-alt-left"></i><span>Agents</span></a>
26
27
  <a href="#workflows"><i class="ti ti-git-branch"></i><span>Workflow</span></a>
27
28
  <a href="#skills"><i class="ti ti-puzzle"></i><span>Skills</span></a>
28
29
  <a href="#timeline"><i class="ti ti-timeline-event"></i><span>Timeline</span></a>
30
+ <a href="#enterprise"><i class="ti ti-building-skyscraper"></i><span>Editions</span></a>
29
31
  </nav>
30
32
  <div class="rail-links">
31
33
  <a href="/chat"><i class="ti ti-message-circle"></i><span>Chat</span></a>
@@ -54,6 +56,18 @@
54
56
 
55
57
  <section class="metric-grid" id="metric-grid"></section>
56
58
 
59
+ <section class="workspace-band" id="workspace-summary-band">
60
+ <div class="section-head">
61
+ <div>
62
+ <div class="eyebrow">You are here</div>
63
+ <h2>Current Workspace</h2>
64
+ </div>
65
+ <span class="status-pill" id="summary-scope-pill">personal</span>
66
+ </div>
67
+ <div id="workspace-summary" class="summary-card"></div>
68
+ <div id="workspace-quickswitch" class="quickswitch-row"></div>
69
+ </section>
70
+
57
71
  <section class="workspace-band" id="organization">
58
72
  <div class="section-head">
59
73
  <div>
@@ -120,6 +134,55 @@
120
134
  </div>
121
135
  </section>
122
136
 
137
+ <section class="workspace-grid two-col" id="graph-explorer">
138
+ <div class="workspace-panel">
139
+ <div class="section-head">
140
+ <div>
141
+ <div class="eyebrow">Knowledge Graph</div>
142
+ <h2>Entity Explorer</h2>
143
+ </div>
144
+ <button class="icon-action" id="reload-entities" title="Reload entities"><i class="ti ti-refresh"></i></button>
145
+ </div>
146
+ <div class="inline-form">
147
+ <input id="entity-search" placeholder="Search entities (concept, person, file…)">
148
+ </div>
149
+ <div id="entity-list" class="list-stack"></div>
150
+ </div>
151
+ <div class="workspace-panel">
152
+ <div class="section-head">
153
+ <div>
154
+ <div class="eyebrow">Relationships</div>
155
+ <h2 id="entity-detail-title">Select an entity</h2>
156
+ </div>
157
+ </div>
158
+ <div id="entity-detail" class="list-stack">
159
+ <div class="list-item"><div class="meta-line">Pick an entity on the left to see its relationships, related entities, and a path back to you.</div></div>
160
+ </div>
161
+ </div>
162
+ </section>
163
+
164
+ <section class="workspace-grid two-col" id="activity">
165
+ <div class="workspace-panel">
166
+ <div class="section-head">
167
+ <div>
168
+ <div class="eyebrow">Workspace</div>
169
+ <h2>Recent Activity</h2>
170
+ </div>
171
+ <button class="icon-action" id="reload-activity" title="Reload activity"><i class="ti ti-refresh"></i></button>
172
+ </div>
173
+ <div id="activity-list" class="list-stack"></div>
174
+ </div>
175
+ <div class="workspace-panel">
176
+ <div class="section-head">
177
+ <div>
178
+ <div class="eyebrow">Memory</div>
179
+ <h2>Workspace Memory Feed</h2>
180
+ </div>
181
+ </div>
182
+ <div id="memory-feed" class="list-stack"></div>
183
+ </div>
184
+ </section>
185
+
123
186
  <section class="workspace-grid two-col" id="snapshots">
124
187
  <div class="workspace-panel">
125
188
  <div class="section-head">
@@ -217,6 +280,12 @@
217
280
  </div>
218
281
  <button class="icon-action" id="reload-skills" title="Reload skills"><i class="ti ti-refresh"></i></button>
219
282
  </div>
283
+ <div class="tab-bar" id="skill-tabs">
284
+ <button class="tab active" data-skill-tab="recommended">Recommended</button>
285
+ <button class="tab" data-skill-tab="popular">Popular</button>
286
+ <button class="tab" data-skill-tab="installed">Installed</button>
287
+ <button class="tab" data-skill-tab="updates">Updates <span class="tab-count" id="skill-updates-count"></span></button>
288
+ </div>
220
289
  <div id="skill-list" class="list-stack"></div>
221
290
  </div>
222
291
  <div class="workspace-panel" id="timeline">
@@ -229,10 +298,22 @@
229
298
  <div id="timeline-list" class="timeline-list"></div>
230
299
  </div>
231
300
  </section>
301
+
302
+ <section class="workspace-band" id="enterprise">
303
+ <div class="section-head">
304
+ <div>
305
+ <div class="eyebrow">Editions</div>
306
+ <h2>Enterprise Capabilities</h2>
307
+ </div>
308
+ <span class="status-pill" id="enterprise-edition-pill">community</span>
309
+ </div>
310
+ <p class="meta-line" id="enterprise-note"></p>
311
+ <div id="capability-grid" class="capability-grid"></div>
312
+ </section>
232
313
  </main>
233
314
  </div>
234
315
 
235
316
  <div class="toast" id="toast"></div>
236
- <script src="/static/scripts/workspace.js?v=1.1.0"></script>
317
+ <script src="/static/scripts/workspace.js?v=1.6.0"></script>
237
318
  </body>
238
319
  </html>