ltcai 1.6.0 → 2.0.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.
Files changed (40) hide show
  1. package/README.md +40 -19
  2. package/docs/CHANGELOG.md +107 -0
  3. package/docs/EDITION_STRATEGY.md +14 -4
  4. package/docs/ENTERPRISE.md +11 -3
  5. package/docs/MULTI_AGENT_RUNTIME.md +410 -0
  6. package/docs/PLUGIN_SDK.md +651 -0
  7. package/docs/REALTIME_COLLABORATION.md +410 -0
  8. package/docs/V2_ARCHITECTURE.md +528 -0
  9. package/docs/WORKFLOW_DESIGNER.md +475 -0
  10. package/latticeai/__init__.py +1 -1
  11. package/latticeai/api/agents.py +98 -0
  12. package/latticeai/api/plugins.py +115 -0
  13. package/latticeai/api/realtime.py +91 -0
  14. package/latticeai/api/workflow_designer.py +207 -0
  15. package/latticeai/core/multi_agent.py +270 -0
  16. package/latticeai/core/plugins.py +400 -0
  17. package/latticeai/core/realtime.py +190 -0
  18. package/latticeai/core/workflow_engine.py +329 -0
  19. package/latticeai/core/workspace_os.py +165 -2
  20. package/latticeai/server_app.py +76 -2
  21. package/latticeai/services/platform_runtime.py +200 -0
  22. package/package.json +17 -2
  23. package/plugins/README.md +35 -0
  24. package/plugins/git-insights/plugin.json +15 -0
  25. package/plugins/hello-world/plugin.json +16 -0
  26. package/plugins/hello-world/skills/hello_skill/SKILL.md +15 -0
  27. package/static/activity.html +70 -0
  28. package/static/admin.html +62 -0
  29. package/static/agents.html +92 -0
  30. package/static/graph.html +7 -1
  31. package/static/lattice-reference.css +184 -0
  32. package/static/platform.css +75 -0
  33. package/static/plugins.html +82 -0
  34. package/static/scripts/admin.js +121 -1
  35. package/static/scripts/graph.js +296 -14
  36. package/static/scripts/platform.js +64 -0
  37. package/static/scripts/workspace.js +107 -10
  38. package/static/workflows.html +121 -0
  39. package/static/workspace.css +73 -0
  40. package/static/workspace.html +18 -2
@@ -0,0 +1,64 @@
1
+ // Lattice AI v2.0 — shared helpers for the Agentic Workspace Platform pages.
2
+ export const NAV = [
3
+ { href: "/workspace", label: "Dashboard" },
4
+ { href: "/plugins/sdk", label: "Plugins" },
5
+ { href: "/workflows", label: "Workflows" },
6
+ { href: "/agents", label: "Agents" },
7
+ { href: "/activity", label: "Activity" },
8
+ { href: "/chat", label: "Chat" },
9
+ ];
10
+
11
+ export function mountHeader(active) {
12
+ const links = NAV.map(
13
+ (n) => `<a href="${n.href}" class="${n.href === active ? "active" : ""}">${n.label}</a>`
14
+ ).join("");
15
+ document.body.insertAdjacentHTML(
16
+ "afterbegin",
17
+ `<header class="app"><div class="brand">Lattice AI<small>v2.0 Platform</small></div><nav>${links}</nav></header>`
18
+ );
19
+ }
20
+
21
+ export async function api(path, opts = {}) {
22
+ const res = await fetch(path, {
23
+ headers: { "Content-Type": "application/json" },
24
+ credentials: "same-origin",
25
+ ...opts,
26
+ });
27
+ if (res.status === 401) {
28
+ location.href = "/account";
29
+ throw new Error("unauthorized");
30
+ }
31
+ const text = await res.text();
32
+ let body;
33
+ try { body = text ? JSON.parse(text) : {}; } catch { body = { raw: text }; }
34
+ if (!res.ok) {
35
+ const detail = body && body.detail ? (typeof body.detail === "string" ? body.detail : JSON.stringify(body.detail)) : res.statusText;
36
+ throw new Error(detail);
37
+ }
38
+ return body;
39
+ }
40
+
41
+ export function el(html) {
42
+ const t = document.createElement("template");
43
+ t.innerHTML = html.trim();
44
+ return t.content.firstElementChild;
45
+ }
46
+
47
+ export function escapeHtml(s) {
48
+ return String(s == null ? "" : s).replace(/[&<>"']/g, (c) =>
49
+ ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c])
50
+ );
51
+ }
52
+
53
+ export function toast(msg) {
54
+ const node = el(`<div class="toast">${escapeHtml(msg)}</div>`);
55
+ document.body.appendChild(node);
56
+ setTimeout(() => node.remove(), 4000);
57
+ }
58
+
59
+ export function badge(status) {
60
+ const cls = { ok: "ok", ready: "ok", valid: "ok", retried_ok: "ok",
61
+ partial: "warn", retry: "warn", skipped: "warn", available: "warn",
62
+ failed: "err", error: "err", blocked: "err" }[status] || "";
63
+ return `<span class="badge ${cls}">${escapeHtml(status || "?")}</span>`;
64
+ }
@@ -8,6 +8,7 @@ const state = {
8
8
  managingWorkspace: null,
9
9
  skillsPayload: null,
10
10
  skillTab: "recommended",
11
+ skillProgress: {},
11
12
  entities: [],
12
13
  activeEntity: null,
13
14
  };
@@ -70,6 +71,56 @@ function renderMetrics(os) {
70
71
  `).join("");
71
72
  }
72
73
 
74
+ function latestTimestamp(...groups) {
75
+ const values = groups.flat().filter(Boolean).map((value) => {
76
+ const stamp = new Date(value);
77
+ return Number.isNaN(stamp.getTime()) ? null : stamp;
78
+ }).filter(Boolean);
79
+ if (!values.length) return "";
80
+ return new Date(Math.max(...values.map((stamp) => stamp.getTime()))).toISOString().slice(0, 19).replace("T", " ");
81
+ }
82
+
83
+ function renderWorkspaceHealth({ os, indexing, skills, timeline }) {
84
+ const counts = os?.counts || {};
85
+ const graph = os?.graph || {};
86
+ const nodes = Object.values(graph.nodes || {}).reduce((sum, value) => sum + Number(value || 0), 0);
87
+ const edges = Object.values(graph.edges || {}).reduce((sum, value) => sum + Number(value || 0), 0);
88
+ const sources = indexing?.sources || [];
89
+ const indexedFiles = sources.reduce((sum, source) => {
90
+ const fileStatus = source.file_status || {};
91
+ return sum + Number(fileStatus.indexed ?? source.success_count ?? 0);
92
+ }, 0);
93
+ const sourceTimes = sources.flatMap((source) => [source.last_run_at, source.last_scanned_at, source.updated_at]);
94
+ const eventTimes = (timeline?.events || []).slice(0, 10).map((event) => event.timestamp);
95
+ const currentModel = os?.models?.current_model || os?.models?.local_model || os?.models?.public_model || "not loaded";
96
+ const status = nodes || indexedFiles || counts.memories || counts.agent_runs ? "ready" : "empty";
97
+ const statusEl = $("workspace-health-status");
98
+ if (statusEl) {
99
+ statusEl.textContent = status;
100
+ statusEl.className = `status-pill ${status === "ready" ? "status-complete" : "status-running"}`;
101
+ }
102
+ const items = [
103
+ ["Indexed Files", indexedFiles, "ti-files", sources.length ? `${sources.length} source(s)` : "No indexed sources"],
104
+ ["Graph Nodes", nodes, "ti-chart-dots-3", `${edges.toLocaleString()} relationship(s)`],
105
+ ["Graph Relationships", edges, "ti-git-branch", "Knowledge links"],
106
+ ["Installed Skills", skills?.total_installed ?? counts.skills ?? 0, "ti-puzzle", `${skills?.total_available ?? 0} available`],
107
+ ["Memory Entries", counts.memories || 0, "ti-book-2", "Workspace memory"],
108
+ ["Agent Runs", counts.agent_runs || 0, "ti-route-alt-left", `${counts.workflows || 0} workflow(s)`],
109
+ ["Current Model", currentModel, "ti-cpu", `${(os?.models?.loaded_models || []).length} loaded`],
110
+ ["Last Sync Time", latestTimestamp(os?.updated_at, sourceTimes, eventTimes) || "not synced", "ti-clock", `v${os?.version || "unknown"}`],
111
+ ];
112
+ const grid = $("workspace-health-grid");
113
+ if (!grid) return;
114
+ grid.innerHTML = items.map(([label, value, icon, meta]) => `
115
+ <div class="health-card">
116
+ <i class="ti ${icon}"></i>
117
+ <span>${escapeHtml(label)}</span>
118
+ <strong>${escapeHtml(value)}</strong>
119
+ <em>${escapeHtml(meta)}</em>
120
+ </div>
121
+ `).join("");
122
+ }
123
+
73
124
  function renderOnboarding(payload) {
74
125
  const steps = payload.steps || [];
75
126
  $("onboarding-steps").innerHTML = steps.map((step) => {
@@ -205,6 +256,10 @@ function skillName(skill) {
205
256
  return skill.skill || skill.name || "skill";
206
257
  }
207
258
 
259
+ function skillProgress(name) {
260
+ return state.skillProgress[name] || null;
261
+ }
262
+
208
263
  // Compute the four marketplace tabs from the registry payload (machine-global
209
264
  // registry + locally-installed state). "Updates" = installed skills whose
210
265
  // registry version differs from the installed version.
@@ -222,17 +277,28 @@ function computeSkillTabs(payload) {
222
277
  const hay = `${skillName(s)} ${s.category || ""} ${s.description || ""}`.toLowerCase();
223
278
  return RECOMMENDED_SKILL_HINTS.some((h) => hay.includes(h));
224
279
  });
225
- return { installed, popular: notInstalled, recommended, updates };
280
+ const popular = notInstalled.slice().sort((a, b) => Number(b.downloads || b.popularity || 0) - Number(a.downloads || a.popularity || 0));
281
+ return { installed, popular, recommended: recommended.length ? recommended : popular.slice(0, 8), updates };
226
282
  }
227
283
 
228
284
  function renderSkillRow(skill, { installed }) {
229
285
  const name = skillName(skill);
230
286
  const enabled = skill.enabled !== false;
231
287
  const version = skill.version || (installed ? "local" : "registry");
232
- const source = skill.plugin || skill.source || (installed ? "installed" : "marketplace");
288
+ const source = skill.plugin || skill.source || skill.source_url || (installed ? "installed" : "marketplace");
289
+ const validation = skill.validation_status || (installed ? "ready" : "not installed");
290
+ const installStatus = skill.install_status || (installed ? "ready" : "available");
291
+ const progress = skillProgress(name);
233
292
  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>`;
293
+ ? `<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>
294
+ <button class="small-action" data-skill-action="update" data-skill="${escapeHtml(name)}"><i class="ti ti-refresh"></i>Update</button>`
295
+ : `<button class="small-action" data-skill-action="install" data-skill="${escapeHtml(name)}" ${progress ? "disabled" : ""}><i class="ti ti-download"></i>Install</button>`;
296
+ const progressHtml = progress ? `
297
+ <div class="skill-progress" aria-label="Install progress">
298
+ <div class="skill-progress-head"><span>${escapeHtml(progress.phase)}</span><span>${escapeHtml(progress.percent)}%</span></div>
299
+ <div class="skill-progress-track"><span style="width:${Math.max(0, Math.min(100, progress.percent))}%"></span></div>
300
+ </div>
301
+ ` : "";
236
302
  return `
237
303
  <div class="list-item">
238
304
  <div class="list-title">
@@ -244,7 +310,10 @@ function renderSkillRow(skill, { installed }) {
244
310
  <span class="tag">v${escapeHtml(version)}</span>
245
311
  ${skill.category ? `<span class="tag">${escapeHtml(skill.category)}</span>` : ""}
246
312
  <span class="tag">${escapeHtml(source)}</span>
313
+ <span class="tag">install: ${escapeHtml(installStatus)}</span>
314
+ <span class="tag">validation: ${escapeHtml(validation)}</span>
247
315
  </div>
316
+ ${progressHtml}
248
317
  <div class="item-actions">${actions}</div>
249
318
  </div>`;
250
319
  }
@@ -561,6 +630,7 @@ async function refreshAll() {
561
630
  ]);
562
631
  state.os = os;
563
632
  renderMetrics(os);
633
+ renderWorkspaceHealth({ os, indexing, skills, timeline });
564
634
  if (os.workspace_registry) renderWorkspaceRegistry(os.workspace_registry, os.edition);
565
635
  renderOnboarding(onboarding);
566
636
  renderTraces(traces);
@@ -645,6 +715,38 @@ async function configureComputerMemory(enabled) {
645
715
  await refreshAll();
646
716
  }
647
717
 
718
+ function setSkillProgress(name, phase, percent) {
719
+ state.skillProgress[name] = { phase, percent };
720
+ renderSkills();
721
+ }
722
+
723
+ function clearSkillProgress(name) {
724
+ delete state.skillProgress[name];
725
+ renderSkills();
726
+ }
727
+
728
+ async function runSkillAction(action, skill) {
729
+ if (action === "install" || action === "update") {
730
+ setSkillProgress(skill, "Download", 24);
731
+ await new Promise((resolve) => setTimeout(resolve, 180));
732
+ setSkillProgress(skill, "Validate", 68);
733
+ }
734
+ try {
735
+ await api(`/workspace/skills/${action}`, {
736
+ method: "POST",
737
+ body: JSON.stringify({ skill }),
738
+ });
739
+ if (action === "install" || action === "update") {
740
+ setSkillProgress(skill, "Ready", 100);
741
+ await new Promise((resolve) => setTimeout(resolve, 260));
742
+ }
743
+ toast(`Skill ${action}`);
744
+ await refreshAll();
745
+ } finally {
746
+ clearSkillProgress(skill);
747
+ }
748
+ }
749
+
648
750
  document.addEventListener("click", async (event) => {
649
751
  const entityBtn = event.target.closest("[data-entity]");
650
752
  if (entityBtn) {
@@ -689,12 +791,7 @@ document.addEventListener("click", async (event) => {
689
791
 
690
792
  const skillBtn = event.target.closest("[data-skill-action]");
691
793
  if (skillBtn) {
692
- await api(`/workspace/skills/${skillBtn.dataset.skillAction}`, {
693
- method: "POST",
694
- body: JSON.stringify({ skill: skillBtn.dataset.skill }),
695
- });
696
- toast(`Skill ${skillBtn.dataset.skillAction}`);
697
- await refreshAll();
794
+ await runSkillAction(skillBtn.dataset.skillAction, skillBtn.dataset.skill);
698
795
  return;
699
796
  }
700
797
 
@@ -0,0 +1,121 @@
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.0" />
6
+ <title>Workflow Designer — Lattice AI</title>
7
+ <link rel="stylesheet" href="/static/platform.css" />
8
+ </head>
9
+ <body>
10
+ <main>
11
+ <h1>Workflow Designer</h1>
12
+ <p class="sub">Compose triggers, tools, skills, plugins, agents, conditions, and outputs into runnable workflows.</p>
13
+
14
+ <div class="row">
15
+ <button id="newBtn">+ New from template</button>
16
+ <button class="ghost" id="validateBtn">Validate</button>
17
+ <button class="ghost" id="saveBtn">Save</button>
18
+ <div class="spacer"></div>
19
+ </div>
20
+
21
+ <div class="section">
22
+ <label>Workflow name</label>
23
+ <input id="wfName" value="My workflow" />
24
+ <label>Nodes (JSON) — trigger → … → output</label>
25
+ <textarea id="wfNodes" spellcheck="false" style="min-height:240px"></textarea>
26
+ <pre id="validateOut" style="display:none"></pre>
27
+ </div>
28
+
29
+ <div class="section">
30
+ <h3>Saved workflows</h3>
31
+ <div id="list" class="grid"><div class="empty">Loading…</div></div>
32
+ </div>
33
+
34
+ <div class="section">
35
+ <h3>Run history</h3>
36
+ <div id="runs"><div class="empty">No runs yet.</div></div>
37
+ </div>
38
+ </main>
39
+
40
+ <script type="module">
41
+ import { mountHeader, api, escapeHtml, badge, toast } from "/static/scripts/platform.js";
42
+ mountHeader("/workflows");
43
+
44
+ const TEMPLATE = [
45
+ { id: "trigger", type: "trigger", name: "Manual start", config: { trigger: "manual" }, next: "review" },
46
+ { id: "review", type: "agent", name: "Multi-agent review", config: { goal: "Review the latest workspace changes", roles: ["planner", "executor", "reviewer"] }, next: "decide" },
47
+ { id: "decide", type: "condition", name: "Passed?", config: { left: "last_output", op: "truthy" }, branches: { true: "done", false: "done" } },
48
+ { id: "done", type: "output", name: "Output", config: {}, next: null },
49
+ ];
50
+
51
+ function loadTemplate() {
52
+ document.getElementById("wfName").value = "My workflow";
53
+ document.getElementById("wfNodes").value = JSON.stringify(TEMPLATE, null, 2);
54
+ }
55
+
56
+ function readNodes() { return JSON.parse(document.getElementById("wfNodes").value); }
57
+
58
+ document.getElementById("newBtn").addEventListener("click", loadTemplate);
59
+
60
+ document.getElementById("validateBtn").addEventListener("click", async () => {
61
+ const out = document.getElementById("validateOut");
62
+ out.style.display = "block";
63
+ try {
64
+ const res = await api("/workflows/api/validate", { method: "POST", body: JSON.stringify({ name: document.getElementById("wfName").value, nodes: readNodes() }) });
65
+ out.textContent = res.ok ? "✓ Valid workflow" : "Errors:\n" + res.errors.join("\n");
66
+ } catch (err) { out.textContent = "Error: " + err.message; }
67
+ });
68
+
69
+ document.getElementById("saveBtn").addEventListener("click", async () => {
70
+ try {
71
+ await api("/workflows/api/definitions", { method: "POST", body: JSON.stringify({ name: document.getElementById("wfName").value, nodes: readNodes() }) });
72
+ toast("Workflow saved"); await loadList();
73
+ } catch (err) { toast(err.message); }
74
+ });
75
+
76
+ async function loadList() {
77
+ const data = await api("/workflows/api/definitions");
78
+ const list = document.getElementById("list");
79
+ const items = data.workflows || [];
80
+ if (!items.length) { list.innerHTML = `<div class="empty">No workflows yet.</div>`; return; }
81
+ list.innerHTML = items.map((w) => `
82
+ <div class="card">
83
+ <h3>${escapeHtml(w.name)}</h3>
84
+ <div class="meta">${(w.nodes||w.steps||[]).length} node(s) · ${escapeHtml(w.updated_at||w.created_at||"")}</div>
85
+ <div class="row" style="margin-top:12px">
86
+ <button data-run="${w.id}">Run</button>
87
+ <a class="btn ghost" href="/workflows/api/export/${w.id}" target="_blank">Export</a>
88
+ </div>
89
+ </div>`).join("");
90
+ }
91
+
92
+ document.getElementById("list").addEventListener("click", async (e) => {
93
+ const btn = e.target.closest("button[data-run]");
94
+ if (!btn) return;
95
+ btn.disabled = true;
96
+ try {
97
+ const res = await api(`/workflows/api/definitions/${btn.dataset.run}/run`, { method: "POST", body: JSON.stringify({ inputs: {} }) });
98
+ toast(`Run ${res.result.status} · ${res.result.step_count} steps`);
99
+ await loadRuns();
100
+ } catch (err) { toast(err.message); } finally { btn.disabled = false; }
101
+ });
102
+
103
+ async function loadRuns() {
104
+ const data = await api("/workflows/api/runs");
105
+ const runs = data.runs || [];
106
+ const box = document.getElementById("runs");
107
+ if (!runs.length) { box.innerHTML = `<div class="empty">No runs yet.</div>`; return; }
108
+ box.innerHTML = runs.map((r) => `
109
+ <div class="card" style="margin-bottom:10px">
110
+ <div class="row"><h3>${escapeHtml(r.name)}</h3><div class="spacer"></div>${badge(r.status)}</div>
111
+ <div class="meta">${escapeHtml(r.created_at)} · ${ (r.timeline||[]).length } steps</div>
112
+ <pre>${escapeHtml(JSON.stringify(r.timeline, null, 2))}</pre>
113
+ </div>`).join("");
114
+ }
115
+
116
+ loadTemplate();
117
+ loadList().catch((e) => toast(e.message));
118
+ loadRuns().catch(() => {});
119
+ </script>
120
+ </body>
121
+ </html>
@@ -611,3 +611,76 @@ textarea {
611
611
  .capability-card.off i { color: var(--muted); }
612
612
  .capability-card.on i { color: var(--green); }
613
613
  .capability-card .cap-name { flex: 1; font-weight: 700; font-size: 13px; color: var(--ink); }
614
+
615
+ /* Workspace health dashboard + skill install lifecycle (v1.7.0) */
616
+ .health-grid {
617
+ display: grid;
618
+ grid-template-columns: repeat(4, minmax(0, 1fr));
619
+ gap: 12px;
620
+ }
621
+ .health-card {
622
+ min-width: 0;
623
+ border: 1px solid var(--line);
624
+ border-radius: 8px;
625
+ background: #fbfcfe;
626
+ padding: 14px;
627
+ display: grid;
628
+ gap: 7px;
629
+ }
630
+ .health-card i {
631
+ color: var(--blue);
632
+ font-size: 20px;
633
+ }
634
+ .health-card span {
635
+ color: var(--muted);
636
+ font-size: 11px;
637
+ font-weight: 800;
638
+ text-transform: uppercase;
639
+ }
640
+ .health-card strong {
641
+ color: var(--ink);
642
+ font-size: 22px;
643
+ line-height: 1.1;
644
+ overflow-wrap: anywhere;
645
+ }
646
+ .health-card em {
647
+ color: var(--muted);
648
+ font-size: 12px;
649
+ font-style: normal;
650
+ }
651
+ .skill-progress {
652
+ display: grid;
653
+ gap: 6px;
654
+ }
655
+ .skill-progress-head {
656
+ display: flex;
657
+ align-items: center;
658
+ justify-content: space-between;
659
+ color: var(--muted);
660
+ font-size: 11px;
661
+ font-weight: 800;
662
+ }
663
+ .skill-progress-track {
664
+ height: 7px;
665
+ border-radius: 999px;
666
+ background: #e5eaf2;
667
+ overflow: hidden;
668
+ }
669
+ .skill-progress-track span {
670
+ display: block;
671
+ height: 100%;
672
+ border-radius: inherit;
673
+ background: linear-gradient(90deg, var(--blue), var(--green));
674
+ }
675
+
676
+ @media (max-width: 1100px) {
677
+ .health-grid {
678
+ grid-template-columns: repeat(2, minmax(0, 1fr));
679
+ }
680
+ }
681
+
682
+ @media (max-width: 760px) {
683
+ .health-grid {
684
+ grid-template-columns: 1fr;
685
+ }
686
+ }
@@ -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.6.0">
11
+ <link rel="stylesheet" href="/static/workspace.css?v=2.0.0">
12
12
  </head>
13
13
  <body>
14
14
  <div class="workspace-shell">
@@ -19,6 +19,7 @@
19
19
  </a>
20
20
  <nav>
21
21
  <a class="active" href="#overview"><i class="ti ti-layout-dashboard"></i><span>Overview</span></a>
22
+ <a href="#health"><i class="ti ti-heartbeat"></i><span>Health</span></a>
22
23
  <a href="#graph"><i class="ti ti-chart-dots-3"></i><span>Graph</span></a>
23
24
  <a href="#graph-explorer"><i class="ti ti-affiliate"></i><span>Explorer</span></a>
24
25
  <a href="#snapshots"><i class="ti ti-stack-2"></i><span>Snapshots</span></a>
@@ -30,6 +31,10 @@
30
31
  <a href="#enterprise"><i class="ti ti-building-skyscraper"></i><span>Editions</span></a>
31
32
  </nav>
32
33
  <div class="rail-links">
34
+ <a href="/plugins/sdk"><i class="ti ti-plug"></i><span>Plugins</span></a>
35
+ <a href="/workflows"><i class="ti ti-sitemap"></i><span>Designer</span></a>
36
+ <a href="/agents"><i class="ti ti-robot"></i><span>Agents</span></a>
37
+ <a href="/activity"><i class="ti ti-broadcast"></i><span>Activity</span></a>
33
38
  <a href="/chat"><i class="ti ti-message-circle"></i><span>Chat</span></a>
34
39
  <a href="/graph"><i class="ti ti-network"></i><span>Graph Canvas</span></a>
35
40
  <a href="/admin"><i class="ti ti-shield-lock"></i><span>Admin</span></a>
@@ -56,6 +61,17 @@
56
61
 
57
62
  <section class="metric-grid" id="metric-grid"></section>
58
63
 
64
+ <section class="workspace-band" id="health">
65
+ <div class="section-head">
66
+ <div>
67
+ <div class="eyebrow">Workspace Health</div>
68
+ <h2>Operational Snapshot</h2>
69
+ </div>
70
+ <span class="status-pill status-complete" id="workspace-health-status">checking</span>
71
+ </div>
72
+ <div class="health-grid" id="workspace-health-grid"></div>
73
+ </section>
74
+
59
75
  <section class="workspace-band" id="workspace-summary-band">
60
76
  <div class="section-head">
61
77
  <div>
@@ -314,6 +330,6 @@
314
330
  </div>
315
331
 
316
332
  <div class="toast" id="toast"></div>
317
- <script src="/static/scripts/workspace.js?v=1.6.0"></script>
333
+ <script src="/static/scripts/workspace.js?v=1.7.0"></script>
318
334
  </body>
319
335
  </html>