ltcai 3.2.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +87 -67
  2. package/docs/CHANGELOG.md +36 -0
  3. package/docs/architecture.md +2 -1
  4. package/docs/assets/v3.4.0/agent-run.png +0 -0
  5. package/docs/assets/v3.4.0/agents.png +0 -0
  6. package/docs/assets/v3.4.0/before/chat-before.png +0 -0
  7. package/docs/assets/v3.4.0/before/files-before.png +0 -0
  8. package/docs/assets/v3.4.0/chat.png +0 -0
  9. package/docs/assets/v3.4.0/connect-folder.png +0 -0
  10. package/docs/assets/v3.4.0/files.png +0 -0
  11. package/docs/assets/v3.4.0/home.png +0 -0
  12. package/docs/assets/v3.4.0/hooks-dispatch.png +0 -0
  13. package/docs/assets/v3.4.0/knowledge-graph.png +0 -0
  14. package/docs/assets/v3.4.0/local-agent.png +0 -0
  15. package/docs/assets/v3.4.0/memory.png +0 -0
  16. package/docs/assets/v3.4.0/settings.png +0 -0
  17. package/docs/assets/v3.4.0/vision-input.png +0 -0
  18. package/docs/assets/v3.4.0/workflows.png +0 -0
  19. package/knowledge_graph.py +45 -0
  20. package/knowledge_graph_api.py +10 -0
  21. package/latticeai/__init__.py +1 -1
  22. package/latticeai/api/agents.py +3 -0
  23. package/latticeai/api/hooks.py +39 -0
  24. package/latticeai/api/local_files.py +41 -0
  25. package/latticeai/api/models.py +36 -1
  26. package/latticeai/api/tools.py +16 -1
  27. package/latticeai/api/workflow_designer.py +2 -1
  28. package/latticeai/core/hooks.py +398 -2
  29. package/latticeai/core/marketplace.py +1 -1
  30. package/latticeai/core/multi_agent.py +1 -1
  31. package/latticeai/core/workflow_engine.py +21 -1
  32. package/latticeai/core/workspace_os.py +1 -1
  33. package/latticeai/server_app.py +40 -0
  34. package/latticeai/services/agent_runtime.py +46 -1
  35. package/latticeai/services/upload_service.py +17 -0
  36. package/package.json +1 -1
  37. package/scripts/build_v3_assets.mjs +7 -1
  38. package/scripts/capture/capture_v340.js +88 -0
  39. package/static/css/{tokens.5a595671.css → tokens.3ba22e37.css} +109 -109
  40. package/static/css/tokens.css +109 -109
  41. package/static/v3/asset-manifest.json +25 -25
  42. package/static/v3/css/{lattice.components.011e988b.css → lattice.components.9b49d614.css} +57 -32
  43. package/static/v3/css/lattice.components.css +57 -32
  44. package/static/v3/css/{lattice.shell.4920f42d.css → lattice.shell.6ceea7c8.css} +75 -31
  45. package/static/v3/css/lattice.shell.css +75 -31
  46. package/static/v3/css/lattice.tokens.css +13 -13
  47. package/static/v3/css/{lattice.tokens.c597ff81.css → lattice.tokens.e7018963.css} +13 -13
  48. package/static/v3/css/{lattice.views.3ee19d4e.css → lattice.views.22f69117.css} +98 -15
  49. package/static/v3/css/lattice.views.css +98 -15
  50. package/static/v3/js/{app.a5adc0f3.js → app.c4acfdd8.js} +1 -1
  51. package/static/v3/js/core/{api.603b978f.js → api.12b568ad.js} +126 -4
  52. package/static/v3/js/core/api.js +126 -4
  53. package/static/v3/js/core/{components.4c83e0a9.js → components.35f02e4c.js} +8 -0
  54. package/static/v3/js/core/components.js +8 -0
  55. package/static/v3/js/core/{routes.07ad6696.js → routes.d214b399.js} +16 -12
  56. package/static/v3/js/core/routes.js +16 -12
  57. package/static/v3/js/core/{shell.ea0b9ae5.js → shell.80a6ad82.js} +37 -9
  58. package/static/v3/js/core/shell.js +34 -6
  59. package/static/v3/js/views/agents.014d0b74.js +541 -0
  60. package/static/v3/js/views/agents.js +305 -57
  61. package/static/v3/js/views/{chat.718144ce.js → chat.e6dd7dd0.js} +162 -10
  62. package/static/v3/js/views/chat.js +162 -10
  63. package/static/v3/js/views/files.adad14c1.js +365 -0
  64. package/static/v3/js/views/files.js +269 -90
  65. package/static/v3/js/views/home.24f8b8ae.js +200 -0
  66. package/static/v3/js/views/home.js +96 -15
  67. package/static/v3/js/views/hooks.13845954.js +215 -0
  68. package/static/v3/js/views/hooks.js +117 -1
  69. package/static/v3/js/views/{memory.d2ed7a7c.js → memory.4ebdf474.js} +5 -4
  70. package/static/v3/js/views/memory.js +5 -4
  71. package/static/v3/js/views/{my-computer.1b2ff621.js → my-computer.c3ef5283.js} +224 -1
  72. package/static/v3/js/views/my-computer.js +224 -1
  73. package/static/v3/js/views/{settings.4f777210.js → settings.8631fa5e.js} +70 -2
  74. package/static/v3/js/views/settings.js +70 -2
  75. package/static/v3/js/views/agents.c373d48c.js +0 -293
  76. package/static/v3/js/views/files.4935197e.js +0 -186
  77. package/static/v3/js/views/home.cdde3b32.js +0 -119
  78. package/static/v3/js/views/hooks.f3edebca.js +0 -99
@@ -82,9 +82,35 @@ export const api = {
82
82
 
83
83
  /* ── Documented future surfaces ─────────────────────────────────────── */
84
84
 
85
- /** GET /api/index/status — KG + Vector + Hybrid pipeline state. */
86
- indexStatus() {
87
- return withFallback("/api/index/status", {}, EMPTY_INDEX_STATUS);
85
+ /** GET /api/index/status — KG + Vector + Hybrid pipeline state.
86
+ * The backend endpoint is vector-centric (status/storage/source_items/…); the
87
+ * home pillars + topbar chip want a `pipelines` view keyed by
88
+ * knowledge_graph / vector_index / hybrid. Synthesize that shape from the real
89
+ * index status (vectors) plus the KG stats endpoint (entities). Nothing is
90
+ * fabricated: if the index endpoint is unavailable we report unavailable (so
91
+ * the UI shows the honest empty state), and a missing graph-stats count yields
92
+ * an "unavailable" graph pillar rather than a fake number. */
93
+ async indexStatus() {
94
+ const res = await raw("/api/index/status");
95
+ if (!(res.ok && res.data && !res.data.raw)) {
96
+ return { ok: false, status: res.status, data: EMPTY_INDEX_STATUS, source: "unavailable", error: res.error };
97
+ }
98
+ const idx = res.data;
99
+ let entities = null;
100
+ const gs = await raw("/knowledge-graph/stats");
101
+ if (gs.ok && gs.data && !gs.data.raw) {
102
+ const g = gs.data;
103
+ const n = g.total_nodes ?? g.nodes_total ?? (g.nodes && (g.nodes.total ?? g.nodes.count));
104
+ if (n !== undefined && n !== null) entities = Number(n) || 0;
105
+ }
106
+ const vectors = Number(idx.indexed_items ?? idx.ready_items) || 0;
107
+ const vstate = idx.status === "ready" ? "ready" : "pending";
108
+ const pipelines = {
109
+ knowledge_graph: { state: entities === null ? "unavailable" : "ready", entities: entities ?? 0 },
110
+ vector_index: { state: vstate, vectors },
111
+ hybrid: { state: vstate, strategy: vstate === "ready" ? "fused" : "pending" },
112
+ };
113
+ return { ok: true, status: res.status, data: { ...idx, pipelines }, source: "live" };
88
114
  },
89
115
 
90
116
  /** POST /api/index/rebuild — rebuild the derived vector index (real run). */
@@ -176,6 +202,31 @@ export const api = {
176
202
  },
177
203
  sysinfo() { return withFallback("/local/sysinfo", {}, EMPTY_SYSINFO); },
178
204
 
205
+ /** POST /upload/document — manual document ingest (multipart/form-data).
206
+ * Real backend path: parse → chunk → embed → knowledge-graph ingest
207
+ * (latticeai/api/tools.py:/upload/document). Returns { ok, status, data,
208
+ * source }; never throws. FormData must NOT carry a JSON Content-Type — the
209
+ * browser sets the multipart boundary itself. */
210
+ async uploadDocument(file) {
211
+ const ws = store.get().workspaceId;
212
+ const form = new FormData();
213
+ form.append("file", file);
214
+ try {
215
+ const res = await fetch("/upload/document", {
216
+ method: "POST",
217
+ credentials: "same-origin",
218
+ headers: { "Accept": "application/json", ...(ws ? { "X-Workspace-Id": ws } : {}) },
219
+ body: form,
220
+ });
221
+ let data = null;
222
+ const text = await res.text();
223
+ if (text) { try { data = JSON.parse(text); } catch { data = { raw: text }; } }
224
+ return { ok: res.ok, status: res.status, data, source: res.ok ? "live" : "unavailable" };
225
+ } catch (err) {
226
+ return { ok: false, status: 0, data: null, source: "unavailable", error: String(err) };
227
+ }
228
+ },
229
+
179
230
  adminSummary() { return withFallback("/admin/summary", {}, EMPTY_ADMIN.summary); },
180
231
  adminUsers() { return withFallback("/admin/users", {}, EMPTY_ADMIN.users); },
181
232
  adminAudit() { return withFallback("/admin/audit", {}, EMPTY_ADMIN.audit); },
@@ -310,7 +361,11 @@ export const api = {
310
361
  const rawData = line.slice(5).trim();
311
362
  if (rawData === "[DONE]") return { source: "live", text, trace, model };
312
363
  let data; try { data = JSON.parse(rawData); } catch { continue; }
313
- if (data.chunk) { text += data.chunk; onChunk && onChunk(data.chunk, text); }
364
+ // Standard chat streams `chunk`; the document-generation path streams
365
+ // `text` (report body + footnotes). Accept both so doc requests render
366
+ // instead of falsely reporting the backend as unreachable.
367
+ const delta = data.chunk || data.text;
368
+ if (delta) { text += delta; onChunk && onChunk(delta, text); }
314
369
  if (data.model) model = data.model;
315
370
  if (data.trace) { trace = data.trace; onTrace && onTrace(trace); }
316
371
  }
@@ -385,6 +440,73 @@ export const api = {
385
440
  mcpClaudeServers() { return withFallback("/mcp/claude-code-servers", {}, { servers: [] }); },
386
441
  mcpCustom() { return withFallback("/mcp/custom", {}, { custom: [] }); },
387
442
  mcpRecommend(query, limit = 6) { return raw("/mcp/recommend", { method: "POST", body: { query, limit } }); },
443
+
444
+ /* ── v3.4 Platform Completion ───────────────────────────────────────────
445
+ * Uploaded documents in Files, Connect Folder + Folder Watch over the real
446
+ * on-device runtime, the Local Agent status, and Hooks dispatch/run-log.
447
+ * All endpoints are real (latticeai/api + knowledge_graph_api); fallback-safe. */
448
+
449
+ /** GET /knowledge-graph/documents — uploaded + indexed docs with index state. */
450
+ async documents(limit = 200) {
451
+ const res = await raw(`/knowledge-graph/documents?limit=${encodeURIComponent(limit)}`);
452
+ if (res.ok && res.data && Array.isArray(res.data.documents)) {
453
+ return { ok: true, status: res.status, data: res.data.documents, source: "live", total: res.data.total };
454
+ }
455
+ return { ok: false, status: res.status, data: [], source: "unavailable", error: res.error };
456
+ },
457
+
458
+ // Local Agent (the on-device Lattice runtime: real GET /api/local-agent/status)
459
+ async localAgent() {
460
+ const res = await raw("/api/local-agent/status");
461
+ if (res.ok && res.data && res.data.agent) {
462
+ return { ok: true, status: res.status, data: res.data, source: "live" };
463
+ }
464
+ return {
465
+ ok: false, status: res.status, source: "unavailable",
466
+ data: { agent: { online: false }, health: {}, folders: { connected: 0, watching: 0 }, watch: { available: false, active: {} }, sources: [] },
467
+ };
468
+ },
469
+
470
+ // Connect Folder + Folder Watch (real backend: /knowledge-graph/local/*)
471
+ localRoots() { return withFallback("/knowledge-graph/local/roots", {}, { roots: [] }); },
472
+ async localSources() {
473
+ const res = await raw("/knowledge-graph/local/sources");
474
+ if (res.ok && res.data && Array.isArray(res.data.sources)) {
475
+ return { ok: true, status: res.status, data: res.data, source: "live" };
476
+ }
477
+ return { ok: false, status: res.status, data: { sources: [], watch: { available: false, active: {} } }, source: "unavailable" };
478
+ },
479
+ localWatchStatus() { return raw("/knowledge-graph/local/watch/status"); },
480
+ localWatchStop(source_id) { return raw("/knowledge-graph/local/watch/stop", { method: "POST", body: { source_id } }); },
481
+ approvePermission(token) { return raw(`/permissions/approve/${encodeURIComponent(token)}`, { method: "POST" }); },
482
+ indexFolder(path, opts = {}) {
483
+ return raw("/knowledge-graph/local/index", { method: "POST", body: { path, ...opts } });
484
+ },
485
+ /** One-call Connect Folder: request → self-approve (the click is the consent)
486
+ * → index (+ optional watch). Returns { ok, data, error }. */
487
+ async connectFolder(path, { watch = true, includeOcr = false } = {}) {
488
+ const probe = await raw("/knowledge-graph/local/index", { method: "POST", body: { path, approved: false } });
489
+ const token = probe.data && probe.data.approval_token;
490
+ if (!token) {
491
+ const detail = (probe.data && (probe.data.detail || probe.data.error)) || "the runtime did not return an approval token";
492
+ return { ok: false, error: detail, status: probe.status };
493
+ }
494
+ const approved = await raw(`/permissions/approve/${encodeURIComponent(token)}`, { method: "POST" });
495
+ if (!approved.ok) {
496
+ const detail = (approved.data && (approved.data.detail || approved.data.error)) || "approval failed";
497
+ return { ok: false, error: detail, status: approved.status };
498
+ }
499
+ const res = await raw("/knowledge-graph/local/index", {
500
+ method: "POST",
501
+ body: { path, approved: true, approval_token: token, watch_enabled: watch, include_ocr: includeOcr, consent: { approved: true, source: "files-ui" } },
502
+ });
503
+ if (res.ok && res.data && !res.data.detail) return { ok: true, data: res.data, status: res.status };
504
+ return { ok: false, error: (res.data && (res.data.detail || res.data.error)) || "indexing failed", status: res.status, data: res.data };
505
+ },
506
+
507
+ // Hooks dispatch (real backend: POST /api/hooks/run + GET /api/hooks/runs)
508
+ hookRun(body) { return raw("/api/hooks/run", { method: "POST", body }); },
509
+ hookRuns(limit = 50, kind) { return withFallback(`/api/hooks/runs?limit=${encodeURIComponent(limit)}${kind ? "&kind=" + encodeURIComponent(kind) : ""}`, {}, { runs: [], total: 0 }); },
388
510
  };
389
511
 
390
512
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -82,9 +82,35 @@ export const api = {
82
82
 
83
83
  /* ── Documented future surfaces ─────────────────────────────────────── */
84
84
 
85
- /** GET /api/index/status — KG + Vector + Hybrid pipeline state. */
86
- indexStatus() {
87
- return withFallback("/api/index/status", {}, EMPTY_INDEX_STATUS);
85
+ /** GET /api/index/status — KG + Vector + Hybrid pipeline state.
86
+ * The backend endpoint is vector-centric (status/storage/source_items/…); the
87
+ * home pillars + topbar chip want a `pipelines` view keyed by
88
+ * knowledge_graph / vector_index / hybrid. Synthesize that shape from the real
89
+ * index status (vectors) plus the KG stats endpoint (entities). Nothing is
90
+ * fabricated: if the index endpoint is unavailable we report unavailable (so
91
+ * the UI shows the honest empty state), and a missing graph-stats count yields
92
+ * an "unavailable" graph pillar rather than a fake number. */
93
+ async indexStatus() {
94
+ const res = await raw("/api/index/status");
95
+ if (!(res.ok && res.data && !res.data.raw)) {
96
+ return { ok: false, status: res.status, data: EMPTY_INDEX_STATUS, source: "unavailable", error: res.error };
97
+ }
98
+ const idx = res.data;
99
+ let entities = null;
100
+ const gs = await raw("/knowledge-graph/stats");
101
+ if (gs.ok && gs.data && !gs.data.raw) {
102
+ const g = gs.data;
103
+ const n = g.total_nodes ?? g.nodes_total ?? (g.nodes && (g.nodes.total ?? g.nodes.count));
104
+ if (n !== undefined && n !== null) entities = Number(n) || 0;
105
+ }
106
+ const vectors = Number(idx.indexed_items ?? idx.ready_items) || 0;
107
+ const vstate = idx.status === "ready" ? "ready" : "pending";
108
+ const pipelines = {
109
+ knowledge_graph: { state: entities === null ? "unavailable" : "ready", entities: entities ?? 0 },
110
+ vector_index: { state: vstate, vectors },
111
+ hybrid: { state: vstate, strategy: vstate === "ready" ? "fused" : "pending" },
112
+ };
113
+ return { ok: true, status: res.status, data: { ...idx, pipelines }, source: "live" };
88
114
  },
89
115
 
90
116
  /** POST /api/index/rebuild — rebuild the derived vector index (real run). */
@@ -176,6 +202,31 @@ export const api = {
176
202
  },
177
203
  sysinfo() { return withFallback("/local/sysinfo", {}, EMPTY_SYSINFO); },
178
204
 
205
+ /** POST /upload/document — manual document ingest (multipart/form-data).
206
+ * Real backend path: parse → chunk → embed → knowledge-graph ingest
207
+ * (latticeai/api/tools.py:/upload/document). Returns { ok, status, data,
208
+ * source }; never throws. FormData must NOT carry a JSON Content-Type — the
209
+ * browser sets the multipart boundary itself. */
210
+ async uploadDocument(file) {
211
+ const ws = store.get().workspaceId;
212
+ const form = new FormData();
213
+ form.append("file", file);
214
+ try {
215
+ const res = await fetch("/upload/document", {
216
+ method: "POST",
217
+ credentials: "same-origin",
218
+ headers: { "Accept": "application/json", ...(ws ? { "X-Workspace-Id": ws } : {}) },
219
+ body: form,
220
+ });
221
+ let data = null;
222
+ const text = await res.text();
223
+ if (text) { try { data = JSON.parse(text); } catch { data = { raw: text }; } }
224
+ return { ok: res.ok, status: res.status, data, source: res.ok ? "live" : "unavailable" };
225
+ } catch (err) {
226
+ return { ok: false, status: 0, data: null, source: "unavailable", error: String(err) };
227
+ }
228
+ },
229
+
179
230
  adminSummary() { return withFallback("/admin/summary", {}, EMPTY_ADMIN.summary); },
180
231
  adminUsers() { return withFallback("/admin/users", {}, EMPTY_ADMIN.users); },
181
232
  adminAudit() { return withFallback("/admin/audit", {}, EMPTY_ADMIN.audit); },
@@ -310,7 +361,11 @@ export const api = {
310
361
  const rawData = line.slice(5).trim();
311
362
  if (rawData === "[DONE]") return { source: "live", text, trace, model };
312
363
  let data; try { data = JSON.parse(rawData); } catch { continue; }
313
- if (data.chunk) { text += data.chunk; onChunk && onChunk(data.chunk, text); }
364
+ // Standard chat streams `chunk`; the document-generation path streams
365
+ // `text` (report body + footnotes). Accept both so doc requests render
366
+ // instead of falsely reporting the backend as unreachable.
367
+ const delta = data.chunk || data.text;
368
+ if (delta) { text += delta; onChunk && onChunk(delta, text); }
314
369
  if (data.model) model = data.model;
315
370
  if (data.trace) { trace = data.trace; onTrace && onTrace(trace); }
316
371
  }
@@ -385,6 +440,73 @@ export const api = {
385
440
  mcpClaudeServers() { return withFallback("/mcp/claude-code-servers", {}, { servers: [] }); },
386
441
  mcpCustom() { return withFallback("/mcp/custom", {}, { custom: [] }); },
387
442
  mcpRecommend(query, limit = 6) { return raw("/mcp/recommend", { method: "POST", body: { query, limit } }); },
443
+
444
+ /* ── v3.4 Platform Completion ───────────────────────────────────────────
445
+ * Uploaded documents in Files, Connect Folder + Folder Watch over the real
446
+ * on-device runtime, the Local Agent status, and Hooks dispatch/run-log.
447
+ * All endpoints are real (latticeai/api + knowledge_graph_api); fallback-safe. */
448
+
449
+ /** GET /knowledge-graph/documents — uploaded + indexed docs with index state. */
450
+ async documents(limit = 200) {
451
+ const res = await raw(`/knowledge-graph/documents?limit=${encodeURIComponent(limit)}`);
452
+ if (res.ok && res.data && Array.isArray(res.data.documents)) {
453
+ return { ok: true, status: res.status, data: res.data.documents, source: "live", total: res.data.total };
454
+ }
455
+ return { ok: false, status: res.status, data: [], source: "unavailable", error: res.error };
456
+ },
457
+
458
+ // Local Agent (the on-device Lattice runtime: real GET /api/local-agent/status)
459
+ async localAgent() {
460
+ const res = await raw("/api/local-agent/status");
461
+ if (res.ok && res.data && res.data.agent) {
462
+ return { ok: true, status: res.status, data: res.data, source: "live" };
463
+ }
464
+ return {
465
+ ok: false, status: res.status, source: "unavailable",
466
+ data: { agent: { online: false }, health: {}, folders: { connected: 0, watching: 0 }, watch: { available: false, active: {} }, sources: [] },
467
+ };
468
+ },
469
+
470
+ // Connect Folder + Folder Watch (real backend: /knowledge-graph/local/*)
471
+ localRoots() { return withFallback("/knowledge-graph/local/roots", {}, { roots: [] }); },
472
+ async localSources() {
473
+ const res = await raw("/knowledge-graph/local/sources");
474
+ if (res.ok && res.data && Array.isArray(res.data.sources)) {
475
+ return { ok: true, status: res.status, data: res.data, source: "live" };
476
+ }
477
+ return { ok: false, status: res.status, data: { sources: [], watch: { available: false, active: {} } }, source: "unavailable" };
478
+ },
479
+ localWatchStatus() { return raw("/knowledge-graph/local/watch/status"); },
480
+ localWatchStop(source_id) { return raw("/knowledge-graph/local/watch/stop", { method: "POST", body: { source_id } }); },
481
+ approvePermission(token) { return raw(`/permissions/approve/${encodeURIComponent(token)}`, { method: "POST" }); },
482
+ indexFolder(path, opts = {}) {
483
+ return raw("/knowledge-graph/local/index", { method: "POST", body: { path, ...opts } });
484
+ },
485
+ /** One-call Connect Folder: request → self-approve (the click is the consent)
486
+ * → index (+ optional watch). Returns { ok, data, error }. */
487
+ async connectFolder(path, { watch = true, includeOcr = false } = {}) {
488
+ const probe = await raw("/knowledge-graph/local/index", { method: "POST", body: { path, approved: false } });
489
+ const token = probe.data && probe.data.approval_token;
490
+ if (!token) {
491
+ const detail = (probe.data && (probe.data.detail || probe.data.error)) || "the runtime did not return an approval token";
492
+ return { ok: false, error: detail, status: probe.status };
493
+ }
494
+ const approved = await raw(`/permissions/approve/${encodeURIComponent(token)}`, { method: "POST" });
495
+ if (!approved.ok) {
496
+ const detail = (approved.data && (approved.data.detail || approved.data.error)) || "approval failed";
497
+ return { ok: false, error: detail, status: approved.status };
498
+ }
499
+ const res = await raw("/knowledge-graph/local/index", {
500
+ method: "POST",
501
+ body: { path, approved: true, approval_token: token, watch_enabled: watch, include_ocr: includeOcr, consent: { approved: true, source: "files-ui" } },
502
+ });
503
+ if (res.ok && res.data && !res.data.detail) return { ok: true, data: res.data, status: res.status };
504
+ return { ok: false, error: (res.data && (res.data.detail || res.data.error)) || "indexing failed", status: res.status, data: res.data };
505
+ },
506
+
507
+ // Hooks dispatch (real backend: POST /api/hooks/run + GET /api/hooks/runs)
508
+ hookRun(body) { return raw("/api/hooks/run", { method: "POST", body }); },
509
+ hookRuns(limit = 50, kind) { return withFallback(`/api/hooks/runs?limit=${encodeURIComponent(limit)}${kind ? "&kind=" + encodeURIComponent(kind) : ""}`, {}, { runs: [], total: 0 }); },
388
510
  };
389
511
 
390
512
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -70,6 +70,14 @@ const STATE_VARIANT = {
70
70
  ready: "ok", active: "ok", indexed: "ok", loaded: "ok", ok: "ok", available: "info",
71
71
  idle: "", standby: "", pending: "warn", indexing: "warn", building: "warn",
72
72
  failed: "err", error: "err", disabled: "err", not_configured: "",
73
+ // v3.4.0 platform-completion states (Files / Folder Watch / Local Agent /
74
+ // Agent runs / Hook dispatch). Keep these honest: amber for in-progress,
75
+ // green for healthy/active, red for blocked/failed, neutral for inert.
76
+ ingested: "warn", ingesting: "warn", watching: "ok", watched: "ok",
77
+ connected: "ok", online: "ok", offline: "err", synced: "ok",
78
+ queued: "warn", running: "warn", retrying: "warn", retried_ok: "ok",
79
+ rejected: "err", cancelled: "", stopped: "", blocked: "err",
80
+ advisory: "warn", skipped: "", complete: "ok", partial: "warn",
73
81
  };
74
82
  export function statePill(state) {
75
83
  return pill(String(state || "unknown"), STATE_VARIANT[String(state).toLowerCase()] ?? "", { dot: true });
@@ -70,6 +70,14 @@ const STATE_VARIANT = {
70
70
  ready: "ok", active: "ok", indexed: "ok", loaded: "ok", ok: "ok", available: "info",
71
71
  idle: "", standby: "", pending: "warn", indexing: "warn", building: "warn",
72
72
  failed: "err", error: "err", disabled: "err", not_configured: "",
73
+ // v3.4.0 platform-completion states (Files / Folder Watch / Local Agent /
74
+ // Agent runs / Hook dispatch). Keep these honest: amber for in-progress,
75
+ // green for healthy/active, red for blocked/failed, neutral for inert.
76
+ ingested: "warn", ingesting: "warn", watching: "ok", watched: "ok",
77
+ connected: "ok", online: "ok", offline: "err", synced: "ok",
78
+ queued: "warn", running: "warn", retrying: "warn", retried_ok: "ok",
79
+ rejected: "err", cancelled: "", stopped: "", blocked: "err",
80
+ advisory: "warn", skipped: "", complete: "ok", partial: "warn",
73
81
  };
74
82
  export function statePill(state) {
75
83
  return pill(String(state || "unknown"), STATE_VARIANT[String(state).toLowerCase()] ?? "", { dot: true });
@@ -11,8 +11,8 @@ export const MODE_RANK = { basic: 0, advanced: 1, admin: 2 };
11
11
  /** Nav groups in display order. */
12
12
  export const GROUPS = [
13
13
  { id: "workspace", label: "Workspace" },
14
- { id: "retrieval", label: "Retrieval" },
15
14
  { id: "data", label: "Data" },
15
+ { id: "retrieval", label: "Retrieval" },
16
16
  { id: "compute", label: "Compute" },
17
17
  { id: "platform", label: "Platform" },
18
18
  { id: "system", label: "System" },
@@ -28,32 +28,35 @@ export const ROUTES = [
28
28
  { key: "home", label: "Home", icon: "layout-dashboard", group: "workspace", minMode: "basic", view: "home", title: "Home", desc: "Your local-first AI workspace at a glance." },
29
29
  { key: "chat", label: "Chat", icon: "message-2", group: "workspace", minMode: "basic", view: "chat", title: "Chat", desc: "Grounded conversation over your indexed workspace." },
30
30
 
31
- // Retrieval (the product identity)
32
- { key: "knowledge-graph", label: "Knowledge Graph", icon: "chart-dots-3", group: "retrieval", minMode: "basic", view: "knowledge-graph", title: "Knowledge Graph", desc: "Entities and relations extracted from your workspace." },
33
- { key: "hybrid-search", label: "Hybrid Search", icon: "arrows-join", group: "retrieval", minMode: "basic", view: "hybrid-search", title: "Hybrid Search", desc: "Graph structure fused with vector similarity." },
34
- { key: "memory", label: "Memory", icon: "brain", group: "retrieval", minMode: "basic", view: "memory", title: "Memory", desc: "Long-term workspace, project, agent, and conversation memory." },
35
-
36
31
  // Data
37
32
  { key: "files", label: "Files", icon: "folders", group: "data", minMode: "basic", view: "files", title: "Files", desc: "Connected sources and indexed documents." },
38
- { key: "pipeline", label: "Pipeline", icon: "git-branch", group: "data", minMode: "advanced", view: "pipeline", title: "Pipeline", desc: "Ingest, embed, and graph-build flows." },
33
+
34
+ // Retrieval (the product identity)
35
+ { key: "hybrid-search", label: "Search", icon: "arrows-join", group: "retrieval", minMode: "basic", view: "hybrid-search", title: "Hybrid Search", desc: "Graph structure fused with vector similarity." },
36
+ { key: "knowledge-graph", label: "Knowledge", icon: "chart-dots-3", group: "retrieval", minMode: "basic", view: "knowledge-graph", title: "Knowledge Graph", desc: "Entities and relations extracted from your workspace." },
37
+ { key: "memory", label: "Memory", icon: "brain", group: "retrieval", minMode: "basic", view: "memory", title: "Memory", desc: "Long-term workspace, project, agent, and conversation memory." },
39
38
 
40
39
  // Compute
40
+ { key: "models", label: "Models", icon: "cpu", group: "compute", minMode: "basic", view: "models", title: "Models", desc: "Local MLX models and embeddings." },
41
41
  { key: "agents", label: "Agents", icon: "robot", group: "compute", minMode: "advanced", view: "agents", title: "Agents", desc: "Multi-agent roles, runs, and handoffs." },
42
42
  { key: "workflows", label: "Workflows", icon: "sitemap", group: "compute", minMode: "advanced", view: "workflows", title: "Workflow Agents", desc: "Trigger → agent chain → tools → memory → result." },
43
- { key: "planning", label: "Planning", icon: "target-arrow", group: "compute", minMode: "advanced", view: "planning", title: "Autonomous Planning", desc: "Goal → plan → execute → review → replan." },
44
- { key: "models", label: "Models", icon: "cpu", group: "compute", minMode: "basic", view: "models", title: "Models", desc: "Local MLX models and embeddings." },
45
- { key: "my-computer", label: "My Computer", icon: "device-desktop-analytics", group: "compute", minMode: "advanced", view: "my-computer", title: "My Computer", desc: "Local hardware, memory, and runtime." },
46
43
 
47
44
  // Platform (the agent ecosystem)
48
- { key: "marketplace", label: "Marketplace", icon: "building-store", group: "platform", minMode: "advanced", view: "marketplace", title: "Marketplace", desc: "Agent templates, agents, plugins, and skills." },
49
45
  { key: "skills", label: "Skills", icon: "puzzle", group: "platform", minMode: "advanced", view: "skills", title: "Skills", desc: "Install, enable, and manage skills." },
50
46
  { key: "hooks", label: "Hooks", icon: "webhook", group: "platform", minMode: "advanced", view: "hooks", title: "Hooks", desc: "Lifecycle hooks across runs, tools, and workflows." },
51
- { key: "tools", label: "Tools", icon: "tools", group: "platform", minMode: "advanced", view: "tools", title: "Tool Registry", desc: "Local, workspace, and MCP tools with governance." },
52
47
  { key: "mcp", label: "MCP", icon: "plug-connected", group: "platform", minMode: "advanced", view: "mcp", title: "MCP Manager", desc: "Connected MCP servers, available tools, and health." },
53
48
 
54
49
  // System
55
50
  { key: "settings", label: "Settings", icon: "settings", group: "system", minMode: "basic", view: "settings", title: "Settings", desc: "Appearance, workspace, and integrations." },
56
51
 
52
+ // Deep-linkable legacy/experimental surfaces. They remain renderable for
53
+ // compatibility, but are not promoted in the production navigation.
54
+ { key: "pipeline", label: "Pipeline", icon: "git-branch", group: "data", minMode: "advanced", view: "pipeline", title: "Pipeline", desc: "Ingest, embed, and graph-build flows.", hidden: true },
55
+ { key: "planning", label: "Planning", icon: "target-arrow", group: "compute", minMode: "advanced", view: "planning", title: "Autonomous Planning", desc: "Goal → plan → execute → review → replan.", hidden: true },
56
+ { key: "my-computer", label: "My Computer", icon: "device-desktop-analytics", group: "compute", minMode: "advanced", view: "my-computer", title: "My Computer", desc: "Local hardware, memory, and runtime.", hidden: true },
57
+ { key: "marketplace", label: "Marketplace", icon: "building-store", group: "platform", minMode: "advanced", view: "marketplace", title: "Marketplace", desc: "Agent templates, agents, plugins, and skills.", hidden: true },
58
+ { key: "tools", label: "Tools", icon: "tools", group: "platform", minMode: "advanced", view: "tools", title: "Tool Registry", desc: "Local, workspace, and MCP tools with governance.", hidden: true },
59
+
57
60
  // Admin
58
61
  { key: "admin/users", label: "Users", icon: "users", group: "admin", minMode: "admin", view: "admin-users", title: "Users", desc: "Workspace members and access.", admin: true },
59
62
  { key: "admin/permissions", label: "Permissions", icon: "key", group: "admin", minMode: "admin", view: "admin-permissions", title: "Permissions", desc: "Roles and capability mapping.", admin: true },
@@ -69,6 +72,7 @@ export const ROUTE_BY_KEY = Object.fromEntries(ROUTES.map((r) => [r.key, r]));
69
72
  export function visibleRoutes(mode) {
70
73
  const rank = MODE_RANK[mode] ?? 0;
71
74
  return ROUTES.filter((r) => {
75
+ if (r.hidden) return false;
72
76
  if (r.admin) return mode === "admin";
73
77
  return (MODE_RANK[r.minMode] ?? 0) <= rank;
74
78
  });
@@ -11,8 +11,8 @@ export const MODE_RANK = { basic: 0, advanced: 1, admin: 2 };
11
11
  /** Nav groups in display order. */
12
12
  export const GROUPS = [
13
13
  { id: "workspace", label: "Workspace" },
14
- { id: "retrieval", label: "Retrieval" },
15
14
  { id: "data", label: "Data" },
15
+ { id: "retrieval", label: "Retrieval" },
16
16
  { id: "compute", label: "Compute" },
17
17
  { id: "platform", label: "Platform" },
18
18
  { id: "system", label: "System" },
@@ -28,32 +28,35 @@ export const ROUTES = [
28
28
  { key: "home", label: "Home", icon: "layout-dashboard", group: "workspace", minMode: "basic", view: "home", title: "Home", desc: "Your local-first AI workspace at a glance." },
29
29
  { key: "chat", label: "Chat", icon: "message-2", group: "workspace", minMode: "basic", view: "chat", title: "Chat", desc: "Grounded conversation over your indexed workspace." },
30
30
 
31
- // Retrieval (the product identity)
32
- { key: "knowledge-graph", label: "Knowledge Graph", icon: "chart-dots-3", group: "retrieval", minMode: "basic", view: "knowledge-graph", title: "Knowledge Graph", desc: "Entities and relations extracted from your workspace." },
33
- { key: "hybrid-search", label: "Hybrid Search", icon: "arrows-join", group: "retrieval", minMode: "basic", view: "hybrid-search", title: "Hybrid Search", desc: "Graph structure fused with vector similarity." },
34
- { key: "memory", label: "Memory", icon: "brain", group: "retrieval", minMode: "basic", view: "memory", title: "Memory", desc: "Long-term workspace, project, agent, and conversation memory." },
35
-
36
31
  // Data
37
32
  { key: "files", label: "Files", icon: "folders", group: "data", minMode: "basic", view: "files", title: "Files", desc: "Connected sources and indexed documents." },
38
- { key: "pipeline", label: "Pipeline", icon: "git-branch", group: "data", minMode: "advanced", view: "pipeline", title: "Pipeline", desc: "Ingest, embed, and graph-build flows." },
33
+
34
+ // Retrieval (the product identity)
35
+ { key: "hybrid-search", label: "Search", icon: "arrows-join", group: "retrieval", minMode: "basic", view: "hybrid-search", title: "Hybrid Search", desc: "Graph structure fused with vector similarity." },
36
+ { key: "knowledge-graph", label: "Knowledge", icon: "chart-dots-3", group: "retrieval", minMode: "basic", view: "knowledge-graph", title: "Knowledge Graph", desc: "Entities and relations extracted from your workspace." },
37
+ { key: "memory", label: "Memory", icon: "brain", group: "retrieval", minMode: "basic", view: "memory", title: "Memory", desc: "Long-term workspace, project, agent, and conversation memory." },
39
38
 
40
39
  // Compute
40
+ { key: "models", label: "Models", icon: "cpu", group: "compute", minMode: "basic", view: "models", title: "Models", desc: "Local MLX models and embeddings." },
41
41
  { key: "agents", label: "Agents", icon: "robot", group: "compute", minMode: "advanced", view: "agents", title: "Agents", desc: "Multi-agent roles, runs, and handoffs." },
42
42
  { key: "workflows", label: "Workflows", icon: "sitemap", group: "compute", minMode: "advanced", view: "workflows", title: "Workflow Agents", desc: "Trigger → agent chain → tools → memory → result." },
43
- { key: "planning", label: "Planning", icon: "target-arrow", group: "compute", minMode: "advanced", view: "planning", title: "Autonomous Planning", desc: "Goal → plan → execute → review → replan." },
44
- { key: "models", label: "Models", icon: "cpu", group: "compute", minMode: "basic", view: "models", title: "Models", desc: "Local MLX models and embeddings." },
45
- { key: "my-computer", label: "My Computer", icon: "device-desktop-analytics", group: "compute", minMode: "advanced", view: "my-computer", title: "My Computer", desc: "Local hardware, memory, and runtime." },
46
43
 
47
44
  // Platform (the agent ecosystem)
48
- { key: "marketplace", label: "Marketplace", icon: "building-store", group: "platform", minMode: "advanced", view: "marketplace", title: "Marketplace", desc: "Agent templates, agents, plugins, and skills." },
49
45
  { key: "skills", label: "Skills", icon: "puzzle", group: "platform", minMode: "advanced", view: "skills", title: "Skills", desc: "Install, enable, and manage skills." },
50
46
  { key: "hooks", label: "Hooks", icon: "webhook", group: "platform", minMode: "advanced", view: "hooks", title: "Hooks", desc: "Lifecycle hooks across runs, tools, and workflows." },
51
- { key: "tools", label: "Tools", icon: "tools", group: "platform", minMode: "advanced", view: "tools", title: "Tool Registry", desc: "Local, workspace, and MCP tools with governance." },
52
47
  { key: "mcp", label: "MCP", icon: "plug-connected", group: "platform", minMode: "advanced", view: "mcp", title: "MCP Manager", desc: "Connected MCP servers, available tools, and health." },
53
48
 
54
49
  // System
55
50
  { key: "settings", label: "Settings", icon: "settings", group: "system", minMode: "basic", view: "settings", title: "Settings", desc: "Appearance, workspace, and integrations." },
56
51
 
52
+ // Deep-linkable legacy/experimental surfaces. They remain renderable for
53
+ // compatibility, but are not promoted in the production navigation.
54
+ { key: "pipeline", label: "Pipeline", icon: "git-branch", group: "data", minMode: "advanced", view: "pipeline", title: "Pipeline", desc: "Ingest, embed, and graph-build flows.", hidden: true },
55
+ { key: "planning", label: "Planning", icon: "target-arrow", group: "compute", minMode: "advanced", view: "planning", title: "Autonomous Planning", desc: "Goal → plan → execute → review → replan.", hidden: true },
56
+ { key: "my-computer", label: "My Computer", icon: "device-desktop-analytics", group: "compute", minMode: "advanced", view: "my-computer", title: "My Computer", desc: "Local hardware, memory, and runtime.", hidden: true },
57
+ { key: "marketplace", label: "Marketplace", icon: "building-store", group: "platform", minMode: "advanced", view: "marketplace", title: "Marketplace", desc: "Agent templates, agents, plugins, and skills.", hidden: true },
58
+ { key: "tools", label: "Tools", icon: "tools", group: "platform", minMode: "advanced", view: "tools", title: "Tool Registry", desc: "Local, workspace, and MCP tools with governance.", hidden: true },
59
+
57
60
  // Admin
58
61
  { key: "admin/users", label: "Users", icon: "users", group: "admin", minMode: "admin", view: "admin-users", title: "Users", desc: "Workspace members and access.", admin: true },
59
62
  { key: "admin/permissions", label: "Permissions", icon: "key", group: "admin", minMode: "admin", view: "admin-permissions", title: "Permissions", desc: "Roles and capability mapping.", admin: true },
@@ -69,6 +72,7 @@ export const ROUTE_BY_KEY = Object.fromEntries(ROUTES.map((r) => [r.key, r]));
69
72
  export function visibleRoutes(mode) {
70
73
  const rank = MODE_RANK[mode] ?? 0;
71
74
  return ROUTES.filter((r) => {
75
+ if (r.hidden) return false;
72
76
  if (r.admin) return mode === "admin";
73
77
  return (MODE_RANK[r.minMode] ?? 0) <= rank;
74
78
  });
@@ -7,10 +7,10 @@
7
7
 
8
8
  import { h, icon, $, $$ } from "./dom.a2773eb0.js";
9
9
  import { store } from "./store.34ebd5e6.js";
10
- import { api } from "./api.603b978f.js";
11
- import * as c from "./components.4c83e0a9.js";
10
+ import { api } from "./api.12b568ad.js";
11
+ import * as c from "./components.35f02e4c.js";
12
12
  import { createRouter } from "./router.584570f2.js";
13
- import { GROUPS, ROUTES, ROUTE_BY_KEY, MODE_RANK, visibleRoutes, loadView } from "./routes.07ad6696.js";
13
+ import { GROUPS, ROUTES, ROUTE_BY_KEY, MODE_RANK, visibleRoutes, loadView } from "./routes.d214b399.js";
14
14
 
15
15
  const MODES = [
16
16
  { key: "basic", label: "Basic", icon: "circle" },
@@ -49,14 +49,17 @@ function buildRail() {
49
49
  return h("aside.lt3-rail", { id: "lt3-rail", "aria-label": "Primary" },
50
50
  h("div.lt3-rail__brand",
51
51
  h("div.lt3-rail__logo", { html: latticeMark() }),
52
- h("div.lt3-rail__word", h("b", "Lattice AI"), h("small", "Local-First Workspace")),
52
+ h("div.lt3-rail__word", h("b", "Lattice AI"), h("small", "Private runtime")),
53
53
  h("button.lt3-iconbtn.lt3-iconbtn--sm.lt3-rail__close", { "aria-label": "Close menu", on: { click: closeDrawer } }, icon("x")),
54
54
  ),
55
55
  h("div.lt3-rail__scope", { id: "lt3-scope" }),
56
56
  h("nav.lt3-rail__nav", { id: "lt3-nav", "aria-label": "Sections" }),
57
57
  h("div.lt3-rail__foot",
58
- h("button.lt3-rail__user", { id: "lt3-user", "aria-label": "Account", on: { click: () => router.navigate("settings") } }),
59
- h("button.lt3-iconbtn", { id: "lt3-theme", "aria-label": "Toggle theme", title: "Toggle theme", on: { click: () => store.toggleTheme() } }, icon("moon")),
58
+ h("div.lt3-rail__status", { id: "lt3-rail-status" }),
59
+ h("div.lt3-rail__foot-row",
60
+ h("button.lt3-rail__user", { id: "lt3-user", "aria-label": "Account", on: { click: () => router.navigate("settings") } }),
61
+ h("button.lt3-iconbtn", { id: "lt3-theme", "aria-label": "Toggle theme", title: "Toggle theme", on: { click: () => store.toggleTheme() } }, icon("moon")),
62
+ ),
60
63
  ),
61
64
  );
62
65
  }
@@ -82,10 +85,14 @@ function navItem(route) {
82
85
  return h("a.lt3-navitem", {
83
86
  href: "#/" + route.key,
84
87
  dataset: { key: route.key },
88
+ title: route.title || route.label,
85
89
  on: { click: () => closeDrawer() },
86
90
  },
87
91
  icon(route.icon),
88
- h("span.lt3-navitem__label", route.label),
92
+ h("span.lt3-navitem__copy",
93
+ h("span.lt3-navitem__label", route.label),
94
+ route.desc ? h("span.lt3-navitem__meta", route.desc) : null,
95
+ ),
89
96
  route.key === "hybrid-search" ? h("span.lt3-navitem__dot", { style: { background: "var(--lt3-pillar-hybrid)" } }) : null,
90
97
  );
91
98
  }
@@ -161,6 +168,23 @@ function renderCrumbs() {
161
168
 
162
169
  function renderIndexChip() {
163
170
  els.idxchip.replaceChildren(c.indexChip(store.get().indexStatus));
171
+ renderRailStatus();
172
+ }
173
+
174
+ function renderRailStatus() {
175
+ if (!els.railStatus) return;
176
+ const status = store.get().indexStatus;
177
+ const pipes = status?.pipelines || {};
178
+ const keys = ["knowledge_graph", "vector_index", "hybrid"];
179
+ const ready = keys.filter((key) => String(pipes[key]?.state || "").toLowerCase() === "ready").length;
180
+ const unavailable = !Object.keys(pipes).length;
181
+ els.railStatus.replaceChildren(
182
+ h("div.lt3-rail__status-top",
183
+ h("span.lt3-rail__status-dot", { dataset: { state: unavailable ? "pending" : ready === keys.length ? "ready" : "partial" } }),
184
+ h("span", unavailable ? "Local index pending" : `${ready}/${keys.length} retrieval signals ready`),
185
+ ),
186
+ h("div.lt3-rail__status-sub", unavailable ? "Start backend to sync live state" : "Graph · vector · hybrid"),
187
+ );
164
188
  }
165
189
 
166
190
  /* ── View rendering ─────────────────────────────────────────────────────── */
@@ -240,8 +264,10 @@ function closeDrawer() { delete els.root.dataset.drawer; }
240
264
 
241
265
  /* ── Command palette ────────────────────────────────────────────────────── */
242
266
  function paletteItems() {
243
- const nav = ROUTES.map((r) => ({
244
- group: "Go to", label: r.label, icon: r.icon, hint: r.group,
267
+ const mode = store.get().mode;
268
+ const currentRoutes = visibleRoutes(mode);
269
+ const nav = currentRoutes.map((r) => ({
270
+ group: "Go to", label: r.title || r.label, icon: r.icon, hint: r.label === r.title ? r.group : r.label,
245
271
  run: () => router.navigate(r.key),
246
272
  }));
247
273
  const actions = [
@@ -340,6 +366,7 @@ function cacheEls(root) {
340
366
  theme: $("#lt3-theme", root),
341
367
  crumbs: $("#lt3-crumbs", root),
342
368
  idxchip: $("#lt3-idxchip", root),
369
+ railStatus: $("#lt3-rail-status", root),
343
370
  outlet: $("#lt3-outlet", root),
344
371
  view: $("#lt3-view", root),
345
372
  };
@@ -349,6 +376,7 @@ function cacheEls(root) {
349
376
  renderMode();
350
377
  updateThemeIcon();
351
378
  renderIndexChip();
379
+ renderRailStatus();
352
380
  }
353
381
 
354
382
  function latticeMark() {