@runtypelabs/persona 4.33.0 → 4.34.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.
- package/dist/activity-ui.d.cts +3 -15
- package/dist/activity-ui.d.ts +3 -15
- package/dist/animations/glyph-cycle.d.cts +1 -3
- package/dist/animations/glyph-cycle.d.ts +1 -3
- package/dist/animations/wipe.d.cts +1 -3
- package/dist/animations/wipe.d.ts +1 -3
- package/dist/approval-ui.cjs +2 -2
- package/dist/approval-ui.d.cts +3 -15
- package/dist/approval-ui.d.ts +3 -15
- package/dist/approval-ui.js +1 -1
- package/dist/artifacts-ui.cjs +1 -1
- package/dist/artifacts-ui.d.cts +3 -21
- package/dist/artifacts-ui.d.ts +3 -21
- package/dist/background-runtime.d.cts +2 -3
- package/dist/background-runtime.d.ts +2 -3
- package/dist/{chunk-gu34lr-P.js → chunk-Brdx4NCz.js} +1 -1
- package/dist/{chunk-dLJWKfOb.js → chunk-C-rux8Q9.js} +1 -1
- package/dist/{chunk-Ds_twaZn.js → chunk-C4v-HNhS.js} +1 -1
- package/dist/{chunk-Bkma5hIU.js → chunk-CE4seJxF.js} +1 -1
- package/dist/{chunk-D6U0E82p.js → chunk-CV8EPIhT.js} +1 -1
- package/dist/{chunk-ql5N2I_t.js → chunk-CWzHNuVK.js} +1 -1
- package/dist/{chunk-DbGLjU2P.js → chunk-CYpt7PsO.js} +1 -1
- package/dist/{chunk-DOZmcve8.js → chunk-Csr59G5V.js} +1 -1
- package/dist/{chunk-DW7Vg_l2.js → chunk-CxNPSY-b.js} +1 -1
- package/dist/chunk-Dqa_GOcj.js +1 -0
- package/dist/chunk-DsWv3EGF.js +1 -0
- package/dist/{chunk-C7-m0EmI.js → chunk-NUNdcnAZ.js} +1 -1
- package/dist/{chunk-CXQJG7pb.js → chunk-YMkMvAsa.js} +2 -2
- package/dist/{chunk-C-EHtpbB.js → chunk-h5aOI_Pa.js} +1 -1
- package/dist/chunk-kizhspe5.js +1 -0
- package/dist/client-dispatch.js +1 -1
- package/dist/client-history.js +1 -1
- package/dist/client-stream.js +1 -1
- package/dist/{code-generators-DqauYyfX.d.cts → code-generators-DPAqTcAz.d.cts} +0 -2
- package/dist/{code-generators-DqauYyfX.d.ts → code-generators-DPAqTcAz.d.ts} +0 -2
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.d.cts +1 -1
- package/dist/codegen.d.ts +1 -1
- package/dist/codegen.js +1 -1
- package/dist/composer-actions-runtime.cjs +1 -1
- package/dist/composer-actions-runtime.d.cts +4 -20
- package/dist/composer-actions-runtime.d.ts +4 -20
- package/dist/composer-actions-runtime.js +1 -1
- package/dist/{composer-input-DnqGCXZz.d.ts → composer-input-i_FiUp6J.d.cts} +1 -7
- package/dist/{composer-input-DnqGCXZz.d.cts → composer-input-i_FiUp6J.d.ts} +1 -7
- package/dist/{composer-store-6Iwy6WLI.d.ts → composer-store-DhGNgG7Z.d.cts} +1 -3
- package/dist/{composer-store-6Iwy6WLI.d.cts → composer-store-DhGNgG7Z.d.ts} +1 -3
- package/dist/config-diagnostics.d.cts +0 -2
- package/dist/config-diagnostics.d.ts +0 -2
- package/dist/{context-mention-manager-B-rHF98n.d.ts → context-mention-manager-DL-xwGfL.d.cts} +2 -6
- package/dist/{context-mention-manager-B-rHF98n.d.cts → context-mention-manager-DL-xwGfL.d.ts} +2 -6
- package/dist/context-mentions-inline.cjs +1 -1
- package/dist/context-mentions-inline.d.cts +2 -4
- package/dist/context-mentions-inline.d.ts +2 -4
- package/dist/context-mentions-inline.js +1 -1
- package/dist/context-mentions.cjs +1 -1
- package/dist/context-mentions.d.cts +3 -9
- package/dist/context-mentions.d.ts +3 -9
- package/dist/{defaults-BuOjqUAG.d.ts → defaults-Bga2Xrsp.d.cts} +1 -5
- package/dist/{defaults-BuOjqUAG.d.cts → defaults-Bga2Xrsp.d.ts} +1 -5
- package/dist/dictation-runtime.cjs +1 -1
- package/dist/dictation-runtime.d.cts +4 -13
- package/dist/dictation-runtime.d.ts +4 -13
- package/dist/dictation-runtime.js +1 -1
- package/dist/{dom-context-DHTabIn7.d.cts → dom-context-Bw-_9Evh.d.cts} +0 -2
- package/dist/{dom-context-DHTabIn7.d.ts → dom-context-Bw-_9Evh.d.ts} +0 -2
- package/dist/event-stream-view.cjs +1 -1
- package/dist/event-stream-view.d.cts +3 -16
- package/dist/event-stream-view.d.ts +3 -16
- package/dist/{fallback-speech-engine-SqEfkGda.d.ts → fallback-speech-engine-kLdyPcmk.d.cts} +1 -3
- package/dist/{fallback-speech-engine-SqEfkGda.d.cts → fallback-speech-engine-kLdyPcmk.d.ts} +1 -3
- package/dist/{feature-preferences-DqAmje-2.d.ts → feature-preferences-CmwZGKrf.d.cts} +1 -5
- package/dist/{feature-preferences-DqAmje-2.d.cts → feature-preferences-CmwZGKrf.d.ts} +1 -5
- package/dist/forms-ui.d.cts +2 -4
- package/dist/forms-ui.d.ts +2 -4
- package/dist/{history-confirm-dialog-B3L7QNcO.d.cts → history-confirm-dialog-i46-lPN_.d.cts} +2 -14
- package/dist/{history-confirm-dialog-B3L7QNcO.d.ts → history-confirm-dialog-i46-lPN_.d.ts} +2 -14
- package/dist/history-shell.cjs +1 -1
- package/dist/history-shell.d.cts +8 -27
- package/dist/history-shell.d.ts +8 -27
- package/dist/history-shell.js +1 -1
- package/dist/history-view.d.cts +1 -1
- package/dist/history-view.d.ts +1 -1
- package/dist/icons-extra.cjs +1 -1
- package/dist/icons-extra.d.cts +0 -2
- package/dist/icons-extra.d.ts +0 -2
- package/dist/index.cjs +45 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -10
- package/dist/index.d.ts +10 -10
- package/dist/index.global.js +1 -1
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +49 -49
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/launcher.global.js +2 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/{loading-label-runtime-loader-JecjOfJS.d.ts → loading-label-runtime-loader-Ccr3ETd3.d.cts} +1 -3
- package/dist/{loading-label-runtime-loader-JecjOfJS.d.cts → loading-label-runtime-loader-Ccr3ETd3.d.ts} +1 -3
- package/dist/loading-label-runtime.cjs +1 -1
- package/dist/loading-label-runtime.d.cts +1 -1
- package/dist/loading-label-runtime.d.ts +1 -1
- package/dist/loading-label-runtime.js +1 -1
- package/dist/markdown-parsers.js +1 -1
- package/dist/{message-bubble-CBYtpDsT.d.ts → message-bubble-C6AVaH7f.d.cts} +20 -3
- package/dist/{message-bubble-CBYtpDsT.d.cts → message-bubble-C6AVaH7f.d.ts} +20 -3
- package/dist/{plugin-kit-C1mRjLgW.d.cts → plugin-kit-BVggqE3C.d.cts} +0 -2
- package/dist/{plugin-kit-C1mRjLgW.d.ts → plugin-kit-BVggqE3C.d.ts} +0 -2
- package/dist/plugin-kit.d.cts +1 -1
- package/dist/plugin-kit.d.ts +1 -1
- package/dist/preferences-runtime.cjs +1 -1
- package/dist/preferences-runtime.d.cts +1 -1
- package/dist/preferences-runtime.d.ts +1 -1
- package/dist/preferences-runtime.js +1 -1
- package/dist/{preview-_UJwwt2L.d.cts → preview-BMdRffo4.d.cts} +2 -6
- package/dist/{preview-_UJwwt2L.d.ts → preview-BMdRffo4.d.ts} +2 -6
- package/dist/{registry-B5ZV3K-h.d.ts → registry-B16l8s1b.d.cts} +1 -8
- package/dist/{registry-B5ZV3K-h.d.cts → registry-B16l8s1b.d.ts} +1 -8
- package/dist/{session-Bys8_XuB.d.cts → session-CtZHe7fc.d.cts} +8 -92
- package/dist/{session-Bys8_XuB.d.ts → session-CtZHe7fc.d.ts} +8 -92
- package/dist/session-reconnect.d.cts +2 -4
- package/dist/session-reconnect.d.ts +2 -4
- package/dist/smart-dom-reader.d.cts +2 -8
- package/dist/smart-dom-reader.d.ts +2 -8
- package/dist/testing.d.cts +0 -2
- package/dist/testing.d.ts +0 -2
- package/dist/theme-editor-preview.cjs +42 -42
- package/dist/theme-editor-preview.d.cts +1 -1
- package/dist/theme-editor-preview.d.ts +1 -1
- package/dist/theme-editor-preview.js +25 -25
- package/dist/theme-editor.cjs +25 -5
- package/dist/theme-editor.d.cts +2 -22
- package/dist/theme-editor.d.ts +2 -22
- package/dist/theme-editor.js +1 -1
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +2 -4
- package/dist/theme-reference.d.ts +2 -4
- package/dist/theme-reference.js +1 -1
- package/dist/{tool-loading-animation-d5qAQcwv.d.ts → tool-loading-animation-D2cyFvmM.d.cts} +1 -7
- package/dist/{tool-loading-animation-d5qAQcwv.d.cts → tool-loading-animation-D2cyFvmM.d.ts} +1 -7
- package/dist/{tooltip-BkW-8MnD.d.cts → tooltip-DbXkAuJL.d.cts} +0 -2
- package/dist/{tooltip-BkW-8MnD.d.ts → tooltip-DbXkAuJL.d.ts} +0 -2
- package/dist/{turn-activity-ui-q6fHVnx-.d.cts → turn-activity-ui-Do1Nd-Ow.d.cts} +2 -12
- package/dist/{turn-activity-ui-q6fHVnx-.d.ts → turn-activity-ui-Do1Nd-Ow.d.ts} +2 -12
- package/dist/turn-activity-ui.d.cts +1 -1
- package/dist/turn-activity-ui.d.ts +1 -1
- package/dist/{types-B_mj21jg.d.cts → types-BC7MRxO5.d.cts} +76 -29
- package/dist/{types-B_mj21jg.d.ts → types-BC7MRxO5.d.ts} +76 -29
- package/dist/ui-extras.js +3 -3
- package/dist/voice-client-tools.d.cts +1 -3
- package/dist/voice-client-tools.d.ts +1 -3
- package/dist/voice-runtime.cjs +1 -1
- package/dist/voice-runtime.d.cts +2 -12
- package/dist/voice-runtime.d.ts +2 -12
- package/dist/voice-runtime.js +2 -2
- package/dist/voice-worklet-player.d.cts +2 -6
- package/dist/voice-worklet-player.d.ts +2 -6
- package/dist/{webmcp-runtime-entry-ClEO3qdI.d.ts → webmcp-runtime-entry-B8g46vS0.d.cts} +1 -5
- package/dist/{webmcp-runtime-entry-ClEO3qdI.d.cts → webmcp-runtime-entry-B8g46vS0.d.ts} +1 -5
- package/dist/webmcp-runtime.d.cts +1 -1
- package/dist/webmcp-runtime.d.ts +1 -1
- package/dist/widget.css +1 -1
- package/package.json +4 -4
- package/src/components/event-stream-view.test.ts +5 -0
- package/src/components/message-bubble.ts +26 -6
- package/src/components/scroll-to-bottom.test.ts +135 -0
- package/src/components/scroll-to-bottom.ts +50 -13
- package/src/components/user-message-collapse.ts +117 -0
- package/src/defaults.ts +6 -0
- package/src/styles/widget.css +56 -0
- package/src/theme-reference.ts +2 -2
- package/src/types/theme.ts +29 -0
- package/src/types.ts +49 -5
- package/src/ui.scroll-additive.test.ts +219 -117
- package/src/ui.scroll.test.ts +176 -13
- package/src/ui.ts +230 -107
- package/src/ui.turn-activity.test.ts +7 -7
- package/src/ui.user-message-collapse.test.ts +540 -0
- package/src/utils/auto-follow.test.ts +0 -38
- package/src/utils/auto-follow.ts +0 -35
- package/src/utils/theme.test.ts +23 -0
- package/src/utils/tokens.ts +16 -0
- package/dist/chunk-CVMIncEY.js +0 -1
- package/dist/chunk-DVgzoVCB.js +0 -1
- package/dist/chunk-d54BJdKd.js +0 -1
|
@@ -39,16 +39,22 @@ const installRafMock = () => {
|
|
|
39
39
|
vi.stubGlobal("cancelAnimationFrame", (id: number) => {
|
|
40
40
|
callbacks.delete(id);
|
|
41
41
|
});
|
|
42
|
+
const run = (maxFrames: number) => {
|
|
43
|
+
let frames = 0;
|
|
44
|
+
while (callbacks.size > 0 && frames < maxFrames) {
|
|
45
|
+
const pending = [...callbacks.values()];
|
|
46
|
+
callbacks.clear();
|
|
47
|
+
frames += 1;
|
|
48
|
+
now += 16;
|
|
49
|
+
pending.forEach((cb) => cb(now));
|
|
50
|
+
}
|
|
51
|
+
};
|
|
42
52
|
return {
|
|
53
|
+
step(frameCount = 1) {
|
|
54
|
+
run(frameCount);
|
|
55
|
+
},
|
|
43
56
|
flush(maxFrames = 80) {
|
|
44
|
-
|
|
45
|
-
while (callbacks.size > 0 && frames < maxFrames) {
|
|
46
|
-
const pending = [...callbacks.values()];
|
|
47
|
-
callbacks.clear();
|
|
48
|
-
frames += 1;
|
|
49
|
-
now += 16;
|
|
50
|
-
pending.forEach((cb) => cb(now));
|
|
51
|
-
}
|
|
57
|
+
run(maxFrames);
|
|
52
58
|
},
|
|
53
59
|
};
|
|
54
60
|
};
|
|
@@ -498,12 +504,11 @@ describe("scrollBehavior.announce (Principle 15)", () => {
|
|
|
498
504
|
});
|
|
499
505
|
|
|
500
506
|
|
|
501
|
-
// Anchor-top pins the sent user message
|
|
502
|
-
// the
|
|
503
|
-
//
|
|
504
|
-
//
|
|
505
|
-
|
|
506
|
-
describe("anchor-top first-unread positioning", () => {
|
|
507
|
+
// Anchor-top pins the sent user message for the whole reply. It never hands
|
|
508
|
+
// off to the reply because the reply starts below the fold: tool runs,
|
|
509
|
+
// artifacts, multi-block answers, and tall questions all stream in beneath
|
|
510
|
+
// the anchor, and the jump-to-latest control is how the reader gets there.
|
|
511
|
+
describe("anchor-top holds the sent message for the whole reply", () => {
|
|
507
512
|
beforeEach(() => {
|
|
508
513
|
installRafMock();
|
|
509
514
|
});
|
|
@@ -515,7 +520,8 @@ describe("anchor-top first-unread positioning", () => {
|
|
|
515
520
|
|
|
516
521
|
const emitToolMessage = (
|
|
517
522
|
controller: ReturnType<typeof createAgentExperience>,
|
|
518
|
-
id: string
|
|
523
|
+
id: string,
|
|
524
|
+
status: "running" | "complete" = "complete"
|
|
519
525
|
) => {
|
|
520
526
|
controller.injectTestMessage({
|
|
521
527
|
type: "message",
|
|
@@ -525,13 +531,14 @@ describe("anchor-top first-unread positioning", () => {
|
|
|
525
531
|
content: "",
|
|
526
532
|
createdAt: CREATED_AT,
|
|
527
533
|
variant: "tool",
|
|
528
|
-
|
|
534
|
+
streaming: status === "running",
|
|
535
|
+
toolCall: { id, name: "search", status },
|
|
529
536
|
},
|
|
530
537
|
});
|
|
531
538
|
};
|
|
532
539
|
|
|
533
540
|
// An artifact / component block: substance lives in rawContent, `content`
|
|
534
|
-
// is empty.
|
|
541
|
+
// is empty.
|
|
535
542
|
const emitArtifactMessage = (
|
|
536
543
|
controller: ReturnType<typeof createAgentExperience>,
|
|
537
544
|
id: string
|
|
@@ -560,6 +567,9 @@ describe("anchor-top first-unread positioning", () => {
|
|
|
560
567
|
Object.defineProperty(bubble!, "offsetTop", { configurable: true, value });
|
|
561
568
|
};
|
|
562
569
|
|
|
570
|
+
const getSpacer = (mount: HTMLElement) =>
|
|
571
|
+
getScrollContainer(mount).querySelector<HTMLElement>("[data-persona-anchor-spacer]")!;
|
|
572
|
+
|
|
563
573
|
const startTurn = (
|
|
564
574
|
controller: ReturnType<typeof createAgentExperience>,
|
|
565
575
|
mount: HTMLElement,
|
|
@@ -571,7 +581,7 @@ describe("anchor-top first-unread positioning", () => {
|
|
|
571
581
|
raf.flush();
|
|
572
582
|
};
|
|
573
583
|
|
|
574
|
-
it("
|
|
584
|
+
it("keeps the sent message anchored through a tool run and an answer below the fold", () => {
|
|
575
585
|
const raf = installRafMock();
|
|
576
586
|
const mount = createMount();
|
|
577
587
|
const controller = createAgentExperience(mount, anchorTopConfig());
|
|
@@ -583,26 +593,31 @@ describe("anchor-top first-unread positioning", () => {
|
|
|
583
593
|
startTurn(controller, mount, raf);
|
|
584
594
|
expect(metrics.getScrollTop()).toBe(284); // parked on the sent message
|
|
585
595
|
|
|
586
|
-
for (let i = 0; i < 6; i += 1)
|
|
596
|
+
for (let i = 0; i < 6; i += 1) {
|
|
597
|
+
emitToolMessage(controller, `t${i}`, "running");
|
|
598
|
+
setBubbleOffsetTop(mount, `t${i}`, 360 + i * 60);
|
|
599
|
+
emitToolMessage(controller, `t${i}`);
|
|
600
|
+
}
|
|
587
601
|
metrics.setScrollHeight(1500);
|
|
588
602
|
raf.flush();
|
|
589
|
-
// Tool calls are chrome: they never move the anchor.
|
|
590
603
|
expect(metrics.getScrollTop()).toBe(284);
|
|
591
604
|
|
|
592
|
-
// The answer starts 400px below the pinned question
|
|
593
|
-
// 400px viewport
|
|
594
|
-
//
|
|
595
|
-
// render to be seen.)
|
|
605
|
+
// The answer starts 400px below the pinned question, past the fold at a
|
|
606
|
+
// 400px viewport. A second render lets any render-time probe see the
|
|
607
|
+
// mocked offset; the anchor still stays on the question.
|
|
596
608
|
emitStreamingAssistant(controller, "a1", "Good news — your order is on track.");
|
|
597
609
|
setBubbleOffsetTop(mount, "a1", 700);
|
|
610
|
+
metrics.setScrollHeight(1700);
|
|
598
611
|
emitStreamingAssistant(controller, "a1", "Good news — your order is on track!");
|
|
599
612
|
raf.flush();
|
|
600
|
-
expect(metrics.getScrollTop()).toBe(
|
|
613
|
+
expect(metrics.getScrollTop()).toBe(284);
|
|
601
614
|
|
|
615
|
+
// The jump control advertises the offscreen answer.
|
|
616
|
+
expect(getScrollToBottomButton(mount).style.display).toBe("");
|
|
602
617
|
controller.destroy();
|
|
603
618
|
});
|
|
604
619
|
|
|
605
|
-
it("
|
|
620
|
+
it("keeps the anchor when an artifact block lands below the fold", () => {
|
|
606
621
|
const raf = installRafMock();
|
|
607
622
|
const mount = createMount();
|
|
608
623
|
const controller = createAgentExperience(mount, anchorTopConfig());
|
|
@@ -617,18 +632,15 @@ describe("anchor-top first-unread positioning", () => {
|
|
|
617
632
|
raf.flush();
|
|
618
633
|
|
|
619
634
|
emitArtifactMessage(controller, "art-msg");
|
|
620
|
-
// 320px below the question: stranded past the 400 - 96 visibility floor,
|
|
621
|
-
// so the anchor hands off. The trailing tool render lets the probe see
|
|
622
|
-
// the mocked offset.
|
|
623
635
|
setBubbleOffsetTop(mount, "art-msg", 620);
|
|
624
636
|
emitToolMessage(controller, "t-after");
|
|
625
637
|
raf.flush();
|
|
626
638
|
|
|
627
|
-
expect(metrics.getScrollTop()).toBe(
|
|
639
|
+
expect(metrics.getScrollTop()).toBe(284);
|
|
628
640
|
controller.destroy();
|
|
629
641
|
});
|
|
630
642
|
|
|
631
|
-
it("holds the
|
|
643
|
+
it("holds the anchor across multiple assistant blocks, streaming updates, and completion", () => {
|
|
632
644
|
const raf = installRafMock();
|
|
633
645
|
const mount = createMount();
|
|
634
646
|
const controller = createAgentExperience(mount, anchorTopConfig());
|
|
@@ -638,24 +650,26 @@ describe("anchor-top first-unread positioning", () => {
|
|
|
638
650
|
});
|
|
639
651
|
|
|
640
652
|
startTurn(controller, mount, raf);
|
|
641
|
-
// First segment starts past the fold (delta 400 at a 400px viewport):
|
|
642
|
-
// the anchor hands off to it.
|
|
643
653
|
emitStreamingAssistant(controller, "a1", "First segment.");
|
|
644
654
|
setBubbleOffsetTop(mount, "a1", 700);
|
|
645
655
|
metrics.setScrollHeight(1400);
|
|
646
|
-
emitStreamingAssistant(controller, "a1", "First segment
|
|
656
|
+
emitStreamingAssistant(controller, "a1", "First segment, still streaming.");
|
|
647
657
|
raf.flush();
|
|
648
|
-
expect(metrics.getScrollTop()).toBe(
|
|
658
|
+
expect(metrics.getScrollTop()).toBe(284);
|
|
649
659
|
|
|
650
|
-
// Interleaved: tool, then a second text segment.
|
|
651
|
-
// first unread block — later blocks stream in below it.
|
|
660
|
+
// Interleaved: tool, then a second text segment.
|
|
652
661
|
emitToolMessage(controller, "t0");
|
|
653
662
|
emitStreamingAssistant(controller, "a2", "Second segment.");
|
|
654
663
|
setBubbleOffsetTop(mount, "a2", 1100);
|
|
655
664
|
metrics.setScrollHeight(1800);
|
|
665
|
+
emitStreamingAssistant(controller, "a2", "Second segment, longer.");
|
|
656
666
|
raf.flush();
|
|
667
|
+
expect(metrics.getScrollTop()).toBe(284);
|
|
657
668
|
|
|
658
|
-
|
|
669
|
+
emitAssistantMessage(controller, "a2", "Second segment, complete.");
|
|
670
|
+
controller.injectTestMessage({ type: "status", status: "idle" });
|
|
671
|
+
raf.flush();
|
|
672
|
+
expect(metrics.getScrollTop()).toBe(284);
|
|
659
673
|
controller.destroy();
|
|
660
674
|
});
|
|
661
675
|
|
|
@@ -671,9 +685,9 @@ describe("anchor-top first-unread positioning", () => {
|
|
|
671
685
|
metrics.setScrollHeight(1500);
|
|
672
686
|
raf.flush();
|
|
673
687
|
|
|
674
|
-
// The reader takes over: wheels
|
|
675
|
-
//
|
|
676
|
-
// clamps and restores scrollTop on its own
|
|
688
|
+
// The reader takes over: wheels to watch the tools run. Engagement is
|
|
689
|
+
// keyed on real input, NOT on the scroll event (streaming content
|
|
690
|
+
// clamps and restores scrollTop on its own; see the regression test
|
|
677
691
|
// below), so the wheel is what must register here.
|
|
678
692
|
metrics.setScrollTop(520);
|
|
679
693
|
sc.dispatchEvent(new WheelEvent("wheel", { deltaY: -120, bubbles: true }));
|
|
@@ -681,6 +695,8 @@ describe("anchor-top first-unread positioning", () => {
|
|
|
681
695
|
|
|
682
696
|
emitStreamingAssistant(controller, "a1", "Good news — your order is on track.");
|
|
683
697
|
setBubbleOffsetTop(mount, "a1", 700);
|
|
698
|
+
emitStreamingAssistant(controller, "a1", "Good news — your order is on track!");
|
|
699
|
+
metrics.setScrollHeight(1700);
|
|
684
700
|
raf.flush();
|
|
685
701
|
|
|
686
702
|
// Their position wins: no re-anchor under them.
|
|
@@ -688,6 +704,40 @@ describe("anchor-top first-unread positioning", () => {
|
|
|
688
704
|
controller.destroy();
|
|
689
705
|
});
|
|
690
706
|
|
|
707
|
+
it("does not move a reader who is selecting transcript text", () => {
|
|
708
|
+
const raf = installRafMock();
|
|
709
|
+
const mount = createMount();
|
|
710
|
+
const controller = createAgentExperience(mount, anchorTopConfig());
|
|
711
|
+
const sc = getScrollContainer(mount);
|
|
712
|
+
const metrics = installScrollMetrics(sc, { scrollHeight: 1000, clientHeight: 400 });
|
|
713
|
+
|
|
714
|
+
startTurn(controller, mount, raf);
|
|
715
|
+
emitStreamingAssistant(controller, "a1", "Tracking shows the parcel left the depot.");
|
|
716
|
+
setBubbleOffsetTop(mount, "a1", 420);
|
|
717
|
+
raf.flush();
|
|
718
|
+
|
|
719
|
+
// Press and drag-select the answer, scrolling a little in the process.
|
|
720
|
+
const answer = sc.querySelector<HTMLElement>('[data-message-id="a1"]')!;
|
|
721
|
+
answer.dispatchEvent(new Event("pointerdown", { bubbles: true }));
|
|
722
|
+
const range = document.createRange();
|
|
723
|
+
range.selectNodeContents(answer);
|
|
724
|
+
document.getSelection()!.removeAllRanges();
|
|
725
|
+
document.getSelection()!.addRange(range);
|
|
726
|
+
metrics.setScrollTop(360);
|
|
727
|
+
sc.dispatchEvent(new Event("scroll"));
|
|
728
|
+
|
|
729
|
+
for (let i = 0; i < 3; i += 1) emitToolMessage(controller, `t${i}`);
|
|
730
|
+
emitStreamingAssistant(controller, "a2", "It arrives tomorrow.");
|
|
731
|
+
setBubbleOffsetTop(mount, "a2", 900);
|
|
732
|
+
metrics.setScrollHeight(1600);
|
|
733
|
+
emitStreamingAssistant(controller, "a2", "It arrives tomorrow morning.");
|
|
734
|
+
raf.flush();
|
|
735
|
+
|
|
736
|
+
expect(metrics.getScrollTop()).toBe(360);
|
|
737
|
+
expect(document.getSelection()!.toString()).toContain("parcel left the depot");
|
|
738
|
+
controller.destroy();
|
|
739
|
+
});
|
|
740
|
+
|
|
691
741
|
it("re-arms on the next send after the reader scrolled away", () => {
|
|
692
742
|
const raf = installRafMock();
|
|
693
743
|
const mount = createMount();
|
|
@@ -701,15 +751,16 @@ describe("anchor-top first-unread positioning", () => {
|
|
|
701
751
|
sc.dispatchEvent(new Event("scroll"));
|
|
702
752
|
|
|
703
753
|
// A new send is an explicit "take me along": the new user message
|
|
704
|
-
// anchors (600 - 16 = 584) and
|
|
705
|
-
//
|
|
754
|
+
// anchors (600 - 16 = 584) and holds while the reply streams beneath,
|
|
755
|
+
// including once the reply grows past the fold.
|
|
706
756
|
emitUserMessage(controller, "u2", "And the second order?");
|
|
707
757
|
setBubbleOffsetTop(mount, "u2", 600);
|
|
708
758
|
metrics.setScrollHeight(1400);
|
|
709
759
|
raf.flush();
|
|
760
|
+
expect(metrics.getScrollTop()).toBe(584);
|
|
710
761
|
|
|
711
762
|
emitStreamingAssistant(controller, "a2", "That one shipped Tuesday.");
|
|
712
|
-
setBubbleOffsetTop(mount, "a2",
|
|
763
|
+
setBubbleOffsetTop(mount, "a2", 1100);
|
|
713
764
|
metrics.setScrollHeight(1600);
|
|
714
765
|
emitStreamingAssistant(controller, "a2", "That one shipped Tuesday!");
|
|
715
766
|
raf.flush();
|
|
@@ -717,33 +768,78 @@ describe("anchor-top first-unread positioning", () => {
|
|
|
717
768
|
expect(metrics.getScrollTop()).toBe(584);
|
|
718
769
|
controller.destroy();
|
|
719
770
|
});
|
|
771
|
+
|
|
772
|
+
it("releases the anchor for good when the reader jumps to the latest", () => {
|
|
773
|
+
const raf = installRafMock();
|
|
774
|
+
const mount = createMount();
|
|
775
|
+
const controller = createAgentExperience(mount, anchorTopConfig());
|
|
776
|
+
const sc = getScrollContainer(mount);
|
|
777
|
+
const metrics = installScrollMetrics(sc, { scrollHeight: 500, clientHeight: 400 });
|
|
778
|
+
|
|
779
|
+
// Short history: the anchor needs a spacer to rest the question at the
|
|
780
|
+
// top (284 + 400 - 500 + 24 slack = 208).
|
|
781
|
+
emitAssistantMessage(controller, "seed", "Earlier reply");
|
|
782
|
+
emitUserMessage(controller, "u1", "Where is my order?");
|
|
783
|
+
setBubbleOffsetTop(mount, "u1", 300);
|
|
784
|
+
raf.step(1);
|
|
785
|
+
expect(getSpacer(mount).style.height).toBe("208px");
|
|
786
|
+
// The mocked metrics don't see the spacer: apply it as a browser would.
|
|
787
|
+
metrics.setScrollHeight(708);
|
|
788
|
+
raf.flush();
|
|
789
|
+
expect(metrics.getScrollTop()).toBe(284);
|
|
790
|
+
|
|
791
|
+
for (let i = 0; i < 4; i += 1) emitToolMessage(controller, `t${i}`);
|
|
792
|
+
emitStreamingAssistant(controller, "a1", "Answer below the tools.");
|
|
793
|
+
setBubbleOffsetTop(mount, "a1", 760);
|
|
794
|
+
metrics.setScrollHeight(1400);
|
|
795
|
+
emitStreamingAssistant(controller, "a1", "Answer below the tools, longer.");
|
|
796
|
+
raf.flush();
|
|
797
|
+
expect(metrics.getScrollTop()).toBe(284);
|
|
798
|
+
|
|
799
|
+
const jump = getScrollToBottomButton(mount);
|
|
800
|
+
expect(jump.style.display).toBe("");
|
|
801
|
+
jump.click();
|
|
802
|
+
raf.flush();
|
|
803
|
+
expect(getSpacer(mount).style.display).toBe("none");
|
|
804
|
+
expect(metrics.getScrollTop()).toBe(metrics.getBottom());
|
|
805
|
+
const releasedAt = metrics.getScrollTop();
|
|
806
|
+
|
|
807
|
+
// More blocks, tool updates, and completion never reinstate the anchor.
|
|
808
|
+
emitToolMessage(controller, "t-late");
|
|
809
|
+
emitStreamingAssistant(controller, "a2", "One more thing.");
|
|
810
|
+
setBubbleOffsetTop(mount, "a2", 1300);
|
|
811
|
+
metrics.setScrollHeight(1600);
|
|
812
|
+
emitAssistantMessage(controller, "a2", "One more thing, done.");
|
|
813
|
+
raf.flush();
|
|
814
|
+
expect(getSpacer(mount).style.display).toBe("none");
|
|
815
|
+
expect(metrics.getScrollTop()).toBe(releasedAt);
|
|
816
|
+
controller.destroy();
|
|
817
|
+
});
|
|
720
818
|
});
|
|
721
819
|
|
|
722
|
-
//
|
|
723
|
-
//
|
|
724
|
-
|
|
820
|
+
// Voice: the recognized user transcript grows in place under one message id
|
|
821
|
+
// (interim → final). It is one send: it anchors once and its updates never
|
|
822
|
+
// re-anchor or chase the reply.
|
|
823
|
+
describe("anchor-top voice transcript updates", () => {
|
|
725
824
|
beforeEach(() => {
|
|
726
825
|
installRafMock();
|
|
727
826
|
});
|
|
728
827
|
|
|
729
|
-
const
|
|
730
|
-
baseConfig({
|
|
731
|
-
features: { scrollBehavior: { mode: "anchor-top", anchorTopOffset: 16 } },
|
|
732
|
-
});
|
|
733
|
-
|
|
734
|
-
const emitTool = (
|
|
828
|
+
const emitVoiceUser = (
|
|
735
829
|
controller: ReturnType<typeof createAgentExperience>,
|
|
736
|
-
|
|
830
|
+
content: string,
|
|
831
|
+
isFinal: boolean
|
|
737
832
|
) => {
|
|
738
833
|
controller.injectTestMessage({
|
|
739
834
|
type: "message",
|
|
740
835
|
message: {
|
|
741
|
-
id,
|
|
742
|
-
role: "
|
|
743
|
-
content
|
|
836
|
+
id: "voice-u1",
|
|
837
|
+
role: "user",
|
|
838
|
+
content,
|
|
744
839
|
createdAt: CREATED_AT,
|
|
745
|
-
|
|
746
|
-
|
|
840
|
+
streaming: false,
|
|
841
|
+
viaVoice: true,
|
|
842
|
+
voiceProcessing: !isFinal,
|
|
747
843
|
},
|
|
748
844
|
});
|
|
749
845
|
};
|
|
@@ -751,86 +847,91 @@ describe("anchor-top first-unread — edge shapes", () => {
|
|
|
751
847
|
const setTop = (mount: HTMLElement, id: string, value: number) => {
|
|
752
848
|
const bubble = getScrollContainer(mount).querySelector<HTMLElement>(
|
|
753
849
|
`[data-message-id="${id}"]`
|
|
850
|
+
)!;
|
|
851
|
+
Object.defineProperty(bubble, "offsetTop", { configurable: true, value });
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
const start = (mount: HTMLElement) => {
|
|
855
|
+
const controller = createAgentExperience(
|
|
856
|
+
mount,
|
|
857
|
+
baseConfig({ features: { scrollBehavior: { mode: "anchor-top", anchorTopOffset: 16 } } })
|
|
754
858
|
);
|
|
755
|
-
|
|
756
|
-
|
|
859
|
+
emitAssistantMessage(controller, "seed", "Earlier reply");
|
|
860
|
+
return controller;
|
|
757
861
|
};
|
|
758
862
|
|
|
759
|
-
|
|
760
|
-
// first unread block, so when a long question strands it the anchor lands
|
|
761
|
-
// there — and the tool run still pushes the real answer toward the fold.
|
|
762
|
-
// That is the policy behaving as specified — chronological, honest about
|
|
763
|
-
// what arrived first — but it is the case most likely to want revisiting.
|
|
764
|
-
// (A preamble starting within view never moves the anchor at all: the user
|
|
765
|
-
// message keeps it.)
|
|
766
|
-
it("anchors a stranded preamble, not the answer that follows the tool run", () => {
|
|
863
|
+
it("keeps a growing transcript anchored from interim through final and the reply", () => {
|
|
767
864
|
const raf = installRafMock();
|
|
768
865
|
const mount = createMount();
|
|
769
|
-
const controller =
|
|
770
|
-
const
|
|
771
|
-
|
|
772
|
-
clientHeight: 400,
|
|
773
|
-
});
|
|
866
|
+
const controller = start(mount);
|
|
867
|
+
const sc = getScrollContainer(mount);
|
|
868
|
+
const metrics = installScrollMetrics(sc, { scrollHeight: 1000, clientHeight: 400 });
|
|
774
869
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
setTop(mount, "u1", 300);
|
|
870
|
+
emitVoiceUser(controller, "Book a", false);
|
|
871
|
+
setTop(mount, "voice-u1", 300);
|
|
778
872
|
raf.flush();
|
|
873
|
+
expect(metrics.getScrollTop()).toBe(284);
|
|
779
874
|
|
|
780
|
-
//
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
875
|
+
// Interim text grows the same bubble past the viewport.
|
|
876
|
+
const words = "table for four near the station on Friday evening ".repeat(20);
|
|
877
|
+
emitVoiceUser(controller, `Book a ${words}`, false);
|
|
878
|
+
metrics.setScrollHeight(1600);
|
|
784
879
|
raf.flush();
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
for (let i = 0; i < 6; i += 1) emitTool(controller, `t${i}`);
|
|
788
|
-
emitStreamingAssistant(controller, "answer", "Good news — it is on track.");
|
|
789
|
-
setTop(mount, "answer", 1300);
|
|
790
|
-
metrics.setScrollHeight(1800);
|
|
880
|
+
emitVoiceUser(controller, `Book a ${words}please.`, true);
|
|
791
881
|
raf.flush();
|
|
882
|
+
expect(metrics.getScrollTop()).toBe(284);
|
|
792
883
|
|
|
793
|
-
//
|
|
794
|
-
|
|
884
|
+
// The reply lands entirely below the long transcript.
|
|
885
|
+
emitStreamingAssistant(controller, "voice-a1", "");
|
|
886
|
+
emitStreamingAssistant(controller, "voice-a1", "Done — four seats at 7pm.");
|
|
887
|
+
setTop(mount, "voice-a1", 1500);
|
|
888
|
+
metrics.setScrollHeight(1900);
|
|
889
|
+
emitAssistantMessage(controller, "voice-a1", "Done — four seats at 7pm on Friday.");
|
|
890
|
+
raf.flush();
|
|
891
|
+
expect(metrics.getScrollTop()).toBe(284);
|
|
795
892
|
controller.destroy();
|
|
796
893
|
});
|
|
797
894
|
|
|
798
|
-
it("
|
|
895
|
+
it("does not pull a reader back when the transcript updates after they scroll", () => {
|
|
799
896
|
const raf = installRafMock();
|
|
800
897
|
const mount = createMount();
|
|
801
|
-
const controller =
|
|
802
|
-
const
|
|
803
|
-
|
|
804
|
-
clientHeight: 400,
|
|
805
|
-
});
|
|
898
|
+
const controller = start(mount);
|
|
899
|
+
const sc = getScrollContainer(mount);
|
|
900
|
+
const metrics = installScrollMetrics(sc, { scrollHeight: 1000, clientHeight: 400 });
|
|
806
901
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
setTop(mount, "u1", 300);
|
|
902
|
+
emitVoiceUser(controller, "What's the", false);
|
|
903
|
+
setTop(mount, "voice-u1", 300);
|
|
810
904
|
raf.flush();
|
|
811
905
|
expect(metrics.getScrollTop()).toBe(284);
|
|
812
906
|
|
|
813
|
-
|
|
814
|
-
metrics.
|
|
907
|
+
// The reader scrolls up to re-read the earlier reply mid-utterance.
|
|
908
|
+
metrics.setScrollTop(40);
|
|
909
|
+
sc.dispatchEvent(new WheelEvent("wheel", { deltaY: -120, bubbles: true }));
|
|
910
|
+
sc.dispatchEvent(new Event("scroll"));
|
|
911
|
+
|
|
912
|
+
emitVoiceUser(controller, "What's the weather", false);
|
|
913
|
+
raf.flush();
|
|
914
|
+
emitVoiceUser(controller, "What's the weather tomorrow?", true);
|
|
915
|
+
emitStreamingAssistant(controller, "voice-a1", "Sunny, 21°C.");
|
|
916
|
+
metrics.setScrollHeight(1200);
|
|
815
917
|
raf.flush();
|
|
816
918
|
|
|
817
|
-
|
|
818
|
-
expect(metrics.getScrollTop()).toBe(284);
|
|
919
|
+
expect(metrics.getScrollTop()).toBe(40);
|
|
819
920
|
controller.destroy();
|
|
820
921
|
});
|
|
821
922
|
});
|
|
822
923
|
|
|
823
924
|
// Regression: streaming content under a pinned anchor makes the browser clamp
|
|
824
|
-
// scrollTop and then restore it
|
|
825
|
-
// user involved (observed live as -12 then +12 during a tool run).
|
|
826
|
-
//
|
|
827
|
-
//
|
|
828
|
-
describe("anchor-top
|
|
925
|
+
// scrollTop and then restore it: a matched pair of real scroll events with no
|
|
926
|
+
// user involved (observed live as -12 then +12 during a tool run). Those must
|
|
927
|
+
// not read as the reader taking over, or a later layout re-pin (host move)
|
|
928
|
+
// would stand down.
|
|
929
|
+
describe("anchor-top — layout scroll is not reader intent", () => {
|
|
829
930
|
beforeEach(() => {
|
|
830
931
|
installRafMock();
|
|
831
932
|
});
|
|
832
933
|
|
|
833
|
-
it("
|
|
934
|
+
it("keeps the anchor and its spacer through a clamp/restore scroll pair", () => {
|
|
834
935
|
const raf = installRafMock();
|
|
835
936
|
const mount = createMount();
|
|
836
937
|
const controller = createAgentExperience(
|
|
@@ -840,17 +941,22 @@ describe("anchor-top first-unread — layout scroll is not reader intent", () =>
|
|
|
840
941
|
})
|
|
841
942
|
);
|
|
842
943
|
const sc = getScrollContainer(mount);
|
|
843
|
-
const metrics = installScrollMetrics(sc, { scrollHeight:
|
|
944
|
+
const metrics = installScrollMetrics(sc, { scrollHeight: 600, clientHeight: 400 });
|
|
844
945
|
|
|
845
946
|
emitAssistantMessage(controller, "seed", "Earlier reply");
|
|
846
947
|
emitUserMessage(controller, "u1", "Where is my order?");
|
|
847
948
|
const userBubble = sc.querySelector<HTMLElement>('[data-message-id="u1"]');
|
|
848
949
|
Object.defineProperty(userBubble!, "offsetTop", { configurable: true, value: 300 });
|
|
950
|
+
raf.step(1);
|
|
951
|
+
const spacer = sc.querySelector<HTMLElement>("[data-persona-anchor-spacer]")!;
|
|
952
|
+
// 284 + 400 - 600 + 24 slack.
|
|
953
|
+
expect(spacer.style.height).toBe("108px");
|
|
954
|
+
metrics.setScrollHeight(708);
|
|
849
955
|
raf.flush();
|
|
850
956
|
expect(metrics.getScrollTop()).toBe(284);
|
|
851
957
|
|
|
852
|
-
// Tool run:
|
|
853
|
-
//
|
|
958
|
+
// Tool run: scrollTop is clamped down, then the growing transcript
|
|
959
|
+
// restores it. Both are layout, not the reader.
|
|
854
960
|
for (let i = 0; i < 6; i += 1) {
|
|
855
961
|
controller.injectTestMessage({
|
|
856
962
|
type: "message",
|
|
@@ -866,21 +972,17 @@ describe("anchor-top first-unread — layout scroll is not reader intent", () =>
|
|
|
866
972
|
}
|
|
867
973
|
metrics.setScrollTop(272);
|
|
868
974
|
sc.dispatchEvent(new Event("scroll"));
|
|
869
|
-
metrics.setScrollHeight(1500);
|
|
870
975
|
metrics.setScrollTop(284);
|
|
871
976
|
sc.dispatchEvent(new Event("scroll"));
|
|
872
|
-
raf.flush();
|
|
873
977
|
|
|
874
|
-
// Stranded answer (delta 400): the clamp/restore pair must not have been
|
|
875
|
-
// read as reader intent, so the handoff still fires. One more render so
|
|
876
|
-
// the strandedness probe sees the mocked offset.
|
|
877
978
|
emitStreamingAssistant(controller, "a1", "Good news — your order is on track.");
|
|
878
979
|
const answer = sc.querySelector<HTMLElement>('[data-message-id="a1"]');
|
|
879
980
|
Object.defineProperty(answer!, "offsetTop", { configurable: true, value: 700 });
|
|
880
981
|
emitStreamingAssistant(controller, "a1", "Good news — your order is on track!");
|
|
881
982
|
raf.flush();
|
|
882
983
|
|
|
883
|
-
expect(metrics.getScrollTop()).toBe(
|
|
984
|
+
expect(metrics.getScrollTop()).toBe(284);
|
|
985
|
+
expect(spacer.style.display).toBe("");
|
|
884
986
|
controller.destroy();
|
|
885
987
|
});
|
|
886
988
|
});
|