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.
Files changed (108) hide show
  1. package/README.md +37 -33
  2. package/docs/CHANGELOG.md +64 -0
  3. package/docs/REALTIME_COLLABORATION.md +3 -3
  4. package/docs/V3_FRONTEND.md +9 -8
  5. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +86 -43
  6. package/docs/kg-schema.md +6 -2
  7. package/docs/spec-vs-impl.md +10 -10
  8. package/kg_schema.py +2 -603
  9. package/knowledge_graph.py +37 -4958
  10. package/latticeai/__init__.py +1 -1
  11. package/latticeai/api/admin.py +15 -16
  12. package/latticeai/api/agents.py +13 -6
  13. package/latticeai/api/auth.py +19 -11
  14. package/latticeai/api/invitations.py +100 -0
  15. package/latticeai/api/knowledge_graph.py +4 -11
  16. package/latticeai/api/plugins.py +3 -6
  17. package/latticeai/api/realtime.py +4 -7
  18. package/latticeai/api/static_routes.py +9 -12
  19. package/latticeai/api/ui_redirects.py +26 -0
  20. package/latticeai/api/workflow_designer.py +39 -6
  21. package/latticeai/api/workspace.py +24 -10
  22. package/latticeai/app_factory.py +88 -17
  23. package/latticeai/brain/_kg_common.py +1123 -0
  24. package/latticeai/brain/discovery.py +1455 -0
  25. package/latticeai/brain/documents.py +218 -0
  26. package/latticeai/brain/ingest.py +644 -0
  27. package/latticeai/brain/projection.py +561 -0
  28. package/latticeai/brain/provenance.py +401 -0
  29. package/latticeai/brain/retrieval.py +1316 -0
  30. package/latticeai/brain/schema.py +640 -0
  31. package/latticeai/brain/store.py +216 -0
  32. package/latticeai/brain/write_master.py +225 -0
  33. package/latticeai/core/invitations.py +131 -0
  34. package/latticeai/core/marketplace.py +1 -1
  35. package/latticeai/core/multi_agent.py +1 -1
  36. package/latticeai/core/policy.py +54 -0
  37. package/latticeai/core/realtime.py +65 -44
  38. package/latticeai/core/sessions.py +31 -5
  39. package/latticeai/core/users.py +147 -0
  40. package/latticeai/core/workspace_os.py +420 -20
  41. package/latticeai/services/agent_runtime.py +242 -4
  42. package/latticeai/services/run_executor.py +328 -0
  43. package/latticeai/services/workspace_service.py +27 -19
  44. package/package.json +2 -14
  45. package/scripts/lint_v3.mjs +23 -0
  46. package/static/v3/asset-manifest.json +21 -14
  47. package/static/v3/js/{app.356e6452.js → app.c5c80c46.js} +1 -1
  48. package/static/v3/js/core/{api.7a308b89.js → api.ba0fbf14.js} +58 -1
  49. package/static/v3/js/core/api.js +57 -0
  50. package/static/v3/js/core/i18n.880e1fec.js +575 -0
  51. package/static/v3/js/core/i18n.js +575 -0
  52. package/static/v3/js/core/routes.37522821.js +101 -0
  53. package/static/v3/js/core/routes.js +71 -63
  54. package/static/v3/js/core/{shell.a1657f20.js → shell.e3f6bbfa.js} +67 -38
  55. package/static/v3/js/core/shell.js +65 -36
  56. package/static/v3/js/core/{store.204a08b2.js → store.7b2aa044.js} +10 -0
  57. package/static/v3/js/core/store.js +10 -0
  58. package/static/v3/js/views/account.eff40715.js +143 -0
  59. package/static/v3/js/views/account.js +143 -0
  60. package/static/v3/js/views/activity.0d271ef9.js +67 -0
  61. package/static/v3/js/views/activity.js +67 -0
  62. package/static/v3/js/views/{admin-users.03bac88c.js → admin-users.f7ac7b43.js} +4 -6
  63. package/static/v3/js/views/admin-users.js +4 -6
  64. package/static/v3/js/views/{agents.014d0b74.js → agents.17c5288d.js} +35 -12
  65. package/static/v3/js/views/agents.js +35 -12
  66. package/static/v3/js/views/{chat.e6dd7dd0.js → chat.e250e2cc.js} +23 -0
  67. package/static/v3/js/views/chat.js +23 -0
  68. package/static/v3/js/views/{knowledge-graph.5e40cbeb.js → knowledge-graph.4d09c537.js} +27 -7
  69. package/static/v3/js/views/knowledge-graph.js +27 -7
  70. package/static/v3/js/views/network.52a4f181.js +97 -0
  71. package/static/v3/js/views/network.js +97 -0
  72. package/static/v3/js/views/{planning.9ac3e313.js → planning.4876fd77.js} +26 -5
  73. package/static/v3/js/views/planning.js +26 -5
  74. package/static/v3/js/views/runs.b63b2afa.js +144 -0
  75. package/static/v3/js/views/runs.js +144 -0
  76. package/static/v3/js/views/{settings.8631fa5e.js → settings.b7140634.js} +7 -8
  77. package/static/v3/js/views/settings.js +7 -8
  78. package/static/v3/js/views/snapshots.6f5db095.js +135 -0
  79. package/static/v3/js/views/snapshots.js +135 -0
  80. package/static/v3/js/views/{workflows.26c57290.js → workflows.7752225a.js} +87 -2
  81. package/static/v3/js/views/workflows.js +87 -2
  82. package/static/v3/js/views/workspace-admin.c466029b.js +156 -0
  83. package/static/v3/js/views/workspace-admin.js +156 -0
  84. package/static/account.html +0 -113
  85. package/static/activity.html +0 -73
  86. package/static/admin.html +0 -486
  87. package/static/agents.html +0 -139
  88. package/static/chat.html +0 -841
  89. package/static/css/reference/account.css +0 -439
  90. package/static/css/reference/admin.css +0 -610
  91. package/static/css/reference/base.css +0 -1661
  92. package/static/css/reference/chat.css +0 -4623
  93. package/static/css/reference/graph.css +0 -1016
  94. package/static/css/responsive.css +0 -861
  95. package/static/graph.html +0 -122
  96. package/static/platform.css +0 -104
  97. package/static/plugins.html +0 -136
  98. package/static/scripts/account.js +0 -238
  99. package/static/scripts/admin.js +0 -1614
  100. package/static/scripts/chat.js +0 -5081
  101. package/static/scripts/graph.js +0 -1804
  102. package/static/scripts/platform.js +0 -64
  103. package/static/scripts/ux.js +0 -167
  104. package/static/scripts/workspace.js +0 -948
  105. package/static/v3/js/core/routes.7222343d.js +0 -93
  106. package/static/workflows.html +0 -146
  107. package/static/workspace.css +0 -1121
  108. 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({ tone: "warn", text: "Enter a URL first." })); return; }
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({ tone: "ok", text: `Added to your Knowledge Graph${d.duplicate ? " (already present — linked)" : ""}. ${d.chunk_count || 0} chunk(s) indexed.` }));
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({ tone: "warn", text: "No readable text was found on that page." }));
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({ tone: "err", text: detail }));
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) { status.replaceChildren(c.banner({ 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
- resultHost.replaceChildren(renderResult(ctx, res.data.result || res.data));
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 ok = (review.outcome || "").toLowerCase() === "approve" || (review.verdict || "").toLowerCase() === "pass";
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
- resultHost.replaceChildren(renderResult(ctx, res.data.result || res.data));
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 ok = (review.outcome || "").toLowerCase() === "approve" || (review.verdict || "").toLowerCase() === "pass";
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
+ }