micro-models-agent 2.16.0 → 2.16.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.
package/CHANGELOG.md CHANGED
@@ -21,6 +21,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
21
21
  - **Web UI boot was dead on 2.15.0** (`src/modules/webui/assets/js/settings.js`): a stray `}` in `setStaleBar` pushed `return` to top level (`Uncaught SyntaxError: Illegal return statement`), killing the whole `app.js` import graph — blank page, no feed, selectors stuck at `–`. One-line brace fix.
22
22
  - **Web e2e boot smoke** (`tests/e2e/web-boot.e2e-test.ts`): `mma web` boots in a real Chromium with zero console/page errors (live session tab, chrome skeleton, settings modal) and every served JS module in the live `/app.js` import graph answers 200; `web-helpers.launch` now names collected console errors when the feed never boots, so the next broken module fails with its `SyntaxError` in the message instead of a bare timeout.
23
23
 
24
+ ## [2.16.1] - 2026-10-01
25
+
26
+ ### Added
27
+
28
+ - **Smooth streamed-text cascade** (`src/modules/webui/assets/js/feed.js`, `md.js`, `styles.css`): live markdown chunks are buffered and flushed once per animation frame (`requestAnimationFrame`, multi-feed safe) instead of write+scroll per SSE event, and each streamed text run renders in a `span.tk` with a short opacity-only `tkIn` fade (staggered `(n%4)*20ms`, `.15s`). History render and code blocks skip the animation; killed by `prefers-reduced-motion`.
29
+
24
30
  ## [2.16.0] - 2026-10-01
25
31
 
26
32
  ### Added
@@ -246,16 +246,20 @@ function resetRun() {
246
246
 
247
247
  // Options passed to the streaming Markdown renderer: localized code-block
248
248
  // copy labels. Highlighting is applied by `md.js` once a fence closes.
249
- function mdOpts() {
249
+ // `animate` wraps live-streamed text chunks in `span.tk` (CSS `tkIn` fade);
250
+ // history render and code blocks skip it.
251
+ function mdOpts(animate) {
250
252
  return {
251
253
  copyLabel: t("web.code.copy"),
252
254
  copiedLabel: t("web.code.copied"),
253
255
  copyFailedLabel: t("web.code.copy_failed"),
256
+ animate: animate === true,
254
257
  };
255
258
  }
256
259
 
257
260
  // Close the active Markdown segment (if any) and clear the text S.state.
258
261
  export function finishMd() {
262
+ flushText();
259
263
  if (S.curMd) {
260
264
  S.curMd.root.classList.remove("streaming");
261
265
  S.curMd.end();
@@ -264,6 +268,38 @@ export function finishMd() {
264
268
  S.curText = null;
265
269
  }
266
270
 
271
+ // Text chunks arrive in bursts (several SSE `text` events per frame); writing
272
+ // and scrolling on every one thrashes layout and stacks overlapping `tkIn`
273
+ // fades ("рвано"). Buffer them and flush once per animation frame instead.
274
+ let textBuf = "";
275
+ let textFeed = null;
276
+ let textScheduled = false;
277
+
278
+ function flushText() {
279
+ textScheduled = false;
280
+ if (!textBuf || !S.curMd) {
281
+ textBuf = "";
282
+ textFeed = null;
283
+ return;
284
+ }
285
+ const buf = textBuf;
286
+ const feed = textFeed;
287
+ textBuf = "";
288
+ textFeed = null;
289
+ S.curMd.write(buf);
290
+ if (feed && feed === activeFeed()) scroll();
291
+ }
292
+
293
+ function scheduleText() {
294
+ if (textScheduled) return;
295
+ textScheduled = true;
296
+ if (typeof window !== "undefined" && typeof window.requestAnimationFrame === "function") {
297
+ window.requestAnimationFrame(flushText);
298
+ } else {
299
+ flushText();
300
+ }
301
+ }
302
+
267
303
  export function onText(chunk, sid) {
268
304
  const feed = feedForEvent(sid);
269
305
  clearBusy(feed);
@@ -275,14 +311,17 @@ export function onText(chunk, sid) {
275
311
  if (S.curText == null) {
276
312
  flushRun();
277
313
  S.curText = "";
278
- S.curMd = mountMarkdown(feed, mdOpts());
314
+ S.curMd = mountMarkdown(feed, mdOpts(true));
279
315
  S.curMd.root.classList.add("streaming");
280
316
  S.turnRoots.push(S.curMd.root);
281
317
  }
282
318
  S.curText += chunk;
283
319
  S.turnMd += chunk;
284
- S.curMd.write(chunk);
285
- if (feed === activeFeed()) scroll();
320
+ // A second session streaming concurrently must not merge into another feed's buffer.
321
+ if (textFeed && textFeed !== feed) flushText();
322
+ textFeed = feed;
323
+ textBuf += chunk;
324
+ scheduleText();
286
325
  }
287
326
 
288
327
  export function finishThink() {
package/dist/assets/md.js CHANGED
@@ -198,6 +198,20 @@ function addText(data, text) {
198
198
  data.code.codeEl.appendChild(document.createTextNode(text));
199
199
  return;
200
200
  }
201
+ if (data.opts.animate) {
202
+ // Stagger cascade within a flush: each span starts before the previous
203
+ // one finishes ((n%4)*20ms delay, .15s duration). Modulo keeps long
204
+ // outputs cascading instead of settling on a permanent max delay.
205
+ const n = data.animN || 0;
206
+ data.animN = n + 1;
207
+ const span = document.createElement("span");
208
+ span.className = "tk";
209
+ const delay = (n % 4) * 20;
210
+ if (delay) span.style.animationDelay = delay + "ms";
211
+ span.appendChild(document.createTextNode(text));
212
+ data.nodes[data.index].appendChild(span);
213
+ return;
214
+ }
201
215
  smd.default_add_text(data, text);
202
216
  }
203
217
 
@@ -656,6 +656,12 @@ body.light .sel > button { border: 1px solid var(--border); }
656
656
  content: "▌"; color: var(--accent); margin-left: 2px;
657
657
  animation: blink 1s steps(2, start) infinite;
658
658
  }
659
+ /* per-chunk typewriter fade for live markdown (md.js wraps each streamed
660
+ text chunk in `span.tk` when `opts.animate` is set; history render and
661
+ code blocks skip it). Opacity-only: `.tk` stays inline so wrapping is
662
+ untouched. Killed by the `prefers-reduced-motion` block below. */
663
+ @keyframes tkIn { from { opacity: 0; } }
664
+ .md.streaming .tk { animation: tkIn .15s ease-out backwards; }
659
665
  /* alive labels: in-feed spinner, the *live* think block, rail phase,
660
666
  a running tool. Finished blocks stay static — a pulsing "думаю" after
661
667
  the turn reads as hung. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "micro-models-agent",
3
- "version": "2.16.0",
3
+ "version": "2.16.1",
4
4
  "description": "Micro Models Agent (MMA) — LLM agent harness for small models (Qwen3.5-9B, 32K-64K context)",
5
5
  "type": "module",
6
6
  "bin": {