ltcai 3.1.0 → 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 (44) hide show
  1. package/README.md +34 -8
  2. package/docs/CHANGELOG.md +53 -0
  3. package/docs/V3_2_AUDIT.md +82 -0
  4. package/docs/V3_FRONTEND.md +1 -1
  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/hooks.py +113 -0
  9. package/latticeai/api/marketplace.py +13 -0
  10. package/latticeai/api/memory.py +109 -0
  11. package/latticeai/core/agent_registry.py +234 -0
  12. package/latticeai/core/hooks.py +284 -0
  13. package/latticeai/core/marketplace.py +87 -2
  14. package/latticeai/core/multi_agent.py +1 -1
  15. package/latticeai/core/workspace_os.py +1 -1
  16. package/latticeai/server_app.py +41 -0
  17. package/latticeai/services/memory_service.py +324 -0
  18. package/package.json +2 -2
  19. package/scripts/build_v3_assets.mjs +1 -1
  20. package/static/v3/asset-manifest.json +16 -8
  21. package/static/v3/js/{app.46fb61d9.js → app.a5adc0f3.js} +1 -1
  22. package/static/v3/js/core/{api.22a41d42.js → api.603b978f.js} +64 -0
  23. package/static/v3/js/core/api.js +64 -0
  24. package/static/v3/js/core/{routes.f935dd50.js → routes.07ad6696.js} +11 -0
  25. package/static/v3/js/core/routes.js +11 -0
  26. package/static/v3/js/core/{shell.1b6199d6.js → shell.ea0b9ae5.js} +2 -2
  27. package/static/v3/js/views/{agents.14e48bdd.js → agents.c373d48c.js} +100 -0
  28. package/static/v3/js/views/agents.js +100 -0
  29. package/static/v3/js/views/hooks.f3edebca.js +99 -0
  30. package/static/v3/js/views/hooks.js +99 -0
  31. package/static/v3/js/views/marketplace.ab0583d4.js +141 -0
  32. package/static/v3/js/views/marketplace.js +141 -0
  33. package/static/v3/js/views/mcp.99b5c6a7.js +114 -0
  34. package/static/v3/js/views/mcp.js +114 -0
  35. package/static/v3/js/views/memory.d2ed7a7c.js +146 -0
  36. package/static/v3/js/views/memory.js +146 -0
  37. package/static/v3/js/views/planning.9ac3e313.js +153 -0
  38. package/static/v3/js/views/planning.js +153 -0
  39. package/static/v3/js/views/skills.c6c2f965.js +109 -0
  40. package/static/v3/js/views/skills.js +109 -0
  41. package/static/v3/js/views/tools.e4f11276.js +108 -0
  42. package/static/v3/js/views/tools.js +108 -0
  43. package/static/v3/js/views/workflows.26c57290.js +128 -0
  44. package/static/v3/js/views/workflows.js +128 -0
@@ -0,0 +1,146 @@
1
+ /* ============================================================================
2
+ * View: Memory — the long-term memory platform + Memory Manager.
3
+ * Reads /api/memory/manager (usage / sources / health / size / type) and offers
4
+ * recall, inspect, prune, compact, rebuild, and clear. Every number comes from a
5
+ * real store; tiers with no backing report unavailable.
6
+ * ========================================================================== */
7
+
8
+ const TIER_ICON = {
9
+ workspace: "building-warehouse", project: "folders", agent: "robot",
10
+ conversation: "messages", graph: "chart-dots-3", vector: "grid-dots",
11
+ };
12
+
13
+ export async function render(ctx) {
14
+ const { h, c } = ctx;
15
+
16
+ const srcBadge = h("span", c.sourceBadge("pending"));
17
+ const statHost = h("div.lt3-statrow", c.loading({ lines: 1 }));
18
+ const sourcesHost = h("div", c.loading({ lines: 3, block: true }));
19
+ const recallHost = h("div");
20
+ const inspectHost = h("div", h("p.lt3-faint", { style: { margin: 0 } }, "Pick a tier to inspect its contents."));
21
+
22
+ const recallInput = h("input.lt3-input", { type: "text", placeholder: "Recall across every memory tier…", "aria-label": "Recall memory" });
23
+
24
+ const root = h("div.lt3-stack-6",
25
+ c.viewHeader({
26
+ eyebrow: "Retrieval",
27
+ title: "Memory",
28
+ sub: "Long-term memory unified across workspace, project, agent, conversation, graph, and vector tiers — recall, inspect, and maintain it without leaving /app.",
29
+ actions: [srcBadge],
30
+ }),
31
+ statHost,
32
+ c.panel({
33
+ title: "Recall", sub: "Unified retrieval across the memory tiers.",
34
+ children: h("div.lt3-stack-3",
35
+ h("div.lt3-row-2",
36
+ recallInput,
37
+ h("button.lt3-btn.lt3-btn--primary", { on: { click: doRecall } }, c.icon("search"), "Recall"),
38
+ ),
39
+ recallHost,
40
+ ),
41
+ }),
42
+ h("section",
43
+ c.sectionHead("Memory sources", h("div.lt3-row-2",
44
+ h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm", { on: { click: () => act("compact") } }, c.icon("arrows-minimize"), "Compact"),
45
+ h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm", { on: { click: () => act("rebuild") } }, c.icon("refresh"), "Rebuild vectors"),
46
+ )),
47
+ sourcesHost,
48
+ ),
49
+ c.panel({ title: "Inspect tier", children: h("div.lt3-stack-3", tierTabs(ctx, inspectHost), inspectHost) }),
50
+ );
51
+
52
+ hydrate();
53
+ return root;
54
+
55
+ async function hydrate() {
56
+ const res = await ctx.api.memoryManager();
57
+ const data = res.data || {};
58
+ const sources = Array.isArray(data.sources) ? data.sources : [];
59
+ srcBadge.replaceChildren(c.sourceBadge(res.source));
60
+
61
+ if (!sources.length) {
62
+ statHost.replaceChildren(c.stat({ label: "Memory", value: "—", icon: "brain" }));
63
+ sourcesHost.replaceChildren(c.emptyState({ icon: "database-off", title: "Memory unavailable", body: "Start the backend to read the memory platform." }));
64
+ return;
65
+ }
66
+ const usage = data.usage || {};
67
+ statHost.replaceChildren(
68
+ c.stat({ label: "Total items", value: c.fmtNum(usage.total_items), icon: "stack-2" }),
69
+ c.stat({ label: "On disk", value: fmtBytes(usage.total_bytes), icon: "database" }),
70
+ c.stat({ label: "Tiers", value: c.fmtNum((data.tiers || []).length), icon: "layers" }),
71
+ c.stat({ label: "Health", value: data.health || "—", icon: "heartbeat" }),
72
+ );
73
+ sourcesHost.replaceChildren(c.table(
74
+ [
75
+ { key: "label", label: "Source", render: (r) => h("div.lt3-row-2", c.icon(TIER_ICON[r.type] || "circle"), h("b", r.label)) },
76
+ { key: "type", label: "Type", width: "1%", render: (r) => c.pill(r.type) },
77
+ { key: "count", label: "Items", width: "1%", render: (r) => h("span", { style: { "font-variant-numeric": "tabular-nums" } }, r.count == null ? "—" : c.fmtNum(r.count)) },
78
+ { key: "size", label: "Size", width: "1%", render: (r) => h("span.lt3-faint", r.size_bytes ? fmtBytes(r.size_bytes) : "—") },
79
+ { key: "health", label: "Health", width: "1%", render: (r) => c.statePill(r.health === "ok" ? "ready" : r.health === "unavailable" ? "failed" : "idle") },
80
+ { key: "detail", label: "Detail", render: (r) => h("span.lt3-muted", r.detail || "") },
81
+ ],
82
+ sources,
83
+ ));
84
+ }
85
+
86
+ async function doRecall() {
87
+ const q = recallInput.value.trim();
88
+ recallHost.replaceChildren(c.loading({ lines: 2 }));
89
+ const res = await ctx.api.memoryRecall(q, 25);
90
+ const items = (res.data && res.data.results) || [];
91
+ if (!res.ok) { recallHost.replaceChildren(c.banner("Recall is unavailable — start the backend.", "warn")); return; }
92
+ if (!items.length) { recallHost.replaceChildren(c.emptyState({ icon: "search-off", title: "No matches", body: "Nothing recalled for that query yet." })); return; }
93
+ recallHost.replaceChildren(h("div.lt3-stack-2", items.map((it) => c.card(
94
+ h("div.lt3-stack-2",
95
+ h("div.lt3-row", { style: { "justify-content": "space-between" } }, h("b", it.title || "memory"), c.pill(it.source, it.source === "graph" ? "info" : "")),
96
+ h("p.lt3-muted", { style: { margin: 0, "font-size": "var(--lt3-text-sm)" } }, it.snippet || ""),
97
+ ), { flat: true },
98
+ ))));
99
+ }
100
+
101
+ async function act(kind) {
102
+ const fn = kind === "compact" ? ctx.api.memoryCompact() : ctx.api.memoryRebuild("vector");
103
+ const res = await fn;
104
+ if (res && (res.ok || res.data)) {
105
+ const d = res.data || res;
106
+ const msg = kind === "compact" ? `Compacted ${d.compacted ?? 0} duplicate memories` : `Rebuild: ${d.status || "ok"}`;
107
+ ctx.toast(msg, d.status === "error" || d.status === "unavailable" ? "err" : "ok");
108
+ } else {
109
+ ctx.toast(`${kind} unavailable`, "err");
110
+ }
111
+ hydrate();
112
+ }
113
+
114
+ function tierTabs(ctx2, host) {
115
+ const tiers = ["workspace", "project", "agent", "conversation", "graph", "vector"];
116
+ let active = null;
117
+ return c.segmented(tiers.map((t) => ({ key: t, label: t })), active, async (key) => {
118
+ host.replaceChildren(c.loading({ lines: 2 }));
119
+ const res = await ctx2.api.memoryInspect(key, 50);
120
+ const d = res.data || {};
121
+ if (key === "graph") { host.replaceChildren(jsonBlock(ctx2, d.stats || {}, d.available)); return; }
122
+ if (key === "vector") { host.replaceChildren(jsonBlock(ctx2, d.index || {}, d.available)); return; }
123
+ const items = d.items || [];
124
+ if (!items.length) { host.replaceChildren(c.emptyState({ icon: "inbox", title: `No ${key} memory`, body: "This tier has no items yet." })); return; }
125
+ host.replaceChildren(h("div.lt3-stack-2", items.slice(0, 50).map((m) => c.card(
126
+ h("div.lt3-stack-2",
127
+ h("div.lt3-row-2", m.kind ? c.pill(m.kind) : null, h("b", { style: { "font-size": "var(--lt3-text-sm)" } }, m.title || m.id || "item")),
128
+ h("p.lt3-muted", { style: { margin: 0, "font-size": "var(--lt3-text-sm)" } }, m.content || (m.messages != null ? `${m.messages} messages` : "")),
129
+ ), { flat: true },
130
+ ))));
131
+ });
132
+ }
133
+
134
+ function jsonBlock(ctx2, obj, available) {
135
+ if (!available) return c.emptyState({ icon: "database-off", title: "Unavailable", body: "This tier is disabled or empty." });
136
+ return h("pre", { style: { margin: 0, "white-space": "pre-wrap", "font-family": "var(--lt3-font-mono)", "font-size": "var(--lt3-text-xs)", color: "var(--muted)" } }, JSON.stringify(obj, null, 2));
137
+ }
138
+ }
139
+
140
+ function fmtBytes(n) {
141
+ const v = Number(n) || 0;
142
+ if (v <= 0) return "—";
143
+ if (v >= 1 << 20) return (v / (1 << 20)).toFixed(1) + " MB";
144
+ if (v >= 1 << 10) return (v / (1 << 10)).toFixed(1) + " KB";
145
+ return v + " B";
146
+ }
@@ -0,0 +1,153 @@
1
+ /* ============================================================================
2
+ * View: Planning — autonomous goal-based planning (goal → plan → execute →
3
+ * review → replan). Drives the real AgentRuntime (/agents/api/run) and renders
4
+ * the generated plan, execution, review, and replanning (retry) decisions, with
5
+ * inspect / replay over recent runs. Synchronous runs are terminal — surfaced
6
+ * honestly rather than faking a pause/stop.
7
+ * ========================================================================== */
8
+
9
+ const FLOW = ["Goal", "Plan", "Execute", "Review", "Replan"];
10
+
11
+ export async function render(ctx) {
12
+ const { h, c } = ctx;
13
+
14
+ const goalInput = h("textarea.lt3-textarea", { rows: "2", placeholder: "Describe a goal — the planner decomposes it, the executor runs it, the reviewer approves or requests a replan.", "aria-label": "Goal" });
15
+ const runBtn = h("button.lt3-btn.lt3-btn--primary", { on: { click: runGoal } }, c.icon("player-play"), "Generate plan & run");
16
+ const resultHost = h("div");
17
+ const runsHost = h("div", c.loading({ lines: 3 }));
18
+ const runsSrc = h("span", c.sourceBadge("pending"));
19
+
20
+ const root = h("div.lt3-stack-6",
21
+ c.viewHeader({
22
+ eyebrow: "Compute",
23
+ title: "Autonomous Planning",
24
+ sub: "Set a goal; the multi-agent runtime plans, executes, reviews, and replans on failure. Every plan, decision, and retry is inspectable and replayable.",
25
+ }),
26
+ flowLegend(ctx),
27
+ c.panel({
28
+ title: "New plan", sub: "Runs locally through planner → executor → reviewer with bounded retries.",
29
+ children: h("div.lt3-stack-3", goalInput, h("div.lt3-row-2", runBtn,
30
+ h("span.lt3-faint", { style: { "font-size": "var(--lt3-text-xs)" } }, "Safeguards: bounded retries, redacted context, replayable timeline."))),
31
+ }),
32
+ resultHost,
33
+ c.panel({
34
+ head: h("div.lt3-row", { style: { "justify-content": "space-between", width: "100%" } },
35
+ h("div", h("div.lt3-eyebrow", "History"), h("h3.lt3-panel__title", "Recent plans & runs")), runsSrc),
36
+ children: runsHost,
37
+ }),
38
+ );
39
+
40
+ loadRuns();
41
+ return root;
42
+
43
+ function flowLegend(ctx2) {
44
+ return h("div.lt3-cluster", { style: { gap: "var(--lt3-space-2)" } }, FLOW.map((step, i) =>
45
+ h("div.lt3-row-2", { style: { gap: "var(--lt3-space-2)" } },
46
+ c.pill(step, i === FLOW.length - 1 ? "warn" : "info"),
47
+ i < FLOW.length - 1 ? c.icon("arrow-right") : null,
48
+ )));
49
+ }
50
+
51
+ async function runGoal() {
52
+ const goal = goalInput.value.trim();
53
+ if (!goal) { ctx.toast("Enter a goal first", "info"); return; }
54
+ runBtn.disabled = true;
55
+ resultHost.replaceChildren(c.panel({ title: "Planning…", children: c.loading({ lines: 4, block: true }) }));
56
+ const res = await ctx.api.runAgent(goal, ["planner", "executor", "reviewer"]);
57
+ runBtn.disabled = false;
58
+ if (!res || !res.ok || !res.data) {
59
+ resultHost.replaceChildren(c.banner("Planning is unavailable — start the local server and load a model.", "warn"));
60
+ return;
61
+ }
62
+ resultHost.replaceChildren(renderResult(ctx, res.data.result || res.data));
63
+ loadRuns();
64
+ }
65
+
66
+ async function loadRuns() {
67
+ const res = await ctx.api.agentRuntime();
68
+ runsSrc.replaceChildren(c.sourceBadge(res.source));
69
+ const runs = (res.data && res.data.runs) || [];
70
+ if (!runs.length) {
71
+ runsHost.replaceChildren(c.emptyState({ icon: "history-off", title: "No plans yet", body: "Generated plans and their runs will appear here." }));
72
+ return;
73
+ }
74
+ runsHost.replaceChildren(c.table(
75
+ [
76
+ { key: "status", label: "Status", width: "1%", render: (r) => c.statePill(mapStatus(r.status)) },
77
+ { key: "goal", label: "Goal / output", render: (r) => h("span.lt3-muted", trunc(r.output || r.input || r.agent_id, 90)) },
78
+ { key: "retries", label: "Retries", width: "1%", render: (r) => h("span.lt3-faint", String(r.retries ?? (r.retry_history ? r.retry_history.length : 0))) },
79
+ { key: "act", label: "", width: "1%", render: (r) => h("button.lt3-btn.lt3-btn--ghost.lt3-btn--sm", { on: { click: () => replay(r) } }, c.icon("player-track-next"), "Replay") },
80
+ ],
81
+ runs.slice(0, 20),
82
+ ));
83
+ }
84
+
85
+ async function replay(run) {
86
+ const id = run.id || run.run_id;
87
+ if (!id) { ctx.toast("Run id unavailable", "info"); return; }
88
+ const res = await ctx.api.agentRunReplay(id);
89
+ if (res && res.ok && res.data) {
90
+ resultHost.replaceChildren(c.panel({ title: `Replay · ${id}`, children: timeline(ctx, (res.data.replay && res.data.replay.timeline) || res.data.replay || []) }));
91
+ resultHost.scrollIntoView({ behavior: "smooth", block: "start" });
92
+ } else {
93
+ ctx.toast("Replay unavailable", "err");
94
+ }
95
+ }
96
+ }
97
+
98
+ function renderResult(ctx, result) {
99
+ const { h, c } = ctx;
100
+ const plan = result.plan || [];
101
+ const review = result.review || {};
102
+ const retries = result.retry_history || [];
103
+ const ok = (review.outcome || "").toLowerCase() === "approve" || (review.verdict || "").toLowerCase() === "pass";
104
+ return c.panel({
105
+ head: h("div.lt3-row", { style: { "justify-content": "space-between", width: "100%" } },
106
+ h("div", h("div.lt3-eyebrow", "Result"), h("h3.lt3-panel__title", "Plan & execution")),
107
+ c.statePill(ok ? "ready" : "warn")),
108
+ children: h("div.lt3-stack-3",
109
+ h("div",
110
+ h("div.lt3-eyebrow", c.icon("list-check"), "Plan"),
111
+ plan.length
112
+ ? h("ol", { style: { margin: "var(--lt3-space-2) 0 0", "padding-left": "1.2em" } }, plan.map((s) =>
113
+ h("li", { style: { "margin-bottom": "4px" } }, h("span", s.description || s.name || `Step`), " ", c.statePill(s.status || "planned"))))
114
+ : h("p.lt3-faint", { style: { margin: 0 } }, "No plan steps."),
115
+ ),
116
+ h("div",
117
+ h("div.lt3-eyebrow", c.icon("checkup-list"), "Review"),
118
+ h("p.lt3-muted", { style: { margin: "var(--lt3-space-2) 0 0" } }, review.reason || "—"),
119
+ ),
120
+ retries.length
121
+ ? h("div",
122
+ h("div.lt3-eyebrow", c.icon("refresh-alert"), `Replanning (${retries.length})`),
123
+ h("div.lt3-stack-2", retries.map((r) => c.card(h("div",
124
+ h("b", `Retry #${r.retry}`), h("p.lt3-muted", { style: { margin: 0, "font-size": "var(--lt3-text-sm)" } }, r.reason || "")), { flat: true }))),
125
+ )
126
+ : null,
127
+ result.timeline ? h("details", h("summary.lt3-faint", { style: { cursor: "pointer" } }, "Timeline"), timeline(ctx, result.timeline)) : null,
128
+ ),
129
+ });
130
+ }
131
+
132
+ function timeline(ctx, events) {
133
+ const { h, c } = ctx;
134
+ const list = Array.isArray(events) ? events : [];
135
+ if (!list.length) return h("p.lt3-faint", { style: { margin: 0 } }, "No timeline events.");
136
+ return h("div.lt3-stack-2", { style: { "margin-top": "var(--lt3-space-2)" } }, list.slice(0, 60).map((e) =>
137
+ h("div.lt3-row-2", { style: { "font-size": "var(--lt3-text-xs)" } },
138
+ c.pill(e.event || "event", "", { dot: true }),
139
+ h("span.lt3-faint", e.role || e.from || ""),
140
+ e.to ? c.icon("arrow-right") : null,
141
+ e.to ? h("span.lt3-faint", e.to) : null,
142
+ )));
143
+ }
144
+
145
+ function mapStatus(s) {
146
+ const v = String(s || "").toLowerCase();
147
+ if (v === "ok" || v === "retried_ok") return "ready";
148
+ if (v === "failed" || v === "rejected") return "failed";
149
+ if (v === "running" || v === "in_progress") return "active";
150
+ return v || "idle";
151
+ }
152
+
153
+ function trunc(s, n) { s = String(s || ""); return s.length > n ? s.slice(0, n) + "…" : s; }
@@ -0,0 +1,153 @@
1
+ /* ============================================================================
2
+ * View: Planning — autonomous goal-based planning (goal → plan → execute →
3
+ * review → replan). Drives the real AgentRuntime (/agents/api/run) and renders
4
+ * the generated plan, execution, review, and replanning (retry) decisions, with
5
+ * inspect / replay over recent runs. Synchronous runs are terminal — surfaced
6
+ * honestly rather than faking a pause/stop.
7
+ * ========================================================================== */
8
+
9
+ const FLOW = ["Goal", "Plan", "Execute", "Review", "Replan"];
10
+
11
+ export async function render(ctx) {
12
+ const { h, c } = ctx;
13
+
14
+ const goalInput = h("textarea.lt3-textarea", { rows: "2", placeholder: "Describe a goal — the planner decomposes it, the executor runs it, the reviewer approves or requests a replan.", "aria-label": "Goal" });
15
+ const runBtn = h("button.lt3-btn.lt3-btn--primary", { on: { click: runGoal } }, c.icon("player-play"), "Generate plan & run");
16
+ const resultHost = h("div");
17
+ const runsHost = h("div", c.loading({ lines: 3 }));
18
+ const runsSrc = h("span", c.sourceBadge("pending"));
19
+
20
+ const root = h("div.lt3-stack-6",
21
+ c.viewHeader({
22
+ eyebrow: "Compute",
23
+ title: "Autonomous Planning",
24
+ sub: "Set a goal; the multi-agent runtime plans, executes, reviews, and replans on failure. Every plan, decision, and retry is inspectable and replayable.",
25
+ }),
26
+ flowLegend(ctx),
27
+ c.panel({
28
+ title: "New plan", sub: "Runs locally through planner → executor → reviewer with bounded retries.",
29
+ children: h("div.lt3-stack-3", goalInput, h("div.lt3-row-2", runBtn,
30
+ h("span.lt3-faint", { style: { "font-size": "var(--lt3-text-xs)" } }, "Safeguards: bounded retries, redacted context, replayable timeline."))),
31
+ }),
32
+ resultHost,
33
+ c.panel({
34
+ head: h("div.lt3-row", { style: { "justify-content": "space-between", width: "100%" } },
35
+ h("div", h("div.lt3-eyebrow", "History"), h("h3.lt3-panel__title", "Recent plans & runs")), runsSrc),
36
+ children: runsHost,
37
+ }),
38
+ );
39
+
40
+ loadRuns();
41
+ return root;
42
+
43
+ function flowLegend(ctx2) {
44
+ return h("div.lt3-cluster", { style: { gap: "var(--lt3-space-2)" } }, FLOW.map((step, i) =>
45
+ h("div.lt3-row-2", { style: { gap: "var(--lt3-space-2)" } },
46
+ c.pill(step, i === FLOW.length - 1 ? "warn" : "info"),
47
+ i < FLOW.length - 1 ? c.icon("arrow-right") : null,
48
+ )));
49
+ }
50
+
51
+ async function runGoal() {
52
+ const goal = goalInput.value.trim();
53
+ if (!goal) { ctx.toast("Enter a goal first", "info"); return; }
54
+ runBtn.disabled = true;
55
+ resultHost.replaceChildren(c.panel({ title: "Planning…", children: c.loading({ lines: 4, block: true }) }));
56
+ const res = await ctx.api.runAgent(goal, ["planner", "executor", "reviewer"]);
57
+ runBtn.disabled = false;
58
+ if (!res || !res.ok || !res.data) {
59
+ resultHost.replaceChildren(c.banner("Planning is unavailable — start the local server and load a model.", "warn"));
60
+ return;
61
+ }
62
+ resultHost.replaceChildren(renderResult(ctx, res.data.result || res.data));
63
+ loadRuns();
64
+ }
65
+
66
+ async function loadRuns() {
67
+ const res = await ctx.api.agentRuntime();
68
+ runsSrc.replaceChildren(c.sourceBadge(res.source));
69
+ const runs = (res.data && res.data.runs) || [];
70
+ if (!runs.length) {
71
+ runsHost.replaceChildren(c.emptyState({ icon: "history-off", title: "No plans yet", body: "Generated plans and their runs will appear here." }));
72
+ return;
73
+ }
74
+ runsHost.replaceChildren(c.table(
75
+ [
76
+ { key: "status", label: "Status", width: "1%", render: (r) => c.statePill(mapStatus(r.status)) },
77
+ { key: "goal", label: "Goal / output", render: (r) => h("span.lt3-muted", trunc(r.output || r.input || r.agent_id, 90)) },
78
+ { key: "retries", label: "Retries", width: "1%", render: (r) => h("span.lt3-faint", String(r.retries ?? (r.retry_history ? r.retry_history.length : 0))) },
79
+ { key: "act", label: "", width: "1%", render: (r) => h("button.lt3-btn.lt3-btn--ghost.lt3-btn--sm", { on: { click: () => replay(r) } }, c.icon("player-track-next"), "Replay") },
80
+ ],
81
+ runs.slice(0, 20),
82
+ ));
83
+ }
84
+
85
+ async function replay(run) {
86
+ const id = run.id || run.run_id;
87
+ if (!id) { ctx.toast("Run id unavailable", "info"); return; }
88
+ const res = await ctx.api.agentRunReplay(id);
89
+ if (res && res.ok && res.data) {
90
+ resultHost.replaceChildren(c.panel({ title: `Replay · ${id}`, children: timeline(ctx, (res.data.replay && res.data.replay.timeline) || res.data.replay || []) }));
91
+ resultHost.scrollIntoView({ behavior: "smooth", block: "start" });
92
+ } else {
93
+ ctx.toast("Replay unavailable", "err");
94
+ }
95
+ }
96
+ }
97
+
98
+ function renderResult(ctx, result) {
99
+ const { h, c } = ctx;
100
+ const plan = result.plan || [];
101
+ const review = result.review || {};
102
+ const retries = result.retry_history || [];
103
+ const ok = (review.outcome || "").toLowerCase() === "approve" || (review.verdict || "").toLowerCase() === "pass";
104
+ return c.panel({
105
+ head: h("div.lt3-row", { style: { "justify-content": "space-between", width: "100%" } },
106
+ h("div", h("div.lt3-eyebrow", "Result"), h("h3.lt3-panel__title", "Plan & execution")),
107
+ c.statePill(ok ? "ready" : "warn")),
108
+ children: h("div.lt3-stack-3",
109
+ h("div",
110
+ h("div.lt3-eyebrow", c.icon("list-check"), "Plan"),
111
+ plan.length
112
+ ? h("ol", { style: { margin: "var(--lt3-space-2) 0 0", "padding-left": "1.2em" } }, plan.map((s) =>
113
+ h("li", { style: { "margin-bottom": "4px" } }, h("span", s.description || s.name || `Step`), " ", c.statePill(s.status || "planned"))))
114
+ : h("p.lt3-faint", { style: { margin: 0 } }, "No plan steps."),
115
+ ),
116
+ h("div",
117
+ h("div.lt3-eyebrow", c.icon("checkup-list"), "Review"),
118
+ h("p.lt3-muted", { style: { margin: "var(--lt3-space-2) 0 0" } }, review.reason || "—"),
119
+ ),
120
+ retries.length
121
+ ? h("div",
122
+ h("div.lt3-eyebrow", c.icon("refresh-alert"), `Replanning (${retries.length})`),
123
+ h("div.lt3-stack-2", retries.map((r) => c.card(h("div",
124
+ h("b", `Retry #${r.retry}`), h("p.lt3-muted", { style: { margin: 0, "font-size": "var(--lt3-text-sm)" } }, r.reason || "")), { flat: true }))),
125
+ )
126
+ : null,
127
+ result.timeline ? h("details", h("summary.lt3-faint", { style: { cursor: "pointer" } }, "Timeline"), timeline(ctx, result.timeline)) : null,
128
+ ),
129
+ });
130
+ }
131
+
132
+ function timeline(ctx, events) {
133
+ const { h, c } = ctx;
134
+ const list = Array.isArray(events) ? events : [];
135
+ if (!list.length) return h("p.lt3-faint", { style: { margin: 0 } }, "No timeline events.");
136
+ return h("div.lt3-stack-2", { style: { "margin-top": "var(--lt3-space-2)" } }, list.slice(0, 60).map((e) =>
137
+ h("div.lt3-row-2", { style: { "font-size": "var(--lt3-text-xs)" } },
138
+ c.pill(e.event || "event", "", { dot: true }),
139
+ h("span.lt3-faint", e.role || e.from || ""),
140
+ e.to ? c.icon("arrow-right") : null,
141
+ e.to ? h("span.lt3-faint", e.to) : null,
142
+ )));
143
+ }
144
+
145
+ function mapStatus(s) {
146
+ const v = String(s || "").toLowerCase();
147
+ if (v === "ok" || v === "retried_ok") return "ready";
148
+ if (v === "failed" || v === "rejected") return "failed";
149
+ if (v === "running" || v === "in_progress") return "active";
150
+ return v || "idle";
151
+ }
152
+
153
+ function trunc(s, n) { s = String(s || ""); return s.length > n ? s.slice(0, n) + "…" : s; }
@@ -0,0 +1,109 @@
1
+ /* ============================================================================
2
+ * View: Skills — the skills registry (install / enable / disable / remove).
3
+ * Reads the live workspace skill registry (/workspace/skills) and toggles real
4
+ * state. The discovery catalog lives in Marketplace; this is management.
5
+ * ========================================================================== */
6
+
7
+ export async function render(ctx) {
8
+ const { h, c } = ctx;
9
+ const src = h("span", c.sourceBadge("pending"));
10
+ const statHost = h("div.lt3-statrow", c.loading({ lines: 1 }));
11
+ const listHost = h("div", c.loading({ lines: 4, block: true }));
12
+
13
+ const root = h("div.lt3-stack-6",
14
+ c.viewHeader({
15
+ eyebrow: "Platform",
16
+ title: "Skills",
17
+ sub: "Install, enable, disable, and remove skills. Installed skills are shared machine-global capabilities the agent can use.",
18
+ actions: [src],
19
+ }),
20
+ statHost,
21
+ h("section", c.sectionHead("Installed & available skills"), listHost),
22
+ );
23
+
24
+ load();
25
+ return root;
26
+
27
+ async function load() {
28
+ const res = await ctx.api.skills();
29
+ src.replaceChildren(c.sourceBadge(res.source));
30
+ const skills = normalize(res.data);
31
+ if (!skills.length) {
32
+ statHost.replaceChildren(c.stat({ label: "Skills", value: "—", icon: "puzzle" }));
33
+ listHost.replaceChildren(c.emptyState({ icon: "puzzle-off", title: "Skills registry unavailable", body: res.source === "live" ? "No skills are registered yet — install one from the Marketplace." : "Start the backend to read the skills registry." }));
34
+ return;
35
+ }
36
+ const enabled = skills.filter((s) => s.enabled).length;
37
+ const installed = skills.filter((s) => s.installed).length;
38
+ statHost.replaceChildren(
39
+ c.stat({ label: "Skills", value: c.fmtNum(skills.length), icon: "puzzle" }),
40
+ c.stat({ label: "Enabled", value: c.fmtNum(enabled), icon: "circle-check" }),
41
+ c.stat({ label: "Installed", value: c.fmtNum(installed), icon: "download" }),
42
+ );
43
+ listHost.replaceChildren(h("div.lt3-grid-auto", skills.map((s) => skillCard(ctx, s))));
44
+ }
45
+
46
+ function skillCard(ctx2, s) {
47
+ return c.card(h("div.lt3-stack-3",
48
+ h("div.lt3-row", { style: { "justify-content": "space-between", "align-items": "flex-start" } },
49
+ h("div", h("b", s.name), h("div.lt3-faint", { style: { "font-size": "var(--lt3-text-2xs)" } }, [s.source, s.version ? `v${s.version}` : null, s.category].filter(Boolean).join(" · "))),
50
+ c.statePill(s.enabled ? "ready" : s.installed ? "idle" : "available"),
51
+ ),
52
+ s.description ? h("p.lt3-muted", { style: { margin: 0, "font-size": "var(--lt3-text-sm)" } }, s.description) : null,
53
+ h("div.lt3-row-2",
54
+ s.installed
55
+ ? h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm", { on: { click: () => toggle(ctx2, s) } }, c.icon(s.enabled ? "player-pause" : "player-play"), s.enabled ? "Disable" : "Enable")
56
+ : h("button.lt3-btn.lt3-btn--primary.lt3-btn--sm", { on: { click: () => install(ctx2, s) } }, c.icon("download"), "Install"),
57
+ s.installed ? h("button.lt3-btn.lt3-btn--ghost.lt3-btn--sm", { on: { click: () => uninstall(ctx2, s) } }, c.icon("trash"), "Remove") : null,
58
+ ),
59
+ ), { interactive: false });
60
+ }
61
+
62
+ async function toggle(ctx2, s) {
63
+ const res = s.enabled ? await ctx2.api.skillDisable(s.name) : await ctx2.api.skillEnable(s.name);
64
+ ctx2.toast(res && res.ok ? `${s.enabled ? "Disabled" : "Enabled"} ${s.name}` : "Action unavailable", res && res.ok ? "ok" : "err");
65
+ load();
66
+ }
67
+ async function install(ctx2, s) {
68
+ const res = await ctx2.api.skillInstall(s.name, s.plugin);
69
+ ctx2.toast(res && res.ok ? `Installed ${s.name}` : (res && res.status === 403 ? "Admin required" : "Install unavailable"), res && res.ok ? "ok" : "err");
70
+ load();
71
+ }
72
+ async function uninstall(ctx2, s) {
73
+ const res = await ctx2.api.skillUninstall(s.name);
74
+ ctx2.toast(res && res.ok ? `Removed ${s.name}` : (res && res.status === 403 ? "Admin required" : "Remove unavailable"), res && res.ok ? "ok" : "err");
75
+ load();
76
+ }
77
+ }
78
+
79
+ function normalize(data) {
80
+ if (!data) return [];
81
+ const raw = [];
82
+ if (Array.isArray(data.skills)) raw.push(...data.skills);
83
+ if (Array.isArray(data.installed)) raw.push(...data.installed);
84
+ if (Array.isArray(data.available)) raw.push(...data.available);
85
+ if (Array.isArray(data.registry)) raw.push(...data.registry);
86
+ else if (data.registry && typeof data.registry === "object") {
87
+ raw.push(...Object.entries(data.registry).map(([name, value]) => ({ name, ...(value || {}) })));
88
+ }
89
+ if (Array.isArray(data)) raw.push(...data);
90
+
91
+ const byName = new Map();
92
+ for (const item of raw) {
93
+ const name = item && (item.name || item.skill || item.id);
94
+ if (!name) continue;
95
+ const prior = byName.get(name) || {};
96
+ byName.set(name, { ...prior, ...item, name });
97
+ }
98
+
99
+ return [...byName.values()].map((s) => ({
100
+ name: s.name || s.skill || s.id || "skill",
101
+ description: s.description || "",
102
+ version: s.version || "",
103
+ source: s.source || "",
104
+ category: s.category || "",
105
+ plugin: s.plugin || "",
106
+ enabled: s.enabled != null ? !!s.enabled : !!s.installed,
107
+ installed: s.installed != null ? !!s.installed : true,
108
+ }));
109
+ }