llm_meta_widget 0.12.1 → 0.13.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 845c38baf1aca6cc874a77c9f324895c6001664dffe4d891d8b31ed2057a3b2b
4
- data.tar.gz: 825acda8a84c599e09e8d11cad9fe738092aa807b41911a99889e76dab15c800
3
+ metadata.gz: 98514af704b8e59db0091c55a0d42f60cc3d9da77ed8debbf8aa2b94e514576d
4
+ data.tar.gz: e0b25d087956a31ad5bc21ce232b25abe1aa0ca78a917abdaf6f0e97c7ba7237
5
5
  SHA512:
6
- metadata.gz: 0244dc664c60f88e1cf29e9cb52ebc9e33bf61e8ab2ab180a8e0873d587bf1cf96d8865eeb9d97b0fe325cec00c43e6e154cef74c511f7f16894a2112d17ad23
7
- data.tar.gz: 5584f49e2c7e848ef93fdb5bf6cb1270fa3154f77beee255eb64e221aab20ce8e4828b4b8381c5268e1503b1b0967d4fab81811be718f23f247951ccdf1c142a
6
+ metadata.gz: 15101bd9cbac468ccc0c2f5e7e5206e153a459335954779f80694e7431713aa5835849b4b9278511df408bd237adc37cfc16ef658b7a85b279b23d6871eb46d8
7
+ data.tar.gz: e81e6404165550d0521eb1fba9aa18d4e10ebc0cac400f50e6ff9453084f84e082baba1f7b386155d0b0d567c2a0e51e6de93d16a6a80fba0260f949ad1460d7
data/CHANGELOG.md CHANGED
@@ -3,6 +3,109 @@
3
3
  This file starts at 0.8.0, the first release with a breaking change worth
4
4
  calling out. Earlier releases are in the git history (`git log --oneline v0.7.6`).
5
5
 
6
+ ## 0.13.0
7
+
8
+ ### Changed: a page card declares its transport in `transports`, not `remotes`
9
+
10
+ The declaration that says how to reach a page's handlers has moved, following
11
+ SEP revision d44bd25a. It is now an array inside the extension data:
12
+
13
+ ```json
14
+ {
15
+ "_meta": {
16
+ "io.modelcontextprotocol/static-primitives": {
17
+ "transports": [ { "type": "page-global", "global": "mcp" } ],
18
+ "tools": [], "prompts": [], "resources": []
19
+ }
20
+ }
21
+ }
22
+ ```
23
+
24
+ The reason is that the Server Card's `Remote.type` can only be
25
+ `streamable-http` or `sse`, so `page-global` was never a valid value at the top
26
+ level. It is an array because a page may come to offer more than one page-local
27
+ transport, and the order is the author's preference.
28
+
29
+ The widget reads the array in order and uses the first type it supports. It
30
+ will not skip a supported entry for a later one. Only `page-global` is
31
+ supported today; a card offering only `page-postmessage`, `page-jsonrpc` or
32
+ `page-worker` gets no handlers rather than the wrong ones, and says so.
33
+
34
+ **Top-level `remotes` still works in this release**, with a warning, so a page
35
+ and a widget can be updated in either order without the page losing its tools
36
+ in between. Move to `transports`; a later release will drop the fallback. If a
37
+ page declares both, `transports` decides and nothing is reported.
38
+
39
+
40
+ ### Added: the panel says when reference data did not fit
41
+
42
+ A resource refused by the budget used to disappear with nothing but a line in
43
+ the browser console: the model answered from less than the page offered, and
44
+ nobody was told. The panel now reports it, naming the resource, its size and
45
+ what the model allows — once per budget, on the first turn it affects, and
46
+ again if a different model changes the figures.
47
+
48
+ The model is deliberately NOT told. It cannot fetch a resource — only the
49
+ client reads those — so telling it would spend context on something it can do
50
+ nothing about. The reader can act: choose a model with a larger window, or ask
51
+ the page author for a smaller reference.
52
+
53
+ A resource the page withholds on purpose (`autoAttach: false`) is not reported,
54
+ since that is the author's choice rather than an omission.
55
+
56
+ ## 0.12.2
57
+
58
+ ### Changed: one oversized tool result is shortened, not paid for by the others
59
+
60
+ A single tool result may now use at most 40% of the model's context window. A
61
+ result above that is cut to the cap, with a note in the result itself telling
62
+ the model how much was shown and to call the tool again with a narrower query.
63
+
64
+ This replaces giving up older results as the first response to a turn that will
65
+ not fit. Dropping the oldest results is a sound rule when results are of
66
+ similar size, and a poor one otherwise: on a real turn (2026-10-17) it gave up
67
+ two results of 80 tokens each — 160 tokens against a shortfall of about 2,600 —
68
+ and left untouched the one result that was causing the problem, at 120,667
69
+ tokens, or 94% of the window. The two results it discarded were ones the model
70
+ had asked for.
71
+
72
+ Measured over 179 real tool results: the median is 2.1% of the window, so the
73
+ cap touches 4.5% of results. All six over-budget requests in that sample were
74
+ caused by one oversized result, and the cap clears every one of them. On the
75
+ turn above it frees about 69,000 tokens and nothing has to be given up at all.
76
+
77
+ Giving up older results remains, for a conversation that is too long in
78
+ aggregate rather than because of one result.
79
+
80
+ The panel reports both, with the numbers: what was shortened or given up, how
81
+ many tokens the turn needed, and how many the model holds.
82
+
83
+ ### Fixed: scrolling the panel after a turn with thinking blocks (Chrome)
84
+
85
+ After a turn that showed thinking blocks, the panel could not be scrolled. The
86
+ scrollbar moved a few tens of pixels and stopped, by wheel, trackpad and
87
+ keyboard alike, while `scrollTop` set from script still worked and held. This
88
+ was reported from two different deployments and took several wrong diagnoses
89
+ from us before the mechanism was found.
90
+
91
+ A thinking block is a `<details>` holding its own scroller. Closing one with
92
+ `open = false`, at a moment when that block is the only reason the message list
93
+ overflows, leaves Chrome with a stale wheel range — the list's overflow from
94
+ that instant, often only tens of pixels. `scrollTop` and `scrollHeight` remain
95
+ correct, which is why nothing in the page looked wrong. Chrome 154 reproduces
96
+ it with no widget code involved at all; Firefox 158 does not, and it has been
97
+ reported upstream.
98
+
99
+ The widget now takes the scroller off the inner box before closing it, and
100
+ closes on the second animation frame, because one frame is not enough for
101
+ Chrome to pick up the new overflow. Measured on a real deployment: 6 of 6 turns
102
+ healthy after the change, against 3 of 4 faulting before, with the triggering
103
+ condition confirmed present 2 to 4 times in every passing run.
104
+
105
+ One consequence: a finished thinking block that a reader reopens is shown at
106
+ full height rather than as a 10em scrolling box. Putting the scroller back when
107
+ it reopens would let a manual close trigger the same Chrome state again.
108
+
6
109
  ## 0.12.1
7
110
 
8
111
  ### Fixed: a tool result is counted as JSON, not as prose
@@ -29,8 +132,8 @@ heard that the reader had moved — and the pin put them back at the end, so the
29
132
  decision never flipped. The question is now asked synchronously, immediately
30
133
  before anything is added, which has no such window.
31
134
 
32
- A separate report of the panel jumping to the TOP after a turn ends is not
33
- explained by this and remains open.
135
+ A separate report of the panel jumping to the TOP after a turn ends had a
136
+ different cause, fixed in 0.12.2 below.
34
137
 
35
138
  ## 0.12.0
36
139
 
data/README.md CHANGED
@@ -61,7 +61,7 @@ Option 3 if it does not.
61
61
 
62
62
  ```ruby
63
63
  # Gemfile
64
- gem "llm_meta_widget", "~> 0.12.1"
64
+ gem "llm_meta_widget", "~> 0.13.0"
65
65
  ```
66
66
 
67
67
  ```bash
@@ -152,23 +152,27 @@ The widget is a custom element in a single self-contained ES module, so the
152
152
  whole integration is two lines of HTML:
153
153
 
154
154
  ```html
155
- <script type="module" src="https://cdn.jsdelivr.net/npm/@aibranch/llm-meta-widget@0.12"></script>
155
+ <script type="module" src="https://cdn.jsdelivr.net/npm/@aibranch/llm-meta-widget@0.13"></script>
156
156
  <llm-meta-widget llm-url="https://your-meta-server.example"
157
157
  model="gpt-oss-120b"
158
158
  greeting="Hi — ask me anything about this page."></llm-meta-widget>
159
159
  ```
160
160
 
161
- **`@0.12` is a version range, and it stops at 0.12.x on purpose.** You receive
161
+ **`@0.13` is a version range, and it stops at 0.13.x on purpose.** You receive
162
162
  bug fixes without editing your page, and a release that breaks your page cannot
163
163
  arrive on its own.
164
164
 
165
- Moving to 0.13 is therefore a change you make yourself. Read
165
+ Moving to 0.14 is therefore a change you make yourself. Read
166
166
  [CHANGELOG.md](CHANGELOG.md) before you do, because this is where what your page
167
167
  must provide can change. For example, 0.10.0 changed how the widget finds a page
168
168
  card: by media type rather than by element id. A page that declared its card the
169
169
  new way while still loading `@0.9` lost every tool, prompt and resource it had
170
170
  declared. Nothing failed, anywhere. The CHANGELOG entry for 0.10.0 describes it.
171
171
 
172
+ 0.13.0 moves where a page declares its transport. That one is safe to take
173
+ first, because 0.13.x still reads the old spelling and warns; the page can move
174
+ afterwards.
175
+
172
176
  A Rails app is protected from that mismatch, because the `Gemfile` will not
173
177
  resolve a widget version the page cannot use. A page that loads the widget from
174
178
  the CDN has no such check, so the version you write in that URL is the only
@@ -169,6 +169,40 @@ function boot(cfg) {
169
169
  // and we stop following; come back to it and we resume.
170
170
  var SCROLL_PIN_SLACK = 40; // px from the end still counts as "at the end"
171
171
 
172
+ // Rounded to the nearest thousand and grouped, so the numbers read as a
173
+ // scale rather than as a precise count the estimate cannot support.
174
+ // Reference data the budget refused, so the panel can say so on the next
175
+ // turn. NOT announced at boot: appendTurn persists, so a boot-time notice
176
+ // would be saved into the transcript and repeat on every reload.
177
+ //
178
+ // The model is not told, deliberately. It cannot fetch a resource — only the
179
+ // client reads those — so telling it would spend tokens on something it can
180
+ // do nothing about. The person CAN act: pick a model with more room, or ask
181
+ // the page author for a smaller reference.
182
+ var skippedResources = [];
183
+ var announcedSkips = false;
184
+
185
+ function kb(bytes) { return Math.round((bytes || 0) / 1024) + " KB"; }
186
+
187
+ // `origin` is "page" for a page-card resource and "host" for one from a
188
+ // host-wide server. It matters when the budget changes: page resources are
189
+ // planned again, so their refusals are re-recorded, while a host resource is
190
+ // listed once at boot and never re-planned. Clearing both on a re-plan
191
+ // erased the host refusal before it could be reported — which silenced the
192
+ // case that matters most, since PubDictionaries' catalog arrives that way.
193
+ function noteSkippedResource(plan, budget, origin) {
194
+ // A resource the page marked autoAttach:false is a deliberate choice, not
195
+ // an omission — only a budget refusal is worth reporting.
196
+ if (plan.reason !== "over-budget") return;
197
+ var name = plan.name || plan.uri;
198
+ skippedResources = skippedResources.filter(function(s) { return s.name !== name; });
199
+ skippedResources.push({ name: name, size: plan.size, budget: budget, origin: origin });
200
+ }
201
+
202
+ function thousands(n) {
203
+ return (Math.round(n / 1000) * 1000).toLocaleString("en-US");
204
+ }
205
+
172
206
  function atBottom() {
173
207
  return historyEl.scrollHeight - historyEl.scrollTop - historyEl.clientHeight <= SCROLL_PIN_SLACK;
174
208
  }
@@ -695,12 +729,19 @@ function boot(cfg) {
695
729
  var budget = resourceBudget();
696
730
  if (pageResourcePlans === null || budget !== pageResourcePlansBudget) {
697
731
  pageResourcePlansBudget = budget;
732
+ // A different budget is a different decision: say it again. Only the
733
+ // page's own refusals are dropped — those are about to be re-recorded
734
+ // by the planning below. A host resource is listed once at boot, so
735
+ // discarding its refusal here would lose it for good.
736
+ skippedResources = skippedResources.filter(function(s) { return s.origin !== "page"; });
737
+ announcedSkips = false;
698
738
  pageResourcePlans = planPageResources({
699
739
  entries: pageCard.resources,
700
740
  budgetBytes: budget,
701
741
  onSkip: function(plan) {
702
742
  console.info("[llm_meta_widget] not attaching " + plan.uri +
703
743
  " (" + plan.reason + ", size=" + plan.size + ")");
744
+ noteSkippedResource(plan, resourceBudget(), "page");
704
745
  }
705
746
  });
706
747
  }
@@ -753,6 +794,7 @@ function boot(cfg) {
753
794
  onSkip: function(plan) {
754
795
  console.info("[llm_meta_widget] not attaching " + plan.uri +
755
796
  " (" + plan.reason + ", size=" + plan.size + ")");
797
+ noteSkippedResource(plan, resourceBudget(), "host");
756
798
  }
757
799
  });
758
800
  // A volatile resource comes back with no context — it is read
@@ -1127,15 +1169,48 @@ function boot(cfg) {
1127
1169
  return body;
1128
1170
  }
1129
1171
 
1172
+ // Collapsing a thinking block is what broke scrolling in Chrome.
1173
+ //
1174
+ // A thinking block is a <details> holding its own scroller (max-height +
1175
+ // overflow-y on .message-thinking-content). Closing it with `open = false`
1176
+ // at a moment when that block is the only reason .lmw-messages overflows
1177
+ // makes the list briefly non-scrollable. When later content makes it
1178
+ // scrollable again, Chrome keeps the WHEEL range from that moment — tens of
1179
+ // pixels — while scrollTop and scrollHeight stay correct. So the reader can
1180
+ // scroll a few pixels and no further, by any input method, while
1181
+ // `scrollTop = 600` set from script still works. Chrome 154 reproduces it
1182
+ // with no widget code at all; Firefox 158 never does.
1183
+ // https://issues.chromium.org/issues/574960594 — docs/chromium-scroll-bug.md has the
1184
+ // measurements and a self-checking test case. Remove this once the fix
1185
+ // has shipped widely enough.
1186
+ //
1187
+ // Taking the scroller off the inner box BEFORE the close avoids the state.
1188
+ // Two frames, because one is not enough for Chrome to pick up the new
1189
+ // overflow before the close lands.
1190
+ //
1191
+ // The box stays a non-scroller afterwards, so a reader who reopens a
1192
+ // finished block sees it at full height instead of a 10em window. Putting
1193
+ // the scroller back on reopen would let a manual close trigger the bug
1194
+ // again, which is a worse trade.
1130
1195
  function collapseThinkingBlock() {
1131
- if (currentThinkingBlock) {
1132
- currentThinkingBlock.classList.remove("thinking-active");
1133
- currentThinkingBlock.open = false;
1134
- var summary = currentThinkingBlock.querySelector("summary");
1135
- if (summary) summary.textContent = "🤔 thinking (finished)";
1136
- }
1196
+ var block = currentThinkingBlock;
1137
1197
  currentThinkingBlock = null;
1138
1198
  currentThinkingBody = null;
1199
+ if (!block) return;
1200
+
1201
+ block.classList.remove("thinking-active");
1202
+ var summary = block.querySelector("summary");
1203
+ if (summary) summary.textContent = "🤔 thinking (finished)";
1204
+
1205
+ var body = block.querySelector(".message-thinking-content");
1206
+ if (body) {
1207
+ body.style.overflow = "visible";
1208
+ body.style.maxHeight = "none";
1209
+ }
1210
+ if (typeof requestAnimationFrame !== "function") { block.open = false; return; }
1211
+ requestAnimationFrame(function() {
1212
+ requestAnimationFrame(function() { block.open = false; });
1213
+ });
1139
1214
  }
1140
1215
 
1141
1216
  // Every declaration channel the widget hands the model carries a
@@ -1271,6 +1346,23 @@ function boot(cfg) {
1271
1346
  if (!userText) return;
1272
1347
  inputEl.value = "";
1273
1348
  appendTurn("user", userText, pendingTurnLabel);
1349
+
1350
+ // Reference data that did not fit, reported once per budget, at the moment
1351
+ // it affects an answer rather than at boot.
1352
+ // Plan the page's resources first: the refusals are recorded while planning,
1353
+ // which otherwise happens later in the turn — after this check had already
1354
+ // looked at an empty list and said nothing.
1355
+ pageResourcePlansNow();
1356
+ if (skippedResources.length && !announcedSkips) {
1357
+ announcedSkips = true;
1358
+ var skipped = skippedResources[0];
1359
+ var others = skippedResources.length - 1;
1360
+ var alsoText = others > 0 ? " and " + others + " other reference" + (others > 1 ? "s" : "") : "";
1361
+ appendTurn("system", "Did not attach the " + skipped.name + " reference" + alsoText +
1362
+ " (" + kb(skipped.size) + ", and a " + MODEL + " turn allows " + kb(skipped.budget) +
1363
+ " for reference data), so the answer may be less precise." +
1364
+ " A model with a larger context window would make room.");
1365
+ }
1274
1366
  pendingTurnLabel = null;
1275
1367
 
1276
1368
  if (currentAbort) { try { currentAbort.abort(); } catch (e) { /* noop */ } }
@@ -1317,12 +1409,31 @@ function boot(cfg) {
1317
1409
  contextWindow: modelWindow,
1318
1410
  provider: LLM_PROVIDER === "ollama" ? "ollama" : "hub",
1319
1411
  signal: currentAbort.signal,
1320
- onContextEvicted: function(dropped) {
1321
- // Said out loud: a context that quietly thins out makes a later answer
1322
- // look inexplicably worse, with nothing on screen to explain it.
1323
- var last = dropped[dropped.length - 1];
1324
- var name = (last && last.name) ? last.name : "tool";
1325
- appendTurn("system", "Dropped an earlier " + name + " result to make room in the model's context. Ask again if you need it.");
1412
+ onContextTruncated: function(list) {
1413
+ // A shortened result is not a failure, but the reader has to know the
1414
+ // model answered from part of it — and the numbers say whether a narrower
1415
+ // question would help.
1416
+ var cut = list[list.length - 1] || {};
1417
+ appendTurn("system", "Shortened an oversized " + (cut.name || "tool") + " result to fit the context: " +
1418
+ thousands(cut.fromTokens || 0) + " tokens, where one result may use at most " +
1419
+ thousands(cut.toTokens || 0) + ". Ask again with a narrower question for the complete result.");
1420
+ },
1421
+ onContextEvicted: function(info) {
1422
+ // Said out loud, with the numbers. A context that quietly thins out makes
1423
+ // a later answer look inexplicably worse, and "an old result was dropped"
1424
+ // alone does not tell a reader whether to retry, shorten the question, or
1425
+ // pick a model with more room.
1426
+ var dropped = info.dropped || [];
1427
+ var names = dropped.map(function(d) { return d.name || "a tool"; });
1428
+ var what = names.length === 1
1429
+ ? "the earliest tool result (" + names[0] + ")"
1430
+ : "the " + names.length + " earliest tool results (" + names.join(", ") + ")";
1431
+ var sizes = (info.needed && info.window)
1432
+ ? " About " + thousands(info.needed) + " tokens were needed and " +
1433
+ thousands(info.window) + " fit in " + MODEL + "'s context."
1434
+ : "";
1435
+ appendTurn("system", "Dropped " + what + " to make room." + sizes +
1436
+ " Ask again, or narrow the question, for an answer based on complete tool results.");
1326
1437
  },
1327
1438
  onToolCall: function(toolCall) {
1328
1439
  announceToolCall(toolCall);
@@ -178,6 +178,8 @@ async function runChatLoop(opts) {
178
178
  // Called when a tool result is dropped to make room, so the panel can say
179
179
  // so rather than the context quietly thinning.
180
180
  onContextEvicted,
181
+ // Called when one oversized result is shortened, so the panel can say so.
182
+ onContextTruncated,
181
183
  // "hub" (default) talks to llm_meta_server; "ollama" talks straight to an
182
184
  // Ollama, with no server component of ours in between. The loop is the
183
185
  // same either way — only the call is swapped.
@@ -211,6 +213,7 @@ async function runChatLoop(opts) {
211
213
  const mergedLocalTools = Object.values(inlineByName);
212
214
  const rounds = [];
213
215
  const evicted = [];
216
+ const truncatedResults = [];
214
217
  const toolsTokens = estimateToolsTokens([].concat(
215
218
  singleOpts.localTools || [],
216
219
  remoteTools || [],
@@ -246,6 +249,15 @@ async function runChatLoop(opts) {
246
249
  for (let round = 0; round < maxRounds; round++) {
247
250
  if (signal?.aborted) throw new DOMException("aborted", "AbortError");
248
251
  onRoundStart?.(round);
252
+ const capped = truncateOversizedToolResults({
253
+ messages,
254
+ window: contextWindow,
255
+ truncated: truncatedResults
256
+ });
257
+ if (capped.messages !== messages) {
258
+ messages = capped.messages;
259
+ onContextTruncated?.(truncatedResults.slice());
260
+ }
249
261
  const room = evictOldToolResults({
250
262
  messages,
251
263
  window: contextWindow,
@@ -255,7 +267,7 @@ async function runChatLoop(opts) {
255
267
  });
256
268
  if (room.messages !== messages) {
257
269
  messages = room.messages;
258
- onContextEvicted?.(evicted.slice());
270
+ onContextEvicted?.({ dropped: evicted.slice(), needed: room.needed, window: room.window });
259
271
  }
260
272
  const call = provider === "ollama" ? ollamaChatCall : singleLlmCall;
261
273
  const turnResult = await call({
@@ -622,10 +634,34 @@ function estimateToolsTokens(tools) {
622
634
  })).join("");
623
635
  return estimateTextTokens(text);
624
636
  }
637
+ var MAX_TOOL_RESULT_SHARE = 0.4;
638
+ function truncateOversizedToolResults({
639
+ messages,
640
+ window: window2,
641
+ share = MAX_TOOL_RESULT_SHARE,
642
+ truncated = []
643
+ } = {}) {
644
+ if (!window2 || !Number.isFinite(window2)) return { messages, truncated };
645
+ const cap = Math.floor(window2 * share);
646
+ let out = null;
647
+ (messages || []).forEach((m, i) => {
648
+ if (m?.role !== "tool" || typeof m.content !== "string") return;
649
+ const tokens = estimateTextTokens(m.content);
650
+ if (tokens <= cap) return;
651
+ const keep = Math.max(1, Math.floor(m.content.length * cap / tokens));
652
+ out = out || messages.slice();
653
+ truncated.push({ name: m.name, fromTokens: tokens, toTokens: cap });
654
+ out[i] = Object.assign({}, m, {
655
+ content: m.content.slice(0, keep) + "\n\n[truncated: " + keep + " of " + m.content.length + " characters shown. One tool result may use at most " + Math.round(share * 100) + "% of the context window. Call the tool again with a narrower query to see more.]"
656
+ });
657
+ });
658
+ return { messages: out || messages, truncated };
659
+ }
625
660
  function evictOldToolResults({ messages, window: window2, reserve = 0, dropped = [] }) {
626
661
  if (!window2 || !Number.isFinite(window2)) return { messages, dropped };
627
662
  const budget = window2 - reserve;
628
- if (estimateTokens(messages) <= budget) return { messages, dropped };
663
+ const needed = estimateTokens(messages) + reserve;
664
+ if (estimateTokens(messages) <= budget) return { messages, dropped, needed, window: window2 };
629
665
  const out = messages.slice();
630
666
  for (let i = 0; i < out.length; i++) {
631
667
  if (estimateTokens(out) <= budget) break;
@@ -637,7 +673,7 @@ function evictOldToolResults({ messages, window: window2, reserve = 0, dropped =
637
673
  content: "[dropped to make room in the context window. Call the tool again if you still need it, with a narrower query.]"
638
674
  });
639
675
  }
640
- return { messages: out, dropped };
676
+ return { messages: out, dropped, needed, window: window2 };
641
677
  }
642
678
  function modelWindowFrom(payload, modelId) {
643
679
  for (const family of payload && payload.llms || []) {
@@ -653,15 +689,29 @@ function modelWindowFrom(payload, modelId) {
653
689
  return null;
654
690
  }
655
691
  var PAGE_GLOBAL_TRANSPORT = "page-global";
656
- function declaredPageGlobal(card) {
692
+ function declaredPageGlobal(card, warn = console.warn) {
693
+ const declared = card && card.transports || [];
694
+ for (const transport of declared) {
695
+ if (!transport || transport.type !== PAGE_GLOBAL_TRANSPORT) continue;
696
+ if (typeof transport.global === "string" && transport.global.trim() !== "") {
697
+ return transport.global.trim();
698
+ }
699
+ }
700
+ if (declared.length) {
701
+ warn("[llm-meta-widget] page card declares transports " + JSON.stringify(declared.map((t) => t && t.type)) + ", none of which this widget supports; not dispatching to the page");
702
+ return null;
703
+ }
657
704
  for (const remote of card && card.remotes || []) {
658
705
  if (!remote || remote.type !== PAGE_GLOBAL_TRANSPORT) continue;
659
- if (typeof remote.global === "string" && remote.global.trim() !== "") return remote.global.trim();
706
+ if (typeof remote.global === "string" && remote.global.trim() !== "") {
707
+ warn(`[llm-meta-widget] page card declares its transport in top-level "remotes", which is deprecated; move it to _meta["${STATIC_PRIMITIVES_META}"].transports`);
708
+ return remote.global.trim();
709
+ }
660
710
  }
661
711
  return null;
662
712
  }
663
713
  function pageHandlers({ card, scope = {}, legacy = {}, warn = console.warn } = {}) {
664
- const name = declaredPageGlobal(card);
714
+ const name = declaredPageGlobal(card, warn);
665
715
  const declared = name ? scope[name] : null;
666
716
  if (name && !declared) {
667
717
  warn(`[llm-meta-widget] the page card declares global "${name}", but window.${name} is not defined \u2014 is the script that defines it loaded, and did it run?`);
@@ -683,16 +733,19 @@ function pageHandlers({ card, scope = {}, legacy = {}, warn = console.warn } = {
683
733
  };
684
734
  }
685
735
  function mergeStaticPrimitivesCards(cards, warn = console.warn) {
686
- const merged = { remotes: [], tools: [], prompts: [], resources: [] };
736
+ const merged = { transports: [], remotes: [], tools: [], prompts: [], resources: [] };
687
737
  for (const card of cards || []) {
688
- for (const remote of card && card.remotes || []) {
689
- const clash = merged.remotes.find((r) => r.type === remote.type && r.global !== remote.global);
690
- if (clash) {
691
- warn(`[llm-meta-widget] two blocks declare different ${remote.type} globals ("${clash.global}" and "${remote.global}"); using "${clash.global}"`);
692
- continue;
693
- }
694
- if (!merged.remotes.some((r) => r.type === remote.type && r.global === remote.global)) {
695
- merged.remotes.push(remote);
738
+ for (const kind of ["transports", "remotes"]) {
739
+ for (const entry of card && card[kind] || []) {
740
+ if (!entry || typeof entry.type !== "string") continue;
741
+ const clash = merged[kind].find((r) => r.type === entry.type && r.global !== entry.global);
742
+ if (clash) {
743
+ warn(`[llm-meta-widget] two blocks declare different ${entry.type} globals ("${clash.global}" and "${entry.global}"); using "${clash.global}"`);
744
+ continue;
745
+ }
746
+ if (!merged[kind].some((r) => r.type === entry.type && r.global === entry.global)) {
747
+ merged[kind].push(entry);
748
+ }
696
749
  }
697
750
  }
698
751
  }
@@ -713,7 +766,7 @@ function mergeStaticPrimitivesCards(cards, warn = console.warn) {
713
766
  return merged;
714
767
  }
715
768
  function readStaticPrimitivesCard(text, warn = console.warn) {
716
- const empty = { remotes: [], tools: [], prompts: [], resources: [] };
769
+ const empty = { transports: [], remotes: [], tools: [], prompts: [], resources: [] };
717
770
  if (typeof text !== "string" || text.trim() === "") return empty;
718
771
  let doc;
719
772
  try {
@@ -733,7 +786,12 @@ function readStaticPrimitivesCard(text, warn = console.warn) {
733
786
  return ok;
734
787
  });
735
788
  return {
736
- // The whole card's transport, so what it declares survives the parse.
789
+ // Where a page says how to reach its handlers. Since SEP d44bd25a this is
790
+ // `transports` inside the extension data, because the Server Card's
791
+ // `Remote.type` enum is streamable-http|sse and `page-global` was never a
792
+ // valid value there. Top-level `remotes` is what 0.11.1-0.12.2 read, and is
793
+ // still accepted for one release — see declaredPageGlobal.
794
+ transports: Array.isArray(ext.transports) ? ext.transports : [],
737
795
  remotes: Array.isArray(doc.remotes) ? doc.remotes : [],
738
796
  tools: named(ext.tools, "tool"),
739
797
  prompts: named(ext.prompts, "prompt"),
@@ -3952,6 +4010,22 @@ function boot(cfg) {
3952
4010
  var toggleBtn = document.getElementById("llm-meta-widget-toggle");
3953
4011
  var historyEl = root.querySelector(".lmw-messages");
3954
4012
  var SCROLL_PIN_SLACK = 40;
4013
+ var skippedResources = [];
4014
+ var announcedSkips = false;
4015
+ function kb(bytes) {
4016
+ return Math.round((bytes || 0) / 1024) + " KB";
4017
+ }
4018
+ function noteSkippedResource(plan, budget, origin) {
4019
+ if (plan.reason !== "over-budget") return;
4020
+ var name = plan.name || plan.uri;
4021
+ skippedResources = skippedResources.filter(function(s) {
4022
+ return s.name !== name;
4023
+ });
4024
+ skippedResources.push({ name, size: plan.size, budget, origin });
4025
+ }
4026
+ function thousands(n) {
4027
+ return (Math.round(n / 1e3) * 1e3).toLocaleString("en-US");
4028
+ }
3955
4029
  function atBottom() {
3956
4030
  return historyEl.scrollHeight - historyEl.scrollTop - historyEl.clientHeight <= SCROLL_PIN_SLACK;
3957
4031
  }
@@ -4343,11 +4417,16 @@ function boot(cfg) {
4343
4417
  var budget = resourceBudget();
4344
4418
  if (pageResourcePlans === null || budget !== pageResourcePlansBudget) {
4345
4419
  pageResourcePlansBudget = budget;
4420
+ skippedResources = skippedResources.filter(function(s) {
4421
+ return s.origin !== "page";
4422
+ });
4423
+ announcedSkips = false;
4346
4424
  pageResourcePlans = planPageResources({
4347
4425
  entries: pageCard.resources,
4348
4426
  budgetBytes: budget,
4349
4427
  onSkip: function(plan) {
4350
4428
  console.info("[llm_meta_widget] not attaching " + plan.uri + " (" + plan.reason + ", size=" + plan.size + ")");
4429
+ noteSkippedResource(plan, resourceBudget(), "page");
4351
4430
  }
4352
4431
  });
4353
4432
  }
@@ -4378,6 +4457,7 @@ function boot(cfg) {
4378
4457
  read: readMcpResource,
4379
4458
  onSkip: function(plan) {
4380
4459
  console.info("[llm_meta_widget] not attaching " + plan.uri + " (" + plan.reason + ", size=" + plan.size + ")");
4460
+ noteSkippedResource(plan, resourceBudget(), "host");
4381
4461
  }
4382
4462
  });
4383
4463
  if (loaded && loaded.plan && loaded.plan.fetch) {
@@ -4643,14 +4723,27 @@ function boot(cfg) {
4643
4723
  return body;
4644
4724
  }
4645
4725
  function collapseThinkingBlock() {
4646
- if (currentThinkingBlock) {
4647
- currentThinkingBlock.classList.remove("thinking-active");
4648
- currentThinkingBlock.open = false;
4649
- var summary = currentThinkingBlock.querySelector("summary");
4650
- if (summary) summary.textContent = "\u{1F914} thinking (finished)";
4651
- }
4726
+ var block2 = currentThinkingBlock;
4652
4727
  currentThinkingBlock = null;
4653
4728
  currentThinkingBody = null;
4729
+ if (!block2) return;
4730
+ block2.classList.remove("thinking-active");
4731
+ var summary = block2.querySelector("summary");
4732
+ if (summary) summary.textContent = "\u{1F914} thinking (finished)";
4733
+ var body = block2.querySelector(".message-thinking-content");
4734
+ if (body) {
4735
+ body.style.overflow = "visible";
4736
+ body.style.maxHeight = "none";
4737
+ }
4738
+ if (typeof requestAnimationFrame !== "function") {
4739
+ block2.open = false;
4740
+ return;
4741
+ }
4742
+ requestAnimationFrame(function() {
4743
+ requestAnimationFrame(function() {
4744
+ block2.open = false;
4745
+ });
4746
+ });
4654
4747
  }
4655
4748
  function stateReaders() {
4656
4749
  var declared = window[STATE_GLOBAL] || {};
@@ -4755,6 +4848,14 @@ function boot(cfg) {
4755
4848
  if (!userText) return;
4756
4849
  inputEl.value = "";
4757
4850
  appendTurn("user", userText, pendingTurnLabel);
4851
+ pageResourcePlansNow();
4852
+ if (skippedResources.length && !announcedSkips) {
4853
+ announcedSkips = true;
4854
+ var skipped = skippedResources[0];
4855
+ var others = skippedResources.length - 1;
4856
+ var alsoText = others > 0 ? " and " + others + " other reference" + (others > 1 ? "s" : "") : "";
4857
+ appendTurn("system", "Did not attach the " + skipped.name + " reference" + alsoText + " (" + kb(skipped.size) + ", and a " + MODEL + " turn allows " + kb(skipped.budget) + " for reference data), so the answer may be less precise. A model with a larger context window would make room.");
4858
+ }
4758
4859
  pendingTurnLabel = null;
4759
4860
  if (currentAbort) {
4760
4861
  try {
@@ -4793,10 +4894,18 @@ function boot(cfg) {
4793
4894
  contextWindow: modelWindow,
4794
4895
  provider: LLM_PROVIDER === "ollama" ? "ollama" : "hub",
4795
4896
  signal: currentAbort.signal,
4796
- onContextEvicted: function(dropped) {
4797
- var last = dropped[dropped.length - 1];
4798
- var name = last && last.name ? last.name : "tool";
4799
- appendTurn("system", "Dropped an earlier " + name + " result to make room in the model's context. Ask again if you need it.");
4897
+ onContextTruncated: function(list2) {
4898
+ var cut = list2[list2.length - 1] || {};
4899
+ appendTurn("system", "Shortened an oversized " + (cut.name || "tool") + " result to fit the context: " + thousands(cut.fromTokens || 0) + " tokens, where one result may use at most " + thousands(cut.toTokens || 0) + ". Ask again with a narrower question for the complete result.");
4900
+ },
4901
+ onContextEvicted: function(info) {
4902
+ var dropped = info.dropped || [];
4903
+ var names = dropped.map(function(d) {
4904
+ return d.name || "a tool";
4905
+ });
4906
+ var what = names.length === 1 ? "the earliest tool result (" + names[0] + ")" : "the " + names.length + " earliest tool results (" + names.join(", ") + ")";
4907
+ var sizes = info.needed && info.window ? " About " + thousands(info.needed) + " tokens were needed and " + thousands(info.window) + " fit in " + MODEL + "'s context." : "";
4908
+ appendTurn("system", "Dropped " + what + " to make room." + sizes + " Ask again, or narrow the question, for an answer based on complete tool results.");
4800
4909
  },
4801
4910
  onToolCall: function(toolCall) {
4802
4911
  announceToolCall(toolCall);
@@ -335,6 +335,8 @@ export async function runChatLoop(opts) {
335
335
  // Called when a tool result is dropped to make room, so the panel can say
336
336
  // so rather than the context quietly thinning.
337
337
  onContextEvicted,
338
+ // Called when one oversized result is shortened, so the panel can say so.
339
+ onContextTruncated,
338
340
  // "hub" (default) talks to llm_meta_server; "ollama" talks straight to an
339
341
  // Ollama, with no server component of ours in between. The loop is the
340
342
  // same either way — only the call is swapped.
@@ -377,6 +379,7 @@ export async function runChatLoop(opts) {
377
379
 
378
380
  const rounds = []
379
381
  const evicted = []
382
+ const truncatedResults = []
380
383
  // Fixed for the turn: the declared tool set does not change between rounds.
381
384
  const toolsTokens = estimateToolsTokens([].concat(
382
385
  singleOpts.localTools || [], remoteTools || [], hostWideTools || []))
@@ -422,6 +425,16 @@ export async function runChatLoop(opts) {
422
425
  onRoundStart?.(round)
423
426
 
424
427
  // The loop is the same whoever answers; only the call differs.
428
+ // One oversized result is capped first: it frees hundreds of times more
429
+ // than dropping old results, and keeps what the model asked for.
430
+ const capped = truncateOversizedToolResults({
431
+ messages, window: contextWindow, truncated: truncatedResults
432
+ });
433
+ if (capped.messages !== messages) {
434
+ messages = capped.messages;
435
+ onContextTruncated?.(truncatedResults.slice());
436
+ }
437
+
425
438
  // Make room before sending, rather than after being refused. Only old
426
439
  // tool results are given up; see evictOldToolResults.
427
440
  const room = evictOldToolResults({
@@ -431,7 +444,7 @@ export async function runChatLoop(opts) {
431
444
  });
432
445
  if (room.messages !== messages) {
433
446
  messages = room.messages;
434
- onContextEvicted?.(evicted.slice());
447
+ onContextEvicted?.({ dropped: evicted.slice(), needed: room.needed, window: room.window });
435
448
  }
436
449
 
437
450
  const call = provider === "ollama" ? ollamaChatCall : singleLlmCall
@@ -1003,6 +1016,55 @@ export function estimateToolsTokens(tools) {
1003
1016
  return estimateTextTokens(text)
1004
1017
  }
1005
1018
 
1019
+ // A single tool result may use at most this share of the window.
1020
+ //
1021
+ // Measured over 179 real tool results (2026-10-17): the median is 2.1% of the
1022
+ // window and the 90th percentile 20%, but one `togovar_search_variant` result
1023
+ // came to 94% — 120,667 tokens — and alone could not coexist with a
1024
+ // conversation. All six over-budget requests in that sample were caused by one
1025
+ // oversized result, so capping it clears every one of them while touching only
1026
+ // 4.5% of results at all.
1027
+ //
1028
+ // 40% keeps a useful prefix of even the worst result (42% of it) and still
1029
+ // leaves most of the window for the conversation, the schemas and the answer.
1030
+ export const MAX_TOOL_RESULT_SHARE = 0.4
1031
+
1032
+ // Shortens any single tool result that exceeds that share.
1033
+ //
1034
+ // This runs BEFORE eviction, because it is strictly the better move: on the
1035
+ // audited turn it frees ~69,000 tokens from one result, where dropping the
1036
+ // oldest results freed 160 and lost two results the model had asked for. The
1037
+ // model is told what happened, in the result itself, so it can narrow its
1038
+ // query instead of assuming it received everything.
1039
+ export function truncateOversizedToolResults({ messages, window,
1040
+ share = MAX_TOOL_RESULT_SHARE,
1041
+ truncated = [] } = {}) {
1042
+ if (!window || !Number.isFinite(window)) return { messages, truncated }
1043
+
1044
+ const cap = Math.floor(window * share)
1045
+ let out = null
1046
+
1047
+ ;(messages || []).forEach((m, i) => {
1048
+ if (m?.role !== "tool" || typeof m.content !== "string") return
1049
+ const tokens = estimateTextTokens(m.content)
1050
+ if (tokens <= cap) return
1051
+
1052
+ // Scale the cut by the measured ratio rather than a fixed chars-per-token,
1053
+ // so a dense result is cut by what it actually costs.
1054
+ const keep = Math.max(1, Math.floor(m.content.length * cap / tokens))
1055
+ out = out || messages.slice()
1056
+ truncated.push({ name: m.name, fromTokens: tokens, toTokens: cap })
1057
+ out[i] = Object.assign({}, m, {
1058
+ content: m.content.slice(0, keep) +
1059
+ "\n\n[truncated: " + keep + " of " + m.content.length + " characters shown. " +
1060
+ "One tool result may use at most " + Math.round(share * 100) +
1061
+ "% of the context window. Call the tool again with a narrower query to see more.]"
1062
+ })
1063
+ })
1064
+
1065
+ return { messages: out || messages, truncated }
1066
+ }
1067
+
1006
1068
  // Makes room for the next round by dropping the OLDEST tool results, which is
1007
1069
  // the one eviction a chat loop can justify: the model has already read them and
1008
1070
  // said what it made of them in the assistant turn that follows, so its own
@@ -1021,7 +1083,12 @@ export function evictOldToolResults({ messages, window, reserve = 0, dropped = [
1021
1083
  if (!window || !Number.isFinite(window)) return { messages, dropped }
1022
1084
 
1023
1085
  const budget = window - reserve
1024
- if (estimateTokens(messages) <= budget) return { messages, dropped }
1086
+ // What the whole turn would have needed, against what the model holds. The
1087
+ // panel shows both, because "an old result was dropped" on its own tells a
1088
+ // reader nothing about whether to retry, shorten the question, or change
1089
+ // model.
1090
+ const needed = estimateTokens(messages) + reserve
1091
+ if (estimateTokens(messages) <= budget) return { messages, dropped, needed, window }
1025
1092
 
1026
1093
  const out = messages.slice()
1027
1094
  for (let i = 0; i < out.length; i++) {
@@ -1036,7 +1103,7 @@ export function evictOldToolResults({ messages, window, reserve = 0, dropped = [
1036
1103
  "if you still need it, with a narrower query.]"
1037
1104
  })
1038
1105
  }
1039
- return { messages: out, dropped }
1106
+ return { messages: out, dropped, needed, window }
1040
1107
  }
1041
1108
 
1042
1109
  export function modelWindowFrom(payload, modelId) {
@@ -1055,10 +1122,41 @@ export function modelWindowFrom(payload, modelId) {
1055
1122
 
1056
1123
  export const PAGE_GLOBAL_TRANSPORT = "page-global"
1057
1124
 
1058
- export function declaredPageGlobal(card) {
1125
+ // Which JS global holds the handlers, from the card's declared transports.
1126
+ //
1127
+ // The array is processed IN ORDER and the first entry whose type we support
1128
+ // wins: the page author's order is their preference, so a supported entry is
1129
+ // never skipped for a later one. Only `page-global` is supported today;
1130
+ // `page-postmessage`, `page-jsonrpc` and `page-worker` are reserved, and a card
1131
+ // declaring only those gets no handlers rather than the wrong ones.
1132
+ //
1133
+ // Top-level `remotes` is the pre-d44bd25a shape that 0.11.1 and 0.12.2 read. It
1134
+ // is accepted for one release so a page and a widget can be updated in either
1135
+ // order without the page going dark in between, and it warns so the author
1136
+ // knows to move.
1137
+ export function declaredPageGlobal(card, warn = console.warn) {
1138
+ const declared = (card && card.transports) || []
1139
+ for (const transport of declared) {
1140
+ if (!transport || transport.type !== PAGE_GLOBAL_TRANSPORT) continue
1141
+ if (typeof transport.global === "string" && transport.global.trim() !== "") {
1142
+ return transport.global.trim()
1143
+ }
1144
+ }
1145
+ if (declared.length) {
1146
+ warn("[llm-meta-widget] page card declares transports " +
1147
+ JSON.stringify(declared.map((t) => t && t.type)) +
1148
+ ", none of which this widget supports; not dispatching to the page")
1149
+ return null
1150
+ }
1151
+
1059
1152
  for (const remote of (card && card.remotes) || []) {
1060
1153
  if (!remote || remote.type !== PAGE_GLOBAL_TRANSPORT) continue
1061
- if (typeof remote.global === "string" && remote.global.trim() !== "") return remote.global.trim()
1154
+ if (typeof remote.global === "string" && remote.global.trim() !== "") {
1155
+ warn('[llm-meta-widget] page card declares its transport in top-level ' +
1156
+ '"remotes", which is deprecated; move it to ' +
1157
+ `_meta["${STATIC_PRIMITIVES_META}"].transports`)
1158
+ return remote.global.trim()
1159
+ }
1062
1160
  }
1063
1161
  return null
1064
1162
  }
@@ -1067,7 +1165,7 @@ export function declaredPageGlobal(card) {
1067
1165
  // to its legacy global independently, so a page part-way through moving over
1068
1166
  // keeps working instead of losing the primitives it has not moved yet.
1069
1167
  export function pageHandlers({ card, scope = {}, legacy = {}, warn = console.warn } = {}) {
1070
- const name = declaredPageGlobal(card)
1168
+ const name = declaredPageGlobal(card, warn)
1071
1169
  const declared = name ? scope[name] : null
1072
1170
 
1073
1171
  // A card that names a global the page never defines is almost always a load
@@ -1112,20 +1210,28 @@ export function pageHandlers({ card, scope = {}, legacy = {}, warn = console.war
1112
1210
  // Picking the first would make which component wins depend on DOM order, which
1113
1211
  // is not something a page author controls when the components are partials.
1114
1212
  export function mergeStaticPrimitivesCards(cards, warn = console.warn) {
1115
- const merged = { remotes: [], tools: [], prompts: [], resources: [] }
1116
- // Each block is a full Server Card and may declare a transport. The first
1117
- // one wins; a second block naming a DIFFERENT global is an authoring error,
1118
- // because only one of them can be dispatched to.
1213
+ const merged = { transports: [], remotes: [], tools: [], prompts: [], resources: [] }
1214
+ // Each block is a full Server Card and may declare transports. They are
1215
+ // concatenated in document order, because the order is the author's
1216
+ // preference and selection honours it. A second block naming a DIFFERENT
1217
+ // global for the same type is an authoring error — only one of them can be
1218
+ // dispatched to — so the first is kept and the clash is reported.
1219
+ //
1220
+ // `remotes` is merged the same way: it is the deprecated spelling of the same
1221
+ // thing, and a page mid-migration may still carry it.
1119
1222
  for (const card of cards || []) {
1120
- for (const remote of (card && card.remotes) || []) {
1121
- const clash = merged.remotes.find((r) => r.type === remote.type && r.global !== remote.global)
1122
- if (clash) {
1123
- warn(`[llm-meta-widget] two blocks declare different ${remote.type} globals ` +
1124
- `("${clash.global}" and "${remote.global}"); using "${clash.global}"`)
1125
- continue
1126
- }
1127
- if (!merged.remotes.some((r) => r.type === remote.type && r.global === remote.global)) {
1128
- merged.remotes.push(remote)
1223
+ for (const kind of [ "transports", "remotes" ]) {
1224
+ for (const entry of (card && card[kind]) || []) {
1225
+ if (!entry || typeof entry.type !== "string") continue
1226
+ const clash = merged[kind].find((r) => r.type === entry.type && r.global !== entry.global)
1227
+ if (clash) {
1228
+ warn(`[llm-meta-widget] two blocks declare different ${entry.type} globals ` +
1229
+ `("${clash.global}" and "${entry.global}"); using "${clash.global}"`)
1230
+ continue
1231
+ }
1232
+ if (!merged[kind].some((r) => r.type === entry.type && r.global === entry.global)) {
1233
+ merged[kind].push(entry)
1234
+ }
1129
1235
  }
1130
1236
  }
1131
1237
  }
@@ -1151,7 +1257,7 @@ export function mergeStaticPrimitivesCards(cards, warn = console.warn) {
1151
1257
  }
1152
1258
 
1153
1259
  export function readStaticPrimitivesCard(text, warn = console.warn) {
1154
- const empty = { remotes: [], tools: [], prompts: [], resources: [] }
1260
+ const empty = { transports: [], remotes: [], tools: [], prompts: [], resources: [] }
1155
1261
  if (typeof text !== "string" || text.trim() === "") return empty
1156
1262
 
1157
1263
  let doc
@@ -1175,7 +1281,12 @@ export function readStaticPrimitivesCard(text, warn = console.warn) {
1175
1281
  })
1176
1282
 
1177
1283
  return {
1178
- // The whole card's transport, so what it declares survives the parse.
1284
+ // Where a page says how to reach its handlers. Since SEP d44bd25a this is
1285
+ // `transports` inside the extension data, because the Server Card's
1286
+ // `Remote.type` enum is streamable-http|sse and `page-global` was never a
1287
+ // valid value there. Top-level `remotes` is what 0.11.1-0.12.2 read, and is
1288
+ // still accepted for one release — see declaredPageGlobal.
1289
+ transports: Array.isArray(ext.transports) ? ext.transports : [],
1179
1290
  remotes: Array.isArray(doc.remotes) ? doc.remotes : [],
1180
1291
  tools: named(ext.tools, "tool"),
1181
1292
  prompts: named(ext.prompts, "prompt"),
@@ -1,3 +1,3 @@
1
1
  module LlmMetaWidget
2
- VERSION = "0.12.1"
2
+ VERSION = "0.13.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llm_meta_widget
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.1
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - jdkim