ltcai 3.3.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.
- package/README.md +85 -66
- package/docs/CHANGELOG.md +36 -0
- package/docs/architecture.md +2 -1
- package/docs/assets/v3.4.0/agent-run.png +0 -0
- package/docs/assets/v3.4.0/agents.png +0 -0
- package/docs/assets/v3.4.0/before/chat-before.png +0 -0
- package/docs/assets/v3.4.0/before/files-before.png +0 -0
- package/docs/assets/v3.4.0/chat.png +0 -0
- package/docs/assets/v3.4.0/connect-folder.png +0 -0
- package/docs/assets/v3.4.0/files.png +0 -0
- package/docs/assets/v3.4.0/home.png +0 -0
- package/docs/assets/v3.4.0/hooks-dispatch.png +0 -0
- package/docs/assets/v3.4.0/knowledge-graph.png +0 -0
- package/docs/assets/v3.4.0/local-agent.png +0 -0
- package/docs/assets/v3.4.0/memory.png +0 -0
- package/docs/assets/v3.4.0/settings.png +0 -0
- package/docs/assets/v3.4.0/vision-input.png +0 -0
- package/docs/assets/v3.4.0/workflows.png +0 -0
- package/knowledge_graph.py +45 -0
- package/knowledge_graph_api.py +10 -0
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/agents.py +3 -0
- package/latticeai/api/hooks.py +39 -0
- package/latticeai/api/local_files.py +41 -0
- package/latticeai/api/models.py +36 -1
- package/latticeai/api/tools.py +16 -1
- package/latticeai/api/workflow_designer.py +2 -1
- package/latticeai/core/hooks.py +398 -2
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/multi_agent.py +1 -1
- package/latticeai/core/workflow_engine.py +21 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/server_app.py +40 -0
- package/latticeai/services/agent_runtime.py +46 -1
- package/latticeai/services/upload_service.py +17 -0
- package/package.json +1 -1
- package/scripts/capture/capture_v340.js +88 -0
- package/static/css/{tokens.8b8e31bd.css → tokens.3ba22e37.css} +109 -109
- package/static/css/tokens.css +109 -109
- package/static/v3/asset-manifest.json +24 -24
- package/static/v3/css/{lattice.components.011e988b.css → lattice.components.9b49d614.css} +57 -32
- package/static/v3/css/lattice.components.css +57 -32
- package/static/v3/css/{lattice.shell.4920f42d.css → lattice.shell.6ceea7c8.css} +75 -31
- package/static/v3/css/lattice.shell.css +75 -31
- package/static/v3/css/lattice.tokens.css +13 -13
- package/static/v3/css/{lattice.tokens.c597ff81.css → lattice.tokens.e7018963.css} +13 -13
- package/static/v3/css/{lattice.views.1d326beb.css → lattice.views.22f69117.css} +93 -15
- package/static/v3/css/lattice.views.css +93 -15
- package/static/v3/js/{app.cf5bb712.js → app.c4acfdd8.js} +1 -1
- package/static/v3/js/core/{api.113660c5.js → api.12b568ad.js} +67 -0
- package/static/v3/js/core/api.js +67 -0
- package/static/v3/js/core/{components.4c83e0a9.js → components.35f02e4c.js} +8 -0
- package/static/v3/js/core/components.js +8 -0
- package/static/v3/js/core/{routes.07ad6696.js → routes.d214b399.js} +16 -12
- package/static/v3/js/core/routes.js +16 -12
- package/static/v3/js/core/{shell.9e707234.js → shell.80a6ad82.js} +37 -9
- package/static/v3/js/core/shell.js +34 -6
- package/static/v3/js/views/agents.014d0b74.js +541 -0
- package/static/v3/js/views/agents.js +305 -57
- package/static/v3/js/views/{chat.c48fd9e2.js → chat.e6dd7dd0.js} +161 -9
- package/static/v3/js/views/chat.js +161 -9
- package/static/v3/js/views/files.adad14c1.js +365 -0
- package/static/v3/js/views/files.js +212 -79
- package/static/v3/js/views/home.24f8b8ae.js +200 -0
- package/static/v3/js/views/home.js +96 -15
- package/static/v3/js/views/hooks.13845954.js +215 -0
- package/static/v3/js/views/hooks.js +117 -1
- package/static/v3/js/views/{my-computer.1b2ff621.js → my-computer.c3ef5283.js} +224 -1
- package/static/v3/js/views/my-computer.js +224 -1
- package/static/v3/js/views/{settings.c7b0cc05.js → settings.8631fa5e.js} +54 -0
- package/static/v3/js/views/settings.js +54 -0
- package/static/v3/js/views/agents.c373d48c.js +0 -293
- package/static/v3/js/views/files.8464634a.js +0 -232
- package/static/v3/js/views/home.cdde3b32.js +0 -119
- package/static/v3/js/views/hooks.f3edebca.js +0 -99
|
@@ -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();
|
|
@@ -262,3 +305,14 @@ function titleCase(s) {
|
|
|
262
305
|
s = String(s || "");
|
|
263
306
|
return s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
|
264
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
|
+
}
|
|
@@ -1,293 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
/* ============================================================================
|
|
2
|
-
* View: Files — connected sources & indexed documents.
|
|
3
|
-
* Lists the sources the workspace has indexed, with a human-readable status
|
|
4
|
-
* roll-up. Data comes from /workspace/indexing (live); when indexing is
|
|
5
|
-
* unavailable, the table renders an empty unavailable state.
|
|
6
|
-
*
|
|
7
|
-
* View contract (shared by all views):
|
|
8
|
-
* export async function render(ctx) -> single DOM node
|
|
9
|
-
* ctx = { h, icon, api, store, c, route, params, navigate, toast }
|
|
10
|
-
* ========================================================================== */
|
|
11
|
-
|
|
12
|
-
import { timeAgo } from "../core/dom.a2773eb0.js";
|
|
13
|
-
|
|
14
|
-
/** Tabler glyph per file kind — keeps the table scannable. */
|
|
15
|
-
const KIND_ICON = {
|
|
16
|
-
markdown: "file-text",
|
|
17
|
-
config: "settings",
|
|
18
|
-
image: "photo",
|
|
19
|
-
data: "table",
|
|
20
|
-
default: "file",
|
|
21
|
-
};
|
|
22
|
-
const iconForKind = (k) => KIND_ICON[k] || KIND_ICON.default;
|
|
23
|
-
|
|
24
|
-
/** Bytes → compact human string (1.0 KB / 4.7 KB / 180 KB / 1.2 MB). */
|
|
25
|
-
function humanSize(bytes) {
|
|
26
|
-
if (bytes === null || bytes === undefined || bytes === "") return "—";
|
|
27
|
-
const n = Number(bytes);
|
|
28
|
-
if (!Number.isFinite(n) || n < 0) return "—";
|
|
29
|
-
if (n < 1024) return `${n} B`;
|
|
30
|
-
const units = ["KB", "MB", "GB", "TB"];
|
|
31
|
-
let v = n / 1024, i = 0;
|
|
32
|
-
while (v >= 1024 && i < units.length - 1) { v /= 1024; i++; }
|
|
33
|
-
return `${v.toFixed(v >= 100 || Number.isInteger(v) ? 0 : 1)} ${units[i]}`;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Live shape is {sources:[...]}; legacy {files:[...]} payloads normalize too. */
|
|
37
|
-
function normalize(data) {
|
|
38
|
-
if (data && Array.isArray(data.sources)) {
|
|
39
|
-
return data.sources.map((source) => ({
|
|
40
|
-
name: source.label || source.id || "local source",
|
|
41
|
-
kind: "default",
|
|
42
|
-
size: null,
|
|
43
|
-
path: source.root_path || source.id || "",
|
|
44
|
-
indexed: Number(source.success_count || 0) > 0,
|
|
45
|
-
updated: source.last_run_at || source.updated_at || null,
|
|
46
|
-
count: Number(source.success_count || 0),
|
|
47
|
-
status: source.status || (source.watch_active ? "watching" : "idle"),
|
|
48
|
-
}));
|
|
49
|
-
}
|
|
50
|
-
const list = Array.isArray(data) ? data : (data && Array.isArray(data.files) ? data.files : null);
|
|
51
|
-
if (!list) return null;
|
|
52
|
-
return list.map((f) => ({
|
|
53
|
-
name: f.name || (f.path ? String(f.path).split("/").pop() : "untitled"),
|
|
54
|
-
kind: f.kind || "default",
|
|
55
|
-
size: Number(f.size) || 0,
|
|
56
|
-
path: f.path || f.name || "",
|
|
57
|
-
indexed: f.indexed === true,
|
|
58
|
-
updated: f.updated || f.modified || f.mtime || null,
|
|
59
|
-
count: Number(f.count || 0),
|
|
60
|
-
status: f.status || null,
|
|
61
|
-
}));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Document types the backend accepts (latticeai/services/upload_service.py). */
|
|
65
|
-
const UPLOAD_ACCEPT = ".pdf,.docx,.xlsx,.pptx,.txt,.md,.csv";
|
|
66
|
-
|
|
67
|
-
export async function render(ctx) {
|
|
68
|
-
const { h, icon, api, c, navigate, toast } = ctx;
|
|
69
|
-
|
|
70
|
-
// Connecting/watching a *folder* needs the desktop local-agent connector,
|
|
71
|
-
// which is not enabled in this build. Say so plainly. Manual document upload
|
|
72
|
-
// below works without it.
|
|
73
|
-
const unavailableToast = () =>
|
|
74
|
-
toast("Connecting a folder requires the Lattice desktop local agent — not available in this build.", "warn");
|
|
75
|
-
|
|
76
|
-
const statHost = h("div.lt3-statrow", c.loading({ lines: 1 }));
|
|
77
|
-
const srcSlot = h("span", c.sourceBadge("pending"));
|
|
78
|
-
const tableHost = h("div", c.loading({ lines: 4 }));
|
|
79
|
-
|
|
80
|
-
// ── Manual upload (works in this build; no desktop agent required) ─────────
|
|
81
|
-
let busy = false;
|
|
82
|
-
const fileInput = h("input", {
|
|
83
|
-
type: "file", multiple: true, accept: UPLOAD_ACCEPT,
|
|
84
|
-
style: { display: "none" }, "aria-hidden": "true",
|
|
85
|
-
on: { change: (e) => uploadFiles(e.target.files) },
|
|
86
|
-
});
|
|
87
|
-
const pickFiles = () => { if (!busy) fileInput.click(); };
|
|
88
|
-
const slots = { statHost, srcSlot, tableHost, pickFiles };
|
|
89
|
-
|
|
90
|
-
async function uploadFiles(fileList) {
|
|
91
|
-
const files = Array.from(fileList || []);
|
|
92
|
-
if (!files.length || busy) return;
|
|
93
|
-
busy = true;
|
|
94
|
-
let ok = 0;
|
|
95
|
-
for (const file of files) {
|
|
96
|
-
toast(`Uploading “${file.name}”…`, "info");
|
|
97
|
-
const res = await api.uploadDocument(file);
|
|
98
|
-
if (res.ok && res.data && !res.data.detail && !res.data.error) {
|
|
99
|
-
ok++;
|
|
100
|
-
} else {
|
|
101
|
-
const detail = (res.data && (res.data.detail || res.data.error)) || "the backend is unavailable";
|
|
102
|
-
toast(`Could not ingest “${file.name}” — ${detail}.`, "warn");
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
fileInput.value = "";
|
|
106
|
-
busy = false;
|
|
107
|
-
if (ok) {
|
|
108
|
-
toast(`Indexed ${ok} document${ok === 1 ? "" : "s"} into the knowledge graph — now searchable in Chat and Hybrid Search.`, "ok");
|
|
109
|
-
}
|
|
110
|
-
hydrate(ctx, slots);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const dropZone = h("div.lt3-drop", {
|
|
114
|
-
on: {
|
|
115
|
-
dragover: (e) => { e.preventDefault(); dropZone.classList.add("is-dragover"); },
|
|
116
|
-
dragleave: () => dropZone.classList.remove("is-dragover"),
|
|
117
|
-
drop: (e) => { e.preventDefault(); dropZone.classList.remove("is-dragover"); uploadFiles(e.dataTransfer && e.dataTransfer.files); },
|
|
118
|
-
},
|
|
119
|
-
},
|
|
120
|
-
fileInput,
|
|
121
|
-
h("div.lt3-pillar__icon", icon("cloud-upload")),
|
|
122
|
-
h("div",
|
|
123
|
-
h("div", { style: { "font-weight": "var(--lt3-weight-semi)" } }, "Drag documents here, or upload manually"),
|
|
124
|
-
h("p.lt3-faint", { style: { "font-size": "var(--lt3-text-sm)", "margin-top": "var(--lt3-space-1)" } },
|
|
125
|
-
"Lattice parses each file, chunks it, embeds it, and links it into the knowledge graph. PDF · DOCX · XLSX · PPTX · TXT · MD · CSV, up to 10 MB each."),
|
|
126
|
-
),
|
|
127
|
-
h("button.lt3-btn.lt3-btn--primary", { type: "button", on: { click: pickFiles } }, icon("upload"), "Upload files"),
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
const root = h("div.lt3-stack-6",
|
|
131
|
-
c.viewHeader({
|
|
132
|
-
eyebrow: "Data",
|
|
133
|
-
title: "Files",
|
|
134
|
-
sub: "Connected sources and the documents Lattice has indexed for retrieval. Everything stays on this machine.",
|
|
135
|
-
actions: [
|
|
136
|
-
h("button.lt3-btn.lt3-btn--ghost", { on: { click: () => navigate("knowledge-graph") } }, icon("chart-dots-3"), "View graph"),
|
|
137
|
-
h("button.lt3-btn.lt3-btn--primary", { on: { click: pickFiles } }, icon("upload"), "Upload files"),
|
|
138
|
-
h("button.lt3-btn.lt3-btn--ghost", { title: "Requires the desktop local agent (not in this build)", on: { click: unavailableToast } }, icon("folder-plus"), "Connect folder"),
|
|
139
|
-
],
|
|
140
|
-
}),
|
|
141
|
-
statHost,
|
|
142
|
-
dropZone,
|
|
143
|
-
c.panel({
|
|
144
|
-
head: h("div.lt3-row", { style: { "justify-content": "space-between", "align-items": "flex-start", width: "100%" } },
|
|
145
|
-
h("div",
|
|
146
|
-
h("div.lt3-eyebrow", "Index"),
|
|
147
|
-
h("h3.lt3-panel__title", "Indexed documents"),
|
|
148
|
-
),
|
|
149
|
-
srcSlot,
|
|
150
|
-
),
|
|
151
|
-
children: tableHost,
|
|
152
|
-
}),
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
hydrate(ctx, slots);
|
|
156
|
-
return root;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async function hydrate(ctx, slots) {
|
|
160
|
-
const { h, icon, api, c, toast } = ctx;
|
|
161
|
-
const { statHost, srcSlot, tableHost, pickFiles } = slots;
|
|
162
|
-
|
|
163
|
-
const probe = await api.get("/workspace/indexing", { sources: [], totals: {} });
|
|
164
|
-
const liveFiles = probe.ok && probe.data ? normalize(probe.data) : null;
|
|
165
|
-
const source = probe.source || (liveFiles ? "live" : "unavailable");
|
|
166
|
-
const files = liveFiles || [];
|
|
167
|
-
srcSlot.replaceChildren(c.sourceBadge(source));
|
|
168
|
-
|
|
169
|
-
// ── Stat roll-up ──────────────────────────────────────────────────────────
|
|
170
|
-
const indexedCount = files.filter((f) => f.indexed).length;
|
|
171
|
-
const sourceCount = new Set(
|
|
172
|
-
files.map((f) => (f.path.includes("/") ? f.path.split("/")[0] : "root")),
|
|
173
|
-
).size;
|
|
174
|
-
const totalBytes = files.reduce((sum, f) => sum + (f.size || 0), 0);
|
|
175
|
-
statHost.replaceChildren(
|
|
176
|
-
c.stat({ label: "Total files", value: c.fmtNum(files.length), icon: "files" }),
|
|
177
|
-
c.stat({ label: "Indexed", value: c.fmtNum(indexedCount), icon: "circle-check" }),
|
|
178
|
-
c.stat({ label: "Sources", value: c.fmtNum(sourceCount), icon: "database" }),
|
|
179
|
-
c.stat({ label: "Total size", value: humanSize(totalBytes), icon: "weight" }),
|
|
180
|
-
);
|
|
181
|
-
|
|
182
|
-
// ── Empty state ─────────────────────────────────────────────────────────────
|
|
183
|
-
if (!files.length) {
|
|
184
|
-
tableHost.replaceChildren(c.emptyState({
|
|
185
|
-
icon: "folder-off",
|
|
186
|
-
title: "No documents indexed yet",
|
|
187
|
-
body: "Upload a document and Lattice will parse, embed, and link it into the knowledge graph for hybrid retrieval.",
|
|
188
|
-
action: h("button.lt3-btn.lt3-btn--primary.lt3-btn--sm",
|
|
189
|
-
{ on: { click: () => (pickFiles ? pickFiles() : null) } },
|
|
190
|
-
icon("upload"), "Upload files"),
|
|
191
|
-
}));
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// ── Table ───────────────────────────────────────────────────────────────────
|
|
196
|
-
const columns = [
|
|
197
|
-
{
|
|
198
|
-
key: "name", label: "Name",
|
|
199
|
-
render: (row) => h("div.lt3-row-2",
|
|
200
|
-
h("span.lt3-filerow__icon", icon(iconForKind(row.kind))),
|
|
201
|
-
h("span", { style: { "font-weight": "var(--lt3-weight-medium)" } }, row.name),
|
|
202
|
-
),
|
|
203
|
-
},
|
|
204
|
-
{
|
|
205
|
-
key: "path", label: "Path", width: "30%",
|
|
206
|
-
render: (row) => h("span.lt3-mono.lt3-faint", row.path || "—"),
|
|
207
|
-
},
|
|
208
|
-
{
|
|
209
|
-
key: "count", label: "Indexed", width: "92px",
|
|
210
|
-
render: (row) => h("span.lt3-mono", row.count ? c.fmtNum(row.count) : humanSize(row.size)),
|
|
211
|
-
},
|
|
212
|
-
{
|
|
213
|
-
key: "status", label: "Status", width: "120px",
|
|
214
|
-
render: (row) => c.statePill(row.indexed ? "indexed" : (row.status || "pending")),
|
|
215
|
-
},
|
|
216
|
-
{
|
|
217
|
-
key: "updated", label: "Updated", width: "104px",
|
|
218
|
-
render: (row) => h("span.lt3-faint", { style: { "font-size": "var(--lt3-text-xs)" } },
|
|
219
|
-
row.updated ? timeAgo(row.updated) : "—"),
|
|
220
|
-
},
|
|
221
|
-
{
|
|
222
|
-
key: "_actions", label: "", width: "44px",
|
|
223
|
-
render: (row) => h("button.lt3-iconbtn.lt3-iconbtn--sm", {
|
|
224
|
-
"aria-label": `Actions for ${row.name}`,
|
|
225
|
-
title: "Requires the desktop local agent (not in this build)",
|
|
226
|
-
on: { click: () => toast(`Per-file actions require the Lattice desktop local agent — not available in this build.`, "warn") },
|
|
227
|
-
}, icon("dots-vertical")),
|
|
228
|
-
},
|
|
229
|
-
];
|
|
230
|
-
|
|
231
|
-
tableHost.replaceChildren(c.table(columns, files));
|
|
232
|
-
}
|