ltcai 2.2.7 → 3.1.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.
- package/README.md +72 -34
- package/docs/CHANGELOG.md +119 -0
- package/docs/V3_BACKEND_ARCHITECTURE.md +138 -0
- package/docs/V3_FRONTEND.md +139 -0
- package/knowledge_graph.py +649 -21
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +47 -0
- package/latticeai/api/agents.py +54 -31
- package/latticeai/api/auth.py +5 -2
- package/latticeai/api/chat.py +10 -2
- package/latticeai/api/search.py +240 -0
- package/latticeai/api/static_routes.py +11 -2
- package/latticeai/core/config.py +18 -0
- package/latticeai/core/embedding_providers.py +625 -0
- package/latticeai/core/local_embeddings.py +86 -0
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/server_app.py +65 -1
- package/latticeai/services/agent_runtime.py +245 -0
- package/latticeai/services/search_service.py +346 -0
- package/package.json +13 -6
- package/scripts/build_v3_assets.mjs +164 -0
- package/scripts/capture/README.md +28 -0
- package/scripts/capture/capture_enterprise.js +8 -0
- package/scripts/capture/capture_graph.js +8 -0
- package/scripts/capture/capture_onboarding.js +8 -0
- package/scripts/capture/capture_page.js +43 -0
- package/scripts/capture/capture_release_media.js +125 -0
- package/scripts/capture/capture_skills.js +8 -0
- package/scripts/capture/capture_workspace.js +8 -0
- package/scripts/generate_diagrams.py +513 -0
- package/scripts/lint_v3.mjs +33 -0
- package/scripts/release-0.3.1.sh +105 -0
- package/scripts/take_screenshots.js +69 -0
- package/scripts/validate_release_artifacts.py +167 -0
- package/static/account.html +9 -9
- package/static/activity.html +4 -4
- package/static/admin.html +8 -8
- package/static/agents.html +4 -4
- package/static/chat.html +10 -10
- package/static/css/reference/account.css +137 -1
- package/static/css/reference/chat.css +31 -37
- package/static/css/responsive.css +42 -0
- package/static/css/tokens.5a595671.css +260 -0
- package/static/css/tokens.css +125 -130
- package/static/graph.html +9 -9
- package/static/manifest.json +3 -3
- package/static/plugins.html +4 -4
- package/static/scripts/account.js +4 -4
- package/static/scripts/chat.js +40 -8
- package/static/scripts/workspace.js +78 -0
- package/static/sw.js +3 -1
- package/static/v3/asset-manifest.json +47 -0
- package/static/v3/css/lattice.base.css +128 -0
- package/static/v3/css/lattice.base.e4cdd05d.css +128 -0
- package/static/v3/css/lattice.components.011e988b.css +447 -0
- package/static/v3/css/lattice.components.css +447 -0
- package/static/v3/css/lattice.shell.4920f42d.css +407 -0
- package/static/v3/css/lattice.shell.css +407 -0
- package/static/v3/css/lattice.tokens.c597ff81.css +132 -0
- package/static/v3/css/lattice.tokens.css +132 -0
- package/static/v3/css/lattice.views.3ee19d4e.css +277 -0
- package/static/v3/css/lattice.views.css +277 -0
- package/static/v3/index.html +69 -0
- package/static/v3/js/app.46fb61d9.js +26 -0
- package/static/v3/js/app.js +26 -0
- package/static/v3/js/core/api.22a41d42.js +344 -0
- package/static/v3/js/core/api.js +344 -0
- package/static/v3/js/core/components.4c83e0a9.js +222 -0
- package/static/v3/js/core/components.js +222 -0
- package/static/v3/js/core/dom.a2773eb0.js +148 -0
- package/static/v3/js/core/dom.js +148 -0
- package/static/v3/js/core/router.584570f2.js +37 -0
- package/static/v3/js/core/router.js +37 -0
- package/static/v3/js/core/routes.f935dd50.js +78 -0
- package/static/v3/js/core/routes.js +78 -0
- package/static/v3/js/core/shell.1b6199d6.js +363 -0
- package/static/v3/js/core/shell.js +363 -0
- package/static/v3/js/core/store.34ebd5e6.js +113 -0
- package/static/v3/js/core/store.js +113 -0
- package/static/v3/js/views/admin-audit.660a1fb1.js +185 -0
- package/static/v3/js/views/admin-audit.js +185 -0
- package/static/v3/js/views/admin-permissions.a7ae5f09.js +177 -0
- package/static/v3/js/views/admin-permissions.js +177 -0
- package/static/v3/js/views/admin-policies.3658fd86.js +102 -0
- package/static/v3/js/views/admin-policies.js +102 -0
- package/static/v3/js/views/admin-private-vpc.7d342d36.js +135 -0
- package/static/v3/js/views/admin-private-vpc.js +135 -0
- package/static/v3/js/views/admin-security.07c66b72.js +180 -0
- package/static/v3/js/views/admin-security.js +180 -0
- package/static/v3/js/views/admin-users.03bac88c.js +168 -0
- package/static/v3/js/views/admin-users.js +168 -0
- package/static/v3/js/views/agents.14e48bdd.js +193 -0
- package/static/v3/js/views/agents.js +193 -0
- package/static/v3/js/views/chat.718144ce.js +449 -0
- package/static/v3/js/views/chat.js +449 -0
- package/static/v3/js/views/files.4935197e.js +186 -0
- package/static/v3/js/views/files.js +186 -0
- package/static/v3/js/views/home.cdde3b32.js +119 -0
- package/static/v3/js/views/home.js +119 -0
- package/static/v3/js/views/hybrid-search.b22b97e0.js +195 -0
- package/static/v3/js/views/hybrid-search.js +195 -0
- package/static/v3/js/views/knowledge-graph.a14ea7e7.js +237 -0
- package/static/v3/js/views/knowledge-graph.js +237 -0
- package/static/v3/js/views/models.a1ffa147.js +256 -0
- package/static/v3/js/views/models.js +256 -0
- package/static/v3/js/views/my-computer.1b2ff621.js +237 -0
- package/static/v3/js/views/my-computer.js +237 -0
- package/static/v3/js/views/pipeline.c522f1ce.js +157 -0
- package/static/v3/js/views/pipeline.js +157 -0
- package/static/v3/js/views/settings.4f777210.js +250 -0
- package/static/v3/js/views/settings.js +250 -0
- package/static/workflows.html +4 -4
- package/static/workspace.css +340 -2
- package/static/workspace.html +43 -24
- package/docs/images/tmp_frames/frame_00.png +0 -0
- package/docs/images/tmp_frames/frame_01.png +0 -0
- package/docs/images/tmp_frames/frame_02.png +0 -0
- package/docs/images/tmp_frames/frame_03.png +0 -0
- package/docs/images/tmp_frames/hero_00.png +0 -0
- package/docs/images/tmp_frames/hero_01.png +0 -0
- package/docs/images/tmp_frames/hero_02.png +0 -0
- package/docs/images/tmp_frames/hero_03.png +0 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
* View: Admin · Users — workspace members and access.
|
|
3
|
+
* Surfaces the membership roster and access summary for the active workspace.
|
|
4
|
+
* Reads from /admin/summary + /admin/users (fallback-safe, badged) and never
|
|
5
|
+
* invents backend mutations — actionable controls report unavailable write
|
|
6
|
+
* operations when the backend does not expose them.
|
|
7
|
+
* ========================================================================== */
|
|
8
|
+
|
|
9
|
+
import { timeAgo } from "../core/dom.js";
|
|
10
|
+
|
|
11
|
+
const UNAVAILABLE = "not available from this read-only users view.";
|
|
12
|
+
|
|
13
|
+
export async function render(ctx) {
|
|
14
|
+
const { h, icon, api, c, toast } = ctx;
|
|
15
|
+
|
|
16
|
+
const statHost = h("div.lt3-statrow", c.loading({ lines: 1 }));
|
|
17
|
+
const tableHost = h("div", c.loading({ lines: 4 }));
|
|
18
|
+
const srcSlot = h("span", c.sourceBadge("pending"));
|
|
19
|
+
|
|
20
|
+
const root = h("div.lt3-stack-6",
|
|
21
|
+
c.viewHeader({
|
|
22
|
+
eyebrow: "Administration",
|
|
23
|
+
title: "Users",
|
|
24
|
+
sub: "Workspace members and access.",
|
|
25
|
+
actions: [
|
|
26
|
+
h("button.lt3-btn.lt3-btn--primary",
|
|
27
|
+
{ on: { click: () => toast("Invite user is " + UNAVAILABLE, "info") } },
|
|
28
|
+
icon("user-plus"), "Invite user"),
|
|
29
|
+
],
|
|
30
|
+
}),
|
|
31
|
+
statHost,
|
|
32
|
+
c.panel({
|
|
33
|
+
eyebrow: "Roster",
|
|
34
|
+
head: h("div.lt3-row", { style: { "justify-content": "space-between", width: "100%" } },
|
|
35
|
+
h("div",
|
|
36
|
+
h("div.lt3-eyebrow", "Roster"),
|
|
37
|
+
h("h3.lt3-panel__title", "Members"),
|
|
38
|
+
),
|
|
39
|
+
srcSlot,
|
|
40
|
+
),
|
|
41
|
+
children: tableHost,
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
async function load() {
|
|
46
|
+
const [summary, users] = await Promise.all([api.adminSummary(), api.adminUsers()]);
|
|
47
|
+
renderStats(summary);
|
|
48
|
+
renderTable(users, c.sourceBadge(users.source));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function renderStats(res) {
|
|
52
|
+
const s = normalizeSummary(res.data);
|
|
53
|
+
statHost.replaceChildren(
|
|
54
|
+
c.stat({ label: "Total users", value: c.fmtNum(s.total_users), icon: "users" }),
|
|
55
|
+
c.stat({ label: "Active", value: c.fmtNum(s.active_users), icon: "user-check" }),
|
|
56
|
+
c.stat({ label: "Admins", value: c.fmtNum(s.admin_users), icon: "shield-lock" }),
|
|
57
|
+
c.stat({ label: "Messages", value: c.fmtNum(s.total_messages), icon: "message-2" }),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function renderTable(res, badge) {
|
|
62
|
+
srcSlot.replaceChildren(badge);
|
|
63
|
+
const rows = normalizeUsers(res.data);
|
|
64
|
+
if (!rows.length) {
|
|
65
|
+
tableHost.replaceChildren(c.emptyState({
|
|
66
|
+
icon: "user-off",
|
|
67
|
+
title: "No members yet",
|
|
68
|
+
body: "Invite teammates to give them access to this workspace.",
|
|
69
|
+
action: h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm",
|
|
70
|
+
{ on: { click: () => toast("Invite user is " + UNAVAILABLE, "info") } },
|
|
71
|
+
icon("user-plus"), "Invite user"),
|
|
72
|
+
}));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
tableHost.replaceChildren(c.table(columns(), rows));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function columns() {
|
|
79
|
+
return [
|
|
80
|
+
{
|
|
81
|
+
key: "user", label: "User",
|
|
82
|
+
render: (r) => h("div.lt3-row-2",
|
|
83
|
+
h("span.lt3-avatar", initials(r.nickname, r.email)),
|
|
84
|
+
h("div",
|
|
85
|
+
h("div", { style: { "font-weight": "var(--lt3-weight-semi)" } }, r.nickname),
|
|
86
|
+
h("div.lt3-faint", { style: { "font-size": "var(--lt3-text-xs)" } }, r.email),
|
|
87
|
+
),
|
|
88
|
+
),
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
key: "role", label: "Role", width: "120px",
|
|
92
|
+
render: (r) => c.pill(titleCase(r.role), roleVariant(r.role)),
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
key: "status", label: "Status", width: "120px",
|
|
96
|
+
render: (r) => c.statePill(r.disabled ? "disabled" : "active"),
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
key: "last_seen", label: "Last seen", width: "130px",
|
|
100
|
+
render: (r) => h("span.lt3-faint", { style: { "font-size": "var(--lt3-text-xs)" } },
|
|
101
|
+
r.last_seen ? timeAgo(r.last_seen) : "—"),
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
key: "actions", label: "", width: "48px",
|
|
105
|
+
render: (r) => h("button.lt3-iconbtn.lt3-iconbtn--sm",
|
|
106
|
+
{
|
|
107
|
+
"aria-label": `Manage ${r.nickname || r.email}`,
|
|
108
|
+
on: { click: () => toast(`Manage ${r.nickname || r.email} is ` + UNAVAILABLE, "info") },
|
|
109
|
+
},
|
|
110
|
+
icon("dots-vertical")),
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
load();
|
|
116
|
+
return root;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* ── helpers ─────────────────────────────────────────────────────────────── */
|
|
120
|
+
function normalizeSummary(data) {
|
|
121
|
+
const d = data && typeof data === "object" ? data : {};
|
|
122
|
+
return {
|
|
123
|
+
total_users: numOr(d.total_users),
|
|
124
|
+
active_users: numOr(d.active_users),
|
|
125
|
+
admin_users: numOr(d.admin_users),
|
|
126
|
+
total_messages: numOr(d.total_messages),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function normalizeUsers(data) {
|
|
131
|
+
const list = Array.isArray(data) ? data : Array.isArray(data && data.users) ? data.users : [];
|
|
132
|
+
return list
|
|
133
|
+
.filter((u) => u && typeof u === "object")
|
|
134
|
+
.map((u) => {
|
|
135
|
+
const email = String(u.email || "").trim();
|
|
136
|
+
return {
|
|
137
|
+
email: email || "—",
|
|
138
|
+
nickname: String(u.nickname || u.name || "").trim() || email.split("@")[0] || "Member",
|
|
139
|
+
role: String(u.role || "member").trim().toLowerCase(),
|
|
140
|
+
disabled: Boolean(u.disabled),
|
|
141
|
+
last_seen: u.last_seen ?? u.lastSeen ?? null,
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function numOr(v) {
|
|
147
|
+
const n = Number(v);
|
|
148
|
+
return Number.isFinite(n) ? n : null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function roleVariant(role) {
|
|
152
|
+
const r = String(role || "").toLowerCase();
|
|
153
|
+
if (r === "owner" || r === "admin") return "info";
|
|
154
|
+
return "";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function initials(nickname, email) {
|
|
158
|
+
const base = String(nickname || email || "").trim();
|
|
159
|
+
if (!base) return "?";
|
|
160
|
+
const words = base.split(/[\s._-]+/).filter(Boolean);
|
|
161
|
+
if (words.length >= 2) return (words[0][0] + words[1][0]).toUpperCase();
|
|
162
|
+
return base.slice(0, 2).toUpperCase();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function titleCase(s) {
|
|
166
|
+
const v = String(s || "");
|
|
167
|
+
return v ? v.charAt(0).toUpperCase() + v.slice(1) : "Member";
|
|
168
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
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 rosterHost = h("div", c.loading({ lines: 2, block: true }));
|
|
13
|
+
const runsHost = h("div", c.loading({ lines: 4 }));
|
|
14
|
+
const rosterSrc = h("span", c.sourceBadge("pending"));
|
|
15
|
+
const runsSrc = h("span", c.sourceBadge("pending"));
|
|
16
|
+
const healthSlot = h("span", c.sourceBadge("pending"));
|
|
17
|
+
|
|
18
|
+
const root = h("div.lt3-stack-6",
|
|
19
|
+
c.viewHeader({
|
|
20
|
+
eyebrow: "Compute",
|
|
21
|
+
title: "Agents",
|
|
22
|
+
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.",
|
|
23
|
+
actions: [healthSlot],
|
|
24
|
+
}),
|
|
25
|
+
statHost,
|
|
26
|
+
h("section",
|
|
27
|
+
c.sectionHead("Agent roster", rosterSrc),
|
|
28
|
+
rosterHost,
|
|
29
|
+
),
|
|
30
|
+
c.panel({
|
|
31
|
+
head: h("div.lt3-row", { style: { "justify-content": "space-between", width: "100%" } },
|
|
32
|
+
h("div",
|
|
33
|
+
h("div.lt3-eyebrow", "Activity"),
|
|
34
|
+
h("h3.lt3-panel__title", "Recent runs"),
|
|
35
|
+
),
|
|
36
|
+
runsSrc,
|
|
37
|
+
),
|
|
38
|
+
children: runsHost,
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
hydrate(ctx, { statHost, rosterHost, runsHost, rosterSrc, runsSrc, healthSlot });
|
|
43
|
+
return root;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function hydrate(ctx, hosts) {
|
|
47
|
+
const { h, icon, c } = ctx;
|
|
48
|
+
const { statHost, rosterHost, runsHost, rosterSrc, runsSrc, healthSlot } = hosts;
|
|
49
|
+
|
|
50
|
+
const res = await ctx.api.agentRuntime();
|
|
51
|
+
const data = res.data || {};
|
|
52
|
+
const agents = normalize(data.agents);
|
|
53
|
+
const runtime = data.runtime || {};
|
|
54
|
+
const health = data.health || { status: "unknown" };
|
|
55
|
+
const runs = Array.isArray(data.runs) ? data.runs : [];
|
|
56
|
+
const byId = new Map(agents.map((a) => [a.id, a.name]));
|
|
57
|
+
|
|
58
|
+
rosterSrc.replaceChildren(c.sourceBadge(res.source));
|
|
59
|
+
runsSrc.replaceChildren(c.sourceBadge(res.source));
|
|
60
|
+
healthSlot.replaceChildren(
|
|
61
|
+
c.statePill(health.status === "ok" ? "ready" : health.status === "degraded" ? "warn" : "idle"),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// ── Stat row (real runtime counts) ────────────────────────────────────
|
|
65
|
+
const available = agents.filter((a) => isAvailable(a.state)).length;
|
|
66
|
+
const totalRuns = Number(runtime.total_runs) || runs.length;
|
|
67
|
+
const handoffs = agents.reduce((sum, a) => sum + a.handoffs.length, 0);
|
|
68
|
+
statHost.replaceChildren(
|
|
69
|
+
c.stat({ label: "Agents", value: c.fmtNum(agents.length), icon: "robot" }),
|
|
70
|
+
c.stat({ label: "Available", value: c.fmtNum(available), icon: "circle-check" }),
|
|
71
|
+
c.stat({ label: "Total runs", value: c.fmtNum(totalRuns), icon: "player-play" }),
|
|
72
|
+
c.stat({ label: "Handoffs", value: c.fmtNum(handoffs), icon: "arrows-exchange" }),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// ── Roster grid ───────────────────────────────────────────────────────
|
|
76
|
+
if (!agents.length) {
|
|
77
|
+
rosterHost.replaceChildren(c.emptyState({
|
|
78
|
+
icon: "robot-off",
|
|
79
|
+
title: "Runtime unavailable",
|
|
80
|
+
body: "The agent runtime did not respond. Start the local server to see the roster.",
|
|
81
|
+
}));
|
|
82
|
+
} else {
|
|
83
|
+
rosterHost.replaceChildren(
|
|
84
|
+
h("div.lt3-grid-auto", agents.map((a) => agentCard(ctx, a, byId))),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── Recent runs ledger (REAL runs from the runtime) ───────────────────
|
|
89
|
+
const rows = runs.map((r) => ({
|
|
90
|
+
agent: byId.get(r.agent_id) || shortId(r.agent_id),
|
|
91
|
+
status: mapStatus(r.status),
|
|
92
|
+
time: fmtTime(r.created_at || r.completed_at),
|
|
93
|
+
note: runNote(r),
|
|
94
|
+
}));
|
|
95
|
+
runsHost.replaceChildren(
|
|
96
|
+
c.table(
|
|
97
|
+
[
|
|
98
|
+
{ key: "agent", label: "Agent", render: (r) => h("div.lt3-row-2",
|
|
99
|
+
h("span.lt3-avatar", { style: { width: "26px", height: "26px" } }, icon("robot")),
|
|
100
|
+
h("b", { style: { "font-size": "var(--lt3-text-sm)" } }, r.agent),
|
|
101
|
+
) },
|
|
102
|
+
{ key: "status", label: "Status", width: "1%", render: (r) => c.statePill(r.status) },
|
|
103
|
+
{ 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) },
|
|
104
|
+
{ key: "note", label: "Note", render: (r) => h("span.lt3-muted", r.note) },
|
|
105
|
+
],
|
|
106
|
+
rows,
|
|
107
|
+
{ empty: c.emptyState({ icon: "history-off", title: "No runs yet", body: "Agent runs recorded by the runtime will appear here." }) },
|
|
108
|
+
),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* ── Agent card ──────────────────────────────────────────────────────────── */
|
|
113
|
+
function agentCard(ctx, agent, byId) {
|
|
114
|
+
const { h, icon, c } = ctx;
|
|
115
|
+
return c.card(
|
|
116
|
+
h("div.lt3-stack-3",
|
|
117
|
+
h("div.lt3-row", { style: { "justify-content": "space-between", "align-items": "flex-start" } },
|
|
118
|
+
h("div.lt3-row-2",
|
|
119
|
+
h("span.lt3-avatar", { style: { width: "40px", height: "40px", "border-radius": "var(--lt3-radius-md)" } }, icon("robot")),
|
|
120
|
+
h("div",
|
|
121
|
+
h("div", { style: { "font-weight": "var(--lt3-weight-semi)", "font-size": "var(--lt3-text-md)" } }, agent.name),
|
|
122
|
+
h("div.lt3-faint", { style: { "font-size": "var(--lt3-text-2xs)", "font-family": "var(--lt3-font-mono)" } }, agent.id),
|
|
123
|
+
),
|
|
124
|
+
),
|
|
125
|
+
c.statePill(agent.state),
|
|
126
|
+
),
|
|
127
|
+
h("p.lt3-muted", { style: { "font-size": "var(--lt3-text-sm)", margin: "0" } }, agent.role),
|
|
128
|
+
h("div.lt3-row-2", { style: { "font-size": "var(--lt3-text-xs)", color: "var(--muted)" } },
|
|
129
|
+
icon("player-play"),
|
|
130
|
+
h("b", { style: { color: "var(--text)" } }, c.fmtNum(agent.runs)),
|
|
131
|
+
"runs",
|
|
132
|
+
),
|
|
133
|
+
agent.handoffs.length
|
|
134
|
+
? h("div.lt3-stack-2",
|
|
135
|
+
h("div.lt3-eyebrow", icon("arrows-exchange"), "Hands off to"),
|
|
136
|
+
h("div.lt3-cluster", agent.handoffs.map((id) => {
|
|
137
|
+
const name = byId.get(id) || shortId(id);
|
|
138
|
+
return h("span.lt3-chip", icon("arrow-right"), name);
|
|
139
|
+
})),
|
|
140
|
+
)
|
|
141
|
+
: h("div.lt3-faint", { style: { "font-size": "var(--lt3-text-xs)" } }, "Terminal role — no handoffs"),
|
|
142
|
+
),
|
|
143
|
+
{ interactive: false },
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* ── helpers ─────────────────────────────────────────────────────────────── */
|
|
148
|
+
function normalize(data) {
|
|
149
|
+
const list = Array.isArray(data) ? data : (data && Array.isArray(data.agents) ? data.agents : []);
|
|
150
|
+
return list.map((a, i) => ({
|
|
151
|
+
id: a.id || `agent:${i}`,
|
|
152
|
+
name: a.name || a.id || `Agent ${i + 1}`,
|
|
153
|
+
role: a.role || a.description || "No role description.",
|
|
154
|
+
state: a.state || a.status || "idle",
|
|
155
|
+
runs: a.runs ?? a.run_count ?? a.runs_count ?? 0,
|
|
156
|
+
handoffs: Array.isArray(a.handoffs) ? a.handoffs
|
|
157
|
+
: Array.isArray(a.relationships) ? a.relationships : [],
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const AVAILABLE_STATES = new Set(["available", "ready", "active", "ok", "idle"]);
|
|
162
|
+
function isAvailable(state) {
|
|
163
|
+
return AVAILABLE_STATES.has(String(state).toLowerCase());
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Map orchestrator run statuses onto the shared state-pill vocabulary.
|
|
167
|
+
function mapStatus(status) {
|
|
168
|
+
const s = String(status || "").toLowerCase();
|
|
169
|
+
if (s === "ok" || s === "retried_ok") return "ready";
|
|
170
|
+
if (s === "failed" || s === "rejected") return "failed";
|
|
171
|
+
if (s === "running" || s === "in_progress") return "active";
|
|
172
|
+
return s || "idle";
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function runNote(r) {
|
|
176
|
+
const out = String(r.output || r.input || "").trim();
|
|
177
|
+
if (out) return out.length > 96 ? out.slice(0, 96) + "…" : out;
|
|
178
|
+
return `Run ${shortId(r.agent_id)} — ${r.status || "recorded"}`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function fmtTime(ts) {
|
|
182
|
+
if (!ts) return "—";
|
|
183
|
+
try {
|
|
184
|
+
const d = new Date(ts);
|
|
185
|
+
if (Number.isNaN(d.getTime())) return String(ts);
|
|
186
|
+
return d.toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
|
|
187
|
+
} catch { return String(ts); }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function shortId(id) {
|
|
191
|
+
const s = String(id || "");
|
|
192
|
+
return s.includes(":") ? s.split(":").pop() : s;
|
|
193
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
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 rosterHost = h("div", c.loading({ lines: 2, block: true }));
|
|
13
|
+
const runsHost = h("div", c.loading({ lines: 4 }));
|
|
14
|
+
const rosterSrc = h("span", c.sourceBadge("pending"));
|
|
15
|
+
const runsSrc = h("span", c.sourceBadge("pending"));
|
|
16
|
+
const healthSlot = h("span", c.sourceBadge("pending"));
|
|
17
|
+
|
|
18
|
+
const root = h("div.lt3-stack-6",
|
|
19
|
+
c.viewHeader({
|
|
20
|
+
eyebrow: "Compute",
|
|
21
|
+
title: "Agents",
|
|
22
|
+
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.",
|
|
23
|
+
actions: [healthSlot],
|
|
24
|
+
}),
|
|
25
|
+
statHost,
|
|
26
|
+
h("section",
|
|
27
|
+
c.sectionHead("Agent roster", rosterSrc),
|
|
28
|
+
rosterHost,
|
|
29
|
+
),
|
|
30
|
+
c.panel({
|
|
31
|
+
head: h("div.lt3-row", { style: { "justify-content": "space-between", width: "100%" } },
|
|
32
|
+
h("div",
|
|
33
|
+
h("div.lt3-eyebrow", "Activity"),
|
|
34
|
+
h("h3.lt3-panel__title", "Recent runs"),
|
|
35
|
+
),
|
|
36
|
+
runsSrc,
|
|
37
|
+
),
|
|
38
|
+
children: runsHost,
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
hydrate(ctx, { statHost, rosterHost, runsHost, rosterSrc, runsSrc, healthSlot });
|
|
43
|
+
return root;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function hydrate(ctx, hosts) {
|
|
47
|
+
const { h, icon, c } = ctx;
|
|
48
|
+
const { statHost, rosterHost, runsHost, rosterSrc, runsSrc, healthSlot } = hosts;
|
|
49
|
+
|
|
50
|
+
const res = await ctx.api.agentRuntime();
|
|
51
|
+
const data = res.data || {};
|
|
52
|
+
const agents = normalize(data.agents);
|
|
53
|
+
const runtime = data.runtime || {};
|
|
54
|
+
const health = data.health || { status: "unknown" };
|
|
55
|
+
const runs = Array.isArray(data.runs) ? data.runs : [];
|
|
56
|
+
const byId = new Map(agents.map((a) => [a.id, a.name]));
|
|
57
|
+
|
|
58
|
+
rosterSrc.replaceChildren(c.sourceBadge(res.source));
|
|
59
|
+
runsSrc.replaceChildren(c.sourceBadge(res.source));
|
|
60
|
+
healthSlot.replaceChildren(
|
|
61
|
+
c.statePill(health.status === "ok" ? "ready" : health.status === "degraded" ? "warn" : "idle"),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// ── Stat row (real runtime counts) ────────────────────────────────────
|
|
65
|
+
const available = agents.filter((a) => isAvailable(a.state)).length;
|
|
66
|
+
const totalRuns = Number(runtime.total_runs) || runs.length;
|
|
67
|
+
const handoffs = agents.reduce((sum, a) => sum + a.handoffs.length, 0);
|
|
68
|
+
statHost.replaceChildren(
|
|
69
|
+
c.stat({ label: "Agents", value: c.fmtNum(agents.length), icon: "robot" }),
|
|
70
|
+
c.stat({ label: "Available", value: c.fmtNum(available), icon: "circle-check" }),
|
|
71
|
+
c.stat({ label: "Total runs", value: c.fmtNum(totalRuns), icon: "player-play" }),
|
|
72
|
+
c.stat({ label: "Handoffs", value: c.fmtNum(handoffs), icon: "arrows-exchange" }),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// ── Roster grid ───────────────────────────────────────────────────────
|
|
76
|
+
if (!agents.length) {
|
|
77
|
+
rosterHost.replaceChildren(c.emptyState({
|
|
78
|
+
icon: "robot-off",
|
|
79
|
+
title: "Runtime unavailable",
|
|
80
|
+
body: "The agent runtime did not respond. Start the local server to see the roster.",
|
|
81
|
+
}));
|
|
82
|
+
} else {
|
|
83
|
+
rosterHost.replaceChildren(
|
|
84
|
+
h("div.lt3-grid-auto", agents.map((a) => agentCard(ctx, a, byId))),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── Recent runs ledger (REAL runs from the runtime) ───────────────────
|
|
89
|
+
const rows = runs.map((r) => ({
|
|
90
|
+
agent: byId.get(r.agent_id) || shortId(r.agent_id),
|
|
91
|
+
status: mapStatus(r.status),
|
|
92
|
+
time: fmtTime(r.created_at || r.completed_at),
|
|
93
|
+
note: runNote(r),
|
|
94
|
+
}));
|
|
95
|
+
runsHost.replaceChildren(
|
|
96
|
+
c.table(
|
|
97
|
+
[
|
|
98
|
+
{ key: "agent", label: "Agent", render: (r) => h("div.lt3-row-2",
|
|
99
|
+
h("span.lt3-avatar", { style: { width: "26px", height: "26px" } }, icon("robot")),
|
|
100
|
+
h("b", { style: { "font-size": "var(--lt3-text-sm)" } }, r.agent),
|
|
101
|
+
) },
|
|
102
|
+
{ key: "status", label: "Status", width: "1%", render: (r) => c.statePill(r.status) },
|
|
103
|
+
{ 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) },
|
|
104
|
+
{ key: "note", label: "Note", render: (r) => h("span.lt3-muted", r.note) },
|
|
105
|
+
],
|
|
106
|
+
rows,
|
|
107
|
+
{ empty: c.emptyState({ icon: "history-off", title: "No runs yet", body: "Agent runs recorded by the runtime will appear here." }) },
|
|
108
|
+
),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* ── Agent card ──────────────────────────────────────────────────────────── */
|
|
113
|
+
function agentCard(ctx, agent, byId) {
|
|
114
|
+
const { h, icon, c } = ctx;
|
|
115
|
+
return c.card(
|
|
116
|
+
h("div.lt3-stack-3",
|
|
117
|
+
h("div.lt3-row", { style: { "justify-content": "space-between", "align-items": "flex-start" } },
|
|
118
|
+
h("div.lt3-row-2",
|
|
119
|
+
h("span.lt3-avatar", { style: { width: "40px", height: "40px", "border-radius": "var(--lt3-radius-md)" } }, icon("robot")),
|
|
120
|
+
h("div",
|
|
121
|
+
h("div", { style: { "font-weight": "var(--lt3-weight-semi)", "font-size": "var(--lt3-text-md)" } }, agent.name),
|
|
122
|
+
h("div.lt3-faint", { style: { "font-size": "var(--lt3-text-2xs)", "font-family": "var(--lt3-font-mono)" } }, agent.id),
|
|
123
|
+
),
|
|
124
|
+
),
|
|
125
|
+
c.statePill(agent.state),
|
|
126
|
+
),
|
|
127
|
+
h("p.lt3-muted", { style: { "font-size": "var(--lt3-text-sm)", margin: "0" } }, agent.role),
|
|
128
|
+
h("div.lt3-row-2", { style: { "font-size": "var(--lt3-text-xs)", color: "var(--muted)" } },
|
|
129
|
+
icon("player-play"),
|
|
130
|
+
h("b", { style: { color: "var(--text)" } }, c.fmtNum(agent.runs)),
|
|
131
|
+
"runs",
|
|
132
|
+
),
|
|
133
|
+
agent.handoffs.length
|
|
134
|
+
? h("div.lt3-stack-2",
|
|
135
|
+
h("div.lt3-eyebrow", icon("arrows-exchange"), "Hands off to"),
|
|
136
|
+
h("div.lt3-cluster", agent.handoffs.map((id) => {
|
|
137
|
+
const name = byId.get(id) || shortId(id);
|
|
138
|
+
return h("span.lt3-chip", icon("arrow-right"), name);
|
|
139
|
+
})),
|
|
140
|
+
)
|
|
141
|
+
: h("div.lt3-faint", { style: { "font-size": "var(--lt3-text-xs)" } }, "Terminal role — no handoffs"),
|
|
142
|
+
),
|
|
143
|
+
{ interactive: false },
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* ── helpers ─────────────────────────────────────────────────────────────── */
|
|
148
|
+
function normalize(data) {
|
|
149
|
+
const list = Array.isArray(data) ? data : (data && Array.isArray(data.agents) ? data.agents : []);
|
|
150
|
+
return list.map((a, i) => ({
|
|
151
|
+
id: a.id || `agent:${i}`,
|
|
152
|
+
name: a.name || a.id || `Agent ${i + 1}`,
|
|
153
|
+
role: a.role || a.description || "No role description.",
|
|
154
|
+
state: a.state || a.status || "idle",
|
|
155
|
+
runs: a.runs ?? a.run_count ?? a.runs_count ?? 0,
|
|
156
|
+
handoffs: Array.isArray(a.handoffs) ? a.handoffs
|
|
157
|
+
: Array.isArray(a.relationships) ? a.relationships : [],
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const AVAILABLE_STATES = new Set(["available", "ready", "active", "ok", "idle"]);
|
|
162
|
+
function isAvailable(state) {
|
|
163
|
+
return AVAILABLE_STATES.has(String(state).toLowerCase());
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Map orchestrator run statuses onto the shared state-pill vocabulary.
|
|
167
|
+
function mapStatus(status) {
|
|
168
|
+
const s = String(status || "").toLowerCase();
|
|
169
|
+
if (s === "ok" || s === "retried_ok") return "ready";
|
|
170
|
+
if (s === "failed" || s === "rejected") return "failed";
|
|
171
|
+
if (s === "running" || s === "in_progress") return "active";
|
|
172
|
+
return s || "idle";
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function runNote(r) {
|
|
176
|
+
const out = String(r.output || r.input || "").trim();
|
|
177
|
+
if (out) return out.length > 96 ? out.slice(0, 96) + "…" : out;
|
|
178
|
+
return `Run ${shortId(r.agent_id)} — ${r.status || "recorded"}`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function fmtTime(ts) {
|
|
182
|
+
if (!ts) return "—";
|
|
183
|
+
try {
|
|
184
|
+
const d = new Date(ts);
|
|
185
|
+
if (Number.isNaN(d.getTime())) return String(ts);
|
|
186
|
+
return d.toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
|
|
187
|
+
} catch { return String(ts); }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function shortId(id) {
|
|
191
|
+
const s = String(id || "");
|
|
192
|
+
return s.includes(":") ? s.split(":").pop() : s;
|
|
193
|
+
}
|