ltcai 4.0.0 → 4.0.1
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 +37 -33
- package/docs/CHANGELOG.md +64 -0
- package/docs/REALTIME_COLLABORATION.md +3 -3
- package/docs/V3_FRONTEND.md +9 -8
- package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +86 -43
- package/docs/kg-schema.md +6 -2
- package/docs/spec-vs-impl.md +10 -10
- package/kg_schema.py +2 -603
- package/knowledge_graph.py +37 -4958
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +15 -16
- package/latticeai/api/agents.py +13 -6
- package/latticeai/api/auth.py +19 -11
- package/latticeai/api/invitations.py +100 -0
- package/latticeai/api/knowledge_graph.py +4 -11
- package/latticeai/api/plugins.py +3 -6
- package/latticeai/api/realtime.py +4 -7
- package/latticeai/api/static_routes.py +9 -12
- package/latticeai/api/ui_redirects.py +26 -0
- package/latticeai/api/workflow_designer.py +39 -6
- package/latticeai/api/workspace.py +24 -10
- package/latticeai/app_factory.py +88 -17
- package/latticeai/brain/_kg_common.py +1123 -0
- package/latticeai/brain/discovery.py +1455 -0
- package/latticeai/brain/documents.py +218 -0
- package/latticeai/brain/ingest.py +644 -0
- package/latticeai/brain/projection.py +561 -0
- package/latticeai/brain/provenance.py +401 -0
- package/latticeai/brain/retrieval.py +1316 -0
- package/latticeai/brain/schema.py +640 -0
- package/latticeai/brain/store.py +216 -0
- package/latticeai/brain/write_master.py +225 -0
- package/latticeai/core/invitations.py +131 -0
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/multi_agent.py +1 -1
- package/latticeai/core/policy.py +54 -0
- package/latticeai/core/realtime.py +65 -44
- package/latticeai/core/sessions.py +31 -5
- package/latticeai/core/users.py +147 -0
- package/latticeai/core/workspace_os.py +420 -20
- package/latticeai/services/agent_runtime.py +242 -4
- package/latticeai/services/run_executor.py +328 -0
- package/latticeai/services/workspace_service.py +27 -19
- package/package.json +2 -14
- package/scripts/lint_v3.mjs +23 -0
- package/static/v3/asset-manifest.json +21 -14
- package/static/v3/js/{app.356e6452.js → app.c5c80c46.js} +1 -1
- package/static/v3/js/core/{api.7a308b89.js → api.ba0fbf14.js} +58 -1
- package/static/v3/js/core/api.js +57 -0
- package/static/v3/js/core/i18n.880e1fec.js +575 -0
- package/static/v3/js/core/i18n.js +575 -0
- package/static/v3/js/core/routes.37522821.js +101 -0
- package/static/v3/js/core/routes.js +71 -63
- package/static/v3/js/core/{shell.a1657f20.js → shell.e3f6bbfa.js} +67 -38
- package/static/v3/js/core/shell.js +65 -36
- package/static/v3/js/core/{store.204a08b2.js → store.7b2aa044.js} +10 -0
- package/static/v3/js/core/store.js +10 -0
- package/static/v3/js/views/account.eff40715.js +143 -0
- package/static/v3/js/views/account.js +143 -0
- package/static/v3/js/views/activity.0d271ef9.js +67 -0
- package/static/v3/js/views/activity.js +67 -0
- package/static/v3/js/views/{admin-users.03bac88c.js → admin-users.f7ac7b43.js} +4 -6
- package/static/v3/js/views/admin-users.js +4 -6
- package/static/v3/js/views/{agents.014d0b74.js → agents.17c5288d.js} +35 -12
- package/static/v3/js/views/agents.js +35 -12
- package/static/v3/js/views/{chat.e6dd7dd0.js → chat.e250e2cc.js} +23 -0
- package/static/v3/js/views/chat.js +23 -0
- package/static/v3/js/views/{knowledge-graph.5e40cbeb.js → knowledge-graph.4d09c537.js} +27 -7
- package/static/v3/js/views/knowledge-graph.js +27 -7
- package/static/v3/js/views/network.52a4f181.js +97 -0
- package/static/v3/js/views/network.js +97 -0
- package/static/v3/js/views/{planning.9ac3e313.js → planning.4876fd77.js} +26 -5
- package/static/v3/js/views/planning.js +26 -5
- package/static/v3/js/views/runs.b63b2afa.js +144 -0
- package/static/v3/js/views/runs.js +144 -0
- package/static/v3/js/views/{settings.8631fa5e.js → settings.b7140634.js} +7 -8
- package/static/v3/js/views/settings.js +7 -8
- package/static/v3/js/views/snapshots.6f5db095.js +135 -0
- package/static/v3/js/views/snapshots.js +135 -0
- package/static/v3/js/views/{workflows.26c57290.js → workflows.7752225a.js} +87 -2
- package/static/v3/js/views/workflows.js +87 -2
- package/static/v3/js/views/workspace-admin.c466029b.js +156 -0
- package/static/v3/js/views/workspace-admin.js +156 -0
- package/static/account.html +0 -113
- package/static/activity.html +0 -73
- package/static/admin.html +0 -486
- package/static/agents.html +0 -139
- package/static/chat.html +0 -841
- package/static/css/reference/account.css +0 -439
- package/static/css/reference/admin.css +0 -610
- package/static/css/reference/base.css +0 -1661
- package/static/css/reference/chat.css +0 -4623
- package/static/css/reference/graph.css +0 -1016
- package/static/css/responsive.css +0 -861
- package/static/graph.html +0 -122
- package/static/platform.css +0 -104
- package/static/plugins.html +0 -136
- package/static/scripts/account.js +0 -238
- package/static/scripts/admin.js +0 -1614
- package/static/scripts/chat.js +0 -5081
- package/static/scripts/graph.js +0 -1804
- package/static/scripts/platform.js +0 -64
- package/static/scripts/ux.js +0 -167
- package/static/scripts/workspace.js +0 -948
- package/static/v3/js/core/routes.7222343d.js +0 -93
- package/static/workflows.html +0 -146
- package/static/workspace.css +0 -1121
- package/static/workspace.html +0 -357
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { escapeHtml } from "../core/dom.js";
|
|
11
11
|
import { createGraphCanvas } from "./graph-canvas.js";
|
|
12
|
+
import { t } from "../core/i18n.js";
|
|
12
13
|
|
|
13
14
|
const TYPE_COLOR = {
|
|
14
15
|
Topic: "var(--lt3-pillar-graph)",
|
|
@@ -247,21 +248,25 @@ function buildExplore(ctx) {
|
|
|
247
248
|
async function renderStatus(ctx, host) {
|
|
248
249
|
const { h, icon, api, c } = ctx;
|
|
249
250
|
host.replaceChildren(c.loading({ lines: 3 }));
|
|
250
|
-
const [port, gs, idx] = await Promise.all([api.kgPortability(), api.graphStats(), api.indexStatus()]);
|
|
251
|
+
const [port, gs, idx, coverage] = await Promise.all([api.kgPortability(), api.graphStats(), api.indexStatus(), api.kgProvenanceCoverage()]);
|
|
251
252
|
const p = port.data || {};
|
|
252
253
|
const prov = p.provenance || {};
|
|
254
|
+
const cov = coverage.data || {};
|
|
253
255
|
const nodes = sumCounts((gs.data && gs.data.nodes) || {});
|
|
254
256
|
const edges = sumCounts((gs.data && gs.data.edges) || {});
|
|
255
257
|
const pipelines = (idx.data && idx.data.pipelines) || {};
|
|
258
|
+
const ratio = Number(cov.coverage_ratio ?? cov.ratio ?? 0);
|
|
259
|
+
const covered = Number(cov.nodes_with_provenance ?? cov.covered_nodes ?? 0);
|
|
256
260
|
|
|
257
261
|
host.replaceChildren(
|
|
258
|
-
h("div.lt3-row-2", c.sourceBadge(port.source), h("span.lt3-muted", { style: { "font-size": "var(--lt3-text-sm)" } },
|
|
262
|
+
h("div.lt3-row-2", c.sourceBadge(port.source === "live" || coverage.source === "live" ? "live" : port.source), h("span.lt3-muted", { style: { "font-size": "var(--lt3-text-sm)" } },
|
|
259
263
|
p.graph_schema_version != null ? `Schema v${p.graph_schema_version} · embed dim ${p.embed_dim ?? "—"}` : "Knowledge Graph status")),
|
|
260
264
|
h("div.lt3-statrow",
|
|
261
265
|
c.stat({ label: "Entities", value: c.fmtNum(nodes), icon: "circles" }),
|
|
262
266
|
c.stat({ label: "Relations", value: c.fmtNum(edges), icon: "vector-triangle" }),
|
|
263
267
|
c.stat({ label: "Ingested items", value: c.fmtNum(prov.total || 0), icon: "package-import" }),
|
|
264
268
|
c.stat({ label: "Embedded (RAG-ready)", value: c.fmtNum(prov.embedded || 0), icon: "vector" }),
|
|
269
|
+
c.stat({ label: t("kg.provenanceCoverage"), value: `${Math.round(ratio * 100)}%`, icon: "shield-check", delta: `${covered}/${cov.total_nodes ?? nodes}` }),
|
|
265
270
|
),
|
|
266
271
|
c.card(
|
|
267
272
|
h("div.lt3-stack-3",
|
|
@@ -271,6 +276,14 @@ async function renderStatus(ctx, host) {
|
|
|
271
276
|
pipelineRow(ctx, "Hybrid retrieval", pipelines.hybrid),
|
|
272
277
|
),
|
|
273
278
|
),
|
|
279
|
+
c.card(h("div.lt3-stack-3",
|
|
280
|
+
h("div.lt3-eyebrow", t("kg.provenanceCoverage")),
|
|
281
|
+
h("dl.lt3-keyval",
|
|
282
|
+
h("dt", t("kg.coveredNodes")), h("dd", `${covered}/${cov.total_nodes ?? nodes}`),
|
|
283
|
+
h("dt", t("kg.sourceTypes")), h("dd", compactCounts(cov.provenance_by_source_type || cov.by_source_type || {})),
|
|
284
|
+
h("dt", t("kg.uncoveredTypes")), h("dd", compactCounts(cov.uncovered_by_type || {})),
|
|
285
|
+
),
|
|
286
|
+
)),
|
|
274
287
|
prov.last_ingested_at
|
|
275
288
|
? h("p.lt3-muted", { style: { "font-size": "var(--lt3-text-sm)" } }, `Last ingestion: ${fmtWhen(prov.last_ingested_at)} · ${prov.duplicates || 0} duplicate(s) linked, not re-stored.`)
|
|
276
289
|
: c.emptyState({ icon: "package-import", title: "Nothing ingested yet", body: "Add files or capture a page to populate the graph." }),
|
|
@@ -327,18 +340,18 @@ function buildCapture(ctx) {
|
|
|
327
340
|
|
|
328
341
|
async function run() {
|
|
329
342
|
const url = (input.value || "").trim();
|
|
330
|
-
if (!url) { result.replaceChildren(c.banner(
|
|
343
|
+
if (!url) { result.replaceChildren(c.banner("Enter a URL first.", "warn", "alert-triangle")); return; }
|
|
331
344
|
result.replaceChildren(c.loading({ lines: 1 }));
|
|
332
345
|
const res = await api.browserReadUrl(url);
|
|
333
346
|
const d = res.data || {};
|
|
334
347
|
if (res.ok && d.status === "ok") {
|
|
335
|
-
result.replaceChildren(c.banner(
|
|
348
|
+
result.replaceChildren(c.banner(`Added to your Knowledge Graph${d.duplicate ? " (already present — linked)" : ""}. ${d.chunk_count || 0} chunk(s) indexed.`, "ok", "circle-check"));
|
|
336
349
|
ctx.toast && ctx.toast("Page added to Knowledge Graph");
|
|
337
350
|
} else if (d.status === "empty") {
|
|
338
|
-
result.replaceChildren(c.banner(
|
|
351
|
+
result.replaceChildren(c.banner("No readable text was found on that page.", "warn", "alert-triangle"));
|
|
339
352
|
} else {
|
|
340
353
|
const detail = d.detail || (res.status === 422 ? "The page is blocked or login-required." : "Could not read that URL.");
|
|
341
|
-
result.replaceChildren(c.banner(
|
|
354
|
+
result.replaceChildren(c.banner(detail, "err", "alert-triangle"));
|
|
342
355
|
}
|
|
343
356
|
}
|
|
344
357
|
|
|
@@ -366,7 +379,9 @@ async function renderPortability(ctx, host) {
|
|
|
366
379
|
const port = await api.kgPortability();
|
|
367
380
|
const status = h("div");
|
|
368
381
|
|
|
369
|
-
function note(tone, text) {
|
|
382
|
+
function note(tone, text) {
|
|
383
|
+
status.replaceChildren(c.banner(text, tone, tone === "err" ? "alert-triangle" : tone === "ok" ? "circle-check" : "info-circle"));
|
|
384
|
+
}
|
|
370
385
|
|
|
371
386
|
async function doExport() {
|
|
372
387
|
note("info", "Exporting…");
|
|
@@ -439,6 +454,11 @@ function sumCounts(obj) {
|
|
|
439
454
|
return Object.values(obj || {}).reduce((a, b) => a + (Number(b) || 0), 0);
|
|
440
455
|
}
|
|
441
456
|
|
|
457
|
+
function compactCounts(obj) {
|
|
458
|
+
const entries = Object.entries(obj || {});
|
|
459
|
+
return entries.length ? entries.map(([k, v]) => `${k}: ${v}`).join(" · ") : "—";
|
|
460
|
+
}
|
|
461
|
+
|
|
442
462
|
function prettySource(k) {
|
|
443
463
|
return ({ web_url: "Web URL", browser_tab: "Browser tab", file: "Files", local_file: "Local files",
|
|
444
464
|
note: "Notes", text: "Text", markdown: "Markdown", code: "Code", upload: "Uploads" })[k] || k;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { t } from "../core/i18n.880e1fec.js";
|
|
2
|
+
|
|
3
|
+
export async function render(ctx) {
|
|
4
|
+
const { h, icon, api, c, toast, store } = ctx;
|
|
5
|
+
const host = h("div.lt3-stack-6", c.loading({ lines: 5, block: true }));
|
|
6
|
+
|
|
7
|
+
async function load() {
|
|
8
|
+
const [identity, peers] = await Promise.all([api.networkIdentity(), api.networkPeers()]);
|
|
9
|
+
host.replaceChildren(
|
|
10
|
+
c.viewHeader({
|
|
11
|
+
eyebrow: t("network.eyebrow"),
|
|
12
|
+
title: t("network.title"),
|
|
13
|
+
sub: t("network.sub"),
|
|
14
|
+
actions: [c.sourceBadge(identity.source === "live" || peers.source === "live" ? "live" : "unavailable")],
|
|
15
|
+
}),
|
|
16
|
+
identityPanel(identity),
|
|
17
|
+
pairPanel(),
|
|
18
|
+
peersPanel(peers),
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function identityPanel(res) {
|
|
23
|
+
const d = res.data || {};
|
|
24
|
+
return c.panel({
|
|
25
|
+
title: t("network.identity"),
|
|
26
|
+
actions: [c.sourceBadge(res.source)],
|
|
27
|
+
children: h("dl.lt3-keyval",
|
|
28
|
+
h("dt", "device_id"), h("dd", h("span.lt3-mono", d.device_id || d.id || "—")),
|
|
29
|
+
h("dt", "fingerprint"), h("dd", h("span.lt3-mono", d.fingerprint || d.public_key_fingerprint || "—")),
|
|
30
|
+
h("dt", t("network.publicKey")), h("dd", h("pre.lt3-code", truncate(d.public_key || "—", 420))),
|
|
31
|
+
),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function pairPanel() {
|
|
36
|
+
const name = h("input.lt3-input", { type: "text", placeholder: t("network.peerName"), "aria-label": t("network.peerName") });
|
|
37
|
+
const base = h("input.lt3-input", { type: "url", placeholder: t("network.baseUrl"), "aria-label": t("network.baseUrl") });
|
|
38
|
+
const key = h("textarea.lt3-textarea", { rows: 3, placeholder: t("network.publicKey"), "aria-label": t("network.publicKey") });
|
|
39
|
+
return c.panel({
|
|
40
|
+
title: t("network.pair"),
|
|
41
|
+
children: h("div.lt3-stack-4",
|
|
42
|
+
h("div.lt3-grid-2", field(ctx, t("network.peerName"), name), field(ctx, t("network.baseUrl"), base)),
|
|
43
|
+
field(ctx, t("network.publicKey"), key),
|
|
44
|
+
h("button.lt3-btn.lt3-btn--primary", { on: { click: async () => {
|
|
45
|
+
const res = await api.pairPeer({ name: name.value.trim(), base_url: base.value.trim(), public_key: key.value.trim() });
|
|
46
|
+
toast(resultText(res, t("network.paired")), res.ok ? "ok" : "err");
|
|
47
|
+
if (res.ok) load();
|
|
48
|
+
} } }, icon("link"), t("network.pair")),
|
|
49
|
+
),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function peersPanel(res) {
|
|
54
|
+
const rows = Array.isArray(res.data?.peers) ? res.data.peers : [];
|
|
55
|
+
return c.panel({
|
|
56
|
+
title: t("network.peers"),
|
|
57
|
+
actions: [c.sourceBadge(res.source)],
|
|
58
|
+
children: rows.length ? c.table([
|
|
59
|
+
{ key: "name", label: t("common.name"), render: (p) => h("div", h("b", p.name || p.peer_id || p.id), h("div.lt3-faint", { style: { "font-family": "var(--lt3-font-mono)", "font-size": "var(--lt3-text-2xs)" } }, p.peer_id || p.id || "")) },
|
|
60
|
+
{ key: "base", label: t("network.baseUrl"), render: (p) => p.base_url || "—" },
|
|
61
|
+
{ key: "fp", label: "fingerprint", render: (p) => h("span.lt3-mono", p.fingerprint || p.public_key_fingerprint || "—") },
|
|
62
|
+
{ key: "act", label: "", width: "1%", render: (p) => h("div.lt3-row-2",
|
|
63
|
+
h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm", { on: { click: () => pushPeer(p.peer_id || p.id) } }, icon("send"), t("network.push")),
|
|
64
|
+
h("button.lt3-btn.lt3-btn--ghost.lt3-btn--sm", { on: { click: () => unpairPeer(p.peer_id || p.id) } }, icon("unlink"), t("network.unpair")),
|
|
65
|
+
) },
|
|
66
|
+
], rows) : c.emptyState({ icon: "network-off", title: t("network.peers"), body: t("common.none") }),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function pushPeer(peerId) {
|
|
71
|
+
const res = await api.pushPeer(peerId, store.get().workspaceId);
|
|
72
|
+
toast(resultText(res, t("network.pushed")), res.ok ? "ok" : "err");
|
|
73
|
+
}
|
|
74
|
+
async function unpairPeer(peerId) {
|
|
75
|
+
const res = await api.unpairPeer(peerId);
|
|
76
|
+
toast(resultText(res, t("network.unpaired")), res.ok ? "ok" : "err");
|
|
77
|
+
if (res.ok) load();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await load();
|
|
81
|
+
return host;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function field({ h }, label, control) {
|
|
85
|
+
return h("div.lt3-field", h("label.lt3-label", label), control);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function truncate(value, n) {
|
|
89
|
+
const s = String(value || "");
|
|
90
|
+
return s.length > n ? `${s.slice(0, n)}…` : s;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function resultText(res, okText) {
|
|
94
|
+
if (res && res.ok) return okText;
|
|
95
|
+
const data = (res && res.data) || {};
|
|
96
|
+
return String(data.detail || data.error || res?.error || t("common.unavailable"));
|
|
97
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { t } from "../core/i18n.js";
|
|
2
|
+
|
|
3
|
+
export async function render(ctx) {
|
|
4
|
+
const { h, icon, api, c, toast, store } = ctx;
|
|
5
|
+
const host = h("div.lt3-stack-6", c.loading({ lines: 5, block: true }));
|
|
6
|
+
|
|
7
|
+
async function load() {
|
|
8
|
+
const [identity, peers] = await Promise.all([api.networkIdentity(), api.networkPeers()]);
|
|
9
|
+
host.replaceChildren(
|
|
10
|
+
c.viewHeader({
|
|
11
|
+
eyebrow: t("network.eyebrow"),
|
|
12
|
+
title: t("network.title"),
|
|
13
|
+
sub: t("network.sub"),
|
|
14
|
+
actions: [c.sourceBadge(identity.source === "live" || peers.source === "live" ? "live" : "unavailable")],
|
|
15
|
+
}),
|
|
16
|
+
identityPanel(identity),
|
|
17
|
+
pairPanel(),
|
|
18
|
+
peersPanel(peers),
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function identityPanel(res) {
|
|
23
|
+
const d = res.data || {};
|
|
24
|
+
return c.panel({
|
|
25
|
+
title: t("network.identity"),
|
|
26
|
+
actions: [c.sourceBadge(res.source)],
|
|
27
|
+
children: h("dl.lt3-keyval",
|
|
28
|
+
h("dt", "device_id"), h("dd", h("span.lt3-mono", d.device_id || d.id || "—")),
|
|
29
|
+
h("dt", "fingerprint"), h("dd", h("span.lt3-mono", d.fingerprint || d.public_key_fingerprint || "—")),
|
|
30
|
+
h("dt", t("network.publicKey")), h("dd", h("pre.lt3-code", truncate(d.public_key || "—", 420))),
|
|
31
|
+
),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function pairPanel() {
|
|
36
|
+
const name = h("input.lt3-input", { type: "text", placeholder: t("network.peerName"), "aria-label": t("network.peerName") });
|
|
37
|
+
const base = h("input.lt3-input", { type: "url", placeholder: t("network.baseUrl"), "aria-label": t("network.baseUrl") });
|
|
38
|
+
const key = h("textarea.lt3-textarea", { rows: 3, placeholder: t("network.publicKey"), "aria-label": t("network.publicKey") });
|
|
39
|
+
return c.panel({
|
|
40
|
+
title: t("network.pair"),
|
|
41
|
+
children: h("div.lt3-stack-4",
|
|
42
|
+
h("div.lt3-grid-2", field(ctx, t("network.peerName"), name), field(ctx, t("network.baseUrl"), base)),
|
|
43
|
+
field(ctx, t("network.publicKey"), key),
|
|
44
|
+
h("button.lt3-btn.lt3-btn--primary", { on: { click: async () => {
|
|
45
|
+
const res = await api.pairPeer({ name: name.value.trim(), base_url: base.value.trim(), public_key: key.value.trim() });
|
|
46
|
+
toast(resultText(res, t("network.paired")), res.ok ? "ok" : "err");
|
|
47
|
+
if (res.ok) load();
|
|
48
|
+
} } }, icon("link"), t("network.pair")),
|
|
49
|
+
),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function peersPanel(res) {
|
|
54
|
+
const rows = Array.isArray(res.data?.peers) ? res.data.peers : [];
|
|
55
|
+
return c.panel({
|
|
56
|
+
title: t("network.peers"),
|
|
57
|
+
actions: [c.sourceBadge(res.source)],
|
|
58
|
+
children: rows.length ? c.table([
|
|
59
|
+
{ key: "name", label: t("common.name"), render: (p) => h("div", h("b", p.name || p.peer_id || p.id), h("div.lt3-faint", { style: { "font-family": "var(--lt3-font-mono)", "font-size": "var(--lt3-text-2xs)" } }, p.peer_id || p.id || "")) },
|
|
60
|
+
{ key: "base", label: t("network.baseUrl"), render: (p) => p.base_url || "—" },
|
|
61
|
+
{ key: "fp", label: "fingerprint", render: (p) => h("span.lt3-mono", p.fingerprint || p.public_key_fingerprint || "—") },
|
|
62
|
+
{ key: "act", label: "", width: "1%", render: (p) => h("div.lt3-row-2",
|
|
63
|
+
h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm", { on: { click: () => pushPeer(p.peer_id || p.id) } }, icon("send"), t("network.push")),
|
|
64
|
+
h("button.lt3-btn.lt3-btn--ghost.lt3-btn--sm", { on: { click: () => unpairPeer(p.peer_id || p.id) } }, icon("unlink"), t("network.unpair")),
|
|
65
|
+
) },
|
|
66
|
+
], rows) : c.emptyState({ icon: "network-off", title: t("network.peers"), body: t("common.none") }),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function pushPeer(peerId) {
|
|
71
|
+
const res = await api.pushPeer(peerId, store.get().workspaceId);
|
|
72
|
+
toast(resultText(res, t("network.pushed")), res.ok ? "ok" : "err");
|
|
73
|
+
}
|
|
74
|
+
async function unpairPeer(peerId) {
|
|
75
|
+
const res = await api.unpairPeer(peerId);
|
|
76
|
+
toast(resultText(res, t("network.unpaired")), res.ok ? "ok" : "err");
|
|
77
|
+
if (res.ok) load();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await load();
|
|
81
|
+
return host;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function field({ h }, label, control) {
|
|
85
|
+
return h("div.lt3-field", h("label.lt3-label", label), control);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function truncate(value, n) {
|
|
89
|
+
const s = String(value || "");
|
|
90
|
+
return s.length > n ? `${s.slice(0, n)}…` : s;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function resultText(res, okText) {
|
|
94
|
+
if (res && res.ok) return okText;
|
|
95
|
+
const data = (res && res.data) || {};
|
|
96
|
+
return String(data.detail || data.error || res?.error || t("common.unavailable"));
|
|
97
|
+
}
|
|
@@ -59,10 +59,28 @@ export async function render(ctx) {
|
|
|
59
59
|
resultHost.replaceChildren(c.banner("Planning is unavailable — start the local server and load a model.", "warn"));
|
|
60
60
|
return;
|
|
61
61
|
}
|
|
62
|
-
|
|
62
|
+
if (res.data.accepted && res.data.run) {
|
|
63
|
+
resultHost.replaceChildren(renderResult(ctx, res.data.run));
|
|
64
|
+
pollRun(res.data.run.id || res.data.run.run_id);
|
|
65
|
+
} else {
|
|
66
|
+
resultHost.replaceChildren(renderResult(ctx, res.data.result || res.data));
|
|
67
|
+
}
|
|
63
68
|
loadRuns();
|
|
64
69
|
}
|
|
65
70
|
|
|
71
|
+
async function pollRun(runId) {
|
|
72
|
+
if (!runId) return;
|
|
73
|
+
for (let i = 0; i < 80; i += 1) {
|
|
74
|
+
await sleep(i < 10 ? 400 : 1200);
|
|
75
|
+
const res = await ctx.api.agentRunDetail(runId);
|
|
76
|
+
const run = res && res.data && res.data.run;
|
|
77
|
+
if (!res || !res.ok || !run) return;
|
|
78
|
+
resultHost.replaceChildren(renderResult(ctx, run));
|
|
79
|
+
loadRuns();
|
|
80
|
+
if (!["queued", "running", "in_progress", "cancelling"].includes(String(run.status || "").toLowerCase())) return;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
66
84
|
async function loadRuns() {
|
|
67
85
|
const res = await ctx.api.agentRuntime();
|
|
68
86
|
runsSrc.replaceChildren(c.sourceBadge(res.source));
|
|
@@ -98,13 +116,14 @@ export async function render(ctx) {
|
|
|
98
116
|
function renderResult(ctx, result) {
|
|
99
117
|
const { h, c } = ctx;
|
|
100
118
|
const plan = result.plan || [];
|
|
101
|
-
const review = result.review || {};
|
|
119
|
+
const review = result.review || result.plan_review || {};
|
|
102
120
|
const retries = result.retry_history || [];
|
|
103
|
-
const
|
|
121
|
+
const status = mapStatus(result.status);
|
|
122
|
+
const ok = status === "ready" || (review.outcome || "").toLowerCase() === "approve" || (review.verdict || "").toLowerCase() === "pass";
|
|
104
123
|
return c.panel({
|
|
105
124
|
head: h("div.lt3-row", { style: { "justify-content": "space-between", width: "100%" } },
|
|
106
125
|
h("div", h("div.lt3-eyebrow", "Result"), h("h3.lt3-panel__title", "Plan & execution")),
|
|
107
|
-
c.statePill(ok ? "ready" : "warn")),
|
|
126
|
+
c.statePill(ok ? "ready" : status || "warn")),
|
|
108
127
|
children: h("div.lt3-stack-3",
|
|
109
128
|
h("div",
|
|
110
129
|
h("div.lt3-eyebrow", c.icon("list-check"), "Plan"),
|
|
@@ -146,8 +165,10 @@ function mapStatus(s) {
|
|
|
146
165
|
const v = String(s || "").toLowerCase();
|
|
147
166
|
if (v === "ok" || v === "retried_ok") return "ready";
|
|
148
167
|
if (v === "failed" || v === "rejected") return "failed";
|
|
149
|
-
if (v === "running" || v === "in_progress") return "active";
|
|
168
|
+
if (v === "running" || v === "in_progress" || v === "queued" || v === "cancelling") return "active";
|
|
169
|
+
if (v === "cancelled" || v === "interrupted") return "warn";
|
|
150
170
|
return v || "idle";
|
|
151
171
|
}
|
|
152
172
|
|
|
153
173
|
function trunc(s, n) { s = String(s || ""); return s.length > n ? s.slice(0, n) + "…" : s; }
|
|
174
|
+
function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
|
|
@@ -59,10 +59,28 @@ export async function render(ctx) {
|
|
|
59
59
|
resultHost.replaceChildren(c.banner("Planning is unavailable — start the local server and load a model.", "warn"));
|
|
60
60
|
return;
|
|
61
61
|
}
|
|
62
|
-
|
|
62
|
+
if (res.data.accepted && res.data.run) {
|
|
63
|
+
resultHost.replaceChildren(renderResult(ctx, res.data.run));
|
|
64
|
+
pollRun(res.data.run.id || res.data.run.run_id);
|
|
65
|
+
} else {
|
|
66
|
+
resultHost.replaceChildren(renderResult(ctx, res.data.result || res.data));
|
|
67
|
+
}
|
|
63
68
|
loadRuns();
|
|
64
69
|
}
|
|
65
70
|
|
|
71
|
+
async function pollRun(runId) {
|
|
72
|
+
if (!runId) return;
|
|
73
|
+
for (let i = 0; i < 80; i += 1) {
|
|
74
|
+
await sleep(i < 10 ? 400 : 1200);
|
|
75
|
+
const res = await ctx.api.agentRunDetail(runId);
|
|
76
|
+
const run = res && res.data && res.data.run;
|
|
77
|
+
if (!res || !res.ok || !run) return;
|
|
78
|
+
resultHost.replaceChildren(renderResult(ctx, run));
|
|
79
|
+
loadRuns();
|
|
80
|
+
if (!["queued", "running", "in_progress", "cancelling"].includes(String(run.status || "").toLowerCase())) return;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
66
84
|
async function loadRuns() {
|
|
67
85
|
const res = await ctx.api.agentRuntime();
|
|
68
86
|
runsSrc.replaceChildren(c.sourceBadge(res.source));
|
|
@@ -98,13 +116,14 @@ export async function render(ctx) {
|
|
|
98
116
|
function renderResult(ctx, result) {
|
|
99
117
|
const { h, c } = ctx;
|
|
100
118
|
const plan = result.plan || [];
|
|
101
|
-
const review = result.review || {};
|
|
119
|
+
const review = result.review || result.plan_review || {};
|
|
102
120
|
const retries = result.retry_history || [];
|
|
103
|
-
const
|
|
121
|
+
const status = mapStatus(result.status);
|
|
122
|
+
const ok = status === "ready" || (review.outcome || "").toLowerCase() === "approve" || (review.verdict || "").toLowerCase() === "pass";
|
|
104
123
|
return c.panel({
|
|
105
124
|
head: h("div.lt3-row", { style: { "justify-content": "space-between", width: "100%" } },
|
|
106
125
|
h("div", h("div.lt3-eyebrow", "Result"), h("h3.lt3-panel__title", "Plan & execution")),
|
|
107
|
-
c.statePill(ok ? "ready" : "warn")),
|
|
126
|
+
c.statePill(ok ? "ready" : status || "warn")),
|
|
108
127
|
children: h("div.lt3-stack-3",
|
|
109
128
|
h("div",
|
|
110
129
|
h("div.lt3-eyebrow", c.icon("list-check"), "Plan"),
|
|
@@ -146,8 +165,10 @@ function mapStatus(s) {
|
|
|
146
165
|
const v = String(s || "").toLowerCase();
|
|
147
166
|
if (v === "ok" || v === "retried_ok") return "ready";
|
|
148
167
|
if (v === "failed" || v === "rejected") return "failed";
|
|
149
|
-
if (v === "running" || v === "in_progress") return "active";
|
|
168
|
+
if (v === "running" || v === "in_progress" || v === "queued" || v === "cancelling") return "active";
|
|
169
|
+
if (v === "cancelled" || v === "interrupted") return "warn";
|
|
150
170
|
return v || "idle";
|
|
151
171
|
}
|
|
152
172
|
|
|
153
173
|
function trunc(s, n) { s = String(s || ""); return s.length > n ? s.slice(0, n) + "…" : s; }
|
|
174
|
+
function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { t } from "../core/i18n.880e1fec.js";
|
|
2
|
+
|
|
3
|
+
const ACTIVE = new Set(["queued", "running", "in_progress", "active", "cancelling"]);
|
|
4
|
+
|
|
5
|
+
export async function render(ctx) {
|
|
6
|
+
const { h, icon, api, c, toast } = ctx;
|
|
7
|
+
const agentHost = h("div", c.loading({ lines: 4 }));
|
|
8
|
+
const workflowHost = h("div", c.loading({ lines: 4 }));
|
|
9
|
+
const approvalHost = h("div", c.loading({ lines: 4 }));
|
|
10
|
+
const progressHost = h("div", c.loading({ lines: 3 }));
|
|
11
|
+
|
|
12
|
+
const root = h("div.lt3-stack-6",
|
|
13
|
+
c.viewHeader({ eyebrow: t("runs.eyebrow"), title: t("runs.title"), sub: t("runs.sub") }),
|
|
14
|
+
h("div.lt3-statrow", progressHost),
|
|
15
|
+
c.panel({ title: t("runs.approvals"), children: approvalHost }),
|
|
16
|
+
c.panel({ title: t("runs.agentRuns"), children: agentHost }),
|
|
17
|
+
c.panel({ title: t("runs.workflowRuns"), children: workflowHost }),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
await load();
|
|
21
|
+
const poll = setInterval(load, 5000);
|
|
22
|
+
root.addEventListener("DOMNodeRemovedFromDocument", () => clearInterval(poll), { once: true });
|
|
23
|
+
return root;
|
|
24
|
+
|
|
25
|
+
async function load() {
|
|
26
|
+
const [agent, workflow, pending] = await Promise.all([
|
|
27
|
+
api.agentRuntime(),
|
|
28
|
+
api.workflowRuns(),
|
|
29
|
+
api.permissionsPending(),
|
|
30
|
+
]);
|
|
31
|
+
const agentRuns = Array.isArray(agent.data?.runs) ? agent.data.runs : [];
|
|
32
|
+
const workflowRuns = Array.isArray(workflow.data?.runs) ? workflow.data.runs : [];
|
|
33
|
+
renderProgress(agentRuns, workflowRuns, pending.data || {});
|
|
34
|
+
agentHost.replaceChildren(runTable(ctx, agentRuns, "agent", agent.source));
|
|
35
|
+
workflowHost.replaceChildren(runTable(ctx, workflowRuns, "workflow", workflow.source));
|
|
36
|
+
approvalHost.replaceChildren(approvalList(ctx, workflowRuns, pending.data || {}, workflow.source || pending.source));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function renderProgress(agentRuns, workflowRuns, pending) {
|
|
40
|
+
const all = [...agentRuns, ...workflowRuns];
|
|
41
|
+
const active = all.filter((r) => ACTIVE.has(String(r.status || "").toLowerCase())).length;
|
|
42
|
+
const paused = workflowRuns.filter((r) => String(r.status || "").toLowerCase() === "awaiting_approval").length;
|
|
43
|
+
const approvals = Object.keys(pending.pending || {}).length + paused;
|
|
44
|
+
progressHost.replaceChildren(
|
|
45
|
+
c.stat({ label: t("runs.progress"), value: String(active), icon: "progress" }),
|
|
46
|
+
c.stat({ label: t("runs.approvals"), value: String(approvals), icon: "circle-check" }),
|
|
47
|
+
c.stat({ label: t("runs.agentRuns"), value: String(agentRuns.length), icon: "robot" }),
|
|
48
|
+
c.stat({ label: t("runs.workflowRuns"), value: String(workflowRuns.length), icon: "sitemap" }),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function runTable(ctx2, rows, kind, source) {
|
|
53
|
+
const { h, c } = ctx2;
|
|
54
|
+
return h("div.lt3-stack-3",
|
|
55
|
+
h("div.lt3-row-2", c.sourceBadge(source)),
|
|
56
|
+
rows.length ? c.table([
|
|
57
|
+
{ key: "status", label: t("common.status"), width: "1%", render: (r) => c.statePill(mapStatus(r.status)) },
|
|
58
|
+
{ key: "mode", label: t("runs.mode"), width: "1%", render: (r) => c.pill(r.mode || r.execution_mode || "live") },
|
|
59
|
+
{ key: "name", label: t("common.name"), render: (r) => h("div", h("b", r.name || r.workflow_name || r.agent_id || r.workflow_id || r.id), h("div.lt3-faint", { style: { "font-family": "var(--lt3-font-mono)", "font-size": "var(--lt3-text-2xs)" } }, r.id || r.run_id || "")) },
|
|
60
|
+
{ key: "when", label: t("common.updated"), width: "1%", render: (r) => h("span.lt3-faint", { style: { "white-space": "nowrap" } }, fmt(r.updated_at || r.created_at || r.completed_at)) },
|
|
61
|
+
{ key: "timeline", label: t("runs.progress"), render: (r) => miniTimeline(ctx2, r.timeline || []) },
|
|
62
|
+
{ key: "act", label: "", width: "1%", render: (r) => ACTIVE.has(String(r.status || "").toLowerCase())
|
|
63
|
+
? h("button.lt3-btn.lt3-btn--danger.lt3-btn--sm", { on: { click: () => cancelRun(kind, r.id || r.run_id) } }, c.icon("player-stop"), t("common.stop"))
|
|
64
|
+
: null },
|
|
65
|
+
], rows.slice(0, 40)) : c.emptyState({ icon: "history-off", title: kind === "agent" ? t("runs.agentRuns") : t("runs.workflowRuns"), body: t("common.none") }),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function approvalList(ctx2, workflowRuns, pending, source) {
|
|
70
|
+
const workflowApprovals = workflowRuns.filter((r) => String(r.status || "").toLowerCase() === "awaiting_approval");
|
|
71
|
+
const permissionRows = Object.entries(pending.pending || {}).map(([token, rec]) => ({ token, ...rec }));
|
|
72
|
+
const nodes = [];
|
|
73
|
+
nodes.push(h("div.lt3-row-2", c.sourceBadge(source)));
|
|
74
|
+
if (workflowApprovals.length) {
|
|
75
|
+
nodes.push(...workflowApprovals.map((run) => c.card(h("div.lt3-stack-3",
|
|
76
|
+
h("div.lt3-row", { style: { "justify-content": "space-between" } },
|
|
77
|
+
h("div", h("b", run.name || run.workflow_name || run.workflow_id), h("div.lt3-faint", t("runs.approvalPaused")), run.pause?.node ? h("div.lt3-faint", run.pause.node) : null),
|
|
78
|
+
c.statePill("pending"),
|
|
79
|
+
),
|
|
80
|
+
miniTimeline(ctx2, run.timeline || []),
|
|
81
|
+
h("div.lt3-row-2",
|
|
82
|
+
h("button.lt3-btn.lt3-btn--primary.lt3-btn--sm", { on: { click: () => decideWorkflow(run.id || run.run_id, true) } }, icon("circle-check"), t("common.approve")),
|
|
83
|
+
h("button.lt3-btn.lt3-btn--ghost.lt3-btn--sm", { on: { click: () => decideWorkflow(run.id || run.run_id, false) } }, icon("circle-x"), t("common.deny")),
|
|
84
|
+
),
|
|
85
|
+
), { flat: true })));
|
|
86
|
+
}
|
|
87
|
+
if (permissionRows.length) {
|
|
88
|
+
nodes.push(...permissionRows.map((rec) => c.card(h("div.lt3-stack-3",
|
|
89
|
+
h("div", h("b", rec.action_label || rec.action || "permission"), h("div.lt3-faint", rec.path || rec.token), h("div.lt3-faint", { style: { "font-family": "var(--lt3-font-mono)", "font-size": "var(--lt3-text-2xs)" } }, rec.token)),
|
|
90
|
+
h("div.lt3-row-2",
|
|
91
|
+
h("button.lt3-btn.lt3-btn--primary.lt3-btn--sm", { on: { click: () => decidePermission(rec.token, true) } }, icon("circle-check"), t("common.approve")),
|
|
92
|
+
h("button.lt3-btn.lt3-btn--ghost.lt3-btn--sm", { on: { click: () => decidePermission(rec.token, false) } }, icon("circle-x"), t("common.deny")),
|
|
93
|
+
),
|
|
94
|
+
), { flat: true })));
|
|
95
|
+
}
|
|
96
|
+
if (nodes.length === 1) nodes.push(c.emptyState({ icon: "circle-check", title: t("runs.approvals"), body: t("common.none") }));
|
|
97
|
+
return h("div.lt3-stack-3", nodes);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function miniTimeline(ctx2, timeline) {
|
|
101
|
+
const { h, c } = ctx2;
|
|
102
|
+
if (!timeline.length) return h("span.lt3-faint", t("common.none"));
|
|
103
|
+
return h("div.lt3-stack-2", timeline.slice(-3).map((item) =>
|
|
104
|
+
h("div.lt3-row-2", c.statePill(mapStatus(item.status || item.event)), h("span.lt3-faint", item.event || item.message || item.step || "event"))));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function cancelRun(kind, runId) {
|
|
108
|
+
if (!runId) return;
|
|
109
|
+
const res = kind === "agent" ? await api.stopAgentRun(runId) : await api.stopWorkflowRun(runId);
|
|
110
|
+
toast(resultText(res, t("runs.cancelled")), res.ok ? "ok" : "err");
|
|
111
|
+
load();
|
|
112
|
+
}
|
|
113
|
+
async function decideWorkflow(runId, approved) {
|
|
114
|
+
const res = await api.resumeWorkflowRun(runId, approved);
|
|
115
|
+
toast(resultText(res, t("runs.decided")), res.ok ? "ok" : "err");
|
|
116
|
+
load();
|
|
117
|
+
}
|
|
118
|
+
async function decidePermission(token, approved) {
|
|
119
|
+
const res = approved ? await api.approvePermission(token) : await api.denyPermission(token);
|
|
120
|
+
toast(resultText(res, t("runs.decided")), res.ok ? "ok" : "err");
|
|
121
|
+
load();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function mapStatus(status) {
|
|
126
|
+
const s = String(status || "").toLowerCase();
|
|
127
|
+
if (s === "ok" || s === "completed" || s === "success" || s === "resumed") return "ready";
|
|
128
|
+
if (s === "failed" || s === "error" || s === "denied" || s === "rejected") return "failed";
|
|
129
|
+
if (s === "running" || s === "queued" || s === "in_progress" || s === "cancelling") return "active";
|
|
130
|
+
if (s === "awaiting_approval" || s === "pending") return "pending";
|
|
131
|
+
if (s === "cancelled" || s === "interrupted") return "warn";
|
|
132
|
+
return s || "idle";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function fmt(ts) {
|
|
136
|
+
if (!ts) return "—";
|
|
137
|
+
try { return new Date(ts).toLocaleString(); } catch { return String(ts); }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function resultText(res, okText) {
|
|
141
|
+
if (res && res.ok) return okText;
|
|
142
|
+
const data = (res && res.data) || {};
|
|
143
|
+
return String(data.detail || data.error || res?.error || t("common.unavailable"));
|
|
144
|
+
}
|