@runtypelabs/persona 3.29.1 → 3.31.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/README.md +15 -2547
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-B_Qazlm4.d.cts → types-quh7NmYD.d.cts} +9 -0
- package/dist/animations/{types-B_Qazlm4.d.ts → types-quh7NmYD.d.ts} +9 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/codegen.cjs +6 -6
- package/dist/codegen.js +4 -4
- package/dist/index.cjs +50 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +164 -7
- package/dist/index.d.ts +164 -7
- package/dist/index.global.js +60 -60
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +49 -49
- package/dist/index.js.map +1 -1
- package/dist/launcher.global.js +2 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/plugin-kit.cjs +1 -0
- package/dist/plugin-kit.d.cts +98 -0
- package/dist/plugin-kit.d.ts +98 -0
- package/dist/plugin-kit.js +1 -0
- package/dist/smart-dom-reader.d.cts +157 -4
- package/dist/smart-dom-reader.d.ts +157 -4
- package/dist/theme-editor.cjs +40 -40
- package/dist/theme-editor.d.cts +161 -6
- package/dist/theme-editor.d.ts +161 -6
- package/dist/theme-editor.js +40 -40
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +2 -0
- package/dist/theme-reference.d.ts +2 -0
- package/dist/theme-reference.js +1 -1
- package/dist/widget.css +19 -0
- package/package.json +8 -2
- package/src/client.ts +6 -0
- package/src/components/approval-bubble.test.ts +320 -0
- package/src/components/approval-bubble.ts +190 -20
- package/src/components/launcher.ts +6 -3
- package/src/components/panel.ts +7 -1
- package/src/components/tool-bubble.test.ts +39 -0
- package/src/components/tool-bubble.ts +4 -0
- package/src/defaults.ts +14 -0
- package/src/generated/runtype-openapi-contract.ts +4 -0
- package/src/index-core.ts +1 -0
- package/src/plugin-kit.test.ts +230 -0
- package/src/plugin-kit.ts +294 -0
- package/src/plugins/types.ts +49 -2
- package/src/session.test.ts +161 -0
- package/src/session.ts +41 -3
- package/src/styles/widget.css +19 -0
- package/src/theme-editor/preview-utils.test.ts +10 -0
- package/src/theme-editor/preview-utils.ts +29 -1
- package/src/theme-editor/sections.test.ts +17 -0
- package/src/theme-editor/sections.ts +31 -0
- package/src/theme-reference.ts +2 -2
- package/src/types/theme.ts +2 -0
- package/src/types.ts +109 -2
- package/src/ui.approval-plugin.test.ts +204 -0
- package/src/ui.scroll.test.ts +383 -0
- package/src/ui.ts +539 -56
- package/src/utils/auto-follow.test.ts +91 -0
- package/src/utils/auto-follow.ts +68 -0
- package/src/utils/theme.test.ts +6 -2
- package/src/utils/theme.ts +0 -8
- package/src/utils/tokens.ts +6 -1
- package/src/webmcp-bridge.test.ts +66 -0
- package/src/webmcp-bridge.ts +49 -0
|
@@ -2,6 +2,86 @@ import { createElement } from "../utils/dom";
|
|
|
2
2
|
import { AgentWidgetMessage, AgentWidgetConfig } from "../types";
|
|
3
3
|
import { formatUnknownValue } from "../utils/formatting";
|
|
4
4
|
import { renderLucideIcon } from "../utils/icons";
|
|
5
|
+
import { WEBMCP_TOOL_PREFIX, getWebMcpToolDisplayTitle } from "../webmcp-bridge";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Per-message expanded/collapsed state for the technical-details section.
|
|
9
|
+
* Absent means "use the config default" (`approval.detailsDisplay`). Lives at
|
|
10
|
+
* module scope so the choice survives idiomorph re-renders, mirroring
|
|
11
|
+
* `toolExpansionState` in tool-bubble.ts.
|
|
12
|
+
*/
|
|
13
|
+
export const approvalDetailsExpansionState = new Map<string, boolean>();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Turn a wire tool name into a user-facing label: strips the `webmcp:`
|
|
17
|
+
* prefix and splits snake_case / kebab-case / camelCase into a sentence
|
|
18
|
+
* (`add_to_cart` → "Add to cart"). Falls back to the input when nothing
|
|
19
|
+
* word-like remains.
|
|
20
|
+
*/
|
|
21
|
+
export const humanizeToolName = (toolName: string): string => {
|
|
22
|
+
const bare = toolName.startsWith(WEBMCP_TOOL_PREFIX)
|
|
23
|
+
? toolName.slice(WEBMCP_TOOL_PREFIX.length)
|
|
24
|
+
: toolName;
|
|
25
|
+
const words = bare
|
|
26
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
27
|
+
.split(/[_\-\s.]+/)
|
|
28
|
+
.filter(Boolean);
|
|
29
|
+
if (words.length === 0) return toolName;
|
|
30
|
+
const sentence = words.join(" ").toLowerCase();
|
|
31
|
+
return sentence.charAt(0).toUpperCase() + sentence.slice(1);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const resolveApprovalConfig = (config?: AgentWidgetConfig) =>
|
|
35
|
+
config?.approval !== false ? config?.approval : undefined;
|
|
36
|
+
|
|
37
|
+
const isDetailsExpanded = (
|
|
38
|
+
messageId: string,
|
|
39
|
+
config?: AgentWidgetConfig
|
|
40
|
+
): boolean => {
|
|
41
|
+
const detailsMode = resolveApprovalConfig(config)?.detailsDisplay ?? "collapsed";
|
|
42
|
+
return approvalDetailsExpansionState.get(messageId) ?? detailsMode === "expanded";
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const applyDetailsToggleState = (
|
|
46
|
+
toggle: HTMLElement,
|
|
47
|
+
expanded: boolean,
|
|
48
|
+
config?: AgentWidgetConfig
|
|
49
|
+
): void => {
|
|
50
|
+
const approvalConfig = resolveApprovalConfig(config);
|
|
51
|
+
toggle.setAttribute("aria-expanded", expanded ? "true" : "false");
|
|
52
|
+
const label = toggle.querySelector("[data-approval-details-label]") as HTMLElement | null;
|
|
53
|
+
if (label) {
|
|
54
|
+
label.textContent = expanded
|
|
55
|
+
? (approvalConfig?.hideDetailsLabel ?? "Hide details")
|
|
56
|
+
: (approvalConfig?.showDetailsLabel ?? "Show details");
|
|
57
|
+
}
|
|
58
|
+
const chevronHolder = toggle.querySelector("[data-approval-details-chevron]") as HTMLElement | null;
|
|
59
|
+
if (chevronHolder) {
|
|
60
|
+
chevronHolder.innerHTML = "";
|
|
61
|
+
const chevron = renderLucideIcon(expanded ? "chevron-up" : "chevron-down", 14, "currentColor", 2);
|
|
62
|
+
if (chevron) {
|
|
63
|
+
chevronHolder.appendChild(chevron);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Sync the technical-details section (toggle label/chevron + visibility) with
|
|
70
|
+
* `approvalDetailsExpansionState`. Called from the ui.ts expansion event
|
|
71
|
+
* delegation after a toggle click.
|
|
72
|
+
*/
|
|
73
|
+
export const updateApprovalDetailsUI = (
|
|
74
|
+
messageId: string,
|
|
75
|
+
bubble: HTMLElement,
|
|
76
|
+
config?: AgentWidgetConfig
|
|
77
|
+
): void => {
|
|
78
|
+
const toggle = bubble.querySelector('button[data-bubble-type="approval"]') as HTMLElement | null;
|
|
79
|
+
const details = bubble.querySelector("[data-approval-details]") as HTMLElement | null;
|
|
80
|
+
if (!toggle || !details) return;
|
|
81
|
+
const expanded = isDetailsExpanded(messageId, config);
|
|
82
|
+
applyDetailsToggleState(toggle, expanded, config);
|
|
83
|
+
details.style.display = expanded ? "" : "none";
|
|
84
|
+
};
|
|
5
85
|
|
|
6
86
|
/**
|
|
7
87
|
* Update an existing approval bubble's UI after status changes.
|
|
@@ -96,6 +176,10 @@ export const createApprovalBubble = (
|
|
|
96
176
|
// Apply styling — use semantic tokens with config overrides
|
|
97
177
|
bubble.style.backgroundColor = approvalConfig?.backgroundColor ?? "var(--persona-approval-bg, #fefce8)";
|
|
98
178
|
bubble.style.borderColor = approvalConfig?.borderColor ?? "var(--persona-approval-border, #fef08a)";
|
|
179
|
+
bubble.style.boxShadow =
|
|
180
|
+
approvalConfig?.shadow !== undefined
|
|
181
|
+
? (approvalConfig.shadow.trim() === "" ? "none" : approvalConfig.shadow)
|
|
182
|
+
: "var(--persona-approval-shadow, 0 5px 15px rgba(15, 23, 42, 0.08))";
|
|
99
183
|
|
|
100
184
|
if (!approval) {
|
|
101
185
|
return bubble;
|
|
@@ -156,30 +240,116 @@ export const createApprovalBubble = (
|
|
|
156
240
|
|
|
157
241
|
content.appendChild(titleRow);
|
|
158
242
|
|
|
159
|
-
//
|
|
160
|
-
|
|
243
|
+
// User-facing summary line. The wire `description` is the tool's
|
|
244
|
+
// agent-facing description (prompt prose, usage rules), so it is not shown
|
|
245
|
+
// here — it lives in the collapsible details section below. Label priority:
|
|
246
|
+
// formatDescription → declared WebMCP `title` → humanized tool name →
|
|
247
|
+
// raw description (no tool name at all).
|
|
248
|
+
const isWebMcpTool =
|
|
249
|
+
approval.toolType === "webmcp" ||
|
|
250
|
+
approval.toolName.startsWith(WEBMCP_TOOL_PREFIX);
|
|
251
|
+
const declaredTitle = isWebMcpTool
|
|
252
|
+
? getWebMcpToolDisplayTitle(approval.toolName)
|
|
253
|
+
: undefined;
|
|
254
|
+
const summaryFromConfig = approvalConfig?.formatDescription?.({
|
|
255
|
+
toolName: approval.toolName,
|
|
256
|
+
toolType: approval.toolType,
|
|
257
|
+
description: approval.description,
|
|
258
|
+
parameters: approval.parameters,
|
|
259
|
+
...(declaredTitle ? { displayTitle: declaredTitle } : {}),
|
|
260
|
+
...(approval.reason ? { reason: approval.reason } : {}),
|
|
261
|
+
});
|
|
262
|
+
const summaryFallsBackToDescription = !approval.toolName;
|
|
263
|
+
const summaryText =
|
|
264
|
+
summaryFromConfig ||
|
|
265
|
+
(summaryFallsBackToDescription
|
|
266
|
+
? approval.description
|
|
267
|
+
: `The assistant wants to use “${declaredTitle ?? humanizeToolName(approval.toolName)}”.`);
|
|
268
|
+
|
|
269
|
+
const summary = createElement("p", "persona-text-sm persona-mt-0.5 persona-text-persona-muted");
|
|
270
|
+
summary.setAttribute("data-approval-summary", "true");
|
|
161
271
|
if (approvalConfig?.descriptionColor) {
|
|
162
|
-
|
|
272
|
+
summary.style.color = approvalConfig.descriptionColor;
|
|
273
|
+
}
|
|
274
|
+
summary.textContent = summaryText;
|
|
275
|
+
content.appendChild(summary);
|
|
276
|
+
|
|
277
|
+
// Agent-authored justification for this specific call. It is the agent's
|
|
278
|
+
// own claim about its intent (attacker-writable under prompt injection), so
|
|
279
|
+
// it is rendered as plain text via textContent — never markdown/HTML — and
|
|
280
|
+
// explicitly attributed to the agent rather than spoken in system voice.
|
|
281
|
+
if (approval.reason) {
|
|
282
|
+
const reasonLine = createElement("p", "persona-text-sm persona-mt-1 persona-text-persona-muted");
|
|
283
|
+
reasonLine.setAttribute("data-approval-reason", "true");
|
|
284
|
+
if (approvalConfig?.reasonColor) {
|
|
285
|
+
reasonLine.style.color = approvalConfig.reasonColor;
|
|
286
|
+
} else if (approvalConfig?.descriptionColor) {
|
|
287
|
+
reasonLine.style.color = approvalConfig.descriptionColor;
|
|
288
|
+
}
|
|
289
|
+
const reasonLabel = createElement("span", "persona-font-medium");
|
|
290
|
+
reasonLabel.textContent = `${approvalConfig?.reasonLabel ?? "Agent's stated reason:"} `;
|
|
291
|
+
reasonLine.appendChild(reasonLabel);
|
|
292
|
+
reasonLine.appendChild(document.createTextNode(approval.reason));
|
|
293
|
+
content.appendChild(reasonLine);
|
|
163
294
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
295
|
+
|
|
296
|
+
// Technical details: agent-facing description + raw parameters JSON,
|
|
297
|
+
// collapsed behind a toggle by default (`approval.detailsDisplay`).
|
|
298
|
+
const detailsMode = approvalConfig?.detailsDisplay ?? "collapsed";
|
|
299
|
+
const showDescriptionInDetails = Boolean(approval.description) && !summaryFallsBackToDescription;
|
|
300
|
+
const hasDetails = showDescriptionInDetails || Boolean(approval.parameters);
|
|
301
|
+
if (detailsMode !== "hidden" && hasDetails) {
|
|
302
|
+
const expanded = isDetailsExpanded(message.id, config);
|
|
303
|
+
|
|
304
|
+
const toggle = createElement(
|
|
305
|
+
"button",
|
|
306
|
+
"persona-inline-flex persona-items-center persona-gap-1 persona-mt-1 persona-p-0 persona-border-none persona-bg-transparent persona-text-xs persona-font-medium persona-cursor-pointer persona-text-persona-muted"
|
|
307
|
+
) as HTMLButtonElement;
|
|
308
|
+
toggle.type = "button";
|
|
309
|
+
toggle.setAttribute("data-expand-header", "true");
|
|
310
|
+
toggle.setAttribute("data-bubble-type", "approval");
|
|
311
|
+
if (approvalConfig?.descriptionColor) {
|
|
312
|
+
toggle.style.color = approvalConfig.descriptionColor;
|
|
175
313
|
}
|
|
176
|
-
|
|
177
|
-
|
|
314
|
+
const toggleLabel = createElement("span");
|
|
315
|
+
toggleLabel.setAttribute("data-approval-details-label", "true");
|
|
316
|
+
const chevronHolder = createElement("span", "persona-inline-flex persona-items-center");
|
|
317
|
+
chevronHolder.setAttribute("data-approval-details-chevron", "true");
|
|
318
|
+
toggle.append(toggleLabel, chevronHolder);
|
|
319
|
+
applyDetailsToggleState(toggle, expanded, config);
|
|
320
|
+
content.appendChild(toggle);
|
|
321
|
+
|
|
322
|
+
const details = createElement("div");
|
|
323
|
+
details.setAttribute("data-approval-details", "true");
|
|
324
|
+
details.style.display = expanded ? "" : "none";
|
|
325
|
+
|
|
326
|
+
if (showDescriptionInDetails) {
|
|
327
|
+
const description = createElement("p", "persona-text-sm persona-mt-1 persona-text-persona-muted");
|
|
328
|
+
if (approvalConfig?.descriptionColor) {
|
|
329
|
+
description.style.color = approvalConfig.descriptionColor;
|
|
330
|
+
}
|
|
331
|
+
description.textContent = approval.description;
|
|
332
|
+
details.appendChild(description);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (approval.parameters) {
|
|
336
|
+
const paramsPre = createElement(
|
|
337
|
+
"pre",
|
|
338
|
+
"persona-mt-2 persona-text-xs persona-p-2 persona-rounded persona-overflow-x-auto persona-max-h-32 persona-bg-persona-container persona-text-persona-primary"
|
|
339
|
+
);
|
|
340
|
+
if (approvalConfig?.parameterBackgroundColor) {
|
|
341
|
+
paramsPre.style.backgroundColor = approvalConfig.parameterBackgroundColor;
|
|
342
|
+
}
|
|
343
|
+
if (approvalConfig?.parameterTextColor) {
|
|
344
|
+
paramsPre.style.color = approvalConfig.parameterTextColor;
|
|
345
|
+
}
|
|
346
|
+
paramsPre.style.fontSize = "0.75rem";
|
|
347
|
+
paramsPre.style.lineHeight = "1rem";
|
|
348
|
+
paramsPre.textContent = formatUnknownValue(approval.parameters);
|
|
349
|
+
details.appendChild(paramsPre);
|
|
178
350
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
paramsPre.textContent = formatUnknownValue(approval.parameters);
|
|
182
|
-
content.appendChild(paramsPre);
|
|
351
|
+
|
|
352
|
+
content.appendChild(details);
|
|
183
353
|
}
|
|
184
354
|
|
|
185
355
|
// Action buttons (only shown when pending)
|
|
@@ -187,10 +187,13 @@ export const createLauncherButton = (
|
|
|
187
187
|
|
|
188
188
|
// Apply launcher border and shadow from config (with defaults matching previous Tailwind classes)
|
|
189
189
|
const defaultBorder = "1px solid var(--persona-border, #e5e7eb)";
|
|
190
|
-
const defaultShadow = "var(--persona-shadow, 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1))";
|
|
191
|
-
|
|
190
|
+
const defaultShadow = "var(--persona-launcher-shadow, 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1))";
|
|
191
|
+
|
|
192
192
|
button.style.border = launcher.border ?? defaultBorder;
|
|
193
|
-
button.style.boxShadow =
|
|
193
|
+
button.style.boxShadow =
|
|
194
|
+
launcher.shadow !== undefined
|
|
195
|
+
? (launcher.shadow.trim() === "" ? "none" : launcher.shadow)
|
|
196
|
+
: defaultShadow;
|
|
194
197
|
|
|
195
198
|
if (dockedMode) {
|
|
196
199
|
// Docked mode uses a 0px column when closed and hides this button; keep no hit target.
|
package/src/components/panel.ts
CHANGED
|
@@ -262,6 +262,9 @@ const buildComposerBarPanel = (
|
|
|
262
262
|
body.style.paddingTop = "48px";
|
|
263
263
|
body.id = "persona-scroll-container";
|
|
264
264
|
body.setAttribute("data-persona-theme-zone", "messages");
|
|
265
|
+
// Reserve the scrollbar gutter so the transcript doesn't shift horizontally
|
|
266
|
+
// when streaming content first overflows and the scrollbar appears.
|
|
267
|
+
body.style.setProperty("scrollbar-gutter", "stable");
|
|
265
268
|
|
|
266
269
|
const introCard = createElement(
|
|
267
270
|
"div",
|
|
@@ -399,7 +402,10 @@ export const buildPanel = (config?: AgentWidgetConfig, showClose = true): PanelE
|
|
|
399
402
|
);
|
|
400
403
|
body.id = "persona-scroll-container";
|
|
401
404
|
body.setAttribute("data-persona-theme-zone", "messages");
|
|
402
|
-
|
|
405
|
+
// Reserve the scrollbar gutter so the transcript doesn't shift horizontally
|
|
406
|
+
// when streaming content first overflows and the scrollbar appears.
|
|
407
|
+
body.style.setProperty("scrollbar-gutter", "stable");
|
|
408
|
+
|
|
403
409
|
const introCard = createElement(
|
|
404
410
|
"div",
|
|
405
411
|
"persona-rounded-2xl persona-bg-persona-surface persona-p-6"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { createToolBubble } from "./tool-bubble";
|
|
5
|
+
import type { AgentWidgetConfig, AgentWidgetMessage } from "../types";
|
|
6
|
+
|
|
7
|
+
const makeMessage = (): AgentWidgetMessage => ({
|
|
8
|
+
id: "msg-1",
|
|
9
|
+
role: "assistant",
|
|
10
|
+
content: "",
|
|
11
|
+
createdAt: new Date().toISOString(),
|
|
12
|
+
variant: "tool",
|
|
13
|
+
streaming: false,
|
|
14
|
+
toolCall: {
|
|
15
|
+
id: "tool-1",
|
|
16
|
+
name: "get_weather",
|
|
17
|
+
status: "complete",
|
|
18
|
+
args: { city: "SF" },
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("createToolBubble shadow", () => {
|
|
23
|
+
it("falls back to the themeable --persona-tool-bubble-shadow variable when no config.shadow is set", () => {
|
|
24
|
+
const bubble = createToolBubble(makeMessage());
|
|
25
|
+
expect(bubble.style.boxShadow).toContain("--persona-tool-bubble-shadow");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("applies a custom shadow inline from config.toolCall.shadow", () => {
|
|
29
|
+
const config = { toolCall: { shadow: "0 8px 24px rgba(0,0,0,0.15)" } } as AgentWidgetConfig;
|
|
30
|
+
const bubble = createToolBubble(makeMessage(), config);
|
|
31
|
+
expect(bubble.style.boxShadow).toBe("0 8px 24px rgba(0,0,0,0.15)");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('maps an empty shadow string to "none"', () => {
|
|
35
|
+
const config = { toolCall: { shadow: " " } } as AgentWidgetConfig;
|
|
36
|
+
const bubble = createToolBubble(makeMessage(), config);
|
|
37
|
+
expect(bubble.style.boxShadow).toBe("none");
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -151,6 +151,10 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
|
|
|
151
151
|
if (toolCallConfig.borderRadius) {
|
|
152
152
|
bubble.style.borderRadius = toolCallConfig.borderRadius;
|
|
153
153
|
}
|
|
154
|
+
bubble.style.boxShadow =
|
|
155
|
+
toolCallConfig.shadow !== undefined
|
|
156
|
+
? (toolCallConfig.shadow.trim() === "" ? "none" : toolCallConfig.shadow)
|
|
157
|
+
: "var(--persona-tool-bubble-shadow, 0 5px 15px rgba(15, 23, 42, 0.08))";
|
|
154
158
|
|
|
155
159
|
if (!tool) {
|
|
156
160
|
return bubble;
|
package/src/defaults.ts
CHANGED
|
@@ -123,6 +123,10 @@ export const DEFAULT_WIDGET_CONFIG: Partial<AgentWidgetConfig> = {
|
|
|
123
123
|
iconName: "arrow-down",
|
|
124
124
|
label: "",
|
|
125
125
|
},
|
|
126
|
+
scrollBehavior: {
|
|
127
|
+
mode: "follow",
|
|
128
|
+
anchorTopOffset: 16,
|
|
129
|
+
},
|
|
126
130
|
toolCallDisplay: {
|
|
127
131
|
collapsedMode: "tool-call",
|
|
128
132
|
activePreview: false,
|
|
@@ -264,6 +268,8 @@ export function mergeWithDefaults(
|
|
|
264
268
|
const ca = config.features?.artifacts;
|
|
265
269
|
const dsb = DEFAULT_WIDGET_CONFIG.features?.scrollToBottom;
|
|
266
270
|
const csb = config.features?.scrollToBottom;
|
|
271
|
+
const dsc = DEFAULT_WIDGET_CONFIG.features?.scrollBehavior;
|
|
272
|
+
const csc = config.features?.scrollBehavior;
|
|
267
273
|
const dsa = DEFAULT_WIDGET_CONFIG.features?.streamAnimation;
|
|
268
274
|
const csa = config.features?.streamAnimation;
|
|
269
275
|
const dau = DEFAULT_WIDGET_CONFIG.features?.askUserQuestion;
|
|
@@ -286,6 +292,13 @@ export function mergeWithDefaults(
|
|
|
286
292
|
...dsb,
|
|
287
293
|
...csb,
|
|
288
294
|
};
|
|
295
|
+
const mergedScrollBehavior =
|
|
296
|
+
dsc === undefined && csc === undefined
|
|
297
|
+
? undefined
|
|
298
|
+
: {
|
|
299
|
+
...dsc,
|
|
300
|
+
...csc,
|
|
301
|
+
};
|
|
289
302
|
const mergedStreamAnimation =
|
|
290
303
|
dsa === undefined && csa === undefined
|
|
291
304
|
? undefined
|
|
@@ -308,6 +321,7 @@ export function mergeWithDefaults(
|
|
|
308
321
|
...DEFAULT_WIDGET_CONFIG.features,
|
|
309
322
|
...config.features,
|
|
310
323
|
...(mergedScrollToBottom !== undefined ? { scrollToBottom: mergedScrollToBottom } : {}),
|
|
324
|
+
...(mergedScrollBehavior !== undefined ? { scrollBehavior: mergedScrollBehavior } : {}),
|
|
311
325
|
...(mergedArtifacts !== undefined ? { artifacts: mergedArtifacts } : {}),
|
|
312
326
|
...(mergedStreamAnimation !== undefined ? { streamAnimation: mergedStreamAnimation } : {}),
|
|
313
327
|
...(mergedAskUserQuestion !== undefined ? { askUserQuestion: mergedAskUserQuestion } : {}),
|
|
@@ -150,6 +150,7 @@ export type RuntypeAgentSSEEvent = {
|
|
|
150
150
|
};
|
|
151
151
|
iteration?: number;
|
|
152
152
|
parameters?: Record<string, unknown>;
|
|
153
|
+
reason?: string;
|
|
153
154
|
seq: number;
|
|
154
155
|
startedAt: string;
|
|
155
156
|
timeout: number;
|
|
@@ -422,6 +423,7 @@ export type RuntypeFlowSSEEvent = {
|
|
|
422
423
|
when: string;
|
|
423
424
|
} | {
|
|
424
425
|
executionId?: string;
|
|
426
|
+
reason?: string;
|
|
425
427
|
seq?: number;
|
|
426
428
|
type: "step_await";
|
|
427
429
|
[key: string]: unknown;
|
|
@@ -701,6 +703,7 @@ export type RuntypeDispatchSSEEvent = {
|
|
|
701
703
|
};
|
|
702
704
|
iteration?: number;
|
|
703
705
|
parameters?: Record<string, unknown>;
|
|
706
|
+
reason?: string;
|
|
704
707
|
seq: number;
|
|
705
708
|
startedAt: string;
|
|
706
709
|
timeout: number;
|
|
@@ -971,6 +974,7 @@ export type RuntypeDispatchSSEEvent = {
|
|
|
971
974
|
when: string;
|
|
972
975
|
} | {
|
|
973
976
|
executionId?: string;
|
|
977
|
+
reason?: string;
|
|
974
978
|
seq?: number;
|
|
975
979
|
type: "step_await";
|
|
976
980
|
[key: string]: unknown;
|
package/src/index-core.ts
CHANGED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
createPopover,
|
|
7
|
+
getStyleRoot,
|
|
8
|
+
injectStyles,
|
|
9
|
+
isEditableEventTarget,
|
|
10
|
+
} from "./plugin-kit";
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
document.body.innerHTML = "";
|
|
14
|
+
document.head.querySelectorAll("[data-persona-plugin-style]").forEach((n) => n.remove());
|
|
15
|
+
vi.restoreAllMocks();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const flushMicrotasks = () => Promise.resolve();
|
|
19
|
+
|
|
20
|
+
describe("injectStyles / getStyleRoot", () => {
|
|
21
|
+
it("injects a <style> into the document head for a connected light-DOM node", () => {
|
|
22
|
+
const el = document.createElement("div");
|
|
23
|
+
document.body.appendChild(el);
|
|
24
|
+
|
|
25
|
+
injectStyles(el, "kit-light", ".x { color: red; }");
|
|
26
|
+
|
|
27
|
+
const style = document.head.querySelector(
|
|
28
|
+
'style[data-persona-plugin-style="kit-light"]'
|
|
29
|
+
);
|
|
30
|
+
expect(style).not.toBeNull();
|
|
31
|
+
expect(style?.textContent).toContain("color: red");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("is idempotent — repeated calls add only one <style>", () => {
|
|
35
|
+
const el = document.createElement("div");
|
|
36
|
+
document.body.appendChild(el);
|
|
37
|
+
|
|
38
|
+
injectStyles(el, "kit-once", ".a {}");
|
|
39
|
+
injectStyles(el, "kit-once", ".a {}");
|
|
40
|
+
injectStyles(el, "kit-once", ".a {}");
|
|
41
|
+
|
|
42
|
+
expect(
|
|
43
|
+
document.head.querySelectorAll('style[data-persona-plugin-style="kit-once"]').length
|
|
44
|
+
).toBe(1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("injects into the shadow root for a node inside shadow DOM", () => {
|
|
48
|
+
const host = document.createElement("div");
|
|
49
|
+
document.body.appendChild(host);
|
|
50
|
+
const shadow = host.attachShadow({ mode: "open" });
|
|
51
|
+
const el = document.createElement("div");
|
|
52
|
+
shadow.appendChild(el);
|
|
53
|
+
|
|
54
|
+
expect(getStyleRoot(el)).toBe(shadow);
|
|
55
|
+
|
|
56
|
+
injectStyles(el, "kit-shadow", ".s {}");
|
|
57
|
+
|
|
58
|
+
expect(shadow.querySelector('style[data-persona-plugin-style="kit-shadow"]')).not.toBeNull();
|
|
59
|
+
// Must NOT leak into the document head.
|
|
60
|
+
expect(
|
|
61
|
+
document.head.querySelector('style[data-persona-plugin-style="kit-shadow"]')
|
|
62
|
+
).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("defers shadow injection for a node that mounts after the call", async () => {
|
|
66
|
+
const host = document.createElement("div");
|
|
67
|
+
document.body.appendChild(host);
|
|
68
|
+
const shadow = host.attachShadow({ mode: "open" });
|
|
69
|
+
|
|
70
|
+
// Build detached, inject, then mount into the shadow root (mirrors a plugin
|
|
71
|
+
// returning an element the widget mounts afterward).
|
|
72
|
+
const el = document.createElement("div");
|
|
73
|
+
injectStyles(el, "kit-deferred", ".d {}");
|
|
74
|
+
shadow.appendChild(el);
|
|
75
|
+
|
|
76
|
+
await flushMicrotasks();
|
|
77
|
+
|
|
78
|
+
expect(shadow.querySelector('style[data-persona-plugin-style="kit-deferred"]')).not.toBeNull();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("accepts an explicit Document or ShadowRoot target", () => {
|
|
82
|
+
const host = document.createElement("div");
|
|
83
|
+
document.body.appendChild(host);
|
|
84
|
+
const shadow = host.attachShadow({ mode: "open" });
|
|
85
|
+
|
|
86
|
+
injectStyles(shadow, "kit-explicit", ".e {}");
|
|
87
|
+
|
|
88
|
+
expect(shadow.querySelector('style[data-persona-plugin-style="kit-explicit"]')).not.toBeNull();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("createPopover", () => {
|
|
93
|
+
const setup = () => {
|
|
94
|
+
const anchor = document.createElement("button");
|
|
95
|
+
anchor.textContent = "anchor";
|
|
96
|
+
document.body.appendChild(anchor);
|
|
97
|
+
const content = document.createElement("div");
|
|
98
|
+
content.textContent = "menu";
|
|
99
|
+
return { anchor, content };
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
it("mounts and positions the content on open, removes it on close", () => {
|
|
103
|
+
const { anchor, content } = setup();
|
|
104
|
+
const popover = createPopover({ anchor, content });
|
|
105
|
+
|
|
106
|
+
expect(popover.isOpen).toBe(false);
|
|
107
|
+
expect(content.isConnected).toBe(false);
|
|
108
|
+
|
|
109
|
+
popover.open();
|
|
110
|
+
expect(popover.isOpen).toBe(true);
|
|
111
|
+
expect(content.isConnected).toBe(true);
|
|
112
|
+
expect(content.style.position).toBe("fixed");
|
|
113
|
+
expect(content.style.top).not.toBe("");
|
|
114
|
+
expect(content.style.left).not.toBe("");
|
|
115
|
+
|
|
116
|
+
popover.close();
|
|
117
|
+
expect(popover.isOpen).toBe(false);
|
|
118
|
+
expect(content.isConnected).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("toggle flips open/closed", () => {
|
|
122
|
+
const { anchor, content } = setup();
|
|
123
|
+
const popover = createPopover({ anchor, content });
|
|
124
|
+
|
|
125
|
+
popover.toggle();
|
|
126
|
+
expect(popover.isOpen).toBe(true);
|
|
127
|
+
popover.toggle();
|
|
128
|
+
expect(popover.isOpen).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("closes on outside pointerdown and fires onDismiss", () => {
|
|
132
|
+
vi.useFakeTimers();
|
|
133
|
+
const onDismiss = vi.fn();
|
|
134
|
+
const { anchor, content } = setup();
|
|
135
|
+
const popover = createPopover({ anchor, content, onDismiss });
|
|
136
|
+
|
|
137
|
+
popover.open();
|
|
138
|
+
// Arming is deferred via setTimeout so the opening click can't dismiss it.
|
|
139
|
+
vi.runAllTimers();
|
|
140
|
+
|
|
141
|
+
document.body.dispatchEvent(new Event("pointerdown", { bubbles: true }));
|
|
142
|
+
|
|
143
|
+
expect(popover.isOpen).toBe(false);
|
|
144
|
+
expect(onDismiss).toHaveBeenCalledWith("outside");
|
|
145
|
+
vi.useRealTimers();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("does not dismiss when the pointerdown is inside the content", () => {
|
|
149
|
+
vi.useFakeTimers();
|
|
150
|
+
const onDismiss = vi.fn();
|
|
151
|
+
const { anchor, content } = setup();
|
|
152
|
+
const popover = createPopover({ anchor, content, onDismiss });
|
|
153
|
+
|
|
154
|
+
popover.open();
|
|
155
|
+
vi.runAllTimers();
|
|
156
|
+
|
|
157
|
+
content.dispatchEvent(new Event("pointerdown", { bubbles: true, composed: true }));
|
|
158
|
+
|
|
159
|
+
expect(popover.isOpen).toBe(true);
|
|
160
|
+
expect(onDismiss).not.toHaveBeenCalled();
|
|
161
|
+
vi.useRealTimers();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("auto-closes when the anchor leaves the DOM on reposition", () => {
|
|
165
|
+
const onDismiss = vi.fn();
|
|
166
|
+
const { anchor, content } = setup();
|
|
167
|
+
const popover = createPopover({ anchor, content, onDismiss });
|
|
168
|
+
|
|
169
|
+
popover.open();
|
|
170
|
+
anchor.remove();
|
|
171
|
+
window.dispatchEvent(new Event("resize"));
|
|
172
|
+
|
|
173
|
+
expect(popover.isOpen).toBe(false);
|
|
174
|
+
expect(onDismiss).toHaveBeenCalledWith("anchor-removed");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("mounts into the anchor's shadow root when shadowed", () => {
|
|
178
|
+
const host = document.createElement("div");
|
|
179
|
+
document.body.appendChild(host);
|
|
180
|
+
const shadow = host.attachShadow({ mode: "open" });
|
|
181
|
+
const anchor = document.createElement("button");
|
|
182
|
+
shadow.appendChild(anchor);
|
|
183
|
+
const content = document.createElement("div");
|
|
184
|
+
|
|
185
|
+
const popover = createPopover({ anchor, content });
|
|
186
|
+
popover.open();
|
|
187
|
+
|
|
188
|
+
expect(content.getRootNode()).toBe(shadow);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("destroy removes content and detaches listeners", () => {
|
|
192
|
+
vi.useFakeTimers();
|
|
193
|
+
const onDismiss = vi.fn();
|
|
194
|
+
const { anchor, content } = setup();
|
|
195
|
+
const popover = createPopover({ anchor, content, onDismiss });
|
|
196
|
+
|
|
197
|
+
popover.open();
|
|
198
|
+
vi.runAllTimers();
|
|
199
|
+
popover.destroy();
|
|
200
|
+
|
|
201
|
+
expect(content.isConnected).toBe(false);
|
|
202
|
+
// Listeners are gone, so a later outside pointerdown is a no-op.
|
|
203
|
+
document.body.dispatchEvent(new Event("pointerdown", { bubbles: true }));
|
|
204
|
+
expect(onDismiss).not.toHaveBeenCalled();
|
|
205
|
+
vi.useRealTimers();
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe("isEditableEventTarget", () => {
|
|
210
|
+
it("is true for input / textarea / contenteditable targets", () => {
|
|
211
|
+
const input = document.createElement("input");
|
|
212
|
+
document.body.appendChild(input);
|
|
213
|
+
const event = new Event("keydown", { bubbles: true, composed: true });
|
|
214
|
+
input.dispatchEvent(event);
|
|
215
|
+
// jsdom keeps composedPath populated only during dispatch; assert via a
|
|
216
|
+
// synthetic event whose composedPath we can rely on.
|
|
217
|
+
const synthetic = {
|
|
218
|
+
composedPath: () => [input, document.body],
|
|
219
|
+
} as unknown as Event;
|
|
220
|
+
expect(isEditableEventTarget(synthetic)).toBe(true);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("is false for a plain button target", () => {
|
|
224
|
+
const button = document.createElement("button");
|
|
225
|
+
const synthetic = {
|
|
226
|
+
composedPath: () => [button, document.body],
|
|
227
|
+
} as unknown as Event;
|
|
228
|
+
expect(isEditableEventTarget(synthetic)).toBe(false);
|
|
229
|
+
});
|
|
230
|
+
});
|