@runtypelabs/persona 4.7.0 → 4.8.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/animations/glyph-cycle.cjs +2 -2
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/glyph-cycle.js +2 -2
- package/dist/animations/{types-CSmiKRVa.d.cts → types-BsZtXPKK.d.cts} +43 -3
- package/dist/animations/{types-CSmiKRVa.d.ts → types-BsZtXPKK.d.ts} +43 -3
- package/dist/animations/wipe.cjs +2 -2
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/chunk-5EIIHQLQ.js +1 -0
- package/dist/codegen.cjs +12 -12
- package/dist/codegen.js +14 -14
- package/dist/index.cjs +91 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +877 -172
- package/dist/index.d.ts +877 -172
- package/dist/index.global.js +81 -57
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +91 -68
- 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 +3 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/markdown-parsers.js +24 -24
- package/dist/plugin-kit.cjs +1 -1
- package/dist/plugin-kit.js +1 -1
- package/dist/runtype-tts-entry-UJAEF7NZ.js +1 -0
- package/dist/runtype-tts.js +1 -1
- package/dist/session-reconnect-JKIJBHS5.js +1 -0
- package/dist/smart-dom-reader.cjs +17 -17
- package/dist/smart-dom-reader.d.cts +753 -15
- package/dist/smart-dom-reader.d.ts +753 -15
- package/dist/smart-dom-reader.js +17 -17
- package/dist/testing.cjs +3 -3
- package/dist/testing.js +3 -3
- package/dist/theme-editor-preview.cjs +81 -58
- package/dist/theme-editor-preview.d.cts +761 -15
- package/dist/theme-editor-preview.d.ts +761 -15
- package/dist/theme-editor-preview.js +81 -58
- package/dist/theme-editor.cjs +6 -6
- package/dist/theme-editor.d.cts +753 -15
- package/dist/theme-editor.d.ts +753 -15
- package/dist/theme-editor.js +10 -10
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +74 -0
- package/dist/theme-reference.d.ts +74 -0
- package/dist/theme-reference.js +1 -1
- package/dist/voice-worklet-player.cjs +2 -2
- package/dist/voice-worklet-player.js +2 -2
- package/dist/webmcp-polyfill.js +2 -2
- package/dist/widget.css +1 -1
- package/package.json +2 -3
- package/src/artifacts-session.test.ts +178 -0
- package/src/client.test.ts +186 -1
- package/src/client.ts +92 -9
- package/src/components/artifact-card.test.ts +333 -0
- package/src/components/artifact-card.ts +75 -28
- package/src/components/artifact-inline.test.ts +1328 -0
- package/src/components/artifact-inline.ts +920 -0
- package/src/components/artifact-pane.test.ts +1042 -0
- package/src/components/artifact-pane.ts +440 -131
- package/src/components/artifact-preview.test.ts +1155 -0
- package/src/components/artifact-preview.ts +994 -0
- package/src/components/pill-composer-builder.test.ts +6 -2
- package/src/components/pill-composer-builder.ts +6 -6
- package/src/components/reasoning-bubble.ts +1 -13
- package/src/components/registry.ts +38 -3
- package/src/components/tool-bubble.ts +1 -13
- package/src/defaults.ts +1 -0
- package/src/generated/runtype-openapi-contract.ts +55 -3
- package/src/index-core.ts +20 -1
- package/src/index.ts +8 -0
- package/src/markdown-parsers-loader.test.ts +158 -0
- package/src/markdown-parsers-loader.ts +74 -9
- package/src/runtime/host-layout.test.ts +163 -0
- package/src/runtime/host-layout.ts +110 -7
- package/src/runtime/init.ts +18 -61
- package/src/session.ts +76 -22
- package/src/styles/widget.css +773 -26
- package/src/theme-editor/preview.ts +2 -0
- package/src/theme-editor/sections.test.ts +26 -1
- package/src/theme-editor/sections.ts +10 -2
- package/src/theme-reference.ts +2 -2
- package/src/tool-call-display-defaults.test.ts +1 -0
- package/src/types/theme.ts +77 -0
- package/src/types.ts +516 -17
- package/src/ui.artifact-pane-gating.test.ts +636 -0
- package/src/ui.component-directive.test.ts +104 -0
- package/src/ui.composer-bar.test.ts +60 -2
- package/src/ui.detached-panel.test.ts +1049 -0
- package/src/ui.tool-display.test.ts +51 -0
- package/src/ui.ts +698 -109
- package/src/utils/artifact-custom-actions.ts +128 -0
- package/src/utils/artifact-display.test.ts +42 -0
- package/src/utils/artifact-display.ts +84 -0
- package/src/utils/artifact-file.test.ts +116 -0
- package/src/utils/artifact-file.ts +117 -0
- package/src/utils/artifact-gate.test.ts +112 -5
- package/src/utils/artifact-gate.ts +39 -14
- package/src/utils/artifact-loading-status.ts +55 -0
- package/src/utils/artifact-status-label.ts +190 -0
- package/src/utils/buttons.ts +7 -1
- package/src/utils/code-highlight.test.ts +186 -0
- package/src/utils/code-highlight.ts +400 -0
- package/src/utils/icons.ts +2 -0
- package/src/utils/roving-tablist.test.ts +152 -0
- package/src/utils/roving-tablist.ts +111 -0
- package/src/utils/spinner.ts +45 -0
- package/src/utils/theme.test.ts +48 -0
- package/src/utils/theme.ts +7 -0
- package/src/utils/tokens.ts +91 -0
- package/src/utils/tool-loading-animation.test.ts +32 -0
- package/src/utils/tool-loading-animation.ts +24 -0
- package/dist/chunk-DFBSCFYN.js +0 -1
- package/dist/runtype-tts-entry-HFUV2UF7.js +0 -1
- package/dist/session-reconnect-U77QFUR7.js +0 -1
|
@@ -26,9 +26,13 @@ describe("buildPillComposer (single-row pill composer)", () => {
|
|
|
26
26
|
expect(elements.sendButton.getAttribute("data-persona-composer-submit")).toBe("");
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
-
it("
|
|
29
|
+
it("keeps suggestions available above the pill and hides status text", () => {
|
|
30
30
|
const elements = buildPillComposer({ config: { apiUrl: "/api" } });
|
|
31
|
-
|
|
31
|
+
const footerChildren = Array.from(elements.footer.children);
|
|
32
|
+
expect(elements.suggestions.style.display).toBe("");
|
|
33
|
+
expect(footerChildren.indexOf(elements.suggestions)).toBeLessThan(
|
|
34
|
+
footerChildren.indexOf(elements.composerForm)
|
|
35
|
+
);
|
|
32
36
|
expect(elements.statusText.style.display).toBe("none");
|
|
33
37
|
});
|
|
34
38
|
|
|
@@ -72,9 +72,10 @@ export const buildPillPeekBanner = (): PillPeekBanner => {
|
|
|
72
72
|
* Returns the same `ComposerElements` shape as `buildComposer` so panel.ts
|
|
73
73
|
* and ui.ts plumbing is unconditional past the choice of builder.
|
|
74
74
|
*
|
|
75
|
-
* Suggestions row + status text are built
|
|
76
|
-
*
|
|
77
|
-
*
|
|
75
|
+
* Suggestions row + status text are built so plugin code that mutates them
|
|
76
|
+
* keeps working and `bindComposerRefsFromFooter` finds them. CSS hides the
|
|
77
|
+
* suggestions while collapsed and reveals them above the pill when expanded;
|
|
78
|
+
* status text stays hidden because the pill UX has no room for it.
|
|
78
79
|
*
|
|
79
80
|
* Attachment previews float ABOVE the pill in their own row when
|
|
80
81
|
* AttachmentManager toggles the previews container's `display` property
|
|
@@ -89,7 +90,6 @@ export const buildPillComposer = (context: ComposerBuildContext): ComposerElemen
|
|
|
89
90
|
});
|
|
90
91
|
|
|
91
92
|
const suggestions = createSuggestionsRow();
|
|
92
|
-
suggestions.style.display = "none";
|
|
93
93
|
const statusText = createStatusText(config);
|
|
94
94
|
statusText.style.display = "none";
|
|
95
95
|
|
|
@@ -150,11 +150,11 @@ export const buildPillComposer = (context: ComposerBuildContext): ComposerElemen
|
|
|
150
150
|
|
|
151
151
|
// Footer assembly:
|
|
152
152
|
// [previews row, hidden until attachments exist]
|
|
153
|
+
// [suggestions, visible only while expanded]
|
|
153
154
|
// [pill form]
|
|
154
|
-
// [hidden suggestions]
|
|
155
155
|
// [hidden status]
|
|
156
156
|
if (attachment) footer.append(attachment.previewsContainer);
|
|
157
|
-
footer.append(
|
|
157
|
+
footer.append(suggestions, composerForm, statusText);
|
|
158
158
|
|
|
159
159
|
// The pill flattens left/right into the form's grid; there's no separate
|
|
160
160
|
// wrapper. Surface the form itself as `actionsRow` to satisfy the
|
|
@@ -2,6 +2,7 @@ import { createElement } from "../utils/dom";
|
|
|
2
2
|
import { AgentWidgetConfig, AgentWidgetMessage } from "../types";
|
|
3
3
|
import { describeReasonStatus, computeReasoningElapsed, parseFormattedTemplate } from "../utils/formatting";
|
|
4
4
|
import { renderLucideIcon } from "../utils/icons";
|
|
5
|
+
import { appendCharSpans } from "../utils/tool-loading-animation";
|
|
5
6
|
|
|
6
7
|
// Expansion state per widget instance
|
|
7
8
|
export const reasoningExpansionState = new Set<string>();
|
|
@@ -156,19 +157,6 @@ export const createReasoningBubble = (message: AgentWidgetMessage, config?: Agen
|
|
|
156
157
|
const currentTemplate = isActive ? activeTemplate : completeTemplate;
|
|
157
158
|
const skipCustomElement = customSummary instanceof HTMLElement;
|
|
158
159
|
|
|
159
|
-
// Helper: append text as individual animated character spans
|
|
160
|
-
const appendCharSpans = (container: HTMLElement, text: string, startIndex: number): number => {
|
|
161
|
-
let idx = startIndex;
|
|
162
|
-
for (const char of text) {
|
|
163
|
-
const span = createElement("span", "persona-tool-char");
|
|
164
|
-
span.style.setProperty("--char-index", String(idx));
|
|
165
|
-
span.textContent = char === " " ? "\u00A0" : char;
|
|
166
|
-
container.appendChild(span);
|
|
167
|
-
idx++;
|
|
168
|
-
}
|
|
169
|
-
return idx;
|
|
170
|
-
};
|
|
171
|
-
|
|
172
160
|
/**
|
|
173
161
|
* Renders a template into the title element, handling:
|
|
174
162
|
* - Inline formatting markers: **bold**, *italic*, ~dim~
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AgentWidgetConfig, AgentWidgetMessage } from "../types";
|
|
2
2
|
import { PersonaArtifactCard } from "./artifact-card";
|
|
3
|
+
import { PersonaArtifactInline } from "./artifact-inline";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Context provided to component renderers
|
|
@@ -21,20 +22,42 @@ export type ComponentRenderer = (
|
|
|
21
22
|
context: ComponentContext
|
|
22
23
|
) => HTMLElement;
|
|
23
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Per-component registration options
|
|
27
|
+
*/
|
|
28
|
+
export interface ComponentRegistrationOptions {
|
|
29
|
+
/**
|
|
30
|
+
* When false, the component directive renders bare in the thread (no
|
|
31
|
+
* persona-message-bubble chrome).
|
|
32
|
+
* @default true
|
|
33
|
+
*/
|
|
34
|
+
bubbleChrome?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
24
37
|
/**
|
|
25
38
|
* Component registry for managing custom components
|
|
26
39
|
*/
|
|
27
40
|
class ComponentRegistry {
|
|
28
41
|
private components: Map<string, ComponentRenderer> = new Map();
|
|
42
|
+
private options: Map<string, ComponentRegistrationOptions> = new Map();
|
|
29
43
|
|
|
30
44
|
/**
|
|
31
45
|
* Register a custom component
|
|
32
46
|
*/
|
|
33
|
-
register(
|
|
47
|
+
register(
|
|
48
|
+
name: string,
|
|
49
|
+
renderer: ComponentRenderer,
|
|
50
|
+
options?: ComponentRegistrationOptions
|
|
51
|
+
): void {
|
|
34
52
|
if (this.components.has(name)) {
|
|
35
53
|
console.warn(`[ComponentRegistry] Component "${name}" is already registered. Overwriting.`);
|
|
36
54
|
}
|
|
37
55
|
this.components.set(name, renderer);
|
|
56
|
+
if (options) {
|
|
57
|
+
this.options.set(name, options);
|
|
58
|
+
} else {
|
|
59
|
+
this.options.delete(name);
|
|
60
|
+
}
|
|
38
61
|
}
|
|
39
62
|
|
|
40
63
|
/**
|
|
@@ -42,6 +65,7 @@ class ComponentRegistry {
|
|
|
42
65
|
*/
|
|
43
66
|
unregister(name: string): void {
|
|
44
67
|
this.components.delete(name);
|
|
68
|
+
this.options.delete(name);
|
|
45
69
|
}
|
|
46
70
|
|
|
47
71
|
/**
|
|
@@ -58,6 +82,13 @@ class ComponentRegistry {
|
|
|
58
82
|
return this.components.has(name);
|
|
59
83
|
}
|
|
60
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Get the registration options for a component, if any were supplied
|
|
87
|
+
*/
|
|
88
|
+
getOptions(name: string): ComponentRegistrationOptions | undefined {
|
|
89
|
+
return this.options.get(name);
|
|
90
|
+
}
|
|
91
|
+
|
|
61
92
|
/**
|
|
62
93
|
* Get all registered component names
|
|
63
94
|
*/
|
|
@@ -70,6 +101,7 @@ class ComponentRegistry {
|
|
|
70
101
|
*/
|
|
71
102
|
clear(): void {
|
|
72
103
|
this.components.clear();
|
|
104
|
+
this.options.clear();
|
|
73
105
|
}
|
|
74
106
|
|
|
75
107
|
/**
|
|
@@ -87,5 +119,8 @@ class ComponentRegistry {
|
|
|
87
119
|
*/
|
|
88
120
|
export const componentRegistry = new ComponentRegistry();
|
|
89
121
|
|
|
90
|
-
// Register built-in components
|
|
91
|
-
|
|
122
|
+
// Register built-in components. The artifact card and inline block carry
|
|
123
|
+
// their own border and surface, so they render bare in the thread to avoid
|
|
124
|
+
// double-boxing.
|
|
125
|
+
componentRegistry.register("PersonaArtifactCard", PersonaArtifactCard, { bubbleChrome: false });
|
|
126
|
+
componentRegistry.register("PersonaArtifactInline", PersonaArtifactInline, { bubbleChrome: false });
|
|
@@ -2,6 +2,7 @@ import { createElement } from "../utils/dom";
|
|
|
2
2
|
import { AgentWidgetMessage, AgentWidgetConfig } from "../types";
|
|
3
3
|
import { formatUnknownValue, describeToolTitle, resolveToolHeaderText, computeToolElapsed, parseFormattedTemplate } from "../utils/formatting";
|
|
4
4
|
import { renderLucideIcon } from "../utils/icons";
|
|
5
|
+
import { appendCharSpans } from "../utils/tool-loading-animation";
|
|
5
6
|
|
|
6
7
|
// Expansion state per widget instance
|
|
7
8
|
export const toolExpansionState = new Set<string>();
|
|
@@ -259,19 +260,6 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
|
|
|
259
260
|
const currentTemplate = isActive ? activeTemplate : completeTemplate;
|
|
260
261
|
const skipCustomElement = customSummary instanceof HTMLElement;
|
|
261
262
|
|
|
262
|
-
// Helper: append text as individual animated character spans
|
|
263
|
-
const appendCharSpans = (container: HTMLElement, text: string, startIndex: number): number => {
|
|
264
|
-
let idx = startIndex;
|
|
265
|
-
for (const char of text) {
|
|
266
|
-
const span = createElement("span", "persona-tool-char");
|
|
267
|
-
span.style.setProperty("--char-index", String(idx));
|
|
268
|
-
span.textContent = char === " " ? "\u00A0" : char;
|
|
269
|
-
container.appendChild(span);
|
|
270
|
-
idx++;
|
|
271
|
-
}
|
|
272
|
-
return idx;
|
|
273
|
-
};
|
|
274
|
-
|
|
275
263
|
/**
|
|
276
264
|
* Renders a template into the title element, handling:
|
|
277
265
|
* - Inline formatting markers: **bold**, *italic*, ~dim~
|
package/src/defaults.ts
CHANGED
|
@@ -22,6 +22,7 @@ export type RuntypeExecutionStreamEvent = ({
|
|
|
22
22
|
totalSteps?: number;
|
|
23
23
|
type: "execution_start";
|
|
24
24
|
}) | ({
|
|
25
|
+
claudeManagedAgentId?: string;
|
|
25
26
|
completedAt?: string;
|
|
26
27
|
durationMs?: number;
|
|
27
28
|
executionId: string;
|
|
@@ -66,6 +67,15 @@ export type RuntypeExecutionStreamEvent = ({
|
|
|
66
67
|
completedAt?: string;
|
|
67
68
|
content?: string;
|
|
68
69
|
cost?: number;
|
|
70
|
+
estimatedContextBreakdown?: {
|
|
71
|
+
categories: Record<string, {
|
|
72
|
+
chars: number;
|
|
73
|
+
estimatedTokens: number;
|
|
74
|
+
}>;
|
|
75
|
+
contextWindowSize?: number;
|
|
76
|
+
estimatedTotalTokens: number;
|
|
77
|
+
estimatedUtilizationPercent?: number;
|
|
78
|
+
};
|
|
69
79
|
executionId: string;
|
|
70
80
|
fallback?: {
|
|
71
81
|
attempts: Array<{
|
|
@@ -103,6 +113,7 @@ export type RuntypeExecutionStreamEvent = ({
|
|
|
103
113
|
type: "step_start";
|
|
104
114
|
} | ({
|
|
105
115
|
completedAt?: string;
|
|
116
|
+
cost?: number;
|
|
106
117
|
durationMs?: number;
|
|
107
118
|
error?: string;
|
|
108
119
|
executionId: string;
|
|
@@ -208,6 +219,11 @@ export type RuntypeExecutionStreamEvent = ({
|
|
|
208
219
|
artifactType: "markdown" | "component";
|
|
209
220
|
component?: string;
|
|
210
221
|
executionId?: string;
|
|
222
|
+
file?: {
|
|
223
|
+
language?: string;
|
|
224
|
+
mimeType: string;
|
|
225
|
+
path: string;
|
|
226
|
+
};
|
|
211
227
|
id: string;
|
|
212
228
|
seq?: number;
|
|
213
229
|
title?: string;
|
|
@@ -316,6 +332,14 @@ export type RuntypeExecutionStreamEvent = ({
|
|
|
316
332
|
awaitReason?: string;
|
|
317
333
|
awaitedAt?: string;
|
|
318
334
|
crawlId?: string;
|
|
335
|
+
elicitation?: {
|
|
336
|
+
message: string;
|
|
337
|
+
mode: "form" | "url";
|
|
338
|
+
pauseCount?: number;
|
|
339
|
+
requestedSchema?: Record<string, unknown>;
|
|
340
|
+
serverName?: string;
|
|
341
|
+
url?: string;
|
|
342
|
+
};
|
|
319
343
|
executionId: string;
|
|
320
344
|
origin?: "webmcp" | "sdk";
|
|
321
345
|
pageOrigin?: string;
|
|
@@ -416,6 +440,14 @@ export type RuntypeFlowSSEEvent = {
|
|
|
416
440
|
awaitReason?: string;
|
|
417
441
|
awaitedAt: string;
|
|
418
442
|
crawlId?: string;
|
|
443
|
+
elicitation?: {
|
|
444
|
+
message: string;
|
|
445
|
+
mode: "form" | "url";
|
|
446
|
+
pauseCount?: number;
|
|
447
|
+
requestedSchema?: Record<string, unknown>;
|
|
448
|
+
serverName?: string;
|
|
449
|
+
url?: string;
|
|
450
|
+
};
|
|
419
451
|
executionId?: string;
|
|
420
452
|
flowId: string;
|
|
421
453
|
origin?: "webmcp" | "sdk";
|
|
@@ -466,7 +498,7 @@ export type RuntypeFlowSSEEvent = {
|
|
|
466
498
|
completedAt?: string;
|
|
467
499
|
duration?: number;
|
|
468
500
|
durationMs?: number;
|
|
469
|
-
error?: string;
|
|
501
|
+
error?: string | null;
|
|
470
502
|
executionId?: string;
|
|
471
503
|
executionTime?: number;
|
|
472
504
|
id?: string;
|
|
@@ -510,13 +542,21 @@ export type RuntypeFlowSSEEvent = {
|
|
|
510
542
|
totalSteps: number;
|
|
511
543
|
type: "step_skip";
|
|
512
544
|
when: string;
|
|
513
|
-
} | {
|
|
545
|
+
} | ({
|
|
546
|
+
elicitation?: {
|
|
547
|
+
message: string;
|
|
548
|
+
mode: "form" | "url";
|
|
549
|
+
pauseCount?: number;
|
|
550
|
+
requestedSchema?: Record<string, unknown>;
|
|
551
|
+
serverName?: string;
|
|
552
|
+
url?: string;
|
|
553
|
+
};
|
|
514
554
|
executionId?: string;
|
|
515
555
|
reason?: string;
|
|
516
556
|
seq?: number;
|
|
517
557
|
type: "step_await";
|
|
518
558
|
[key: string]: unknown;
|
|
519
|
-
} | ({
|
|
559
|
+
}) | ({
|
|
520
560
|
agentContext?: {
|
|
521
561
|
executionId: string;
|
|
522
562
|
iteration: number;
|
|
@@ -735,6 +775,18 @@ export type RuntypeClientChatRequest = {
|
|
|
735
775
|
export type RuntypeClientChatStreamEvent = RuntypeExecutionStreamEvent;
|
|
736
776
|
|
|
737
777
|
export type RuntypeClientResumeRequest = {
|
|
778
|
+
clientTools?: Array<{
|
|
779
|
+
description: string;
|
|
780
|
+
name: string;
|
|
781
|
+
origin?: "webmcp" | "sdk";
|
|
782
|
+
pageOrigin?: string;
|
|
783
|
+
parametersSchema: {
|
|
784
|
+
type: "object";
|
|
785
|
+
[key: string]: unknown;
|
|
786
|
+
};
|
|
787
|
+
untrustedContentHint?: boolean;
|
|
788
|
+
}>;
|
|
789
|
+
clientToolsFingerprint?: string;
|
|
738
790
|
executionId: string;
|
|
739
791
|
messages?: Array<{
|
|
740
792
|
content: string | (Array<{
|
package/src/index-core.ts
CHANGED
|
@@ -10,8 +10,12 @@ export type {
|
|
|
10
10
|
AgentWidgetFeatureFlags,
|
|
11
11
|
AgentWidgetArtifactsFeature,
|
|
12
12
|
AgentWidgetArtifactsLayoutConfig,
|
|
13
|
+
PersonaArtifactDisplayMode,
|
|
13
14
|
PersonaArtifactKind,
|
|
14
15
|
PersonaArtifactRecord,
|
|
16
|
+
PersonaArtifactActionContext,
|
|
17
|
+
PersonaArtifactStatusLabelContext,
|
|
18
|
+
PersonaArtifactCustomAction,
|
|
15
19
|
PersonaArtifactManualUpsert,
|
|
16
20
|
ArtifactConfigPayload,
|
|
17
21
|
AgentWidgetInitOptions,
|
|
@@ -188,6 +192,17 @@ export {
|
|
|
188
192
|
createDirectivePostprocessor
|
|
189
193
|
} from "./postprocessors";
|
|
190
194
|
export type { MarkdownProcessorOptions } from "./postprocessors";
|
|
195
|
+
// Markdown parser-readiness escape hatch. On the IIFE/CDN build `marked` +
|
|
196
|
+
// `DOMPurify` load lazily, so content injected right after init can render as
|
|
197
|
+
// escaped plain text until the chunk resolves. Hosts can `await
|
|
198
|
+
// loadMarkdownParsers()` before injecting, or `onMarkdownParsersReady(cb)` to be
|
|
199
|
+
// told when they land (both resolve/no-op instantly on the npm build, where the
|
|
200
|
+
// parsers are bundled eagerly).
|
|
201
|
+
export {
|
|
202
|
+
loadMarkdownParsers,
|
|
203
|
+
onMarkdownParsersReady
|
|
204
|
+
} from "./markdown-parsers-loader";
|
|
205
|
+
export type { MarkdownParsersModule } from "./markdown-parsers-loader";
|
|
191
206
|
export {
|
|
192
207
|
createDefaultSanitizer,
|
|
193
208
|
resolveSanitizer
|
|
@@ -361,7 +376,11 @@ export type {
|
|
|
361
376
|
|
|
362
377
|
// Component system exports
|
|
363
378
|
export { componentRegistry } from "./components/registry";
|
|
364
|
-
export type {
|
|
379
|
+
export type {
|
|
380
|
+
ComponentRenderer,
|
|
381
|
+
ComponentContext,
|
|
382
|
+
ComponentRegistrationOptions
|
|
383
|
+
} from "./components/registry";
|
|
365
384
|
export {
|
|
366
385
|
createComponentStreamParser,
|
|
367
386
|
isComponentDirectiveType
|
package/src/index.ts
CHANGED
|
@@ -37,3 +37,11 @@ export type {
|
|
|
37
37
|
DemoCarouselOptions,
|
|
38
38
|
DemoCarouselHandle
|
|
39
39
|
} from "./components/demo-carousel";
|
|
40
|
+
|
|
41
|
+
// Accessible roving-tabindex tablist helper for custom artifact tab bars
|
|
42
|
+
// (features.artifacts.renderTabBar).
|
|
43
|
+
export { createRovingTablist } from "./utils/roving-tablist";
|
|
44
|
+
export type {
|
|
45
|
+
RovingTablistController,
|
|
46
|
+
RovingTablistOptions
|
|
47
|
+
} from "./utils/roving-tablist";
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { MarkdownParsersModule } from "./markdown-parsers-loader";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tests for the parser-ready subscription registry.
|
|
6
|
+
*
|
|
7
|
+
* The loader holds module-level singleton state (`moduleCache`, subscribers), and
|
|
8
|
+
* `vitest.setup.ts` eager-provides parsers into the shared graph — which would make
|
|
9
|
+
* `onMarkdownParsersReady` no-op everywhere. So each test imports a FRESH copy of
|
|
10
|
+
* the loader via `vi.resetModules()` + dynamic import, starting from the unloaded
|
|
11
|
+
* (IIFE/CDN) state, and drives the transition itself.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// A stand-in parsers module; the registry never touches its fields.
|
|
15
|
+
const FAKE_PARSERS = {} as unknown as MarkdownParsersModule;
|
|
16
|
+
|
|
17
|
+
const freshLoader = async () => {
|
|
18
|
+
vi.resetModules();
|
|
19
|
+
return import("./markdown-parsers-loader");
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
vi.restoreAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("onMarkdownParsersReady", () => {
|
|
27
|
+
it("fires a pending subscriber once when the lazy chunk resolves", async () => {
|
|
28
|
+
const loader = await freshLoader();
|
|
29
|
+
loader.setMarkdownParsersLoader(() => Promise.resolve(FAKE_PARSERS));
|
|
30
|
+
|
|
31
|
+
const cb = vi.fn();
|
|
32
|
+
loader.onMarkdownParsersReady(cb);
|
|
33
|
+
expect(cb).not.toHaveBeenCalled(); // not yet: chunk not loaded
|
|
34
|
+
|
|
35
|
+
await loader.loadMarkdownParsers();
|
|
36
|
+
expect(cb).toHaveBeenCalledTimes(1);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("kicks the load itself so a lone surface still heals", async () => {
|
|
40
|
+
const loader = await freshLoader();
|
|
41
|
+
const load = vi.fn(() => Promise.resolve(FAKE_PARSERS));
|
|
42
|
+
loader.setMarkdownParsersLoader(load);
|
|
43
|
+
|
|
44
|
+
const cb = vi.fn();
|
|
45
|
+
// No explicit loadMarkdownParsers() call — subscribing must trigger it.
|
|
46
|
+
loader.onMarkdownParsersReady(cb);
|
|
47
|
+
await vi.waitFor(() => expect(cb).toHaveBeenCalledTimes(1));
|
|
48
|
+
expect(load).toHaveBeenCalledTimes(1);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("does not fire (and returns a no-op) when parsers are already loaded", async () => {
|
|
52
|
+
const loader = await freshLoader();
|
|
53
|
+
loader.provideMarkdownParsers(FAKE_PARSERS); // eager/ESM steady state
|
|
54
|
+
|
|
55
|
+
const cb = vi.fn();
|
|
56
|
+
const unsub = loader.onMarkdownParsersReady(cb);
|
|
57
|
+
await Promise.resolve();
|
|
58
|
+
expect(cb).not.toHaveBeenCalled();
|
|
59
|
+
expect(() => unsub()).not.toThrow();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("unsubscribe prevents the callback from firing", async () => {
|
|
63
|
+
const loader = await freshLoader();
|
|
64
|
+
loader.setMarkdownParsersLoader(() => Promise.resolve(FAKE_PARSERS));
|
|
65
|
+
|
|
66
|
+
const cb = vi.fn();
|
|
67
|
+
const unsub = loader.onMarkdownParsersReady(cb);
|
|
68
|
+
unsub();
|
|
69
|
+
|
|
70
|
+
await loader.loadMarkdownParsers();
|
|
71
|
+
expect(cb).not.toHaveBeenCalled();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("fires each subscriber exactly once (no re-fire on a later load call)", async () => {
|
|
75
|
+
const loader = await freshLoader();
|
|
76
|
+
loader.setMarkdownParsersLoader(() => Promise.resolve(FAKE_PARSERS));
|
|
77
|
+
|
|
78
|
+
const cb = vi.fn();
|
|
79
|
+
loader.onMarkdownParsersReady(cb);
|
|
80
|
+
|
|
81
|
+
await loader.loadMarkdownParsers();
|
|
82
|
+
await loader.loadMarkdownParsers(); // cached; must not re-notify
|
|
83
|
+
expect(cb).toHaveBeenCalledTimes(1);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("provideMarkdownParsers (eager) flushes pending subscribers", async () => {
|
|
87
|
+
const loader = await freshLoader();
|
|
88
|
+
// Subscribe while still unloaded, then have the eager path provide.
|
|
89
|
+
loader.setMarkdownParsersLoader(() => new Promise(() => {})); // never resolves
|
|
90
|
+
const cb = vi.fn();
|
|
91
|
+
loader.onMarkdownParsersReady(cb);
|
|
92
|
+
|
|
93
|
+
loader.provideMarkdownParsers(FAKE_PARSERS);
|
|
94
|
+
expect(cb).toHaveBeenCalledTimes(1);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("keeps pending subscribers across a failed chunk load so a successful retry heals them", async () => {
|
|
98
|
+
const loader = await freshLoader();
|
|
99
|
+
let attempt = 0;
|
|
100
|
+
loader.setMarkdownParsersLoader(() => {
|
|
101
|
+
attempt += 1;
|
|
102
|
+
return attempt === 1
|
|
103
|
+
? Promise.reject(new Error("chunk 404"))
|
|
104
|
+
: Promise.resolve(FAKE_PARSERS);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const cb = vi.fn();
|
|
108
|
+
loader.onMarkdownParsersReady(cb);
|
|
109
|
+
await expect(loader.loadMarkdownParsers()).rejects.toThrow("chunk 404");
|
|
110
|
+
// Failed attempt: not fired, but still registered.
|
|
111
|
+
expect(cb).not.toHaveBeenCalled();
|
|
112
|
+
|
|
113
|
+
// A later subscribe re-triggers the load (rejected promise was not cached)
|
|
114
|
+
// and the retry heals BOTH the new subscriber and the one that rendered
|
|
115
|
+
// escaped during the failed attempt.
|
|
116
|
+
const cb2 = vi.fn();
|
|
117
|
+
loader.onMarkdownParsersReady(cb2);
|
|
118
|
+
await vi.waitFor(() => expect(cb2).toHaveBeenCalledTimes(1));
|
|
119
|
+
expect(cb).toHaveBeenCalledTimes(1);
|
|
120
|
+
expect(attempt).toBe(2);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("unsubscribing after a failed load still prevents firing on the retry", async () => {
|
|
124
|
+
const loader = await freshLoader();
|
|
125
|
+
let attempt = 0;
|
|
126
|
+
loader.setMarkdownParsersLoader(() => {
|
|
127
|
+
attempt += 1;
|
|
128
|
+
return attempt === 1
|
|
129
|
+
? Promise.reject(new Error("chunk 404"))
|
|
130
|
+
: Promise.resolve(FAKE_PARSERS);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const cb = vi.fn();
|
|
134
|
+
const unsub = loader.onMarkdownParsersReady(cb);
|
|
135
|
+
await expect(loader.loadMarkdownParsers()).rejects.toThrow("chunk 404");
|
|
136
|
+
unsub();
|
|
137
|
+
|
|
138
|
+
await loader.loadMarkdownParsers();
|
|
139
|
+
expect(cb).not.toHaveBeenCalled();
|
|
140
|
+
expect(attempt).toBe(2);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("one throwing subscriber does not starve the others", async () => {
|
|
144
|
+
const loader = await freshLoader();
|
|
145
|
+
loader.setMarkdownParsersLoader(() => Promise.resolve(FAKE_PARSERS));
|
|
146
|
+
|
|
147
|
+
const boom = vi.fn(() => {
|
|
148
|
+
throw new Error("subscriber blew up");
|
|
149
|
+
});
|
|
150
|
+
const ok = vi.fn();
|
|
151
|
+
loader.onMarkdownParsersReady(boom);
|
|
152
|
+
loader.onMarkdownParsersReady(ok);
|
|
153
|
+
|
|
154
|
+
await loader.loadMarkdownParsers();
|
|
155
|
+
expect(boom).toHaveBeenCalledTimes(1);
|
|
156
|
+
expect(ok).toHaveBeenCalledTimes(1);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -10,10 +10,66 @@ let loader: (() => Promise<MarkdownParsersModule>) | null = null;
|
|
|
10
10
|
let moduleCache: MarkdownParsersModule | null = null;
|
|
11
11
|
let loadPromise: Promise<MarkdownParsersModule> | null = null;
|
|
12
12
|
|
|
13
|
+
// Surfaces that want to self-heal when the lazy `markdown-parsers.js` chunk
|
|
14
|
+
// lands. See `onMarkdownParsersReady` below for why this is centralized.
|
|
15
|
+
const readySubscribers = new Set<() => void>();
|
|
16
|
+
|
|
17
|
+
// Flip `moduleCache` and fan out to everyone waiting to re-render, exactly once
|
|
18
|
+
// per subscriber. Called from BOTH the eager (`provideMarkdownParsers`) and lazy
|
|
19
|
+
// (`loadMarkdownParsers`) paths so a subscriber can't miss the transition.
|
|
20
|
+
const markParsersReady = (mod: MarkdownParsersModule): MarkdownParsersModule => {
|
|
21
|
+
moduleCache = mod;
|
|
22
|
+
// Snapshot + clear first: fire-once semantics, and a callback that re-subscribes
|
|
23
|
+
// (it won't — `onMarkdownParsersReady` no-ops once ready) can't loop.
|
|
24
|
+
const subs = [...readySubscribers];
|
|
25
|
+
readySubscribers.clear();
|
|
26
|
+
for (const cb of subs) {
|
|
27
|
+
// One bad subscriber must not starve the others (or leave messages escaped).
|
|
28
|
+
try {
|
|
29
|
+
cb();
|
|
30
|
+
} catch {
|
|
31
|
+
/* subscriber threw: swallow so the remaining re-renders still run */
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return mod;
|
|
35
|
+
};
|
|
36
|
+
|
|
13
37
|
export const setMarkdownParsersLoader = (l: () => Promise<MarkdownParsersModule>) => {
|
|
14
38
|
loader = l;
|
|
15
39
|
};
|
|
16
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Register `cb` to run once the markdown parsers (marked + DOMPurify) become
|
|
43
|
+
* available, i.e. when the lazy `markdown-parsers.js` chunk resolves on the
|
|
44
|
+
* IIFE/CDN build. Returns an unsubscribe function.
|
|
45
|
+
*
|
|
46
|
+
* This exists so every markdown render surface (chat messages, artifact pane,
|
|
47
|
+
* and any future one) shares a SINGLE self-heal path instead of each wiring its
|
|
48
|
+
* own `loadMarkdownParsers().then(reRender)`. Before this, a new surface that
|
|
49
|
+
* forgot to do that rendered escaped plain text until a user interaction forced
|
|
50
|
+
* a re-render — the recurring first-render bug (chat messages, then the artifact
|
|
51
|
+
* pane) this centralizes away.
|
|
52
|
+
*
|
|
53
|
+
* If the parsers are ALREADY loaded, `cb` is not scheduled and a no-op
|
|
54
|
+
* unsubscribe is returned: the caller's first render already used real markdown,
|
|
55
|
+
* so there is nothing to heal (this is the ESM/CJS build's steady state, and the
|
|
56
|
+
* CDN build's state after the first chunk load). Registering also kicks the load
|
|
57
|
+
* so a surface that renders before anything else triggers it still heals.
|
|
58
|
+
* Fires at most once per subscription.
|
|
59
|
+
*/
|
|
60
|
+
export const onMarkdownParsersReady = (cb: () => void): (() => void) => {
|
|
61
|
+
if (moduleCache) return () => {};
|
|
62
|
+
readySubscribers.add(cb);
|
|
63
|
+
// Ensure the chunk is actually being fetched; harmless if already in flight.
|
|
64
|
+
// Swallow rejection here — on failure the subscriber stays registered (see
|
|
65
|
+
// `onLoadFailure`) and the surface keeps its escaped fallback until a retry
|
|
66
|
+
// succeeds; this catch only avoids an unhandled rejection.
|
|
67
|
+
void loadMarkdownParsers().catch(() => {});
|
|
68
|
+
return () => {
|
|
69
|
+
readySubscribers.delete(cb);
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
|
|
17
73
|
/**
|
|
18
74
|
* Register the parsers synchronously. Used by the ESM/CJS build (where `marked`
|
|
19
75
|
* and `dompurify` are bundled directly via `markdown-parsers-eager.ts`), so
|
|
@@ -23,7 +79,22 @@ export const setMarkdownParsersLoader = (l: () => Promise<MarkdownParsersModule>
|
|
|
23
79
|
* it lazy-loads the `markdown-parsers.js` chunk instead.
|
|
24
80
|
*/
|
|
25
81
|
export const provideMarkdownParsers = (mod: MarkdownParsersModule): void => {
|
|
26
|
-
|
|
82
|
+
markParsersReady(mod);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// On a failed chunk load (404, ad blocker, offline), don't cache the rejected
|
|
86
|
+
// promise: reset it so a later subscribe/load call can retry. Pending
|
|
87
|
+
// subscribers are KEPT — a surface that rendered escaped during the failed
|
|
88
|
+
// attempt must still heal when a retry (kicked by a new subscriber or an
|
|
89
|
+
// explicit loadMarkdownParsers() call) succeeds; dropping them here left that
|
|
90
|
+
// surface escaped forever after a transient failure. They're only retained
|
|
91
|
+
// while the parsers stay unloaded — success releases them (fire-once in
|
|
92
|
+
// `markParsersReady`), and callers can unsubscribe. On a permanently failing
|
|
93
|
+
// setup they persist for the page's life, which is the price of healing.
|
|
94
|
+
// Re-throw so awaiters of loadMarkdownParsers() still observe the rejection.
|
|
95
|
+
const onLoadFailure = (err: unknown): never => {
|
|
96
|
+
loadPromise = null;
|
|
97
|
+
throw err;
|
|
27
98
|
};
|
|
28
99
|
|
|
29
100
|
export const loadMarkdownParsers = (): Promise<MarkdownParsersModule> => {
|
|
@@ -31,16 +102,10 @@ export const loadMarkdownParsers = (): Promise<MarkdownParsersModule> => {
|
|
|
31
102
|
if (loadPromise) return loadPromise;
|
|
32
103
|
if (!loader) {
|
|
33
104
|
// Fallback for regular ESM/CJS consumers (they import directly)
|
|
34
|
-
loadPromise = import("./markdown-parsers-entry").then(
|
|
35
|
-
moduleCache = mod;
|
|
36
|
-
return mod;
|
|
37
|
-
});
|
|
105
|
+
loadPromise = import("./markdown-parsers-entry").then(markParsersReady, onLoadFailure);
|
|
38
106
|
return loadPromise;
|
|
39
107
|
}
|
|
40
|
-
loadPromise = loader().then(
|
|
41
|
-
moduleCache = mod;
|
|
42
|
-
return mod;
|
|
43
|
-
});
|
|
108
|
+
loadPromise = loader().then(markParsersReady, onLoadFailure);
|
|
44
109
|
return loadPromise;
|
|
45
110
|
};
|
|
46
111
|
|