ltcai 1.6.0 → 1.7.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 +28 -17
- package/docs/CHANGELOG.md +42 -0
- package/docs/EDITION_STRATEGY.md +4 -0
- package/docs/ENTERPRISE.md +8 -2
- package/latticeai/__init__.py +1 -1
- package/latticeai/core/workspace_os.py +11 -1
- package/package.json +10 -1
- package/static/admin.html +62 -0
- package/static/graph.html +7 -1
- package/static/lattice-reference.css +184 -0
- package/static/scripts/admin.js +121 -1
- package/static/scripts/graph.js +296 -14
- package/static/scripts/workspace.js +107 -10
- package/static/workspace.css +73 -0
- package/static/workspace.html +14 -2
|
@@ -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
|
|
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=1.
|
|
11
|
+
<link rel="stylesheet" href="/static/workspace.css?v=1.7.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>
|
|
@@ -56,6 +57,17 @@
|
|
|
56
57
|
|
|
57
58
|
<section class="metric-grid" id="metric-grid"></section>
|
|
58
59
|
|
|
60
|
+
<section class="workspace-band" id="health">
|
|
61
|
+
<div class="section-head">
|
|
62
|
+
<div>
|
|
63
|
+
<div class="eyebrow">Workspace Health</div>
|
|
64
|
+
<h2>Operational Snapshot</h2>
|
|
65
|
+
</div>
|
|
66
|
+
<span class="status-pill status-complete" id="workspace-health-status">checking</span>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="health-grid" id="workspace-health-grid"></div>
|
|
69
|
+
</section>
|
|
70
|
+
|
|
59
71
|
<section class="workspace-band" id="workspace-summary-band">
|
|
60
72
|
<div class="section-head">
|
|
61
73
|
<div>
|
|
@@ -314,6 +326,6 @@
|
|
|
314
326
|
</div>
|
|
315
327
|
|
|
316
328
|
<div class="toast" id="toast"></div>
|
|
317
|
-
<script src="/static/scripts/workspace.js?v=1.
|
|
329
|
+
<script src="/static/scripts/workspace.js?v=1.7.0"></script>
|
|
318
330
|
</body>
|
|
319
331
|
</html>
|