@runtypelabs/persona 4.31.0 → 4.32.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.cjs +2 -2
- package/dist/activity-ui.d.cts +11 -3
- package/dist/activity-ui.d.ts +11 -3
- package/dist/activity-ui.js +1 -1
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/approval-ui.d.cts +2 -2
- package/dist/approval-ui.d.ts +2 -2
- package/dist/artifacts-ui.d.cts +3 -3
- package/dist/artifacts-ui.d.ts +3 -3
- package/dist/background-runtime.d.cts +1 -1
- package/dist/background-runtime.d.ts +1 -1
- package/dist/chunk-CkE90FtA.js +2 -0
- package/dist/{chunk-Co4LmTFv.js → chunk-CzZBmTLd.js} +1 -1
- package/dist/chunk-DDAwu89-.js +1 -0
- package/dist/{chunk-WA9UucGf.js → chunk-DZg9bFK6.js} +1 -1
- package/dist/{chunk-BBtVdcKU.js → chunk-FEzOcZof.js} +1 -1
- package/dist/client-dispatch.js +1 -1
- package/dist/client-history.js +1 -1
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/composer-actions-runtime.d.cts +2 -2
- package/dist/composer-actions-runtime.d.ts +2 -2
- package/dist/{composer-input-C2_WizXV.d.cts → composer-input-dwg_o9Z-.d.cts} +1 -1
- package/dist/{composer-input-C2_WizXV.d.ts → composer-input-dwg_o9Z-.d.ts} +1 -1
- package/dist/{composer-store-BV6kTlKv.d.cts → composer-store-Dw7Zbqxr.d.cts} +1 -1
- package/dist/{composer-store-BV6kTlKv.d.ts → composer-store-Dw7Zbqxr.d.ts} +1 -1
- package/dist/{context-mention-manager-BfZzcydV.d.cts → context-mention-manager-D2p0-JH-.d.cts} +2 -2
- package/dist/{context-mention-manager-BfZzcydV.d.ts → context-mention-manager-D2p0-JH-.d.ts} +2 -2
- package/dist/context-mentions-inline.d.cts +2 -2
- package/dist/context-mentions-inline.d.ts +2 -2
- package/dist/context-mentions.d.cts +3 -3
- package/dist/context-mentions.d.ts +3 -3
- package/dist/{defaults-DiTeXyr3.d.cts → defaults-CjX0kKzb.d.cts} +1 -1
- package/dist/{defaults-DiTeXyr3.d.ts → defaults-CjX0kKzb.d.ts} +1 -1
- package/dist/dictation-runtime.d.cts +2 -2
- package/dist/dictation-runtime.d.ts +2 -2
- package/dist/event-stream-view.d.cts +1 -1
- package/dist/event-stream-view.d.ts +1 -1
- package/dist/{fallback-speech-engine-DZ6Of7VU.d.cts → fallback-speech-engine-DmF7MczI.d.cts} +1 -1
- package/dist/{fallback-speech-engine-DZ6Of7VU.d.ts → fallback-speech-engine-DmF7MczI.d.ts} +1 -1
- package/dist/{feature-preferences-BT549D_E.d.cts → feature-preferences-CVFDt9Xf.d.cts} +1 -1
- package/dist/{feature-preferences-BT549D_E.d.ts → feature-preferences-CVFDt9Xf.d.ts} +1 -1
- package/dist/forms-ui.d.cts +2 -2
- package/dist/forms-ui.d.ts +2 -2
- package/dist/{history-confirm-dialog-Cm4oK4am.d.cts → history-confirm-dialog-DJEXsTyE.d.cts} +1 -1
- package/dist/{history-confirm-dialog-Cm4oK4am.d.ts → history-confirm-dialog-DJEXsTyE.d.ts} +1 -1
- package/dist/history-shell.d.cts +6 -6
- package/dist/history-shell.d.ts +6 -6
- package/dist/history-view.d.cts +1 -1
- package/dist/history-view.d.ts +1 -1
- package/dist/index.cjs +10 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -9
- package/dist/index.d.ts +9 -9
- package/dist/index.global.js +1 -1
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +23 -22
- package/dist/index.js.map +1 -1
- package/dist/{loading-label-runtime-loader-DmiPDkBu.d.cts → loading-label-runtime-loader-CPxnoaoi.d.cts} +1 -1
- package/dist/{loading-label-runtime-loader-DmiPDkBu.d.ts → loading-label-runtime-loader-CPxnoaoi.d.ts} +1 -1
- package/dist/loading-label-runtime.d.cts +1 -1
- package/dist/loading-label-runtime.d.ts +1 -1
- package/dist/{message-bubble-D0rosKuK.d.cts → message-bubble-CkZGlCLC.d.cts} +1 -1
- package/dist/{message-bubble-D0rosKuK.d.ts → message-bubble-CkZGlCLC.d.ts} +1 -1
- package/dist/preferences-runtime.d.cts +1 -1
- package/dist/preferences-runtime.d.ts +1 -1
- package/dist/{preview-DRU27q-X.d.cts → preview-DdUvoLb4.d.cts} +2 -2
- package/dist/{preview-DRU27q-X.d.ts → preview-DdUvoLb4.d.ts} +2 -2
- package/dist/{registry-DRk3VMFc.d.cts → registry-Dz4iHNST.d.cts} +1 -1
- package/dist/{registry-DRk3VMFc.d.ts → registry-Dz4iHNST.d.ts} +1 -1
- package/dist/{session-Dj4bOm5y.d.cts → session-Baxp9iRK.d.cts} +14 -8
- package/dist/{session-Dj4bOm5y.d.ts → session-Baxp9iRK.d.ts} +14 -8
- package/dist/session-reconnect.d.cts +2 -2
- package/dist/session-reconnect.d.ts +2 -2
- package/dist/smart-dom-reader.d.cts +1 -1
- package/dist/smart-dom-reader.d.ts +1 -1
- package/dist/theme-editor-preview.cjs +46 -45
- package/dist/theme-editor-preview.d.cts +1 -1
- package/dist/theme-editor-preview.d.ts +1 -1
- package/dist/theme-editor-preview.js +19 -18
- package/dist/theme-editor.d.cts +2 -2
- package/dist/theme-editor.d.ts +2 -2
- package/dist/theme-reference.d.cts +2 -2
- package/dist/theme-reference.d.ts +2 -2
- package/dist/{tool-loading-animation-D1MS5Ace.d.cts → tool-loading-animation-un3kA5Ur.d.cts} +1 -1
- package/dist/{tool-loading-animation-D1MS5Ace.d.ts → tool-loading-animation-un3kA5Ur.d.ts} +1 -1
- package/dist/{turn-activity-ui-CorwdlI0.d.cts → turn-activity-ui-BGNhtMcH.d.cts} +2 -2
- package/dist/{turn-activity-ui-CorwdlI0.d.ts → turn-activity-ui-BGNhtMcH.d.ts} +2 -2
- package/dist/turn-activity-ui.d.cts +1 -1
- package/dist/turn-activity-ui.d.ts +1 -1
- package/dist/{types-CSXHl5cN.d.cts → types-BOZHQIk0.d.cts} +8 -1
- package/dist/{types-CSXHl5cN.d.ts → types-BOZHQIk0.d.ts} +8 -1
- package/dist/voice-client-tools.d.cts +1 -1
- package/dist/voice-client-tools.d.ts +1 -1
- package/dist/voice-runtime.cjs +1 -1
- package/dist/voice-runtime.d.cts +2 -2
- package/dist/voice-runtime.d.ts +2 -2
- package/dist/voice-runtime.js +1 -1
- package/dist/voice-worklet-player.d.cts +2 -2
- package/dist/voice-worklet-player.d.ts +2 -2
- package/dist/{webmcp-runtime-entry-DpDN6k-6.d.cts → webmcp-runtime-entry-DuAG1_yF.d.cts} +1 -1
- package/dist/{webmcp-runtime-entry-DpDN6k-6.d.ts → webmcp-runtime-entry-DuAG1_yF.d.ts} +1 -1
- 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 +1 -1
- package/src/activity-ui.ts +23 -0
- package/src/components/disclosure-animation.ts +5 -0
- package/src/components/tool-bubble.ts +1 -0
- package/src/index-core.ts +1 -0
- package/src/styles/widget.css +6 -0
- package/src/types.ts +8 -0
- package/src/ui.footer-visibility.test.ts +54 -0
- package/src/ui.reveal-tool-call.test.ts +258 -0
- package/src/ui.ts +127 -5
- package/dist/chunk-B3OrH9Ng.js +0 -2
- package/dist/chunk-CjhA_5Jk.js +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runtypelabs/persona",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.32.0",
|
|
4
4
|
"description": "Themeable, pluggable streaming agent widget for websites, in plain JS with support for voice input and reasoning / tool output.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
package/src/activity-ui.ts
CHANGED
|
@@ -8,6 +8,29 @@ import { updateToolBubbleUI } from "./components/tool-bubble";
|
|
|
8
8
|
import { updateReasoningBubbleUI } from "./components/reasoning-bubble";
|
|
9
9
|
import type { AgentWidgetConfig, AgentWidgetMessage } from "./types";
|
|
10
10
|
import { createActivityOverflow } from "./utils/activity-overflow";
|
|
11
|
+
import { afterDisclosureAnimation } from "./components/disclosure-animation";
|
|
12
|
+
|
|
13
|
+
type DisclosureAnchor = { spacerHeight: number; initialSpacerHeight: number; contentHeightAtAnchor: number; pending: number };
|
|
14
|
+
|
|
15
|
+
/** Keep scroll reserve throughout manual disclosure animations and reversals. */
|
|
16
|
+
export function holdScroll(
|
|
17
|
+
bubble: HTMLElement, body: HTMLElement, anchor: DisclosureAnchor,
|
|
18
|
+
measure: (spacer: number) => number, setSpacer: (height: number) => void,
|
|
19
|
+
slack: number, isCurrent: () => boolean
|
|
20
|
+
): () => void {
|
|
21
|
+
// Reserve the whole possible collapse before changing display or starting an
|
|
22
|
+
// animation. ResizeObserver must not drain it during intermediate frames.
|
|
23
|
+
setSpacer(anchor.spacerHeight + bubble.offsetHeight);
|
|
24
|
+
anchor.pending++;
|
|
25
|
+
return () => afterDisclosureAnimation(bubble, () => {
|
|
26
|
+
if (--anchor.pending || !isCurrent()) return;
|
|
27
|
+
// Respect scrolling that the reader did while the animation was running.
|
|
28
|
+
setSpacer(body.scrollTop + body.clientHeight + slack - measure(anchor.spacerHeight));
|
|
29
|
+
anchor.initialSpacerHeight = anchor.spacerHeight;
|
|
30
|
+
// Match the resize drain, including the flex gap before the spacer.
|
|
31
|
+
anchor.contentHeightAtAnchor = body.scrollHeight - anchor.spacerHeight;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
11
34
|
|
|
12
35
|
/** Keep automatic disclosure updates in the optional activity chunk. */
|
|
13
36
|
export function createActivityLifecycle(
|
|
@@ -7,6 +7,11 @@ type Settings = { animation: "instant" | "height"; duration: number; easing: str
|
|
|
7
7
|
type Running = { effect: Animation; expanded: boolean; settings: string; finish: () => void };
|
|
8
8
|
const running = new WeakMap<HTMLElement, Running>();
|
|
9
9
|
|
|
10
|
+
/** Observe this disclosure's height effect without waiting for looping children. */
|
|
11
|
+
export function afterDisclosureAnimation(bubble: HTMLElement, finish: () => void): void {
|
|
12
|
+
running.get(bubble)?.effect.finished?.then(finish, finish) ?? finish();
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
export function disclosureSettings(config?: AgentWidgetConfig): Settings {
|
|
11
16
|
const options = config?.expansion;
|
|
12
17
|
return {
|
|
@@ -131,6 +131,7 @@ export const createToolBubble = (
|
|
|
131
131
|
if (!tool) {
|
|
132
132
|
return applyActivityRow(bubble, message, config, "tool");
|
|
133
133
|
}
|
|
134
|
+
if (tool.id) bubble.dataset.toolCallId = tool.id;
|
|
134
135
|
|
|
135
136
|
const toolDisplayConfig = config.features!.toolCallDisplay!;
|
|
136
137
|
const expandable = toolDisplayConfig.expandable ?? DEFAULT_TOOL_CALL_DISPLAY.expandable;
|
package/src/index-core.ts
CHANGED
|
@@ -226,6 +226,7 @@ export type {
|
|
|
226
226
|
// Visitor conversation history (client-token mode) types
|
|
227
227
|
AgentWidgetHistoryFeature,
|
|
228
228
|
AgentWidgetOpenConversationOptions,
|
|
229
|
+
AgentWidgetRevealToolCallOptions,
|
|
229
230
|
BackgroundConversationAdapter,
|
|
230
231
|
ConversationExecutionState,
|
|
231
232
|
AgentWidgetHistoryListAction,
|
package/src/styles/widget.css
CHANGED
|
@@ -6773,6 +6773,12 @@
|
|
|
6773
6773
|
outline-offset: 2px;
|
|
6774
6774
|
}
|
|
6775
6775
|
|
|
6776
|
+
/* revealToolCall() marks the revealed tool briefly. */
|
|
6777
|
+
.persona-tool-bubble[data-persona-revealed] {
|
|
6778
|
+
outline: 2px solid var(--persona-focus-ring, var(--persona-primary, #171717));
|
|
6779
|
+
outline-offset: 2px;
|
|
6780
|
+
}
|
|
6781
|
+
|
|
6776
6782
|
/* Compact approval disclosures share the tool detail surfaces. */
|
|
6777
6783
|
.persona-approval-row .persona-approval-details {
|
|
6778
6784
|
display: grid;
|
package/src/types.ts
CHANGED
|
@@ -3568,6 +3568,14 @@ export type AgentWidgetOpenConversationOptions = {
|
|
|
3568
3568
|
busy?: "reject" | "stop" | "background";
|
|
3569
3569
|
};
|
|
3570
3570
|
|
|
3571
|
+
/** Options for `controller.revealToolCall()`. */
|
|
3572
|
+
export type AgentWidgetRevealToolCallOptions = {
|
|
3573
|
+
/** Expand the tool's details. Default true. */
|
|
3574
|
+
expand?: boolean;
|
|
3575
|
+
/** How long the bubble keeps `data-persona-revealed`, in ms; 0 skips the mark. Default 2000. */
|
|
3576
|
+
highlightMs?: number;
|
|
3577
|
+
};
|
|
3578
|
+
|
|
3571
3579
|
/** The single in-flight history operation, or `null` when idle. */
|
|
3572
3580
|
export type HistoryPendingAction =
|
|
3573
3581
|
| { kind: "refresh" | "load-more" | "start-new" | "clear" | "reset" }
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { initAgentWidget } from "./runtime/init";
|
|
5
|
+
import { createAgentExperience, type AgentWidgetController } from "./ui";
|
|
6
|
+
|
|
7
|
+
let mount: HTMLElement;
|
|
8
|
+
let controller: { destroy: () => void } | undefined;
|
|
9
|
+
|
|
10
|
+
const footer = () => mount.querySelector<HTMLElement>(".persona-widget-footer");
|
|
11
|
+
const settle = async () => {
|
|
12
|
+
await vi.dynamicImportSettled();
|
|
13
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
window.localStorage.clear();
|
|
18
|
+
window.scrollTo = vi.fn();
|
|
19
|
+
mount = document.createElement("div");
|
|
20
|
+
mount.id = "chat";
|
|
21
|
+
document.body.appendChild(mount);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
controller?.destroy();
|
|
26
|
+
controller = undefined;
|
|
27
|
+
document.body.replaceChildren();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe.each([false, true])("layout.showFooter: false (V5 defaults: %s)", (v5Defaults) => {
|
|
31
|
+
const config = {
|
|
32
|
+
apiUrl: "/test",
|
|
33
|
+
persistState: false,
|
|
34
|
+
future: { v5Defaults },
|
|
35
|
+
launcher: { enabled: false },
|
|
36
|
+
layout: { embedded: true, showFooter: false },
|
|
37
|
+
initialMessages: [{ id: "m1", role: "user" as const, content: "Hi", createdAt: "2026-01-01T00:00:00Z" }],
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
it("keeps the footer hidden after the widget is ready", async () => {
|
|
41
|
+
controller = initAgentWidget({ target: "#chat", useShadowDom: false, config });
|
|
42
|
+
await settle();
|
|
43
|
+
expect(footer()?.style.display).toBe("none");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("keeps the footer hidden when an update rebuilds the composer", async () => {
|
|
47
|
+
const experience: AgentWidgetController = createAgentExperience(mount, config);
|
|
48
|
+
controller = experience;
|
|
49
|
+
await settle();
|
|
50
|
+
experience.update({ contextMentions: { enabled: false } });
|
|
51
|
+
await settle();
|
|
52
|
+
expect(footer()?.style.display).toBe("none");
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { createAgentExperience, type AgentWidgetController } from "./ui";
|
|
5
|
+
|
|
6
|
+
let controller: AgentWidgetController;
|
|
7
|
+
let mount: HTMLElement;
|
|
8
|
+
|
|
9
|
+
const injectTool = (id: string, sequence: number, status: "running" | "complete" = "complete", chunks = ["Tool details"]) =>
|
|
10
|
+
controller.injectTestMessage({
|
|
11
|
+
type: "message",
|
|
12
|
+
message: {
|
|
13
|
+
id: `msg-${id}`,
|
|
14
|
+
role: "assistant",
|
|
15
|
+
content: "",
|
|
16
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
17
|
+
sequence,
|
|
18
|
+
streaming: status === "running",
|
|
19
|
+
variant: "tool",
|
|
20
|
+
toolCall: { id, name: "Search", status, chunks },
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const setup = (grouped: boolean, v5Defaults = true) => {
|
|
25
|
+
controller = createAgentExperience(mount, {
|
|
26
|
+
apiUrl: "/test",
|
|
27
|
+
persistState: false,
|
|
28
|
+
future: { v5Defaults },
|
|
29
|
+
launcher: { enabled: false },
|
|
30
|
+
features: {
|
|
31
|
+
toolCallDisplay: { grouped, groupedMode: "collapsible", autoExpand: false, autoCollapseDelay: false },
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
injectTool("call-1", 1);
|
|
35
|
+
injectTool("call-2", 2);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const toolBubble = (toolCallId: string) =>
|
|
39
|
+
mount.querySelector<HTMLElement>(`.persona-tool-bubble[data-tool-call-id="${toolCallId}"]`);
|
|
40
|
+
const ownHeader = (bubble: HTMLElement) =>
|
|
41
|
+
Array.from(bubble.querySelectorAll<HTMLElement>('button[data-expand-header="true"]'))
|
|
42
|
+
.find((header) => header.closest(".persona-tool-bubble") === bubble)!;
|
|
43
|
+
|
|
44
|
+
const installScrollMetrics = () => {
|
|
45
|
+
const body = mount.querySelector<HTMLElement>(".persona-widget-body")!;
|
|
46
|
+
let height = 1000;
|
|
47
|
+
Object.defineProperties(body, {
|
|
48
|
+
clientHeight: { configurable: true, get: () => 400 },
|
|
49
|
+
scrollHeight: { configurable: true, get: () => height },
|
|
50
|
+
});
|
|
51
|
+
body.scrollTo = (options?: ScrollToOptions | number) => {
|
|
52
|
+
if (options && typeof options === "object") body.scrollTop = options.top ?? 0;
|
|
53
|
+
};
|
|
54
|
+
for (const [id, top] of [["call-1", 100], ["call-2", 900]] as const) {
|
|
55
|
+
Object.defineProperties(ownHeader(toolBubble(id)!), {
|
|
56
|
+
offsetTop: { configurable: true, get: () => top },
|
|
57
|
+
offsetParent: { configurable: true, get: () => body },
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return { body, grow: () => { height += 400; } };
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
vi.useFakeTimers();
|
|
65
|
+
window.localStorage.clear();
|
|
66
|
+
window.scrollTo = vi.fn();
|
|
67
|
+
mount = document.createElement("div");
|
|
68
|
+
document.body.appendChild(mount);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
controller?.destroy();
|
|
73
|
+
document.body.replaceChildren();
|
|
74
|
+
vi.unstubAllGlobals();
|
|
75
|
+
vi.useRealTimers();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("revealToolCall", () => {
|
|
79
|
+
it("stamps each tool bubble with its tool call id", () => {
|
|
80
|
+
setup(false);
|
|
81
|
+
expect(toolBubble("call-1")?.dataset.messageId).toBe("msg-call-1");
|
|
82
|
+
expect(toolBubble("call-2")?.dataset.messageId).toBe("msg-call-2");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("expands a standalone tool and marks it until the highlight expires", () => {
|
|
86
|
+
setup(false);
|
|
87
|
+
const bubble = toolBubble("call-2")!;
|
|
88
|
+
expect(ownHeader(bubble).getAttribute("aria-expanded")).toBe("false");
|
|
89
|
+
|
|
90
|
+
expect(controller.revealToolCall("call-2")).toBe(true);
|
|
91
|
+
expect(ownHeader(toolBubble("call-2")!).getAttribute("aria-expanded")).toBe("true");
|
|
92
|
+
expect(toolBubble("call-2")!.hasAttribute("data-persona-revealed")).toBe(true);
|
|
93
|
+
expect(ownHeader(toolBubble("call-1")!).getAttribute("aria-expanded")).toBe("false");
|
|
94
|
+
|
|
95
|
+
vi.advanceTimersByTime(2000);
|
|
96
|
+
expect(toolBubble("call-2")!.hasAttribute("data-persona-revealed")).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("opens the collapsed group that holds the tool before expanding it", () => {
|
|
100
|
+
setup(true);
|
|
101
|
+
const group = mount.querySelector<HTMLElement>('[data-message-id="tool-group-msg-call-1"]')!;
|
|
102
|
+
expect(ownHeader(group).getAttribute("aria-expanded")).toBe("false");
|
|
103
|
+
|
|
104
|
+
expect(controller.revealToolCall("call-2")).toBe(true);
|
|
105
|
+
expect(ownHeader(group).getAttribute("aria-expanded")).toBe("true");
|
|
106
|
+
expect(ownHeader(toolBubble("call-2")!).getAttribute("aria-expanded")).toBe("true");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it.each([[false, false], [false, true], [true, false], [true, true]])(
|
|
110
|
+
"keeps the highlight through streaming renders (V5: %s, grouped: %s)",
|
|
111
|
+
(v5Defaults, grouped) => {
|
|
112
|
+
setup(grouped, v5Defaults);
|
|
113
|
+
injectTool("call-2", 2, "running", ["Searching"]);
|
|
114
|
+
controller.revealToolCall("call-2");
|
|
115
|
+
|
|
116
|
+
vi.advanceTimersByTime(1000);
|
|
117
|
+
injectTool("call-2", 2, "running", ["Searching", "Found a page"]);
|
|
118
|
+
expect(toolBubble("call-2")!.textContent).toContain("Found a page");
|
|
119
|
+
expect(toolBubble("call-2")!.hasAttribute("data-persona-revealed")).toBe(true);
|
|
120
|
+
|
|
121
|
+
injectTool("call-2", 2, "complete", ["Search complete"]);
|
|
122
|
+
expect(toolBubble("call-2")!.hasAttribute("data-persona-revealed")).toBe(true);
|
|
123
|
+
vi.advanceTimersByTime(999);
|
|
124
|
+
expect(toolBubble("call-2")!.hasAttribute("data-persona-revealed")).toBe(true);
|
|
125
|
+
vi.advanceTimersByTime(1);
|
|
126
|
+
expect(toolBubble("call-2")!.hasAttribute("data-persona-revealed")).toBe(false);
|
|
127
|
+
injectTool("call-2", 2, "complete", ["Updated result"]);
|
|
128
|
+
expect(toolBubble("call-2")!.hasAttribute("data-persona-revealed")).toBe(false);
|
|
129
|
+
},
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
it("restarts the highlight duration when revealing the same tool again", () => {
|
|
133
|
+
setup(false);
|
|
134
|
+
controller.revealToolCall("call-2");
|
|
135
|
+
vi.advanceTimersByTime(1000);
|
|
136
|
+
controller.revealToolCall("call-2");
|
|
137
|
+
vi.advanceTimersByTime(1000);
|
|
138
|
+
expect(toolBubble("call-2")!.hasAttribute("data-persona-revealed")).toBe(true);
|
|
139
|
+
vi.advanceTimersByTime(1000);
|
|
140
|
+
expect(toolBubble("call-2")!.hasAttribute("data-persona-revealed")).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("keeps the highlight when regrouping the transcript", () => {
|
|
144
|
+
setup(false);
|
|
145
|
+
controller.revealToolCall("call-2");
|
|
146
|
+
controller.update({ features: { toolCallDisplay: { grouped: true } } });
|
|
147
|
+
expect(toolBubble("call-2")!.parentElement?.closest("[data-persona-tool-group]")).not.toBeNull();
|
|
148
|
+
expect(toolBubble("call-2")!.hasAttribute("data-persona-revealed")).toBe(true);
|
|
149
|
+
vi.advanceTimersByTime(2000);
|
|
150
|
+
expect(toolBubble("call-2")!.hasAttribute("data-persona-revealed")).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("does not carry a highlight into a cleared conversation with the same tool ids", () => {
|
|
154
|
+
setup(false);
|
|
155
|
+
controller.revealToolCall("call-2");
|
|
156
|
+
controller.clearChat();
|
|
157
|
+
injectTool("call-2", 2);
|
|
158
|
+
expect(toolBubble("call-2")!.hasAttribute("data-persona-revealed")).toBe(false);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it.each([false, true])("keeps following paused when centering reaches the bottom (reduced motion: %s)", (reduced) => {
|
|
162
|
+
vi.stubGlobal("matchMedia", vi.fn(() => ({ matches: reduced, addEventListener: vi.fn(), removeEventListener: vi.fn() })));
|
|
163
|
+
setup(false);
|
|
164
|
+
controller.update({ features: { scrollBehavior: { mode: "follow" } } });
|
|
165
|
+
const { body, grow } = installScrollMetrics();
|
|
166
|
+
controller.revealToolCall("call-2", { expand: false, highlightMs: 0 });
|
|
167
|
+
vi.advanceTimersByTime(500);
|
|
168
|
+
expect(body.scrollTop).toBe(600);
|
|
169
|
+
body.dispatchEvent(new Event("scroll"));
|
|
170
|
+
vi.advanceTimersByTime(200);
|
|
171
|
+
body.dispatchEvent(new Event("scroll"));
|
|
172
|
+
grow();
|
|
173
|
+
injectTool("call-2", 2, "running", ["More streamed output"]);
|
|
174
|
+
vi.advanceTimersByTime(500);
|
|
175
|
+
expect(body.scrollTop).toBe(600);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("cancels an earlier jump to latest and keeps following paused", () => {
|
|
179
|
+
setup(false);
|
|
180
|
+
controller.update({ features: {
|
|
181
|
+
scrollBehavior: { mode: "follow" },
|
|
182
|
+
scrollToBottom: { scrollBehavior: "smooth", scrollDurationMs: 800 },
|
|
183
|
+
} });
|
|
184
|
+
vi.advanceTimersByTime(1000);
|
|
185
|
+
const { body, grow } = installScrollMetrics();
|
|
186
|
+
body.scrollTop = 0;
|
|
187
|
+
body.dispatchEvent(new Event("scroll"));
|
|
188
|
+
mount.querySelector<HTMLElement>("[data-persona-scroll-to-bottom]")!.dispatchEvent(new MouseEvent("click"));
|
|
189
|
+
vi.advanceTimersByTime(80);
|
|
190
|
+
expect(body.scrollTop).toBeGreaterThan(0);
|
|
191
|
+
controller.revealToolCall("call-1", { expand: false });
|
|
192
|
+
vi.advanceTimersByTime(1000);
|
|
193
|
+
expect(body.scrollTop).toBe(0);
|
|
194
|
+
grow();
|
|
195
|
+
injectTool("call-2", 2, "running", ["More streamed output"]);
|
|
196
|
+
vi.advanceTimersByTime(500);
|
|
197
|
+
expect(body.scrollTop).toBe(0);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it.each([
|
|
201
|
+
["wheel", 80, false],
|
|
202
|
+
["wheel", 250, false],
|
|
203
|
+
["keydown", 80, false],
|
|
204
|
+
["touchmove", 80, false],
|
|
205
|
+
["pointerdown", 80, false],
|
|
206
|
+
["wheel", 80, true],
|
|
207
|
+
] as const)("yields to %s at %sms (reduced motion: %s)", (input, elapsed, reduced) => {
|
|
208
|
+
vi.stubGlobal("matchMedia", vi.fn(() => ({ matches: reduced, addEventListener: vi.fn(), removeEventListener: vi.fn() })));
|
|
209
|
+
setup(false);
|
|
210
|
+
controller.update({ features: { scrollBehavior: { mode: "follow" } } });
|
|
211
|
+
const { body } = installScrollMetrics();
|
|
212
|
+
controller.revealToolCall("call-2", { expand: false });
|
|
213
|
+
if (elapsed > 200) {
|
|
214
|
+
vi.advanceTimersByTime(180);
|
|
215
|
+
// A disclosure transition moves the target before the settle pass.
|
|
216
|
+
Object.defineProperty(ownHeader(toolBubble("call-2")!), "offsetTop", { configurable: true, get: () => 750 });
|
|
217
|
+
vi.advanceTimersByTime(elapsed - 180);
|
|
218
|
+
} else vi.advanceTimersByTime(elapsed);
|
|
219
|
+
body.dispatchEvent(input === "wheel" ? new WheelEvent("wheel", { deltaY: -100 })
|
|
220
|
+
: input === "keydown" ? new KeyboardEvent("keydown", { key: "ArrowUp" }) : new Event(input));
|
|
221
|
+
body.scrollTop = 123;
|
|
222
|
+
body.dispatchEvent(new Event("scroll"));
|
|
223
|
+
vi.advanceTimersByTime(20);
|
|
224
|
+
expect(body.scrollTop).toBe(123);
|
|
225
|
+
vi.advanceTimersByTime(480);
|
|
226
|
+
expect(body.scrollTop).toBe(123);
|
|
227
|
+
expect(toolBubble("call-2")!.hasAttribute("data-persona-revealed")).toBe(true);
|
|
228
|
+
controller.revealToolCall("call-2", { expand: false });
|
|
229
|
+
vi.advanceTimersByTime(500);
|
|
230
|
+
expect(body.scrollTop).toBe(elapsed > 200 ? 550 : 600);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("cancels the previous reveal's delayed centering", () => {
|
|
234
|
+
vi.stubGlobal("matchMedia", vi.fn(() => ({ matches: true, addEventListener: vi.fn(), removeEventListener: vi.fn() })));
|
|
235
|
+
setup(false);
|
|
236
|
+
const { body } = installScrollMetrics();
|
|
237
|
+
controller.revealToolCall("call-1", { expand: false, highlightMs: 0 });
|
|
238
|
+
vi.advanceTimersByTime(100);
|
|
239
|
+
controller.revealToolCall("call-2", { expand: false, highlightMs: 0 });
|
|
240
|
+
expect(body.scrollTop).toBe(600);
|
|
241
|
+
vi.advanceTimersByTime(150);
|
|
242
|
+
expect(body.scrollTop).toBe(600);
|
|
243
|
+
vi.advanceTimersByTime(50);
|
|
244
|
+
expect(body.scrollTop).toBe(600);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("leaves details closed and skips the mark when asked", () => {
|
|
248
|
+
setup(false);
|
|
249
|
+
expect(controller.revealToolCall("call-1", { expand: false, highlightMs: 0 })).toBe(true);
|
|
250
|
+
expect(ownHeader(toolBubble("call-1")!).getAttribute("aria-expanded")).toBe("false");
|
|
251
|
+
expect(toolBubble("call-1")!.hasAttribute("data-persona-revealed")).toBe(false);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("returns false for an unknown tool call id", () => {
|
|
255
|
+
setup(false);
|
|
256
|
+
expect(controller.revealToolCall("missing")).toBe(false);
|
|
257
|
+
});
|
|
258
|
+
});
|