ltcai 3.0.1 → 3.2.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 (123) hide show
  1. package/README.md +54 -21
  2. package/docs/CHANGELOG.md +90 -0
  3. package/docs/V3_2_AUDIT.md +82 -0
  4. package/docs/V3_FRONTEND.md +20 -17
  5. package/docs/architecture.md +6 -0
  6. package/latticeai/__init__.py +1 -1
  7. package/latticeai/api/agent_registry.py +103 -0
  8. package/latticeai/api/auth.py +4 -1
  9. package/latticeai/api/hooks.py +113 -0
  10. package/latticeai/api/marketplace.py +13 -0
  11. package/latticeai/api/memory.py +109 -0
  12. package/latticeai/api/search.py +4 -0
  13. package/latticeai/core/agent_registry.py +234 -0
  14. package/latticeai/core/config.py +2 -0
  15. package/latticeai/core/embedding_providers.py +123 -0
  16. package/latticeai/core/hooks.py +284 -0
  17. package/latticeai/core/marketplace.py +87 -2
  18. package/latticeai/core/multi_agent.py +1 -1
  19. package/latticeai/core/workspace_os.py +1 -1
  20. package/latticeai/server_app.py +63 -6
  21. package/latticeai/services/memory_service.py +324 -0
  22. package/package.json +9 -4
  23. package/scripts/build_v3_assets.mjs +164 -0
  24. package/scripts/capture/README.md +28 -0
  25. package/scripts/capture/capture_enterprise.js +8 -0
  26. package/scripts/capture/capture_graph.js +8 -0
  27. package/scripts/capture/capture_onboarding.js +8 -0
  28. package/scripts/capture/capture_page.js +43 -0
  29. package/scripts/capture/capture_release_media.js +125 -0
  30. package/scripts/capture/capture_skills.js +8 -0
  31. package/scripts/capture/capture_workspace.js +8 -0
  32. package/scripts/generate_diagrams.py +513 -0
  33. package/scripts/lint_v3.mjs +33 -0
  34. package/scripts/release-0.3.1.sh +105 -0
  35. package/scripts/take_screenshots.js +69 -0
  36. package/scripts/validate_release_artifacts.py +167 -0
  37. package/static/account.html +9 -9
  38. package/static/activity.html +4 -4
  39. package/static/admin.html +8 -8
  40. package/static/agents.html +4 -4
  41. package/static/chat.html +9 -9
  42. package/static/css/tokens.5a595671.css +260 -0
  43. package/static/css/tokens.css +1 -1
  44. package/static/graph.html +9 -9
  45. package/static/plugins.html +4 -4
  46. package/static/sw.js +3 -1
  47. package/static/v3/asset-manifest.json +55 -0
  48. package/static/v3/css/lattice.base.e4cdd05d.css +128 -0
  49. package/static/v3/css/lattice.components.011e988b.css +447 -0
  50. package/static/v3/css/lattice.components.css +2 -2
  51. package/static/v3/css/lattice.shell.4920f42d.css +407 -0
  52. package/static/v3/css/lattice.tokens.c597ff81.css +132 -0
  53. package/static/v3/css/lattice.views.3ee19d4e.css +277 -0
  54. package/static/v3/index.html +38 -9
  55. package/static/v3/js/app.a5adc0f3.js +26 -0
  56. package/static/v3/js/core/api.603b978f.js +408 -0
  57. package/static/v3/js/core/api.js +132 -51
  58. package/static/v3/js/core/components.4c83e0a9.js +222 -0
  59. package/static/v3/js/core/components.js +9 -2
  60. package/static/v3/js/core/dom.a2773eb0.js +148 -0
  61. package/static/v3/js/core/router.584570f2.js +37 -0
  62. package/static/v3/js/core/routes.07ad6696.js +89 -0
  63. package/static/v3/js/core/routes.js +17 -1
  64. package/static/v3/js/core/shell.ea0b9ae5.js +363 -0
  65. package/static/v3/js/core/store.34ebd5e6.js +113 -0
  66. package/static/v3/js/views/admin-audit.660a1fb1.js +185 -0
  67. package/static/v3/js/views/admin-audit.js +1 -1
  68. package/static/v3/js/views/admin-permissions.a7ae5f09.js +177 -0
  69. package/static/v3/js/views/admin-permissions.js +4 -5
  70. package/static/v3/js/views/admin-policies.3658fd86.js +102 -0
  71. package/static/v3/js/views/admin-policies.js +4 -5
  72. package/static/v3/js/views/admin-private-vpc.7d342d36.js +135 -0
  73. package/static/v3/js/views/admin-private-vpc.js +2 -5
  74. package/static/v3/js/views/admin-security.07c66b72.js +180 -0
  75. package/static/v3/js/views/admin-security.js +4 -5
  76. package/static/v3/js/views/admin-users.03bac88c.js +168 -0
  77. package/static/v3/js/views/admin-users.js +6 -6
  78. package/static/v3/js/views/agents.c373d48c.js +293 -0
  79. package/static/v3/js/views/agents.js +101 -2
  80. package/static/v3/js/views/chat.718144ce.js +449 -0
  81. package/static/v3/js/views/chat.js +2 -3
  82. package/static/v3/js/views/files.4935197e.js +186 -0
  83. package/static/v3/js/views/files.js +27 -21
  84. package/static/v3/js/views/home.cdde3b32.js +119 -0
  85. package/static/v3/js/views/hooks.f3edebca.js +99 -0
  86. package/static/v3/js/views/hooks.js +99 -0
  87. package/static/v3/js/views/hybrid-search.b22b97e0.js +195 -0
  88. package/static/v3/js/views/hybrid-search.js +1 -1
  89. package/static/v3/js/views/knowledge-graph.a14ea7e7.js +237 -0
  90. package/static/v3/js/views/knowledge-graph.js +2 -3
  91. package/static/v3/js/views/marketplace.ab0583d4.js +141 -0
  92. package/static/v3/js/views/marketplace.js +141 -0
  93. package/static/v3/js/views/mcp.99b5c6a7.js +114 -0
  94. package/static/v3/js/views/mcp.js +114 -0
  95. package/static/v3/js/views/memory.d2ed7a7c.js +146 -0
  96. package/static/v3/js/views/memory.js +146 -0
  97. package/static/v3/js/views/models.a1ffa147.js +256 -0
  98. package/static/v3/js/views/models.js +17 -8
  99. package/static/v3/js/views/my-computer.1b2ff621.js +237 -0
  100. package/static/v3/js/views/my-computer.js +5 -5
  101. package/static/v3/js/views/pipeline.c522f1ce.js +157 -0
  102. package/static/v3/js/views/pipeline.js +3 -7
  103. package/static/v3/js/views/planning.9ac3e313.js +153 -0
  104. package/static/v3/js/views/planning.js +153 -0
  105. package/static/v3/js/views/settings.4f777210.js +250 -0
  106. package/static/v3/js/views/settings.js +6 -14
  107. package/static/v3/js/views/skills.c6c2f965.js +109 -0
  108. package/static/v3/js/views/skills.js +109 -0
  109. package/static/v3/js/views/tools.e4f11276.js +108 -0
  110. package/static/v3/js/views/tools.js +108 -0
  111. package/static/v3/js/views/workflows.26c57290.js +128 -0
  112. package/static/v3/js/views/workflows.js +128 -0
  113. package/static/workflows.html +4 -4
  114. package/static/workspace.html +5 -5
  115. package/docs/images/tmp_frames/frame_00.png +0 -0
  116. package/docs/images/tmp_frames/frame_01.png +0 -0
  117. package/docs/images/tmp_frames/frame_02.png +0 -0
  118. package/docs/images/tmp_frames/frame_03.png +0 -0
  119. package/docs/images/tmp_frames/hero_00.png +0 -0
  120. package/docs/images/tmp_frames/hero_01.png +0 -0
  121. package/docs/images/tmp_frames/hero_02.png +0 -0
  122. package/docs/images/tmp_frames/hero_03.png +0 -0
  123. package/static/v3/js/core/fixtures.js +0 -171
@@ -0,0 +1,293 @@
1
+ /* ============================================================================
2
+ * View: Agents — the multi-agent runtime (roles, real runs, health).
3
+ * Reads the AgentRuntime boundary (/agents/api/runtime/status): the canonical
4
+ * role roster enriched with real run counts, the live recent-runs ledger, and
5
+ * runtime health. Reports unavailable state when the runtime is unreachable.
6
+ * ========================================================================== */
7
+
8
+ export async function render(ctx) {
9
+ const { h, icon, c } = ctx;
10
+
11
+ const statHost = h("div.lt3-statrow", c.loading({ lines: 1 }));
12
+ const registryHost = h("div", c.loading({ lines: 3, block: true }));
13
+ const rosterHost = h("div", c.loading({ lines: 2, block: true }));
14
+ const runsHost = h("div", c.loading({ lines: 4 }));
15
+ const registrySrc = h("span", c.sourceBadge("pending"));
16
+ const rosterSrc = h("span", c.sourceBadge("pending"));
17
+ const runsSrc = h("span", c.sourceBadge("pending"));
18
+ const healthSlot = h("span", c.sourceBadge("pending"));
19
+
20
+ const root = h("div.lt3-stack-6",
21
+ c.viewHeader({
22
+ eyebrow: "Compute",
23
+ title: "Agents",
24
+ sub: "The multi-agent runtime: who plans, who builds, who reviews — and how work hands off between them. Every run stays local to this workspace.",
25
+ actions: [healthSlot],
26
+ }),
27
+ statHost,
28
+ h("section",
29
+ c.sectionHead("Agent Registry", registrySrc),
30
+ registryHost,
31
+ ),
32
+ h("section",
33
+ c.sectionHead("Agent roster", rosterSrc),
34
+ rosterHost,
35
+ ),
36
+ c.panel({
37
+ head: h("div.lt3-row", { style: { "justify-content": "space-between", width: "100%" } },
38
+ h("div",
39
+ h("div.lt3-eyebrow", "Activity"),
40
+ h("h3.lt3-panel__title", "Recent runs"),
41
+ ),
42
+ runsSrc,
43
+ ),
44
+ children: runsHost,
45
+ }),
46
+ );
47
+
48
+ hydrate(ctx, { statHost, rosterHost, runsHost, rosterSrc, runsSrc, healthSlot });
49
+ loadRegistry(ctx, { registryHost, registrySrc });
50
+ return root;
51
+ }
52
+
53
+ async function hydrate(ctx, hosts) {
54
+ const { h, icon, c } = ctx;
55
+ const { statHost, rosterHost, runsHost, rosterSrc, runsSrc, healthSlot } = hosts;
56
+
57
+ const res = await ctx.api.agentRuntime();
58
+ const data = res.data || {};
59
+ const agents = normalize(data.agents);
60
+ const runtime = data.runtime || {};
61
+ const health = data.health || { status: "unknown" };
62
+ const runs = Array.isArray(data.runs) ? data.runs : [];
63
+ const byId = new Map(agents.map((a) => [a.id, a.name]));
64
+
65
+ rosterSrc.replaceChildren(c.sourceBadge(res.source));
66
+ runsSrc.replaceChildren(c.sourceBadge(res.source));
67
+ healthSlot.replaceChildren(
68
+ c.statePill(health.status === "ok" ? "ready" : health.status === "degraded" ? "warn" : "idle"),
69
+ );
70
+
71
+ // ── Stat row (real runtime counts) ────────────────────────────────────
72
+ const available = agents.filter((a) => isAvailable(a.state)).length;
73
+ const totalRuns = Number(runtime.total_runs) || runs.length;
74
+ const handoffs = agents.reduce((sum, a) => sum + a.handoffs.length, 0);
75
+ statHost.replaceChildren(
76
+ c.stat({ label: "Agents", value: c.fmtNum(agents.length), icon: "robot" }),
77
+ c.stat({ label: "Available", value: c.fmtNum(available), icon: "circle-check" }),
78
+ c.stat({ label: "Total runs", value: c.fmtNum(totalRuns), icon: "player-play" }),
79
+ c.stat({ label: "Handoffs", value: c.fmtNum(handoffs), icon: "arrows-exchange" }),
80
+ );
81
+
82
+ // ── Roster grid ───────────────────────────────────────────────────────
83
+ if (!agents.length) {
84
+ rosterHost.replaceChildren(c.emptyState({
85
+ icon: "robot-off",
86
+ title: "Runtime unavailable",
87
+ body: "The agent runtime did not respond. Start the local server to see the roster.",
88
+ }));
89
+ } else {
90
+ rosterHost.replaceChildren(
91
+ h("div.lt3-grid-auto", agents.map((a) => agentCard(ctx, a, byId))),
92
+ );
93
+ }
94
+
95
+ // ── Recent runs ledger (REAL runs from the runtime) ───────────────────
96
+ const rows = runs.map((r) => ({
97
+ agent: byId.get(r.agent_id) || shortId(r.agent_id),
98
+ status: mapStatus(r.status),
99
+ time: fmtTime(r.created_at || r.completed_at),
100
+ note: runNote(r),
101
+ }));
102
+ runsHost.replaceChildren(
103
+ c.table(
104
+ [
105
+ { key: "agent", label: "Agent", render: (r) => h("div.lt3-row-2",
106
+ h("span.lt3-avatar", { style: { width: "26px", height: "26px" } }, icon("robot")),
107
+ h("b", { style: { "font-size": "var(--lt3-text-sm)" } }, r.agent),
108
+ ) },
109
+ { key: "status", label: "Status", width: "1%", render: (r) => c.statePill(r.status) },
110
+ { key: "time", label: "Started", width: "1%", render: (r) => h("span.lt3-faint", { style: { "white-space": "nowrap", "font-family": "var(--lt3-font-mono)", "font-size": "var(--lt3-text-2xs)" } }, r.time) },
111
+ { key: "note", label: "Note", render: (r) => h("span.lt3-muted", r.note) },
112
+ ],
113
+ rows,
114
+ { empty: c.emptyState({ icon: "history-off", title: "No runs yet", body: "Agent runs recorded by the runtime will appear here." }) },
115
+ ),
116
+ );
117
+ }
118
+
119
+ async function loadRegistry(ctx, hosts) {
120
+ const { h, c } = ctx;
121
+ const { registryHost, registrySrc } = hosts;
122
+ const [registryRes, capsRes] = await Promise.all([ctx.api.agentRegistry(), ctx.api.agentCapabilities()]);
123
+ const agents = normalizeRegistry(registryRes.data);
124
+ const caps = (capsRes.data && capsRes.data.capabilities) || {};
125
+ registrySrc.replaceChildren(c.sourceBadge(registryRes.source === "live" || capsRes.source === "live" ? "live" : "unavailable"));
126
+
127
+ const nameInput = h("input.lt3-input", { type: "text", placeholder: "Custom agent name", "aria-label": "Custom agent name" });
128
+ const capsInput = h("input.lt3-input", { type: "text", placeholder: "capability-a, capability-b", "aria-label": "Custom agent capabilities" });
129
+ const registerBtn = h("button.lt3-btn.lt3-btn--primary.lt3-btn--sm", { on: { click: register } }, c.icon("plus"), "Register");
130
+
131
+ const capList = Object.keys(caps).sort();
132
+ const body = h("div.lt3-stack-4",
133
+ h("div.lt3-grid-2",
134
+ h("div.lt3-field", h("label", "Name"), nameInput),
135
+ h("div.lt3-field", h("label", "Capabilities"), capsInput),
136
+ ),
137
+ h("div.lt3-row-2", registerBtn,
138
+ h("span.lt3-faint", { style: { "font-size": "var(--lt3-text-xs)" } }, "Custom agents persist in the local registry.")),
139
+ capList.length
140
+ ? h("div.lt3-cluster", capList.slice(0, 18).map((cap) => h("span.lt3-chip", c.icon("sparkles"), `${cap} (${caps[cap].length})`)))
141
+ : h("p.lt3-faint", { style: { margin: 0 } }, "Capabilities appear here when the registry is live."),
142
+ agents.length
143
+ ? h("div.lt3-grid-auto", agents.map((agent) => registryCard(ctx, agent)))
144
+ : c.emptyState({ icon: "robot-off", title: "Agent registry unavailable", body: "Start the local server to register and configure agents." }),
145
+ );
146
+ registryHost.replaceChildren(c.panel({ title: "Registry controls", sub: "Register, discover, and configure built-in or custom agents.", children: body }));
147
+
148
+ async function register() {
149
+ const name = nameInput.value.trim();
150
+ if (!name) { ctx.toast("Enter an agent name", "info"); return; }
151
+ const capabilities = capsInput.value.split(",").map((s) => s.trim()).filter(Boolean);
152
+ registerBtn.disabled = true;
153
+ const res = await ctx.api.registerAgent({ name, type: "custom", capabilities });
154
+ registerBtn.disabled = false;
155
+ if (res && res.ok) {
156
+ ctx.toast(`Registered ${name}`, "ok");
157
+ loadRegistry(ctx, hosts);
158
+ } else {
159
+ ctx.toast("Register unavailable", "err");
160
+ }
161
+ }
162
+ }
163
+
164
+ function registryCard(ctx, agent) {
165
+ const { h, c } = ctx;
166
+ return c.card(h("div.lt3-stack-3",
167
+ h("div.lt3-row", { style: { "justify-content": "space-between", "align-items": "flex-start" } },
168
+ h("div",
169
+ h("b", agent.name),
170
+ h("div.lt3-faint", { style: { "font-size": "var(--lt3-text-2xs)", "font-family": "var(--lt3-font-mono)" } }, agent.id),
171
+ ),
172
+ c.pill(agent.source === "builtin" ? "built-in" : "custom", agent.source === "builtin" ? "info" : "warn"),
173
+ ),
174
+ h("p.lt3-muted", { style: { "font-size": "var(--lt3-text-sm)", margin: 0 } }, agent.description || "No description."),
175
+ h("div.lt3-cluster", [c.statePill(agent.enabled ? "ready" : "idle"), c.pill(agent.type), c.pill(`v${agent.version || "1.0.0"}`)]),
176
+ agent.capabilities.length ? h("div.lt3-cluster", agent.capabilities.slice(0, 8).map((cap) => h("span.lt3-chip", cap))) : null,
177
+ h("div.lt3-row-2",
178
+ h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm", { on: { click: () => toggleAgent(ctx, agent) } }, c.icon(agent.enabled ? "toggle-right" : "toggle-left"), agent.enabled ? "Disable" : "Enable"),
179
+ agent.removable ? h("button.lt3-btn.lt3-btn--ghost.lt3-btn--sm", { on: { click: () => removeAgent(ctx, agent) } }, c.icon("trash"), "Remove") : null,
180
+ ),
181
+ ), { interactive: false });
182
+ }
183
+
184
+ async function toggleAgent(ctx, agent) {
185
+ const res = await ctx.api.updateAgent(agent.id, { config: agent.config || {}, enabled: !agent.enabled });
186
+ ctx.toast(res && res.ok ? `${agent.name}: ${agent.enabled ? "disabled" : "enabled"}` : "Agent update unavailable", res && res.ok ? "ok" : "err");
187
+ if (res && res.ok) ctx.navigate("agents");
188
+ }
189
+
190
+ async function removeAgent(ctx, agent) {
191
+ const res = await ctx.api.removeAgent(agent.id);
192
+ ctx.toast(res && res.ok ? `Removed ${agent.name}` : "Agent remove unavailable", res && res.ok ? "ok" : "err");
193
+ if (res && res.ok) ctx.navigate("agents");
194
+ }
195
+
196
+ /* ── Agent card ──────────────────────────────────────────────────────────── */
197
+ function agentCard(ctx, agent, byId) {
198
+ const { h, icon, c } = ctx;
199
+ return c.card(
200
+ h("div.lt3-stack-3",
201
+ h("div.lt3-row", { style: { "justify-content": "space-between", "align-items": "flex-start" } },
202
+ h("div.lt3-row-2",
203
+ h("span.lt3-avatar", { style: { width: "40px", height: "40px", "border-radius": "var(--lt3-radius-md)" } }, icon("robot")),
204
+ h("div",
205
+ h("div", { style: { "font-weight": "var(--lt3-weight-semi)", "font-size": "var(--lt3-text-md)" } }, agent.name),
206
+ h("div.lt3-faint", { style: { "font-size": "var(--lt3-text-2xs)", "font-family": "var(--lt3-font-mono)" } }, agent.id),
207
+ ),
208
+ ),
209
+ c.statePill(agent.state),
210
+ ),
211
+ h("p.lt3-muted", { style: { "font-size": "var(--lt3-text-sm)", margin: "0" } }, agent.role),
212
+ h("div.lt3-row-2", { style: { "font-size": "var(--lt3-text-xs)", color: "var(--muted)" } },
213
+ icon("player-play"),
214
+ h("b", { style: { color: "var(--text)" } }, c.fmtNum(agent.runs)),
215
+ "runs",
216
+ ),
217
+ agent.handoffs.length
218
+ ? h("div.lt3-stack-2",
219
+ h("div.lt3-eyebrow", icon("arrows-exchange"), "Hands off to"),
220
+ h("div.lt3-cluster", agent.handoffs.map((id) => {
221
+ const name = byId.get(id) || shortId(id);
222
+ return h("span.lt3-chip", icon("arrow-right"), name);
223
+ })),
224
+ )
225
+ : h("div.lt3-faint", { style: { "font-size": "var(--lt3-text-xs)" } }, "Terminal role — no handoffs"),
226
+ ),
227
+ { interactive: false },
228
+ );
229
+ }
230
+
231
+ /* ── helpers ─────────────────────────────────────────────────────────────── */
232
+ function normalize(data) {
233
+ const list = Array.isArray(data) ? data : (data && Array.isArray(data.agents) ? data.agents : []);
234
+ return list.map((a, i) => ({
235
+ id: a.id || `agent:${i}`,
236
+ name: a.name || a.id || `Agent ${i + 1}`,
237
+ role: a.role || a.description || "No role description.",
238
+ state: a.state || a.status || "idle",
239
+ runs: a.runs ?? a.run_count ?? a.runs_count ?? 0,
240
+ handoffs: Array.isArray(a.handoffs) ? a.handoffs
241
+ : Array.isArray(a.relationships) ? a.relationships : [],
242
+ }));
243
+ }
244
+
245
+ function normalizeRegistry(data) {
246
+ const list = Array.isArray(data) ? data : (data && Array.isArray(data.agents) ? data.agents : []);
247
+ return list.map((agent, i) => ({
248
+ id: agent.id || `agent:${i}`,
249
+ name: agent.name || agent.id || `Agent ${i + 1}`,
250
+ type: agent.type || "custom",
251
+ version: agent.version || "1.0.0",
252
+ description: agent.description || "",
253
+ capabilities: Array.isArray(agent.capabilities) ? agent.capabilities : [],
254
+ source: agent.source || "user",
255
+ enabled: agent.enabled !== false,
256
+ removable: !!agent.removable,
257
+ config: agent.config || {},
258
+ }));
259
+ }
260
+
261
+ const AVAILABLE_STATES = new Set(["available", "ready", "active", "ok", "idle"]);
262
+ function isAvailable(state) {
263
+ return AVAILABLE_STATES.has(String(state).toLowerCase());
264
+ }
265
+
266
+ // Map orchestrator run statuses onto the shared state-pill vocabulary.
267
+ function mapStatus(status) {
268
+ const s = String(status || "").toLowerCase();
269
+ if (s === "ok" || s === "retried_ok") return "ready";
270
+ if (s === "failed" || s === "rejected") return "failed";
271
+ if (s === "running" || s === "in_progress") return "active";
272
+ return s || "idle";
273
+ }
274
+
275
+ function runNote(r) {
276
+ const out = String(r.output || r.input || "").trim();
277
+ if (out) return out.length > 96 ? out.slice(0, 96) + "…" : out;
278
+ return `Run ${shortId(r.agent_id)} — ${r.status || "recorded"}`;
279
+ }
280
+
281
+ function fmtTime(ts) {
282
+ if (!ts) return "—";
283
+ try {
284
+ const d = new Date(ts);
285
+ if (Number.isNaN(d.getTime())) return String(ts);
286
+ return d.toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
287
+ } catch { return String(ts); }
288
+ }
289
+
290
+ function shortId(id) {
291
+ const s = String(id || "");
292
+ return s.includes(":") ? s.split(":").pop() : s;
293
+ }
@@ -2,16 +2,17 @@
2
2
  * View: Agents — the multi-agent runtime (roles, real runs, health).
3
3
  * Reads the AgentRuntime boundary (/agents/api/runtime/status): the canonical
4
4
  * role roster enriched with real run counts, the live recent-runs ledger, and
5
- * runtime health. Falls back to a clearly-badged sample roster (never a
6
- * fabricated run ledger) when the runtime is unreachable.
5
+ * runtime health. Reports unavailable state when the runtime is unreachable.
7
6
  * ========================================================================== */
8
7
 
9
8
  export async function render(ctx) {
10
9
  const { h, icon, c } = ctx;
11
10
 
12
11
  const statHost = h("div.lt3-statrow", c.loading({ lines: 1 }));
12
+ const registryHost = h("div", c.loading({ lines: 3, block: true }));
13
13
  const rosterHost = h("div", c.loading({ lines: 2, block: true }));
14
14
  const runsHost = h("div", c.loading({ lines: 4 }));
15
+ const registrySrc = h("span", c.sourceBadge("pending"));
15
16
  const rosterSrc = h("span", c.sourceBadge("pending"));
16
17
  const runsSrc = h("span", c.sourceBadge("pending"));
17
18
  const healthSlot = h("span", c.sourceBadge("pending"));
@@ -24,6 +25,10 @@ export async function render(ctx) {
24
25
  actions: [healthSlot],
25
26
  }),
26
27
  statHost,
28
+ h("section",
29
+ c.sectionHead("Agent Registry", registrySrc),
30
+ registryHost,
31
+ ),
27
32
  h("section",
28
33
  c.sectionHead("Agent roster", rosterSrc),
29
34
  rosterHost,
@@ -41,6 +46,7 @@ export async function render(ctx) {
41
46
  );
42
47
 
43
48
  hydrate(ctx, { statHost, rosterHost, runsHost, rosterSrc, runsSrc, healthSlot });
49
+ loadRegistry(ctx, { registryHost, registrySrc });
44
50
  return root;
45
51
  }
46
52
 
@@ -110,6 +116,83 @@ async function hydrate(ctx, hosts) {
110
116
  );
111
117
  }
112
118
 
119
+ async function loadRegistry(ctx, hosts) {
120
+ const { h, c } = ctx;
121
+ const { registryHost, registrySrc } = hosts;
122
+ const [registryRes, capsRes] = await Promise.all([ctx.api.agentRegistry(), ctx.api.agentCapabilities()]);
123
+ const agents = normalizeRegistry(registryRes.data);
124
+ const caps = (capsRes.data && capsRes.data.capabilities) || {};
125
+ registrySrc.replaceChildren(c.sourceBadge(registryRes.source === "live" || capsRes.source === "live" ? "live" : "unavailable"));
126
+
127
+ const nameInput = h("input.lt3-input", { type: "text", placeholder: "Custom agent name", "aria-label": "Custom agent name" });
128
+ const capsInput = h("input.lt3-input", { type: "text", placeholder: "capability-a, capability-b", "aria-label": "Custom agent capabilities" });
129
+ const registerBtn = h("button.lt3-btn.lt3-btn--primary.lt3-btn--sm", { on: { click: register } }, c.icon("plus"), "Register");
130
+
131
+ const capList = Object.keys(caps).sort();
132
+ const body = h("div.lt3-stack-4",
133
+ h("div.lt3-grid-2",
134
+ h("div.lt3-field", h("label", "Name"), nameInput),
135
+ h("div.lt3-field", h("label", "Capabilities"), capsInput),
136
+ ),
137
+ h("div.lt3-row-2", registerBtn,
138
+ h("span.lt3-faint", { style: { "font-size": "var(--lt3-text-xs)" } }, "Custom agents persist in the local registry.")),
139
+ capList.length
140
+ ? h("div.lt3-cluster", capList.slice(0, 18).map((cap) => h("span.lt3-chip", c.icon("sparkles"), `${cap} (${caps[cap].length})`)))
141
+ : h("p.lt3-faint", { style: { margin: 0 } }, "Capabilities appear here when the registry is live."),
142
+ agents.length
143
+ ? h("div.lt3-grid-auto", agents.map((agent) => registryCard(ctx, agent)))
144
+ : c.emptyState({ icon: "robot-off", title: "Agent registry unavailable", body: "Start the local server to register and configure agents." }),
145
+ );
146
+ registryHost.replaceChildren(c.panel({ title: "Registry controls", sub: "Register, discover, and configure built-in or custom agents.", children: body }));
147
+
148
+ async function register() {
149
+ const name = nameInput.value.trim();
150
+ if (!name) { ctx.toast("Enter an agent name", "info"); return; }
151
+ const capabilities = capsInput.value.split(",").map((s) => s.trim()).filter(Boolean);
152
+ registerBtn.disabled = true;
153
+ const res = await ctx.api.registerAgent({ name, type: "custom", capabilities });
154
+ registerBtn.disabled = false;
155
+ if (res && res.ok) {
156
+ ctx.toast(`Registered ${name}`, "ok");
157
+ loadRegistry(ctx, hosts);
158
+ } else {
159
+ ctx.toast("Register unavailable", "err");
160
+ }
161
+ }
162
+ }
163
+
164
+ function registryCard(ctx, agent) {
165
+ const { h, c } = ctx;
166
+ return c.card(h("div.lt3-stack-3",
167
+ h("div.lt3-row", { style: { "justify-content": "space-between", "align-items": "flex-start" } },
168
+ h("div",
169
+ h("b", agent.name),
170
+ h("div.lt3-faint", { style: { "font-size": "var(--lt3-text-2xs)", "font-family": "var(--lt3-font-mono)" } }, agent.id),
171
+ ),
172
+ c.pill(agent.source === "builtin" ? "built-in" : "custom", agent.source === "builtin" ? "info" : "warn"),
173
+ ),
174
+ h("p.lt3-muted", { style: { "font-size": "var(--lt3-text-sm)", margin: 0 } }, agent.description || "No description."),
175
+ h("div.lt3-cluster", [c.statePill(agent.enabled ? "ready" : "idle"), c.pill(agent.type), c.pill(`v${agent.version || "1.0.0"}`)]),
176
+ agent.capabilities.length ? h("div.lt3-cluster", agent.capabilities.slice(0, 8).map((cap) => h("span.lt3-chip", cap))) : null,
177
+ h("div.lt3-row-2",
178
+ h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm", { on: { click: () => toggleAgent(ctx, agent) } }, c.icon(agent.enabled ? "toggle-right" : "toggle-left"), agent.enabled ? "Disable" : "Enable"),
179
+ agent.removable ? h("button.lt3-btn.lt3-btn--ghost.lt3-btn--sm", { on: { click: () => removeAgent(ctx, agent) } }, c.icon("trash"), "Remove") : null,
180
+ ),
181
+ ), { interactive: false });
182
+ }
183
+
184
+ async function toggleAgent(ctx, agent) {
185
+ const res = await ctx.api.updateAgent(agent.id, { config: agent.config || {}, enabled: !agent.enabled });
186
+ ctx.toast(res && res.ok ? `${agent.name}: ${agent.enabled ? "disabled" : "enabled"}` : "Agent update unavailable", res && res.ok ? "ok" : "err");
187
+ if (res && res.ok) ctx.navigate("agents");
188
+ }
189
+
190
+ async function removeAgent(ctx, agent) {
191
+ const res = await ctx.api.removeAgent(agent.id);
192
+ ctx.toast(res && res.ok ? `Removed ${agent.name}` : "Agent remove unavailable", res && res.ok ? "ok" : "err");
193
+ if (res && res.ok) ctx.navigate("agents");
194
+ }
195
+
113
196
  /* ── Agent card ──────────────────────────────────────────────────────────── */
114
197
  function agentCard(ctx, agent, byId) {
115
198
  const { h, icon, c } = ctx;
@@ -159,6 +242,22 @@ function normalize(data) {
159
242
  }));
160
243
  }
161
244
 
245
+ function normalizeRegistry(data) {
246
+ const list = Array.isArray(data) ? data : (data && Array.isArray(data.agents) ? data.agents : []);
247
+ return list.map((agent, i) => ({
248
+ id: agent.id || `agent:${i}`,
249
+ name: agent.name || agent.id || `Agent ${i + 1}`,
250
+ type: agent.type || "custom",
251
+ version: agent.version || "1.0.0",
252
+ description: agent.description || "",
253
+ capabilities: Array.isArray(agent.capabilities) ? agent.capabilities : [],
254
+ source: agent.source || "user",
255
+ enabled: agent.enabled !== false,
256
+ removable: !!agent.removable,
257
+ config: agent.config || {},
258
+ }));
259
+ }
260
+
162
261
  const AVAILABLE_STATES = new Set(["available", "ready", "active", "ok", "idle"]);
163
262
  function isAvailable(state) {
164
263
  return AVAILABLE_STATES.has(String(state).toLowerCase());