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.
- package/README.md +40 -19
- package/docs/CHANGELOG.md +107 -0
- package/docs/EDITION_STRATEGY.md +14 -4
- package/docs/ENTERPRISE.md +11 -3
- package/docs/MULTI_AGENT_RUNTIME.md +410 -0
- package/docs/PLUGIN_SDK.md +651 -0
- package/docs/REALTIME_COLLABORATION.md +410 -0
- package/docs/V2_ARCHITECTURE.md +528 -0
- package/docs/WORKFLOW_DESIGNER.md +475 -0
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/agents.py +98 -0
- package/latticeai/api/plugins.py +115 -0
- package/latticeai/api/realtime.py +91 -0
- package/latticeai/api/workflow_designer.py +207 -0
- package/latticeai/core/multi_agent.py +270 -0
- package/latticeai/core/plugins.py +400 -0
- package/latticeai/core/realtime.py +190 -0
- package/latticeai/core/workflow_engine.py +329 -0
- package/latticeai/core/workspace_os.py +165 -2
- package/latticeai/server_app.py +76 -2
- package/latticeai/services/platform_runtime.py +200 -0
- package/package.json +17 -2
- package/plugins/README.md +35 -0
- package/plugins/git-insights/plugin.json +15 -0
- package/plugins/hello-world/plugin.json +16 -0
- package/plugins/hello-world/skills/hello_skill/SKILL.md +15 -0
- package/static/activity.html +70 -0
- package/static/admin.html +62 -0
- package/static/agents.html +92 -0
- package/static/graph.html +7 -1
- package/static/lattice-reference.css +184 -0
- package/static/platform.css +75 -0
- package/static/plugins.html +82 -0
- package/static/scripts/admin.js +121 -1
- package/static/scripts/graph.js +296 -14
- package/static/scripts/platform.js +64 -0
- package/static/scripts/workspace.js +107 -10
- package/static/workflows.html +121 -0
- package/static/workspace.css +73 -0
- 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
|
+
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[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
|
-
|
|
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
|
-
|
|
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
|
|
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>
|
package/static/workspace.css
CHANGED
|
@@ -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
|
+
}
|
package/static/workspace.html
CHANGED
|
@@ -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=
|
|
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.
|
|
333
|
+
<script src="/static/scripts/workspace.js?v=1.7.0"></script>
|
|
318
334
|
</body>
|
|
319
335
|
</html>
|