@fusion.dev/helix-headless 0.212.1 → 0.212.2

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.
@@ -346,6 +346,8 @@ export declare class HelixHeadlessChat extends HelixHeadlessChat_base {
346
346
  private grower;
347
347
  /** What `grower` is watching, so a render that changed nothing re-observes nothing. */
348
348
  private watched;
349
+ /** The frame `watchSizes` will observe in, while one is booked. */
350
+ private watchFrame;
349
351
  /** The scroll offset this element last set, which the reader has not moved from. */
350
352
  private stuckAt;
351
353
  /** The turn at the top of the reader's view, and how far below the top it sat. */
@@ -424,7 +426,15 @@ export declare class HelixHeadlessChat extends HelixHeadlessChat_base {
424
426
  * a reader asking to go to the bottom means.
425
427
  */
426
428
  resume(): void;
427
- /** Scrolls to the bottom, and remembers where that was. */
429
+ /**
430
+ * Scrolls to the bottom, and remembers where that was.
431
+ *
432
+ * **At once, whatever `scroll-behavior` the page gives the log.** Pinned,
433
+ * this runs on every update, every token of a streaming turn included, and a
434
+ * smooth scroll sent again each time never arrives: in WebKit, a log that had
435
+ * streamed for a second under `scroll-behavior: smooth` rested 6 pixels short
436
+ * of the bottom, and `stuckAt` held where each scroll began.
437
+ */
428
438
  private stick;
429
439
  /**
430
440
  * Asks to pin or unpin, and reports whether the request stood.
@@ -496,8 +506,34 @@ export declare class HelixHeadlessChat extends HelixHeadlessChat_base {
496
506
  * in a conversation of several knows who spoke.
497
507
  */
498
508
  private phrase;
499
- /** Watches what is drawn for size, re-observing only when that changed. */
509
+ /**
510
+ * Watches what is drawn for size, re-observing only when that changed.
511
+ *
512
+ * **In the next frame's animation callbacks, never in the update that asked.**
513
+ * A resize notice asks for an update, the update draws new turns while the
514
+ * browser is still delivering notices, and turns observed then are held to
515
+ * the next frame with "ResizeObserver loop completed with undelivered
516
+ * notifications": twice in WebKit's run of Chat's specs and once in
517
+ * Chromium's, on October 11, 2026. Observed from the next frame's callbacks,
518
+ * their first notice comes before that frame paints, and a turn drawn for the
519
+ * first time is answered sooner still, by `settleDrawn`.
520
+ */
500
521
  private watchSizes;
522
+ /** Observes the log and the turns drawn now, where that changed. */
523
+ private observeSizes;
524
+ /**
525
+ * Answers the turns this render drew for the first time, once each has drawn
526
+ * what is in it: a microtask after this update, and before the frame.
527
+ *
528
+ * **Sooner than the resize observer, which `watchSizes` starts a frame
529
+ * later.** A turn is drawn empty and fills in after this update, and until
530
+ * it is measured the reader's turn sits where the estimates put it. A timer
531
+ * that ran before the next frame read that place as the reader's: on a Linux
532
+ * runner, the controller's rest after a scroll left a reader's turn 55.5
533
+ * pixels from where it was when history arrived above it, in WebKit, on
534
+ * October 11, 2026. Answered here, the history is held before any timer runs.
535
+ */
536
+ private settleDrawn;
501
537
  /**
502
538
  * A turn or the log changed size, which moves nothing on its own.
503
539
  *
@@ -62,6 +62,7 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
62
62
  this.settling = undefined;
63
63
  this.grower = undefined;
64
64
  this.watched = [];
65
+ this.watchFrame = 0;
65
66
  this.stuckAt = -1;
66
67
  this.anchor = undefined;
67
68
  this.asked = false;
@@ -259,7 +260,12 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
259
260
  stick() {
260
261
  const log = this.log;
261
262
  if (log) {
262
- log.scrollTop = log.scrollHeight;
263
+ if (typeof log.scrollTo === 'function') {
264
+ log.scrollTo({ behavior: 'instant', top: log.scrollHeight });
265
+ }
266
+ else {
267
+ log.scrollTop = log.scrollHeight;
268
+ }
263
269
  this.stuckAt = log.scrollTop;
264
270
  }
265
271
  }
@@ -335,7 +341,7 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
335
341
  }
336
342
  const found = turns[low];
337
343
  this.anchor = found
338
- ? { id: found.id, offset: found.element.getBoundingClientRect().top - top }
344
+ ? { at: log.scrollTop, id: found.id, offset: found.element.getBoundingClientRect().top - top }
339
345
  : undefined;
340
346
  }
341
347
  restore() {
@@ -344,6 +350,10 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
344
350
  if (!log || !anchor || (this.fed && this.virtualize.scrolling())) {
345
351
  return;
346
352
  }
353
+ if (!this.fed && Math.abs(log.scrollTop - anchor.at) >= 1) {
354
+ this.remember();
355
+ return;
356
+ }
347
357
  const found = this.rendered().find((turn) => turn.id === anchor.id);
348
358
  if (!found) {
349
359
  return;
@@ -418,9 +428,21 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
418
428
  : formatWords(this, 'arrivalLabel', { author: turn.by, text: turn.text });
419
429
  }
420
430
  watchSizes() {
421
- if (typeof ResizeObserver === 'undefined') {
431
+ if (typeof ResizeObserver === 'undefined' || this.watchFrame !== 0) {
432
+ return;
433
+ }
434
+ if (typeof requestAnimationFrame !== 'function') {
435
+ this.observeSizes();
422
436
  return;
423
437
  }
438
+ this.watchFrame = requestAnimationFrame(() => {
439
+ this.watchFrame = 0;
440
+ if (this.isConnected) {
441
+ this.observeSizes();
442
+ }
443
+ });
444
+ }
445
+ observeSizes() {
424
446
  const log = this.log;
425
447
  const targets = [...(log ? [log] : []), ...this.rendered().map((turn) => turn.element)];
426
448
  if (targets.length === this.watched.length &&
@@ -434,6 +456,18 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
434
456
  }
435
457
  this.watched = targets;
436
458
  }
459
+ settleDrawn() {
460
+ const waiting = this.drawn().filter((row) => !drawnInFull(row));
461
+ if (waiting.length === 0) {
462
+ return;
463
+ }
464
+ void Promise.all(waiting.map(({ element }) => element.firstElementChild
465
+ ?.updateComplete)).then(() => {
466
+ if (this.isConnected && this.fed) {
467
+ this.handleResize();
468
+ }
469
+ });
470
+ }
437
471
  connectedCallback() {
438
472
  super.connectedCallback();
439
473
  this.addEventListener('keydown', this.handleKeydown);
@@ -458,6 +492,10 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
458
492
  this.settling = undefined;
459
493
  this.grower?.disconnect();
460
494
  this.watched = [];
495
+ if (this.watchFrame !== 0) {
496
+ cancelAnimationFrame(this.watchFrame);
497
+ this.watchFrame = 0;
498
+ }
461
499
  }
462
500
  willUpdate(changed) {
463
501
  super.willUpdate(changed);
@@ -518,6 +556,7 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
518
556
  }
519
557
  if (this.fed) {
520
558
  this.virtualize.measure();
559
+ this.settleDrawn();
521
560
  }
522
561
  this.watchSizes();
523
562
  this.markScroller();