@circuitwall/jarela 1.9.0 → 1.9.1

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 (57) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-path-routes-manifest.json +2 -2
  3. package/.next/standalone/.next/build-manifest.json +2 -2
  4. package/.next/standalone/.next/prerender-manifest.json +3 -3
  5. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  6. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  12. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  15. package/.next/standalone/.next/server/app/_not-found.html +2 -2
  16. package/.next/standalone/.next/server/app/_not-found.rsc +2 -2
  17. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  18. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  20. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  21. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  22. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  23. package/.next/standalone/.next/server/app/page.js +30057 -29350
  24. package/.next/standalone/.next/server/app/page.js.map +1 -1
  25. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  26. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  27. package/.next/standalone/.next/server/app-paths-manifest.json +2 -2
  28. package/.next/standalone/.next/server/chunks/8954.js +35 -0
  29. package/.next/standalone/.next/server/chunks/8954.js.map +1 -1
  30. package/.next/standalone/.next/server/middleware-build-manifest.js +2 -2
  31. package/.next/standalone/.next/server/pages/404.html +2 -2
  32. package/.next/standalone/.next/server/pages/500.html +1 -1
  33. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  34. package/.next/standalone/.next/static/chunks/{9476-ea25d4553680515b.js → 1998-31a617131197a83a.js} +53 -2
  35. package/.next/standalone/.next/static/chunks/1998-31a617131197a83a.js.map +1 -0
  36. package/.next/standalone/.next/static/chunks/{2747-d06d5caf7bca15e5.js → 2747-4a6287cacd57d231.js} +36 -1
  37. package/.next/standalone/.next/static/chunks/2747-4a6287cacd57d231.js.map +1 -0
  38. package/.next/standalone/.next/static/chunks/app/{page-a9dcc924ad127090.js → page-cd662565eba5ef59.js} +675 -4
  39. package/.next/standalone/.next/static/chunks/app/page-cd662565eba5ef59.js.map +1 -0
  40. package/.next/standalone/.next/static/css/11aaed27d2989cc1.css +5 -0
  41. package/.next/standalone/.next/static/css/11aaed27d2989cc1.css.map +1 -0
  42. package/.next/standalone/package.json +1 -1
  43. package/CHANGELOG.md +24 -0
  44. package/README.md +6 -0
  45. package/api/client.ts +57 -0
  46. package/api/types.ts +81 -1
  47. package/components/tools/LangChainPackagesPanel.tsx +469 -0
  48. package/components/tools/ToolsPanel.tsx +21 -2
  49. package/hooks/usePackages.ts +111 -0
  50. package/package.json +1 -1
  51. package/.next/standalone/.next/static/chunks/2747-d06d5caf7bca15e5.js.map +0 -1
  52. package/.next/standalone/.next/static/chunks/9476-ea25d4553680515b.js.map +0 -1
  53. package/.next/standalone/.next/static/chunks/app/page-a9dcc924ad127090.js.map +0 -1
  54. package/.next/standalone/.next/static/css/fb519215ea1c4fd8.css +0 -5
  55. package/.next/standalone/.next/static/css/fb519215ea1c4fd8.css.map +0 -1
  56. /package/.next/standalone/.next/static/{TE5LAkHzp2Kipn0ypeCyH → tTk-KuLcT7O-E0z6PdMmO}/_buildManifest.js +0 -0
  57. /package/.next/standalone/.next/static/{TE5LAkHzp2Kipn0ypeCyH → tTk-KuLcT7O-E0z6PdMmO}/_ssgManifest.js +0 -0
package/api/types.ts CHANGED
@@ -1159,4 +1159,84 @@ export interface ExtensionsListResponse {
1159
1159
  providers: ExtensionInfo[];
1160
1160
  tools: ExternalToolInfo[];
1161
1161
  errors: ExtensionLoadError[];
1162
- }
1162
+ }
1163
+
1164
+ /**
1165
+ * Hot-loaded vanilla LangChain tool packages. Surface for the
1166
+ * Tools → LangChain packages UI panel.
1167
+ */
1168
+ export interface LangChainPackageManifest {
1169
+ package: string;
1170
+ export: string;
1171
+ category: string;
1172
+ capability: "read" | "write" | "execute";
1173
+ args?: Record<string, unknown>;
1174
+ requiredEnv?: string[];
1175
+ }
1176
+
1177
+ export interface LangChainPackageManifestRecord {
1178
+ name: string;
1179
+ manifest: LangChainPackageManifest;
1180
+ }
1181
+
1182
+ export interface LangChainPackageLoadResult {
1183
+ registered: string[];
1184
+ skipped: { manifest: string; reason: string }[];
1185
+ errors: { manifest: string; error: string }[];
1186
+ }
1187
+
1188
+ export interface LangChainPackageListResponse extends LangChainPackageLoadResult {
1189
+ packagesDir: string;
1190
+ }
1191
+
1192
+ export interface LangChainPackageManifestInput {
1193
+ name: string;
1194
+ package: string;
1195
+ export?: string;
1196
+ category: string;
1197
+ capability?: "read" | "write" | "execute";
1198
+ args?: Record<string, unknown>;
1199
+ requiredEnv?: string[];
1200
+ }
1201
+
1202
+ export interface LangChainPackagePendingInstall {
1203
+ id: string;
1204
+ spec: string;
1205
+ version: string | null;
1206
+ publisher: string;
1207
+ reason: string;
1208
+ createdAt: string;
1209
+ }
1210
+
1211
+ export interface LangChainPackageIntrospectedTool {
1212
+ export: string;
1213
+ name: string;
1214
+ description: string;
1215
+ requiredEnv: string[];
1216
+ }
1217
+
1218
+ export interface LangChainPackageInstallResult {
1219
+ status: "installed";
1220
+ spec: string;
1221
+ publisher: string;
1222
+ resolvedPackage: string;
1223
+ installedVersion: string | null;
1224
+ tools: LangChainPackageIntrospectedTool[];
1225
+ }
1226
+
1227
+ export interface LangChainPackagePendingResponse {
1228
+ status: "pending";
1229
+ approvalId: string;
1230
+ publisher: string;
1231
+ spec: string;
1232
+ reason: string;
1233
+ }
1234
+
1235
+ export type LangChainPackageInstallResponse =
1236
+ | LangChainPackageInstallResult
1237
+ | LangChainPackagePendingResponse;
1238
+
1239
+ export interface LangChainPackageManifestCreateResult {
1240
+ record: LangChainPackageManifestRecord;
1241
+ load: LangChainPackageLoadResult;
1242
+ }
@@ -0,0 +1,469 @@
1
+ "use client";
2
+
3
+ import { useMemo, useState } from "react";
4
+ import { RefreshCw, Trash2, Check, X, Package } from "lucide-react";
5
+ import { usePackages } from "@/hooks/usePackages";
6
+ import { pushErrorToast } from "@/lib/ui/error-report";
7
+ import type {
8
+ LangChainPackageInstallResponse,
9
+ LangChainPackageManifestRecord,
10
+ } from "@/api/types";
11
+
12
+ const CATEGORIES = [
13
+ "Memory", "Documents", "Files", "Shell", "Web", "Images", "Voice",
14
+ "Schedule", "Atlassian", "JiraAlign", "GitHub", "Mail", "Calendar",
15
+ "Config", "Agent",
16
+ ] as const;
17
+
18
+ const CAPABILITIES = ["read", "write", "execute"] as const;
19
+
20
+ interface ManifestFormState {
21
+ name: string;
22
+ package: string;
23
+ exportName: string;
24
+ category: (typeof CATEGORIES)[number];
25
+ capability: (typeof CAPABILITIES)[number];
26
+ requiredEnv: string;
27
+ }
28
+
29
+ const EMPTY_FORM: ManifestFormState = {
30
+ name: "",
31
+ package: "",
32
+ exportName: "",
33
+ category: "Web",
34
+ capability: "execute",
35
+ requiredEnv: "",
36
+ };
37
+
38
+ export function LangChainPackagesPanel() {
39
+ const {
40
+ loadResult,
41
+ manifests,
42
+ pending,
43
+ loading,
44
+ refresh,
45
+ install,
46
+ approveInstall,
47
+ denyInstall,
48
+ createManifest,
49
+ deleteManifest,
50
+ reload,
51
+ } = usePackages();
52
+
53
+ const [installSpec, setInstallSpec] = useState("");
54
+ const [installVersion, setInstallVersion] = useState("");
55
+ const [installing, setInstalling] = useState(false);
56
+ const [installNotice, setInstallNotice] = useState<string | null>(null);
57
+
58
+ const [form, setForm] = useState<ManifestFormState>(EMPTY_FORM);
59
+ const [savingManifest, setSavingManifest] = useState(false);
60
+
61
+ const errorRows = useMemo(() => loadResult?.errors ?? [], [loadResult]);
62
+ const skippedRows = useMemo(() => loadResult?.skipped ?? [], [loadResult]);
63
+
64
+ async function handleInstall(e: React.FormEvent) {
65
+ e.preventDefault();
66
+ if (!installSpec.trim()) return;
67
+ setInstalling(true);
68
+ setInstallNotice(null);
69
+ try {
70
+ const res: LangChainPackageInstallResponse = await install(
71
+ installSpec.trim(),
72
+ installVersion.trim() || undefined,
73
+ );
74
+ if (res.status === "pending") {
75
+ setInstallNotice(
76
+ `Publisher "${res.publisher}" not in allowlist — pending approval below.`,
77
+ );
78
+ } else {
79
+ setInstallNotice(
80
+ `Installed ${res.resolvedPackage}@${res.installedVersion ?? "?"} (${res.tools.length} tool${res.tools.length === 1 ? "" : "s"} found).`,
81
+ );
82
+ setInstallSpec("");
83
+ setInstallVersion("");
84
+ }
85
+ } catch (e) {
86
+ pushErrorToast({
87
+ title: "Install failed",
88
+ error: e,
89
+ context: { panel: "packages", action: "install", spec: installSpec },
90
+ });
91
+ } finally {
92
+ setInstalling(false);
93
+ }
94
+ }
95
+
96
+ async function handleApprove(id: string) {
97
+ try {
98
+ await approveInstall(id);
99
+ } catch (e) {
100
+ pushErrorToast({
101
+ title: "Approval failed",
102
+ error: e,
103
+ context: { panel: "packages", action: "approve", id },
104
+ });
105
+ }
106
+ }
107
+
108
+ async function handleDeny(id: string) {
109
+ try {
110
+ await denyInstall(id);
111
+ } catch (e) {
112
+ pushErrorToast({
113
+ title: "Deny failed",
114
+ error: e,
115
+ context: { panel: "packages", action: "deny", id },
116
+ });
117
+ }
118
+ }
119
+
120
+ async function handleCreateManifest(e: React.FormEvent) {
121
+ e.preventDefault();
122
+ if (!form.name.trim() || !form.package.trim()) return;
123
+ setSavingManifest(true);
124
+ try {
125
+ const requiredEnv = form.requiredEnv
126
+ .split(",")
127
+ .map((s) => s.trim())
128
+ .filter(Boolean);
129
+ await createManifest({
130
+ name: form.name.trim(),
131
+ package: form.package.trim(),
132
+ export: form.exportName.trim() || undefined,
133
+ category: form.category,
134
+ capability: form.capability,
135
+ requiredEnv: requiredEnv.length > 0 ? requiredEnv : undefined,
136
+ });
137
+ setForm(EMPTY_FORM);
138
+ } catch (e) {
139
+ pushErrorToast({
140
+ title: "Couldn't create manifest",
141
+ error: e,
142
+ context: { panel: "packages", action: "create-manifest", name: form.name },
143
+ });
144
+ } finally {
145
+ setSavingManifest(false);
146
+ }
147
+ }
148
+
149
+ async function handleDeleteManifest(record: LangChainPackageManifestRecord) {
150
+ if (typeof window !== "undefined") {
151
+ if (!window.confirm(`Delete manifest "${record.name}"?`)) return;
152
+ }
153
+ try {
154
+ await deleteManifest(record.name);
155
+ } catch (e) {
156
+ pushErrorToast({
157
+ title: "Couldn't delete manifest",
158
+ error: e,
159
+ context: { panel: "packages", action: "delete-manifest", name: record.name },
160
+ });
161
+ }
162
+ }
163
+
164
+ async function handleReload() {
165
+ try {
166
+ await reload();
167
+ } catch (e) {
168
+ pushErrorToast({
169
+ title: "Reload failed",
170
+ error: e,
171
+ context: { panel: "packages", action: "reload" },
172
+ });
173
+ }
174
+ }
175
+
176
+ return (
177
+ <div className="p-4 space-y-5">
178
+ <header className="space-y-1">
179
+ <h2 className="text-sm font-semibold text-fg">LangChain packages</h2>
180
+ <p className="text-xs text-fg-faint">
181
+ Hot-load vanilla LangChain tool packages from npm without a rebuild. Trusted
182
+ publishers (<code className="font-mono text-[11px]">@langchain/</code>,{" "}
183
+ <code className="font-mono text-[11px]">@circuitwall/</code>,{" "}
184
+ <code className="font-mono text-[11px]">langchain</code>) install
185
+ immediately; everything else waits for approval below.
186
+ </p>
187
+ {loadResult?.packagesDir && (
188
+ <p className="text-[11px] text-fg-faint font-mono">
189
+ {loadResult.packagesDir}
190
+ </p>
191
+ )}
192
+ </header>
193
+
194
+ <section className="space-y-2">
195
+ <div className="flex items-center gap-2">
196
+ <h3 className="text-sm font-semibold text-fg flex-1">Install from npm</h3>
197
+ <button
198
+ type="button"
199
+ onClick={handleReload}
200
+ className="shrink-0 rounded-md border border-border px-2.5 py-1.5 text-xs text-fg-muted hover:text-fg hover:bg-surface-2 inline-flex items-center gap-1.5"
201
+ aria-label="Reload installed packages"
202
+ >
203
+ <RefreshCw className="w-3.5 h-3.5" /> Reload
204
+ </button>
205
+ </div>
206
+ <form
207
+ onSubmit={handleInstall}
208
+ className="grid gap-2 md:grid-cols-[minmax(0,1fr)_140px_auto]"
209
+ >
210
+ <input
211
+ type="text"
212
+ value={installSpec}
213
+ onChange={(e) => setInstallSpec(e.target.value)}
214
+ placeholder="@langchain/community"
215
+ className="rounded-md border border-border bg-surface-2 px-3 py-2 text-sm text-fg outline-none focus:border-fg-faint"
216
+ aria-label="Package spec"
217
+ />
218
+ <input
219
+ type="text"
220
+ value={installVersion}
221
+ onChange={(e) => setInstallVersion(e.target.value)}
222
+ placeholder="version (optional)"
223
+ className="rounded-md border border-border bg-surface-2 px-3 py-2 text-sm text-fg outline-none focus:border-fg-faint"
224
+ aria-label="Version"
225
+ />
226
+ <button
227
+ type="submit"
228
+ disabled={installing || !installSpec.trim()}
229
+ className="rounded-md border border-border bg-surface-2 px-3 py-2 text-sm text-fg hover:bg-surface-3 disabled:opacity-50 disabled:cursor-not-allowed"
230
+ >
231
+ {installing ? "Installing…" : "Install"}
232
+ </button>
233
+ </form>
234
+ {installNotice && (
235
+ <p className="text-xs text-fg-muted">{installNotice}</p>
236
+ )}
237
+ </section>
238
+
239
+ {pending.length > 0 && (
240
+ <section className="space-y-2">
241
+ <h3 className="text-sm font-semibold text-fg">
242
+ Pending approvals ({pending.length})
243
+ </h3>
244
+ <ul className="space-y-2">
245
+ {pending.map((p) => (
246
+ <li
247
+ key={p.id}
248
+ className="rounded-lg border border-border bg-surface-2 px-3 py-2.5"
249
+ >
250
+ <div className="flex items-start gap-3">
251
+ <div className="flex-1 min-w-0">
252
+ <div className="text-sm font-mono text-fg">
253
+ {p.spec}
254
+ {p.version ? `@${p.version}` : ""}
255
+ </div>
256
+ <p className="text-xs text-fg-muted mt-0.5">
257
+ Publisher{" "}
258
+ <span className="font-mono">{p.publisher}</span> · {p.reason}
259
+ </p>
260
+ </div>
261
+ <div className="flex gap-1.5 shrink-0">
262
+ <button
263
+ type="button"
264
+ onClick={() => handleApprove(p.id)}
265
+ className="rounded-md border border-border px-2 py-1.5 text-xs text-fg-muted hover:text-fg hover:bg-surface-3 inline-flex items-center gap-1"
266
+ aria-label={`Approve ${p.spec}`}
267
+ >
268
+ <Check className="w-3.5 h-3.5" /> Approve
269
+ </button>
270
+ <button
271
+ type="button"
272
+ onClick={() => handleDeny(p.id)}
273
+ className="rounded-md border border-border px-2 py-1.5 text-xs text-fg-muted hover:text-fg hover:bg-surface-3 inline-flex items-center gap-1"
274
+ aria-label={`Deny ${p.spec}`}
275
+ >
276
+ <X className="w-3.5 h-3.5" /> Deny
277
+ </button>
278
+ </div>
279
+ </div>
280
+ </li>
281
+ ))}
282
+ </ul>
283
+ </section>
284
+ )}
285
+
286
+ <section className="space-y-2 pt-3 border-t border-border">
287
+ <h3 className="text-sm font-semibold text-fg">Tool manifests</h3>
288
+ <p className="text-xs text-fg-faint">
289
+ A manifest maps an exported class from an installed npm package to a
290
+ built-in category. Surfaced under{" "}
291
+ <code className="font-mono text-[11px]">/api/v1/tools</code> after a
292
+ reload.
293
+ </p>
294
+
295
+ {loading && (
296
+ <p className="text-fg-faint text-sm py-3 text-center">Loading…</p>
297
+ )}
298
+
299
+ {!loading && manifests.length === 0 && (
300
+ <p className="text-xs text-fg-faint py-2">
301
+ No manifests yet. Add one below.
302
+ </p>
303
+ )}
304
+
305
+ <ul className="space-y-2">
306
+ {manifests.map((r) => (
307
+ <li
308
+ key={r.name}
309
+ className="rounded-lg border border-border bg-surface-2 px-3 py-2.5"
310
+ >
311
+ <div className="flex items-start gap-3">
312
+ <Package className="w-4 h-4 text-fg-faint mt-0.5 shrink-0" />
313
+ <div className="flex-1 min-w-0">
314
+ <div className="flex items-baseline gap-2 flex-wrap">
315
+ <span className="text-sm font-semibold text-fg">{r.name}</span>
316
+ <span className="text-[11px] text-fg-faint">
317
+ {r.manifest.category} · {r.manifest.capability}
318
+ </span>
319
+ </div>
320
+ <p className="text-xs text-fg-muted mt-0.5 font-mono break-all">
321
+ {r.manifest.package}#{r.manifest.export}
322
+ </p>
323
+ {r.manifest.requiredEnv && r.manifest.requiredEnv.length > 0 && (
324
+ <p className="text-[11px] text-fg-faint mt-1">
325
+ env: {r.manifest.requiredEnv.join(", ")}
326
+ </p>
327
+ )}
328
+ </div>
329
+ <button
330
+ type="button"
331
+ onClick={() => handleDeleteManifest(r)}
332
+ className="rounded-md border border-border px-2 py-1.5 text-xs text-fg-muted hover:text-fg hover:bg-surface-3 shrink-0"
333
+ aria-label={`Delete manifest ${r.name}`}
334
+ >
335
+ <Trash2 className="w-3.5 h-3.5" />
336
+ </button>
337
+ </div>
338
+ </li>
339
+ ))}
340
+ </ul>
341
+
342
+ <form
343
+ onSubmit={handleCreateManifest}
344
+ className="rounded-lg border border-border bg-surface-2 p-3 space-y-2"
345
+ >
346
+ <h4 className="text-xs font-semibold text-fg-muted uppercase tracking-wide">
347
+ Add manifest
348
+ </h4>
349
+ <div className="grid gap-2 md:grid-cols-2">
350
+ <input
351
+ type="text"
352
+ value={form.name}
353
+ onChange={(e) => setForm({ ...form, name: e.target.value })}
354
+ placeholder="name (e.g. tavily)"
355
+ className="rounded-md border border-border bg-surface-2 px-3 py-2 text-sm text-fg outline-none focus:border-fg-faint"
356
+ aria-label="Manifest name"
357
+ />
358
+ <input
359
+ type="text"
360
+ value={form.package}
361
+ onChange={(e) => setForm({ ...form, package: e.target.value })}
362
+ placeholder="package (e.g. @langchain/community/tools/tavily_search)"
363
+ className="rounded-md border border-border bg-surface-2 px-3 py-2 text-sm text-fg outline-none focus:border-fg-faint"
364
+ aria-label="Package import path"
365
+ />
366
+ <input
367
+ type="text"
368
+ value={form.exportName}
369
+ onChange={(e) => setForm({ ...form, exportName: e.target.value })}
370
+ placeholder="export (default: default)"
371
+ className="rounded-md border border-border bg-surface-2 px-3 py-2 text-sm text-fg outline-none focus:border-fg-faint"
372
+ aria-label="Export name"
373
+ />
374
+ <input
375
+ type="text"
376
+ value={form.requiredEnv}
377
+ onChange={(e) => setForm({ ...form, requiredEnv: e.target.value })}
378
+ placeholder="required env, comma-separated"
379
+ className="rounded-md border border-border bg-surface-2 px-3 py-2 text-sm text-fg outline-none focus:border-fg-faint"
380
+ aria-label="Required env vars"
381
+ />
382
+ <select
383
+ value={form.category}
384
+ onChange={(e) =>
385
+ setForm({
386
+ ...form,
387
+ category: e.target.value as (typeof CATEGORIES)[number],
388
+ })
389
+ }
390
+ className="rounded-md border border-border bg-surface-2 px-3 py-2 text-sm text-fg outline-none focus:border-fg-faint"
391
+ aria-label="Category"
392
+ >
393
+ {CATEGORIES.map((c) => (
394
+ <option key={c} value={c}>
395
+ {c}
396
+ </option>
397
+ ))}
398
+ </select>
399
+ <select
400
+ value={form.capability}
401
+ onChange={(e) =>
402
+ setForm({
403
+ ...form,
404
+ capability: e.target.value as (typeof CAPABILITIES)[number],
405
+ })
406
+ }
407
+ className="rounded-md border border-border bg-surface-2 px-3 py-2 text-sm text-fg outline-none focus:border-fg-faint"
408
+ aria-label="Capability"
409
+ >
410
+ {CAPABILITIES.map((c) => (
411
+ <option key={c} value={c}>
412
+ {c}
413
+ </option>
414
+ ))}
415
+ </select>
416
+ </div>
417
+ <div className="flex justify-end">
418
+ <button
419
+ type="submit"
420
+ disabled={savingManifest || !form.name.trim() || !form.package.trim()}
421
+ className="rounded-md border border-border bg-surface-2 px-3 py-1.5 text-xs text-fg hover:bg-surface-3 disabled:opacity-50 disabled:cursor-not-allowed"
422
+ >
423
+ {savingManifest ? "Saving…" : "Add manifest"}
424
+ </button>
425
+ </div>
426
+ </form>
427
+ </section>
428
+
429
+ {(errorRows.length > 0 || skippedRows.length > 0) && (
430
+ <section className="space-y-2 pt-3 border-t border-border">
431
+ <div className="flex items-center gap-2">
432
+ <h3 className="text-sm font-semibold text-fg flex-1">Load status</h3>
433
+ <button
434
+ type="button"
435
+ onClick={refresh}
436
+ className="shrink-0 rounded-md border border-border px-2.5 py-1.5 text-xs text-fg-muted hover:text-fg hover:bg-surface-2"
437
+ >
438
+ Refresh
439
+ </button>
440
+ </div>
441
+ {errorRows.length > 0 && (
442
+ <ul className="space-y-1">
443
+ {errorRows.map((e) => (
444
+ <li
445
+ key={e.manifest}
446
+ className="rounded border border-red-500/40 bg-red-500/10 px-2 py-1.5 text-xs text-fg"
447
+ >
448
+ <span className="font-mono">{e.manifest}</span>: {e.error}
449
+ </li>
450
+ ))}
451
+ </ul>
452
+ )}
453
+ {skippedRows.length > 0 && (
454
+ <ul className="space-y-1">
455
+ {skippedRows.map((s) => (
456
+ <li
457
+ key={s.manifest}
458
+ className="rounded border border-border bg-surface-2 px-2 py-1.5 text-xs text-fg-muted"
459
+ >
460
+ <span className="font-mono">{s.manifest}</span> skipped — {s.reason}
461
+ </li>
462
+ ))}
463
+ </ul>
464
+ )}
465
+ </section>
466
+ )}
467
+ </div>
468
+ );
469
+ }
@@ -6,10 +6,12 @@ import { DocumentsPanel } from "@/components/documents/DocumentsPanel";
6
6
  import { MemoryPanel } from "@/components/memory/MemoryPanel";
7
7
  import { BridgesPanel } from "@/components/bridges/BridgesPanel";
8
8
  import { BuiltinToolsPanel } from "./BuiltinToolsPanel";
9
+ import { LangChainPackagesPanel } from "./LangChainPackagesPanel";
9
10
 
10
11
  // "Tools" is about *capability presence* — every surface that determines
11
12
  // what an agent can sense or act on lives here:
12
13
  // - "Built-in" — categories of tools that ship with Jarela.
14
+ // - "Packages" — vanilla LangChain tool packages hot-loaded from npm.
13
15
  // - "Documents" — indexed knowledge sources the agent can search.
14
16
  // - "Memory" — long-lived facts persisted across conversations.
15
17
  // - "MCP" — external Model Context Protocol servers.
@@ -18,12 +20,28 @@ import { BuiltinToolsPanel } from "./BuiltinToolsPanel";
18
20
  // Credentials still flow through the Credentials tab; this surface owns
19
21
  // the capability roster.
20
22
 
21
- type Sub = "builtin" | "documents" | "memory" | "mcp" | "extensions" | "bridges";
23
+ type Sub =
24
+ | "builtin"
25
+ | "packages"
26
+ | "documents"
27
+ | "memory"
28
+ | "mcp"
29
+ | "extensions"
30
+ | "bridges";
22
31
 
23
- const SUBS: Sub[] = ["builtin", "documents", "memory", "mcp", "extensions", "bridges"];
32
+ const SUBS: Sub[] = [
33
+ "builtin",
34
+ "packages",
35
+ "documents",
36
+ "memory",
37
+ "mcp",
38
+ "extensions",
39
+ "bridges",
40
+ ];
24
41
 
25
42
  const SUB_TITLES: Record<Sub, string> = {
26
43
  builtin: "Built-in",
44
+ packages: "LangChain packages",
27
45
  documents: "Documents",
28
46
  memory: "Memory",
29
47
  mcp: "MCP servers",
@@ -70,6 +88,7 @@ export function ToolsPanel() {
70
88
  </div>
71
89
  <div className="flex-1 min-h-0 overflow-auto">
72
90
  {active === "builtin" && <BuiltinToolsPanel />}
91
+ {active === "packages" && <LangChainPackagesPanel />}
73
92
  {active === "documents" && <DocumentsPanel />}
74
93
  {active === "memory" && <MemoryPanel />}
75
94
  {active === "mcp" && <MCPPanel />}