@aibranch/llm-meta-widget 0.7.2 → 0.7.4
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 +13 -9
- package/docs/custom-element-distribution.md +65 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Embeddable browser chat widget for the [llm_meta](https://github.com/pubannotation) ecosystem.
|
|
4
4
|
|
|
5
|
-
Client-orchestrated: the widget fetches host-side action schemas + host-published `.well-known/mcp.json` manifests at boot, dispatches tool_calls locally (page-embedded actions) or directly to MCP endpoints (host-wide well-known), and consumes the meta-server's SSE `single_llm_calls` API. Ships
|
|
5
|
+
Client-orchestrated: the widget fetches host-side action schemas + host-published `.well-known/mcp.json` manifests at boot, dispatches tool_calls locally (page-embedded actions) or directly to MCP endpoints (host-wide well-known), and consumes the meta-server's SSE `single_llm_calls` API. Ships as a custom element in one self-contained ES module — on npm for any host, and in a Rails gem whose helper serves the identical file.
|
|
6
6
|
|
|
7
7
|
**No** Devise, DB migrations, ChatManager, or PromptNavigator. Adds only `rails >= 8.0` as a runtime dep — so hosts that haven't bumped to 8.1 can adopt it without a Rails upgrade.
|
|
8
8
|
|
|
@@ -32,7 +32,7 @@ step, no Node at runtime.
|
|
|
32
32
|
|
|
33
33
|
```ruby
|
|
34
34
|
# Gemfile
|
|
35
|
-
gem "llm_meta_widget", "~> 0.
|
|
35
|
+
gem "llm_meta_widget", "~> 0.7"
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
```
|
|
@@ -62,8 +62,8 @@ the role label turns into a spinning gear reading "Working…", and the reply
|
|
|
62
62
|
streams in.
|
|
63
63
|
|
|
64
64
|
If the panel opens but nothing comes back, it is almost always CORS. The hub
|
|
65
|
-
must list your app's origin in its `
|
|
66
|
-
`/api/*` resource, because the widget calls the hub **from the visitor's
|
|
65
|
+
must list your app's origin in its `CORS_ORIGINS` environment variable for
|
|
66
|
+
the `/api/*` resource, because the widget calls the hub **from the visitor's
|
|
67
67
|
browser**, not from your server. Open the browser console: a blocked request
|
|
68
68
|
says so explicitly.
|
|
69
69
|
|
|
@@ -248,7 +248,7 @@ By default the widget renders two pickers below the input textarea:
|
|
|
248
248
|
- **Model dropdown** — populated from the hub's `GET /api/llms` (anon path returns Ollama-only, since the widget's LLM calls use `api_key_uuid: "ollama-local"`).
|
|
249
249
|
- **Tool picker** — populated from the hub's `GET /api/mcp_servers` (anon path returns `public_to_anonymous: true` servers). Two-level UX: server bulk-toggle + individual tool checkboxes on expand.
|
|
250
250
|
|
|
251
|
-
Both pickers require **CORS**: the meta-server must
|
|
251
|
+
Both pickers require **CORS**: the meta-server must allow the host's origin on its `/api/*` resource. That allowlist is an environment variable, not code — `CORS_ORIGINS`, a comma-separated list — so adding a host is a deployment change and a restart, with no commit and nothing about one deployment's hosts published in the repo. Without CORS the fetch is silently blocked and the pickers stay empty.
|
|
252
252
|
|
|
253
253
|
To adjust picker behavior at the helper call site:
|
|
254
254
|
|
|
@@ -257,8 +257,8 @@ To adjust picker behavior at the helper call site:
|
|
|
257
257
|
llm_url: "https://your-meta-server.example",
|
|
258
258
|
tool_hub_url: "https://your-meta-server.example",
|
|
259
259
|
model: "qwen3-6-35b-fast", # initial selection
|
|
260
|
-
enable_model_picker: true, # false →
|
|
261
|
-
enable_tool_picker: true, # false →
|
|
260
|
+
enable_model_picker: true, # false → omit picker, use fixed `model:`
|
|
261
|
+
enable_tool_picker: true, # false → omit picker, no Class-1 tools
|
|
262
262
|
models: nil, # nil = all anon models; ["qwen3-6-35b-fast", …] = allowlist
|
|
263
263
|
hub_tools: nil # nil = all anon-public MCPs; ["togomcp", …] = allowlist by server name
|
|
264
264
|
) %>
|
|
@@ -289,8 +289,8 @@ To lock the widget to the fixed `model:` prop and disable Class-1 tools entirely
|
|
|
289
289
|
| `well_known_urls:` | `nil` | `nil` = auto-discover same-origin; explicit array = fetch those; `[]` = disable |
|
|
290
290
|
| `greeting:` | `nil` | First thing a visitor sees when the panel opens, above the offered prompt templates. `nil` = a generic line |
|
|
291
291
|
| `max_rounds:` | `3` | Cap on tool-call rounds per LLM turn. Page actions cost a round each since 0.4.0 — raise it for multi-step flows |
|
|
292
|
-
| `enable_model_picker:` | `true` | Show model dropdown (Level-1) |
|
|
293
|
-
| `enable_tool_picker:` | `true` | Show tool picker (Level-1) |
|
|
292
|
+
| `enable_model_picker:` | `true` | Show model dropdown (Level-1). `false` removes it from the DOM |
|
|
293
|
+
| `enable_tool_picker:` | `true` | Show tool picker (Level-1). `false` removes it from the DOM |
|
|
294
294
|
| `models:` | `nil` | Model-name allowlist; `nil` = all anon-available |
|
|
295
295
|
| `hub_tools:` | `nil` | MCP-server-name allowlist; `nil` = all anon-public |
|
|
296
296
|
|
|
@@ -404,6 +404,10 @@ it most, since they must already know your form to press it.
|
|
|
404
404
|
three tool classes plus prompts and resources. Its
|
|
405
405
|
`app/views/annotation/text_annotation.html.erb` and `app/controllers/mcp_controller.rb`
|
|
406
406
|
are the fullest example available.
|
|
407
|
+
- **Shipping the element** — `docs/custom-element-distribution.md` is the design note
|
|
408
|
+
behind the custom-element move: the npm package and its CDN URL, why `main`,
|
|
409
|
+
`exports` and `sideEffects` are set the way they are, and why the gem and the npm
|
|
410
|
+
package must never drift in version.
|
|
407
411
|
- **Issues and questions** — <https://github.com/jdkim/llm_meta_widget/issues>
|
|
408
412
|
|
|
409
413
|
## License
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# Design note — distributing the widget as a custom element
|
|
2
2
|
|
|
3
|
-
**Status:** implemented 2026-10-03
|
|
4
|
-
publication
|
|
5
|
-
|
|
3
|
+
**Status:** implemented and published 2026-10-03. The element shipped in 0.7.0,
|
|
4
|
+
npm/CDN publication followed the same day, and 0.7.2 fixed the one regression the
|
|
5
|
+
move introduced — see the correction below. Written as a proposal and kept as the
|
|
6
|
+
record of why the shape is what it is, with outcomes marked inline rather than
|
|
7
|
+
rewritten away.
|
|
6
8
|
**Problem owner:** adopters whose host application is not Rails.
|
|
7
9
|
|
|
8
10
|
**What landed, and what did not**
|
|
@@ -14,9 +16,12 @@ runs), a route and controller action serving the bundle, and the partial
|
|
|
14
16
|
reduced to the element tag. The gem keeps its helper signature, so
|
|
15
17
|
PubDictionaries needs no edit.
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
Landed since: the npm package and the CDN URL. `@aibranch/llm-meta-widget` is on
|
|
20
|
+
npm, and `https://cdn.jsdelivr.net/npm/@aibranch/llm-meta-widget@0.7` resolves to
|
|
21
|
+
the bundle, so the two-line integration in the example below is now literally
|
|
22
|
+
true — no gem, no Rails and no self-hosting required. The gem and the npm package
|
|
23
|
+
ship the identical file, which is checked rather than assumed: both 0.7.2 copies
|
|
24
|
+
are sha1 `82d448cdd51fb81a`.
|
|
20
25
|
|
|
21
26
|
Verified on a plain HTML page served by `python3 -m http.server`, with no
|
|
22
27
|
Rails anywhere: the element upgrades and builds its own DOM, injects its own
|
|
@@ -32,6 +37,24 @@ a visitor changes model, and nothing before the browser would catch it. And
|
|
|
32
37
|
the 985 lines of panel logic contain no ERB at all, which is why they moved
|
|
33
38
|
verbatim rather than being rewritten.
|
|
34
39
|
|
|
40
|
+
**Correction — the markup did contain ERB (shipped broken, fixed in 0.7.2).**
|
|
41
|
+
That second finding was true of the panel's JavaScript and false of its HTML.
|
|
42
|
+
The template carried six `<% if enable_model_picker %>`-style tags plus the tail
|
|
43
|
+
of an ERB comment. Moved verbatim into a JS template literal they became inert
|
|
44
|
+
text, so they rendered as visible characters in the panel *and* the two picker
|
|
45
|
+
flags they implemented silently stopped being honoured — a disabled tools picker
|
|
46
|
+
rendered anyway. It shipped in 0.7.0, shipped again in 0.7.1, and was found by
|
|
47
|
+
eye in a screenshot rather than by any test. 0.7.2 removes the ERB and prunes the
|
|
48
|
+
pickers in `boot()` instead, which is where that conditionality has to live once
|
|
49
|
+
the template is static. `markup.test.mjs` now fails on any `<%` or `%>` in the
|
|
50
|
+
source or the built bundle, and the plain-host e2e run reports any template
|
|
51
|
+
syntax visible in the rendered panel.
|
|
52
|
+
|
|
53
|
+
The lesson is narrower than "grep for ERB". Moving a template between two
|
|
54
|
+
languages that both use angle brackets fails *silently*, because the receiving
|
|
55
|
+
language has no opinion about the sending language's delimiters — there is no
|
|
56
|
+
parse error to catch, only characters that happen to be inert.
|
|
57
|
+
|
|
35
58
|
## Why
|
|
36
59
|
|
|
37
60
|
The widget is a Rails engine. Integration means `bundle add llm_meta_widget`, a
|
|
@@ -204,6 +227,30 @@ exists:
|
|
|
204
227
|
3. Later releases run from Actions with `permissions: id-token: write`, no
|
|
205
228
|
stored token. GitHub-hosted runners only; self-hosted is unsupported.
|
|
206
229
|
|
|
230
|
+
All three are done: 0.7.0 was published by hand, the Trusted Publisher is
|
|
231
|
+
configured, and 0.7.2 was the first release published end-to-end from a tag with
|
|
232
|
+
no credential anywhere. Two things each cost a release and are easy to miss:
|
|
233
|
+
|
|
234
|
+
- **Tick `npm publish` under Allowed actions.** `npm stage publish` is always
|
|
235
|
+
allowed and cannot be switched off, so a configuration with only that looks
|
|
236
|
+
permissive while quietly routing every release into staged publishing, which
|
|
237
|
+
waits for a maintainer to approve it with 2FA. `npm dist-tag` is not needed:
|
|
238
|
+
the implicit `latest` assignment is part of the publish operation.
|
|
239
|
+
- **Do not give `actions/setup-node` a `registry-url`.** With no token supplied
|
|
240
|
+
it still exports the literal placeholder `NODE_AUTH_TOKEN=XXXXX-XXXXX-XXXXX-XXXXX`
|
|
241
|
+
and writes it into a step `.npmrc`. npm sends that credential instead of
|
|
242
|
+
performing the OIDC exchange, and the registry answers `E404 … could not be
|
|
243
|
+
found or you do not have permission`, which reads like a missing package rather
|
|
244
|
+
than an auth failure. Trusted publishing wants no credential configured at all.
|
|
245
|
+
|
|
246
|
+
Diagnosing either needs `npm publish --loglevel verbose`: npm reports a failed
|
|
247
|
+
exchange only at verbose level before falling back to token auth, so the visible
|
|
248
|
+
symptom is a bare `ENEEDAUTH`. Two more things that look like failures and are
|
|
249
|
+
not — a successful publish answers `PUT 202`, not 201, and takes roughly 90
|
|
250
|
+
seconds to appear, so an empty packument straight after a green run means
|
|
251
|
+
nothing; and a version-conflict error proves nothing about auth, because npm
|
|
252
|
+
raises it from the public packument GET before any authenticated request.
|
|
253
|
+
|
|
207
254
|
Note the asymmetry: the trusted publisher keys on the **GitHub** repo, which
|
|
208
255
|
lives under `jdkim/`, while the npm scope is `@aibranch`. That works — they are
|
|
209
256
|
different namespaces — but it is worth knowing before someone goes looking for
|
|
@@ -215,11 +262,15 @@ fail on a missing interpreter. `npm test` runs both locally.
|
|
|
215
262
|
|
|
216
263
|
## Risks
|
|
217
264
|
|
|
218
|
-
**The refactor touches the least-tested half of the codebase.**
|
|
219
|
-
tests
|
|
220
|
-
nothing more — and this
|
|
221
|
-
|
|
222
|
-
|
|
265
|
+
**The refactor touches the least-tested half of the codebase.** At the time the
|
|
266
|
+
node tests covered `orchestrator.js` only; the panel had ESLint and the browser
|
|
267
|
+
e2e runs, nothing more — and this moved 1,454 lines of it. Attribute-parsing
|
|
268
|
+
tests were written before the move as this said to (`config.test.mjs`), and the
|
|
269
|
+
suite now stands at 131. This risk still landed, though, because what went
|
|
270
|
+
untested was the *markup*: `element.js` cannot be imported headlessly — its CSS
|
|
271
|
+
imports need esbuild's loader — so the template had no unit coverage of any kind
|
|
272
|
+
until `markup.test.mjs`, and the ERB regression above walked straight through the
|
|
273
|
+
gap.
|
|
223
274
|
|
|
224
275
|
**Version sync.** The gem version and the npm version must not drift. Either one
|
|
225
276
|
release process drives both, or the gem pins an exact npm version. Two
|
|
@@ -244,5 +295,6 @@ one static asset.
|
|
|
244
295
|
- Should the element emit DOM events (`llm-widget:turn-complete`, etc.) so hosts
|
|
245
296
|
can react without reaching into internals? Cheap to add at build time,
|
|
246
297
|
awkward to retrofit later.
|
|
247
|
-
- npm package name
|
|
248
|
-
gem name
|
|
298
|
+
- ~~npm package name~~ — **resolved:** `@aibranch/llm-meta-widget`. Scoped to the
|
|
299
|
+
`aibranch` org rather than matching the bare gem name; see Publishing to npm
|
|
300
|
+
for why that scope and not `@pubann`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aibranch/llm-meta-widget",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"//version": "Must stay in step with lib/llm_meta_widget/version.rb. Two independently versioned copies of the same client is the drift docs/custom-element-distribution.md warns about: bump both, release both.",
|
|
5
5
|
"description": "Embeddable browser chat widget: a custom element that discovers tools at page, origin and hub scope and dispatches each tool call to the right one. Two lines of HTML on any host, Rails or not.",
|
|
6
6
|
"keywords": [
|