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
@@ -20,6 +20,8 @@ export async function render(ctx) {
20
20
  const srcSlot = h("span", c.sourceBadge("pending"));
21
21
  const gaugeHost = h("div.lt3-grid-3", c.loading({ lines: 0, block: true }));
22
22
  const runtimeHost = h("div", c.loading({ lines: 4 }));
23
+ const agentHost = h("div", c.loading({ lines: 5 }));
24
+ const foldersHost = h("div", c.loading({ lines: 4 }));
23
25
 
24
26
  const root = h("div.lt3-stack-6",
25
27
  c.viewHeader({
@@ -28,7 +30,7 @@ export async function render(ctx) {
28
30
  sub: "The local hardware and MLX runtime powering this workspace. Inference and indexing run here — on Apple Silicon — never on an external server.",
29
31
  actions: [
30
32
  srcSlot,
31
- h("button.lt3-btn.lt3-btn--ghost", { on: { click: () => load() } }, icon("refresh"), "Refresh"),
33
+ h("button.lt3-btn.lt3-btn--ghost", { on: { click: () => { load(); loadAgent(); } } }, icon("refresh"), "Refresh"),
32
34
  ],
33
35
  }),
34
36
 
@@ -39,6 +41,25 @@ export async function render(ctx) {
39
41
  gaugeHost,
40
42
  ),
41
43
 
44
+ // Local Agent — the on-device Lattice runtime acting as the local agent.
45
+ c.panel({
46
+ eyebrow: "Local agent",
47
+ title: "Local agent",
48
+ sub: "The local agent is the on-device Lattice runtime; no separate desktop install is required.",
49
+ children: agentHost,
50
+ }),
51
+
52
+ // Connect Folder + Folder Watch — over the same on-device runtime.
53
+ c.panel({
54
+ eyebrow: "On-device",
55
+ title: "Connect folder & folder watch",
56
+ sub: "Index a folder on this computer and keep it in sync. Files never leave the machine.",
57
+ actions: [
58
+ h("button.lt3-btn.lt3-btn--primary.lt3-btn--sm", { on: { click: () => connectFolder() } }, icon("folder-plus"), "Connect folder"),
59
+ ],
60
+ children: foldersHost,
61
+ }),
62
+
42
63
  h("div.lt3-grid-2",
43
64
  c.panel({
44
65
  eyebrow: "Runtime",
@@ -61,6 +82,45 @@ export async function render(ctx) {
61
82
  runtimeHost.replaceChildren(buildRuntime(ctx, sys, models));
62
83
  }
63
84
 
85
+ // Hydrate the Local Agent + Connect Folder panels from the live runtime.
86
+ async function loadAgent() {
87
+ agentHost.replaceChildren(c.loading({ lines: 5 }));
88
+ foldersHost.replaceChildren(c.loading({ lines: 4 }));
89
+
90
+ const res = await api.localAgent();
91
+ agentHost.replaceChildren(buildAgent(ctx, res));
92
+ foldersHost.replaceChildren(buildFolders(ctx, res, { connectFolder, stopWatching }));
93
+ }
94
+
95
+ // stopWatching lives here so it can reach the real adapter + toast.
96
+ async function stopWatching(id) {
97
+ const notify = ctx.toast || c.toast;
98
+ const r = await api.localWatchStop(id);
99
+ if (r && r.ok) {
100
+ notify("Stopped watching that folder. It remains indexed.", "info");
101
+ loadAgent();
102
+ return true;
103
+ }
104
+ const detail = (r && r.data && (r.data.detail || r.data.error)) || "the runtime is unavailable";
105
+ notify(`Could not stop watching — ${detail}.`, "warn");
106
+ return false;
107
+ }
108
+
109
+ // Prompt for a path, connect (index + watch) it, then re-hydrate.
110
+ async function connectFolder() {
111
+ const notify = ctx.toast || c.toast;
112
+ const path = window.prompt("Connect a folder on this computer (it will be indexed and watched):", "~/Documents");
113
+ if (!path || !path.trim()) return;
114
+ notify(`Connecting ${path.trim()} — indexing on this computer…`, "info");
115
+ const res = await api.connectFolder(path.trim(), { watch: true });
116
+ if (res && res.ok) {
117
+ notify(`Connected ${path.trim()}. Indexing and folder watch are active.`, "ok");
118
+ loadAgent();
119
+ } else {
120
+ notify((res && res.error) || "Could not connect that folder.", "warn");
121
+ }
122
+ }
123
+
64
124
  // Reflect real local-memory state (enabled + recorded activity) from the backend.
65
125
  async function loadMemory() {
66
126
  const res = await api.computerMemory();
@@ -72,6 +132,7 @@ export async function render(ctx) {
72
132
  }
73
133
 
74
134
  load();
135
+ loadAgent();
75
136
  loadMemory();
76
137
  return root;
77
138
  }
@@ -129,6 +190,168 @@ function buildRuntime({ h, icon, c }, sys, models) {
129
190
  );
130
191
  }
131
192
 
193
+ /* ── Local agent panel (wired to /api/local-agent/status) ─────────────────── */
194
+ function buildAgent({ h, icon, c }, res) {
195
+ // Honesty: if the runtime isn't reachable, never imply a "ready" agent.
196
+ if (!res || !res.ok || res.source === "unavailable") {
197
+ return h("div.lt3-stack-3",
198
+ h("div.lt3-row-2", c.sourceBadge("unavailable")),
199
+ c.emptyState({
200
+ icon: "plug-connected-x",
201
+ title: "Local runtime not reachable",
202
+ body: "The on-device Lattice runtime is not responding, so the local agent's status can't be confirmed. Start the server, then Refresh.",
203
+ }),
204
+ );
205
+ }
206
+
207
+ const d = res.data || {};
208
+ const agent = d.agent || {};
209
+ const handshake = d.handshake || {};
210
+ const health = d.health || {};
211
+ const folders = d.folders || {};
212
+
213
+ const online = !!agent.online;
214
+
215
+ const rows = [
216
+ { k: "Platform", v: agent.platform || "—", icon: "device-desktop" },
217
+ { k: "Machine", v: agent.machine || "—", mono: true, icon: "cpu" },
218
+ { k: "Python", v: agent.python || "—", mono: true, icon: "code" },
219
+ { k: "Kind", v: agent.kind || "on-device runtime", icon: "robot" },
220
+ ];
221
+
222
+ return h("div.lt3-stack-3",
223
+ // Identity + live online state.
224
+ h("div.lt3-row", { style: { "justify-content": "space-between", "align-items": "center" } },
225
+ h("div.lt3-row-2",
226
+ icon("robot"),
227
+ h("div",
228
+ h("div", { style: { "font-weight": "var(--lt3-weight-semi)", "font-size": "var(--lt3-text-sm)" } },
229
+ agent.name || "Lattice local agent"),
230
+ agent.id && h("div.lt3-faint", { style: { "font-size": "var(--lt3-text-2xs)" } },
231
+ h("span.lt3-mono", agent.id)),
232
+ ),
233
+ ),
234
+ // online is reported by the live endpoint — never fabricated.
235
+ c.statePill(online ? "active" : "idle"),
236
+ ),
237
+
238
+ h("dl.lt3-keyval",
239
+ rows.flatMap((r) => [
240
+ h("dt", h("span.lt3-row-2", icon(r.icon), r.k)),
241
+ h("dd", r.mono ? h("span.lt3-mono", r.v) : r.v),
242
+ ]),
243
+ ),
244
+
245
+ // Handshake + health, read straight from the runtime.
246
+ h("div.lt3-stack-2", { style: { "margin-top": "var(--lt3-space-2)" } },
247
+ h("div.lt3-row", { style: { "justify-content": "space-between", "align-items": "center" } },
248
+ h("span.lt3-row-2", icon("plug-connected"),
249
+ handshake.ok
250
+ ? `Handshake OK · ${handshake.transport || "local"}`
251
+ : "Handshake not established"),
252
+ c.statePill(handshake.ok ? "active" : "idle"),
253
+ ),
254
+ handshake.detail && h("div.lt3-faint", { style: { "font-size": "var(--lt3-text-2xs)" } }, handshake.detail),
255
+ h("div.lt3-row", { style: { "justify-content": "space-between", "align-items": "center" } },
256
+ h("span.lt3-row-2", icon("folder-search"), "Filesystem access"),
257
+ c.statePill(health.filesystem_access ? "active" : "idle"),
258
+ ),
259
+ h("div.lt3-row", { style: { "justify-content": "space-between", "align-items": "center" } },
260
+ h("span.lt3-row-2", icon("eye"), "Watcher available"),
261
+ c.statePill(health.watcher_available ? "active" : "idle"),
262
+ ),
263
+ ),
264
+
265
+ // Folder counts as stats.
266
+ h("div.lt3-grid-2", { style: { "margin-top": "var(--lt3-space-2)" } },
267
+ c.stat({ label: "Folders connected", value: c.fmtNum(folders.connected ?? 0), icon: "folder" }),
268
+ c.stat({ label: "Folders watching", value: c.fmtNum(folders.watching ?? 0), icon: "eye" }),
269
+ ),
270
+
271
+ h("div.lt3-row-2", { style: { "margin-top": "var(--lt3-space-2)" } },
272
+ c.sourceBadge(res.source),
273
+ h("span.lt3-faint", { style: { "font-size": "var(--lt3-text-2xs)" } },
274
+ "On-device runtime — no separate desktop install"),
275
+ ),
276
+ );
277
+ }
278
+
279
+ /* ── Connect Folder + Folder Watch panel (wired to /knowledge-graph/local) ── */
280
+ function buildFolders({ h, icon, c }, res, { connectFolder, stopWatching }) {
281
+ const d = (res && res.data) || {};
282
+ const watch = d.watch || {};
283
+ const sources = Array.isArray(d.sources) ? d.sources : [];
284
+
285
+ // Runtime unreachable → honest unavailable, no fabricated folder list.
286
+ if (!res || !res.ok || res.source === "unavailable") {
287
+ return h("div.lt3-stack-3",
288
+ h("div.lt3-row-2", c.sourceBadge("unavailable")),
289
+ c.emptyState({
290
+ icon: "folder-off",
291
+ title: "Connected folders unavailable",
292
+ body: "The on-device runtime is not reachable, so connected folders can't be listed. Start the server, then Refresh.",
293
+ action: h("button.lt3-btn.lt3-btn--ghost.lt3-btn--sm", { on: { click: () => connectFolder() } }, icon("folder-plus"), "Connect folder"),
294
+ }),
295
+ );
296
+ }
297
+
298
+ // Honest note when the watchdog dependency is missing.
299
+ const watchNote = watch.available === false
300
+ ? c.banner(
301
+ `Folder watch needs the watchdog dependency${watch.error ? ` — ${watch.error}` : ""}. Folders can still be indexed once; live sync is paused until it's installed.`,
302
+ "warn", "alert-triangle")
303
+ : null;
304
+
305
+ let body;
306
+ if (!sources.length) {
307
+ body = c.emptyState({
308
+ icon: "folder-plus",
309
+ title: "No folders connected yet",
310
+ body: "Connect a folder to index its files on this computer. Indexing and content stay local.",
311
+ action: h("button.lt3-btn.lt3-btn--primary.lt3-btn--sm", { on: { click: () => connectFolder() } }, icon("folder-plus"), "Connect folder"),
312
+ });
313
+ } else {
314
+ body = h("div.lt3-list",
315
+ sources.map((s) => {
316
+ const watching = !!s.watch_active;
317
+ const indexed = Number(s.success_count ?? s.indexed_count ?? 0) || 0;
318
+ const last = s.last_event_at || s.last_indexed_at;
319
+ const metaParts = [`${c.fmtNum(indexed)} indexed`];
320
+ if (last) metaParts.push(`last ${last}`);
321
+ return h("div.lt3-list__item",
322
+ icon(watching ? "folder-search" : "folder"),
323
+ h("div.lt3-list__body",
324
+ h("div.lt3-list__title", h("span.lt3-mono", s.root_path || s.path || s.id || "—")),
325
+ h("div.lt3-list__meta", metaParts.join(" · ")),
326
+ ),
327
+ h("div.lt3-row-2", { style: { "align-items": "center" } },
328
+ c.statePill(watching ? "watching" : "idle"),
329
+ watching && s.id && h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm", {
330
+ on: {
331
+ click: async (e) => {
332
+ const btn = e.currentTarget;
333
+ btn.disabled = true;
334
+ const ok = await stopWatching(s.id);
335
+ if (!ok) btn.disabled = false;
336
+ },
337
+ },
338
+ }, icon("player-stop"), "Stop watching"),
339
+ ),
340
+ );
341
+ }),
342
+ );
343
+ }
344
+
345
+ return h("div.lt3-stack-3",
346
+ watchNote,
347
+ body,
348
+ h("div.lt3-row-2", { style: { "margin-top": "var(--lt3-space-2)" } },
349
+ c.sourceBadge(res.source),
350
+ h("span.lt3-faint", { style: { "font-size": "var(--lt3-text-2xs)" } }, "Indexed and watched on this computer"),
351
+ ),
352
+ );
353
+ }
354
+
132
355
  /* ── Local memory panel (wired to /workspace/computer-memory) ─────────────── */
133
356
  function buildMemoryPanel(ctx, state) {
134
357
  const { h, icon, c } = ctx;
@@ -20,6 +20,8 @@ export async function render(ctx) {
20
20
  const srcSlot = h("span", c.sourceBadge("pending"));
21
21
  const gaugeHost = h("div.lt3-grid-3", c.loading({ lines: 0, block: true }));
22
22
  const runtimeHost = h("div", c.loading({ lines: 4 }));
23
+ const agentHost = h("div", c.loading({ lines: 5 }));
24
+ const foldersHost = h("div", c.loading({ lines: 4 }));
23
25
 
24
26
  const root = h("div.lt3-stack-6",
25
27
  c.viewHeader({
@@ -28,7 +30,7 @@ export async function render(ctx) {
28
30
  sub: "The local hardware and MLX runtime powering this workspace. Inference and indexing run here — on Apple Silicon — never on an external server.",
29
31
  actions: [
30
32
  srcSlot,
31
- h("button.lt3-btn.lt3-btn--ghost", { on: { click: () => load() } }, icon("refresh"), "Refresh"),
33
+ h("button.lt3-btn.lt3-btn--ghost", { on: { click: () => { load(); loadAgent(); } } }, icon("refresh"), "Refresh"),
32
34
  ],
33
35
  }),
34
36
 
@@ -39,6 +41,25 @@ export async function render(ctx) {
39
41
  gaugeHost,
40
42
  ),
41
43
 
44
+ // Local Agent — the on-device Lattice runtime acting as the local agent.
45
+ c.panel({
46
+ eyebrow: "Local agent",
47
+ title: "Local agent",
48
+ sub: "The local agent is the on-device Lattice runtime; no separate desktop install is required.",
49
+ children: agentHost,
50
+ }),
51
+
52
+ // Connect Folder + Folder Watch — over the same on-device runtime.
53
+ c.panel({
54
+ eyebrow: "On-device",
55
+ title: "Connect folder & folder watch",
56
+ sub: "Index a folder on this computer and keep it in sync. Files never leave the machine.",
57
+ actions: [
58
+ h("button.lt3-btn.lt3-btn--primary.lt3-btn--sm", { on: { click: () => connectFolder() } }, icon("folder-plus"), "Connect folder"),
59
+ ],
60
+ children: foldersHost,
61
+ }),
62
+
42
63
  h("div.lt3-grid-2",
43
64
  c.panel({
44
65
  eyebrow: "Runtime",
@@ -61,6 +82,45 @@ export async function render(ctx) {
61
82
  runtimeHost.replaceChildren(buildRuntime(ctx, sys, models));
62
83
  }
63
84
 
85
+ // Hydrate the Local Agent + Connect Folder panels from the live runtime.
86
+ async function loadAgent() {
87
+ agentHost.replaceChildren(c.loading({ lines: 5 }));
88
+ foldersHost.replaceChildren(c.loading({ lines: 4 }));
89
+
90
+ const res = await api.localAgent();
91
+ agentHost.replaceChildren(buildAgent(ctx, res));
92
+ foldersHost.replaceChildren(buildFolders(ctx, res, { connectFolder, stopWatching }));
93
+ }
94
+
95
+ // stopWatching lives here so it can reach the real adapter + toast.
96
+ async function stopWatching(id) {
97
+ const notify = ctx.toast || c.toast;
98
+ const r = await api.localWatchStop(id);
99
+ if (r && r.ok) {
100
+ notify("Stopped watching that folder. It remains indexed.", "info");
101
+ loadAgent();
102
+ return true;
103
+ }
104
+ const detail = (r && r.data && (r.data.detail || r.data.error)) || "the runtime is unavailable";
105
+ notify(`Could not stop watching — ${detail}.`, "warn");
106
+ return false;
107
+ }
108
+
109
+ // Prompt for a path, connect (index + watch) it, then re-hydrate.
110
+ async function connectFolder() {
111
+ const notify = ctx.toast || c.toast;
112
+ const path = window.prompt("Connect a folder on this computer (it will be indexed and watched):", "~/Documents");
113
+ if (!path || !path.trim()) return;
114
+ notify(`Connecting ${path.trim()} — indexing on this computer…`, "info");
115
+ const res = await api.connectFolder(path.trim(), { watch: true });
116
+ if (res && res.ok) {
117
+ notify(`Connected ${path.trim()}. Indexing and folder watch are active.`, "ok");
118
+ loadAgent();
119
+ } else {
120
+ notify((res && res.error) || "Could not connect that folder.", "warn");
121
+ }
122
+ }
123
+
64
124
  // Reflect real local-memory state (enabled + recorded activity) from the backend.
65
125
  async function loadMemory() {
66
126
  const res = await api.computerMemory();
@@ -72,6 +132,7 @@ export async function render(ctx) {
72
132
  }
73
133
 
74
134
  load();
135
+ loadAgent();
75
136
  loadMemory();
76
137
  return root;
77
138
  }
@@ -129,6 +190,168 @@ function buildRuntime({ h, icon, c }, sys, models) {
129
190
  );
130
191
  }
131
192
 
193
+ /* ── Local agent panel (wired to /api/local-agent/status) ─────────────────── */
194
+ function buildAgent({ h, icon, c }, res) {
195
+ // Honesty: if the runtime isn't reachable, never imply a "ready" agent.
196
+ if (!res || !res.ok || res.source === "unavailable") {
197
+ return h("div.lt3-stack-3",
198
+ h("div.lt3-row-2", c.sourceBadge("unavailable")),
199
+ c.emptyState({
200
+ icon: "plug-connected-x",
201
+ title: "Local runtime not reachable",
202
+ body: "The on-device Lattice runtime is not responding, so the local agent's status can't be confirmed. Start the server, then Refresh.",
203
+ }),
204
+ );
205
+ }
206
+
207
+ const d = res.data || {};
208
+ const agent = d.agent || {};
209
+ const handshake = d.handshake || {};
210
+ const health = d.health || {};
211
+ const folders = d.folders || {};
212
+
213
+ const online = !!agent.online;
214
+
215
+ const rows = [
216
+ { k: "Platform", v: agent.platform || "—", icon: "device-desktop" },
217
+ { k: "Machine", v: agent.machine || "—", mono: true, icon: "cpu" },
218
+ { k: "Python", v: agent.python || "—", mono: true, icon: "code" },
219
+ { k: "Kind", v: agent.kind || "on-device runtime", icon: "robot" },
220
+ ];
221
+
222
+ return h("div.lt3-stack-3",
223
+ // Identity + live online state.
224
+ h("div.lt3-row", { style: { "justify-content": "space-between", "align-items": "center" } },
225
+ h("div.lt3-row-2",
226
+ icon("robot"),
227
+ h("div",
228
+ h("div", { style: { "font-weight": "var(--lt3-weight-semi)", "font-size": "var(--lt3-text-sm)" } },
229
+ agent.name || "Lattice local agent"),
230
+ agent.id && h("div.lt3-faint", { style: { "font-size": "var(--lt3-text-2xs)" } },
231
+ h("span.lt3-mono", agent.id)),
232
+ ),
233
+ ),
234
+ // online is reported by the live endpoint — never fabricated.
235
+ c.statePill(online ? "active" : "idle"),
236
+ ),
237
+
238
+ h("dl.lt3-keyval",
239
+ rows.flatMap((r) => [
240
+ h("dt", h("span.lt3-row-2", icon(r.icon), r.k)),
241
+ h("dd", r.mono ? h("span.lt3-mono", r.v) : r.v),
242
+ ]),
243
+ ),
244
+
245
+ // Handshake + health, read straight from the runtime.
246
+ h("div.lt3-stack-2", { style: { "margin-top": "var(--lt3-space-2)" } },
247
+ h("div.lt3-row", { style: { "justify-content": "space-between", "align-items": "center" } },
248
+ h("span.lt3-row-2", icon("plug-connected"),
249
+ handshake.ok
250
+ ? `Handshake OK · ${handshake.transport || "local"}`
251
+ : "Handshake not established"),
252
+ c.statePill(handshake.ok ? "active" : "idle"),
253
+ ),
254
+ handshake.detail && h("div.lt3-faint", { style: { "font-size": "var(--lt3-text-2xs)" } }, handshake.detail),
255
+ h("div.lt3-row", { style: { "justify-content": "space-between", "align-items": "center" } },
256
+ h("span.lt3-row-2", icon("folder-search"), "Filesystem access"),
257
+ c.statePill(health.filesystem_access ? "active" : "idle"),
258
+ ),
259
+ h("div.lt3-row", { style: { "justify-content": "space-between", "align-items": "center" } },
260
+ h("span.lt3-row-2", icon("eye"), "Watcher available"),
261
+ c.statePill(health.watcher_available ? "active" : "idle"),
262
+ ),
263
+ ),
264
+
265
+ // Folder counts as stats.
266
+ h("div.lt3-grid-2", { style: { "margin-top": "var(--lt3-space-2)" } },
267
+ c.stat({ label: "Folders connected", value: c.fmtNum(folders.connected ?? 0), icon: "folder" }),
268
+ c.stat({ label: "Folders watching", value: c.fmtNum(folders.watching ?? 0), icon: "eye" }),
269
+ ),
270
+
271
+ h("div.lt3-row-2", { style: { "margin-top": "var(--lt3-space-2)" } },
272
+ c.sourceBadge(res.source),
273
+ h("span.lt3-faint", { style: { "font-size": "var(--lt3-text-2xs)" } },
274
+ "On-device runtime — no separate desktop install"),
275
+ ),
276
+ );
277
+ }
278
+
279
+ /* ── Connect Folder + Folder Watch panel (wired to /knowledge-graph/local) ── */
280
+ function buildFolders({ h, icon, c }, res, { connectFolder, stopWatching }) {
281
+ const d = (res && res.data) || {};
282
+ const watch = d.watch || {};
283
+ const sources = Array.isArray(d.sources) ? d.sources : [];
284
+
285
+ // Runtime unreachable → honest unavailable, no fabricated folder list.
286
+ if (!res || !res.ok || res.source === "unavailable") {
287
+ return h("div.lt3-stack-3",
288
+ h("div.lt3-row-2", c.sourceBadge("unavailable")),
289
+ c.emptyState({
290
+ icon: "folder-off",
291
+ title: "Connected folders unavailable",
292
+ body: "The on-device runtime is not reachable, so connected folders can't be listed. Start the server, then Refresh.",
293
+ action: h("button.lt3-btn.lt3-btn--ghost.lt3-btn--sm", { on: { click: () => connectFolder() } }, icon("folder-plus"), "Connect folder"),
294
+ }),
295
+ );
296
+ }
297
+
298
+ // Honest note when the watchdog dependency is missing.
299
+ const watchNote = watch.available === false
300
+ ? c.banner(
301
+ `Folder watch needs the watchdog dependency${watch.error ? ` — ${watch.error}` : ""}. Folders can still be indexed once; live sync is paused until it's installed.`,
302
+ "warn", "alert-triangle")
303
+ : null;
304
+
305
+ let body;
306
+ if (!sources.length) {
307
+ body = c.emptyState({
308
+ icon: "folder-plus",
309
+ title: "No folders connected yet",
310
+ body: "Connect a folder to index its files on this computer. Indexing and content stay local.",
311
+ action: h("button.lt3-btn.lt3-btn--primary.lt3-btn--sm", { on: { click: () => connectFolder() } }, icon("folder-plus"), "Connect folder"),
312
+ });
313
+ } else {
314
+ body = h("div.lt3-list",
315
+ sources.map((s) => {
316
+ const watching = !!s.watch_active;
317
+ const indexed = Number(s.success_count ?? s.indexed_count ?? 0) || 0;
318
+ const last = s.last_event_at || s.last_indexed_at;
319
+ const metaParts = [`${c.fmtNum(indexed)} indexed`];
320
+ if (last) metaParts.push(`last ${last}`);
321
+ return h("div.lt3-list__item",
322
+ icon(watching ? "folder-search" : "folder"),
323
+ h("div.lt3-list__body",
324
+ h("div.lt3-list__title", h("span.lt3-mono", s.root_path || s.path || s.id || "—")),
325
+ h("div.lt3-list__meta", metaParts.join(" · ")),
326
+ ),
327
+ h("div.lt3-row-2", { style: { "align-items": "center" } },
328
+ c.statePill(watching ? "watching" : "idle"),
329
+ watching && s.id && h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm", {
330
+ on: {
331
+ click: async (e) => {
332
+ const btn = e.currentTarget;
333
+ btn.disabled = true;
334
+ const ok = await stopWatching(s.id);
335
+ if (!ok) btn.disabled = false;
336
+ },
337
+ },
338
+ }, icon("player-stop"), "Stop watching"),
339
+ ),
340
+ );
341
+ }),
342
+ );
343
+ }
344
+
345
+ return h("div.lt3-stack-3",
346
+ watchNote,
347
+ body,
348
+ h("div.lt3-row-2", { style: { "margin-top": "var(--lt3-space-2)" } },
349
+ c.sourceBadge(res.source),
350
+ h("span.lt3-faint", { style: { "font-size": "var(--lt3-text-2xs)" } }, "Indexed and watched on this computer"),
351
+ ),
352
+ );
353
+ }
354
+
132
355
  /* ── Local memory panel (wired to /workspace/computer-memory) ─────────────── */
133
356
  function buildMemoryPanel(ctx, state) {
134
357
  const { h, icon, c } = ctx;
@@ -24,6 +24,7 @@ export async function render(ctx) {
24
24
  const probesHost = h("div", c.loading({ lines: 3 }));
25
25
 
26
26
  const embedHost = h("div", c.loading({ lines: 2 }));
27
+ const runtimeHost = h("div", c.loading({ lines: 3 }));
27
28
 
28
29
  const root = h("div.lt3-stack-6",
29
30
  c.viewHeader({
@@ -35,6 +36,13 @@ export async function render(ctx) {
35
36
  appearancePanel(ctx),
36
37
  workspacePanel(ctx),
37
38
 
39
+ c.panel({
40
+ eyebrow: "Runtime",
41
+ title: "Local readiness",
42
+ sub: "Backend, local-agent, and host signals used by Chat, Files, Search, and Models.",
43
+ children: runtimeHost,
44
+ }),
45
+
38
46
  c.panel({
39
47
  eyebrow: "Models",
40
48
  title: "Embeddings",
@@ -58,9 +66,44 @@ export async function render(ctx) {
58
66
 
59
67
  probeEndpoints(ctx, probesHost);
60
68
  renderEmbeddings(ctx, embedHost);
69
+ renderRuntime(ctx, runtimeHost);
61
70
  return root;
62
71
  }
63
72
 
73
+ async function renderRuntime(ctx, host) {
74
+ const { h, icon, api, c } = ctx;
75
+ const [health, sysinfo, models] = await Promise.all([
76
+ api.raw("/health"),
77
+ api.sysinfo(),
78
+ api.models(),
79
+ ]);
80
+ const backendLive = !!(health && health.ok);
81
+ const currentModel = models.data && models.data.current;
82
+ host.replaceChildren(
83
+ h("div.lt3-readiness",
84
+ runtimeRow(ctx, "server", "Backend API", backendLive ? `Live${health.data?.version ? ` · v${health.data.version}` : ""}` : "Unavailable", backendLive ? "ready" : "pending"),
85
+ runtimeRow(ctx, "folder-plus", "Desktop local agent", "Not available in this browser build; manual upload remains available", "idle"),
86
+ runtimeRow(ctx, "cpu", "Model runtime", currentModel ? shortModel(currentModel) : "No model loaded", currentModel ? "ready" : "pending"),
87
+ runtimeRow(ctx, "activity", "Host telemetry", sysinfo.source === "live" ? `CPU ${pct(sysinfo.data?.cpu_pct)} · RAM ${pct(sysinfo.data?.ram_pct)}` : "Unavailable", sysinfo.source === "live" ? "ready" : "idle"),
88
+ ),
89
+ h("div.lt3-code", { style: { "margin-top": "var(--lt3-space-4)" } },
90
+ [
91
+ "LATTICEAI_EMBEDDING_PROVIDER=hash | mlx | ollama | openai | custom",
92
+ "Folder watching requires the desktop local agent.",
93
+ "Cloud deployment is not reported as ready from this local-first shell.",
94
+ ].join("\n")),
95
+ );
96
+ }
97
+
98
+ function runtimeRow(ctx, ic, title, meta, state) {
99
+ const { h, icon, c } = ctx;
100
+ return h("div.lt3-readiness__row",
101
+ h("div.lt3-readiness__icon", icon(ic)),
102
+ h("div", h("div.lt3-readiness__title", title), h("div.lt3-readiness__meta", meta)),
103
+ c.statePill(state),
104
+ );
105
+ }
106
+
64
107
  /* ── Embeddings (Settings → Models → Embeddings) ────────────────────────── */
65
108
  export function embeddingStatePill({ h, c }, st) {
66
109
  const state = String(st.state || st.grade || "fallback").toLowerCase();
@@ -228,7 +271,21 @@ async function probeEndpoints({ h, icon, api, c }, host) {
228
271
  }
229
272
 
230
273
  /* ── About ──────────────────────────────────────────────────────────────── */
231
- function aboutPanel({ h, icon, c }) {
274
+ /* Version is read live from /health (which derives it from the backend's single
275
+ * source of truth, WORKSPACE_OS_VERSION) — never hard-coded in the frontend.
276
+ * If the backend is unreachable we say "unavailable" rather than inventing a
277
+ * number. */
278
+ function aboutPanel({ h, icon, c, api }) {
279
+ const versionSlot = h("dd", h("span.lt3-mono.lt3-faint", "checking…"));
280
+ (async () => {
281
+ const res = await api.raw("/health");
282
+ const v = res && res.ok && res.data && res.data.version;
283
+ versionSlot.replaceChildren(
284
+ v
285
+ ? h("span.lt3-mono", `v${String(v).replace(/^v/i, "")}`)
286
+ : h("span.lt3-mono.lt3-faint", "unavailable"),
287
+ );
288
+ })();
232
289
  return c.panel({
233
290
  eyebrow: "About",
234
291
  title: "Lattice AI",
@@ -236,7 +293,7 @@ function aboutPanel({ h, icon, c }) {
236
293
  children: h("div.lt3-stack-4",
237
294
  h("dl.lt3-keyval",
238
295
  h("dt", "Application"), h("dd", "Lattice AI"),
239
- h("dt", "Version"), h("dd", h("span.lt3-mono", "v3.1.0")),
296
+ h("dt", "Version"), versionSlot,
240
297
  h("dt", "Edition"), h("dd", "Local-first AI workspace"),
241
298
  ),
242
299
  ),
@@ -248,3 +305,14 @@ function titleCase(s) {
248
305
  s = String(s || "");
249
306
  return s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
250
307
  }
308
+
309
+ function pct(value) {
310
+ const n = Number(value);
311
+ return Number.isFinite(n) ? `${Math.round(n)}%` : "—";
312
+ }
313
+
314
+ function shortModel(id) {
315
+ const s = String(id || "");
316
+ const tail = s.includes("/") ? s.split("/").pop() : s;
317
+ return tail.length > 30 ? tail.slice(0, 29) + "…" : tail;
318
+ }