@runtypelabs/persona 4.7.0 → 4.9.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 +1 -0
- 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-4ROVJ1gA.d.cts} +85 -3
- package/dist/animations/{types-CSmiKRVa.d.ts → types-4ROVJ1gA.d.ts} +85 -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/chunk-IO5VVUKP.js +3 -0
- package/dist/chunk-IPVK3KOM.js +1 -0
- package/dist/chunk-UPO4GUFC.js +1 -0
- package/dist/codegen.cjs +13 -13
- package/dist/codegen.js +14 -14
- package/dist/context-mentions-7S5KVUTG.js +169 -0
- package/dist/context-mentions-inline-TTCN7ZM2.js +4 -0
- package/dist/context-mentions-inline.cjs +4 -0
- package/dist/context-mentions-inline.d.cts +203 -0
- package/dist/context-mentions-inline.d.ts +203 -0
- package/dist/context-mentions-inline.js +4 -0
- package/dist/context-mentions.cjs +295 -0
- package/dist/context-mentions.d.cts +7025 -0
- package/dist/context-mentions.d.ts +7025 -0
- package/dist/context-mentions.js +295 -0
- package/dist/index.cjs +99 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1507 -182
- package/dist/index.d.ts +1507 -182
- package/dist/index.global.js +88 -56
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +99 -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.d.cts +17 -0
- package/dist/plugin-kit.d.ts +17 -0
- 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 +18 -17
- package/dist/smart-dom-reader.d.cts +1260 -16
- package/dist/smart-dom-reader.d.ts +1260 -16
- package/dist/smart-dom-reader.js +18 -17
- package/dist/testing.cjs +3 -3
- package/dist/testing.js +3 -3
- package/dist/theme-editor-preview.cjs +260 -58
- package/dist/theme-editor-preview.d.cts +1234 -15
- package/dist/theme-editor-preview.d.ts +1234 -15
- package/dist/theme-editor-preview.js +87 -58
- package/dist/theme-editor.cjs +10 -10
- package/dist/theme-editor.d.cts +1226 -15
- package/dist/theme-editor.d.ts +1226 -15
- package/dist/theme-editor.js +10 -10
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +76 -0
- package/dist/theme-reference.d.ts +76 -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 +16 -5
- package/src/artifacts-session.test.ts +178 -0
- package/src/client.test.ts +255 -1
- package/src/client.ts +157 -60
- 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/composer-parts.ts +3 -12
- package/src/components/context-mention-button.test.ts +70 -0
- package/src/components/context-mention-button.ts +82 -0
- package/src/components/context-mention-chip.ts +134 -0
- package/src/components/context-mention-menu.test.ts +508 -0
- package/src/components/context-mention-menu.ts +0 -0
- package/src/components/message-bubble.test.ts +175 -0
- package/src/components/message-bubble.ts +177 -19
- package/src/components/panel.ts +7 -10
- 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/context-mentions-bundle.test.ts +163 -0
- package/src/context-mentions-entry.ts +185 -0
- package/src/context-mentions-inline-entry.test.ts +136 -0
- package/src/context-mentions-inline-entry.ts +226 -0
- package/src/context-mentions-inline-loader.test.ts +30 -0
- package/src/context-mentions-inline-loader.ts +36 -0
- package/src/context-mentions-inline.ts +15 -0
- package/src/context-mentions-loader.ts +32 -0
- package/src/context-mentions.ts +16 -0
- package/src/defaults.ts +1 -0
- package/src/generated/runtype-openapi-contract.ts +55 -3
- package/src/index-core.ts +46 -1
- package/src/index-global.ts +51 -0
- package/src/index.ts +8 -0
- package/src/markdown-parsers-loader.test.ts +158 -0
- package/src/markdown-parsers-loader.ts +83 -27
- package/src/plugin-kit.test.ts +40 -0
- package/src/plugin-kit.ts +39 -5
- 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.mentions.test.ts +175 -0
- package/src/session.test.ts +52 -4
- package/src/session.ts +197 -27
- package/src/smart-dom-reader.test.ts +129 -2
- package/src/smart-dom-reader.ts +127 -1
- package/src/styles/context-mention-menu-css.ts +176 -0
- package/src/styles/widget.css +1002 -152
- package/src/theme-editor/preview.ts +2 -0
- package/src/theme-editor/sections.test.ts +26 -1
- package/src/theme-editor/sections.ts +13 -5
- package/src/theme-reference.ts +2 -2
- package/src/tool-call-display-defaults.test.ts +1 -0
- package/src/types/theme.ts +79 -0
- package/src/types.ts +1041 -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.mention-submit.test.ts +235 -0
- package/src/ui.tool-display.test.ts +51 -0
- package/src/ui.ts +998 -159
- 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/chunk-loader.test.ts +97 -0
- package/src/utils/chunk-loader.ts +88 -0
- package/src/utils/code-highlight.test.ts +186 -0
- package/src/utils/code-highlight.ts +400 -0
- package/src/utils/composer-contenteditable.test.ts +507 -0
- package/src/utils/composer-contenteditable.ts +626 -0
- package/src/utils/composer-document.test.ts +280 -0
- package/src/utils/composer-document.ts +293 -0
- package/src/utils/composer-history.test.ts +35 -7
- package/src/utils/composer-history.ts +30 -20
- package/src/utils/composer-input.ts +159 -0
- package/src/utils/context-mention-controller.test.ts +1215 -0
- package/src/utils/context-mention-controller.ts +1186 -0
- package/src/utils/context-mention-manager.test.ts +422 -0
- package/src/utils/context-mention-manager.ts +410 -0
- package/src/utils/context-mention-orchestrator.test.ts +538 -0
- package/src/utils/context-mention-orchestrator.ts +348 -0
- package/src/utils/icons.ts +2 -0
- package/src/utils/live-region.test.ts +108 -0
- package/src/utils/live-region.ts +94 -0
- package/src/utils/mention-channels.ts +63 -0
- package/src/utils/mention-llm-format.test.ts +91 -0
- package/src/utils/mention-llm-format.ts +79 -0
- package/src/utils/mention-matcher.test.ts +86 -0
- package/src/utils/mention-matcher.ts +221 -0
- package/src/utils/mention-token.ts +72 -0
- package/src/utils/mention-trigger.test.ts +155 -0
- package/src/utils/mention-trigger.ts +156 -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 +76 -4
- package/src/utils/theme.ts +7 -0
- package/src/utils/tokens.ts +118 -11
- 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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createElement } from "../utils/dom";
|
|
1
|
+
import { createElement, createNode } from "../utils/dom";
|
|
2
2
|
import {
|
|
3
3
|
AgentWidgetMessage,
|
|
4
4
|
AgentWidgetMessageLayoutConfig,
|
|
@@ -11,9 +11,14 @@ import {
|
|
|
11
11
|
AudioContentPart,
|
|
12
12
|
VideoContentPart,
|
|
13
13
|
FileContentPart,
|
|
14
|
+
AgentWidgetContextMentionRef,
|
|
15
|
+
AgentWidgetContentSegment,
|
|
16
|
+
AgentWidgetContextMentionTokenRenderContext,
|
|
14
17
|
StopReasonKind
|
|
15
18
|
} from "../types";
|
|
19
|
+
import { createMentionTokenElement } from "../utils/mention-token";
|
|
16
20
|
import { createIconButton } from "../utils/buttons";
|
|
21
|
+
import { renderLucideIcon } from "../utils/icons";
|
|
17
22
|
import { IMAGE_ONLY_MESSAGE_FALLBACK_TEXT } from "../utils/content";
|
|
18
23
|
import {
|
|
19
24
|
applyStreamBuffer,
|
|
@@ -186,6 +191,126 @@ const getMessageFileParts = (message: AgentWidgetMessage): FileContentPart[] =>
|
|
|
186
191
|
);
|
|
187
192
|
};
|
|
188
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Read-only context-mention pills for a sent user bubble (from
|
|
196
|
+
* `message.contextMentions`). Same compact pill look as the composer chips, but
|
|
197
|
+
* no spinner / remove control. Returns null when the message carries no mentions.
|
|
198
|
+
*/
|
|
199
|
+
const createMessageMentionChips = (
|
|
200
|
+
mentions: AgentWidgetContextMentionRef[] | undefined
|
|
201
|
+
): HTMLElement | null => {
|
|
202
|
+
if (!mentions || mentions.length === 0) return null;
|
|
203
|
+
const row = createNode("div", {
|
|
204
|
+
className: "persona-mention-context-row persona-message-mentions",
|
|
205
|
+
attrs: { "data-message-mentions": "" },
|
|
206
|
+
});
|
|
207
|
+
row.style.display = "flex";
|
|
208
|
+
for (const ref of mentions) {
|
|
209
|
+
const chip = createNode("div", {
|
|
210
|
+
className: "persona-mention-chip persona-mention-chip-readonly",
|
|
211
|
+
attrs: { "data-status": "ready", title: ref.label },
|
|
212
|
+
});
|
|
213
|
+
const iconHost = createElement("span", "persona-mention-chip-icon");
|
|
214
|
+
const icon = renderLucideIcon(ref.iconName ?? "at-sign", 13, "currentColor", 2);
|
|
215
|
+
if (icon) iconHost.appendChild(icon);
|
|
216
|
+
chip.appendChild(iconHost);
|
|
217
|
+
chip.appendChild(
|
|
218
|
+
createNode("span", { className: "persona-mention-chip-label", text: ref.label })
|
|
219
|
+
);
|
|
220
|
+
row.appendChild(chip);
|
|
221
|
+
}
|
|
222
|
+
return row;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Render a sent user bubble's prose with atomic mention tokens in place (inline
|
|
227
|
+
* display mode, from `message.contentSegments`). Text runs become text nodes;
|
|
228
|
+
* mention segments become read-only pill tokens (shared with the composer, so a
|
|
229
|
+
* host `renderMentionToken` + per-item `color` apply here too). Display/transcript
|
|
230
|
+
* concern only — the model saw the resolved bodies via `llmContent`. Returns a
|
|
231
|
+
* document fragment for `replaceChildren`.
|
|
232
|
+
*/
|
|
233
|
+
export const createMessageInlineMentions = (
|
|
234
|
+
segments: AgentWidgetContentSegment[],
|
|
235
|
+
render?: (
|
|
236
|
+
ctx: AgentWidgetContextMentionTokenRenderContext
|
|
237
|
+
) => HTMLElement
|
|
238
|
+
): DocumentFragment => {
|
|
239
|
+
const frag = document.createDocumentFragment();
|
|
240
|
+
for (const seg of segments) {
|
|
241
|
+
if (seg.kind === "text") {
|
|
242
|
+
frag.appendChild(document.createTextNode(seg.text));
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
frag.appendChild(
|
|
246
|
+
createMentionTokenElement(seg.ref, { readonly: true, render })
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
return frag;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
/* PUA sentinels survive escapeHtml, markdown, and DOMPurify as plain text. */
|
|
253
|
+
const MENTION_PH_OPEN = "\uE000";
|
|
254
|
+
const MENTION_PH_CLOSE = "\uE001";
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Render segmented prose through the full message transform (actions, markdown,
|
|
258
|
+
* postprocess, sanitize): mention slots ride the pipeline as placeholder
|
|
259
|
+
* sentinels and are swapped for atomic token elements in the parsed output.
|
|
260
|
+
* Falls back to verbatim segment rendering when a transform drops or rewrites
|
|
261
|
+
* a slot, so a mention is never lost.
|
|
262
|
+
*/
|
|
263
|
+
export const renderSegmentsWithTransform = (
|
|
264
|
+
contentDiv: HTMLElement,
|
|
265
|
+
segments: AgentWidgetContentSegment[],
|
|
266
|
+
transformSource: (source: string) => string,
|
|
267
|
+
render?: (
|
|
268
|
+
ctx: AgentWidgetContextMentionTokenRenderContext
|
|
269
|
+
) => HTMLElement
|
|
270
|
+
): void => {
|
|
271
|
+
// Nonce keeps user-typed sentinel lookalikes from matching a real slot.
|
|
272
|
+
const nonce = Math.random().toString(36).slice(2, 8);
|
|
273
|
+
const placeholder = (i: number) =>
|
|
274
|
+
`${MENTION_PH_OPEN}${nonce}:${i}${MENTION_PH_CLOSE}`;
|
|
275
|
+
const source = segments
|
|
276
|
+
.map((seg, i) => (seg.kind === "text" ? seg.text : placeholder(i)))
|
|
277
|
+
.join("");
|
|
278
|
+
const html = transformSource(source);
|
|
279
|
+
// String-level check runs before the parse: a slot that was dropped,
|
|
280
|
+
// entity-encoded, or split by an inserted tag fails `includes`.
|
|
281
|
+
const survived = segments.every(
|
|
282
|
+
(seg, i) => seg.kind === "text" || html.includes(placeholder(i))
|
|
283
|
+
);
|
|
284
|
+
if (!survived) {
|
|
285
|
+
contentDiv.replaceChildren(createMessageInlineMentions(segments, render));
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
contentDiv.innerHTML = html;
|
|
289
|
+
const pattern = new RegExp(
|
|
290
|
+
`${MENTION_PH_OPEN}${nonce}:(\\d+)${MENTION_PH_CLOSE}`
|
|
291
|
+
);
|
|
292
|
+
const walker = document.createTreeWalker(contentDiv, NodeFilter.SHOW_TEXT);
|
|
293
|
+
const textNodes: Text[] = [];
|
|
294
|
+
for (let n = walker.nextNode(); n; n = walker.nextNode()) {
|
|
295
|
+
textNodes.push(n as Text);
|
|
296
|
+
}
|
|
297
|
+
for (const node of textNodes) {
|
|
298
|
+
let current = node;
|
|
299
|
+
for (let match = pattern.exec(current.data); match; match = pattern.exec(current.data)) {
|
|
300
|
+
const seg = segments[Number(match[1])];
|
|
301
|
+
const after = current.splitText(match.index);
|
|
302
|
+
after.data = after.data.slice(match[0].length);
|
|
303
|
+
if (seg?.kind === "mention") {
|
|
304
|
+
after.parentNode?.insertBefore(
|
|
305
|
+
createMentionTokenElement(seg.ref, { readonly: true, render }),
|
|
306
|
+
after
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
current = after;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
189
314
|
const createMessageImagePreviews = (
|
|
190
315
|
imageParts: ImageContentPart[],
|
|
191
316
|
hasVisibleText: boolean,
|
|
@@ -820,33 +945,57 @@ export const createStandardBubble = (
|
|
|
820
945
|
)
|
|
821
946
|
: (message.content ?? "");
|
|
822
947
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
});
|
|
835
|
-
} else if (streamAnimationActive && streamPlugin?.wrap === "word") {
|
|
836
|
-
animatedContent = wrapStreamAnimation(transformedContent, "word", message.id, {
|
|
837
|
-
skipTags: streamPlugin.skipTags,
|
|
948
|
+
// Feeds the two text branches below; the inline-mention branch calls the
|
|
949
|
+
// transform itself with a placeholder-bearing source built from
|
|
950
|
+
// `contentSegments` instead of `bufferedContent`. Kept lazy so that branch
|
|
951
|
+
// doesn't pay for output it never reads. `bufferedContent` stays eager —
|
|
952
|
+
// the streaming skeleton logic reads it.
|
|
953
|
+
const computeAnimatedContent = (): string => {
|
|
954
|
+
const transformedContent = transform({
|
|
955
|
+
text: bufferedContent,
|
|
956
|
+
message,
|
|
957
|
+
streaming: Boolean(message.streaming),
|
|
958
|
+
raw: message.rawContent
|
|
838
959
|
});
|
|
839
|
-
|
|
960
|
+
if (streamAnimationActive && streamPlugin?.wrap === "char") {
|
|
961
|
+
return wrapStreamAnimation(transformedContent, "char", message.id, {
|
|
962
|
+
skipTags: streamPlugin.skipTags,
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
if (streamAnimationActive && streamPlugin?.wrap === "word") {
|
|
966
|
+
return wrapStreamAnimation(transformedContent, "word", message.id, {
|
|
967
|
+
skipTags: streamPlugin.skipTags,
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
return transformedContent;
|
|
971
|
+
};
|
|
840
972
|
|
|
841
973
|
let textContentDiv: HTMLElement | null = null;
|
|
842
974
|
|
|
843
975
|
if (shouldHideTextUntilPreviewFails) {
|
|
844
976
|
textContentDiv = document.createElement("div");
|
|
845
|
-
textContentDiv.innerHTML =
|
|
977
|
+
textContentDiv.innerHTML = computeAnimatedContent();
|
|
846
978
|
textContentDiv.style.display = "none";
|
|
847
979
|
contentDiv.appendChild(textContentDiv);
|
|
980
|
+
} else if (message.contentSegments?.length) {
|
|
981
|
+
// Inline-mention user bubble: prose runs through the same transform
|
|
982
|
+
// pipeline as every other bubble (markdown/postprocess/sanitize apply);
|
|
983
|
+
// mention slots ride through as placeholders and come back as atomic
|
|
984
|
+
// tokens. The resolved context reached the model via `llmContent`.
|
|
985
|
+
renderSegmentsWithTransform(
|
|
986
|
+
contentDiv,
|
|
987
|
+
message.contentSegments,
|
|
988
|
+
(source) =>
|
|
989
|
+
transform({
|
|
990
|
+
text: source,
|
|
991
|
+
message,
|
|
992
|
+
streaming: Boolean(message.streaming),
|
|
993
|
+
raw: message.rawContent,
|
|
994
|
+
}),
|
|
995
|
+
options?.widgetConfig?.contextMentions?.renderMentionToken
|
|
996
|
+
);
|
|
848
997
|
} else {
|
|
849
|
-
contentDiv.innerHTML =
|
|
998
|
+
contentDiv.innerHTML = computeAnimatedContent();
|
|
850
999
|
}
|
|
851
1000
|
|
|
852
1001
|
if (
|
|
@@ -931,6 +1080,15 @@ export const createStandardBubble = (
|
|
|
931
1080
|
}
|
|
932
1081
|
}
|
|
933
1082
|
|
|
1083
|
+
// Context mention pills (read-only) for the sent user bubble. Skipped in inline
|
|
1084
|
+
// mode — the tokens render within the prose (above) instead of a separate row.
|
|
1085
|
+
const mentionChips = message.contentSegments?.length
|
|
1086
|
+
? null
|
|
1087
|
+
: createMessageMentionChips(message.contextMentions);
|
|
1088
|
+
if (mentionChips) {
|
|
1089
|
+
bubble.appendChild(mentionChips);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
934
1092
|
bubble.appendChild(contentDiv);
|
|
935
1093
|
|
|
936
1094
|
// Add timestamp below if configured
|
package/src/components/panel.ts
CHANGED
|
@@ -282,10 +282,8 @@ const buildComposerBarPanel = (
|
|
|
282
282
|
className: "persona-rounded-2xl persona-p-6",
|
|
283
283
|
attrs: { "data-persona-intro-card": "" },
|
|
284
284
|
style: {
|
|
285
|
-
background:
|
|
286
|
-
|
|
287
|
-
boxShadow:
|
|
288
|
-
"var(--persona-intro-card-shadow, 0 5px 15px rgba(15, 23, 42, 0.08))",
|
|
285
|
+
background: "var(--persona-intro-card-bg, transparent)",
|
|
286
|
+
boxShadow: "var(--persona-intro-card-shadow, none)",
|
|
289
287
|
},
|
|
290
288
|
},
|
|
291
289
|
introTitle,
|
|
@@ -416,20 +414,19 @@ export const buildPanel = (config?: AgentWidgetConfig, showClose = true): PanelE
|
|
|
416
414
|
"Ask anything about your account or products.",
|
|
417
415
|
});
|
|
418
416
|
// Background and box-shadow flow through the themable `components.introCard`
|
|
419
|
-
// tokens (--persona-intro-card-bg / --persona-intro-card-shadow)
|
|
420
|
-
//
|
|
421
|
-
// when
|
|
417
|
+
// tokens (--persona-intro-card-bg / --persona-intro-card-shadow); both
|
|
418
|
+
// default to flat (transparent / none). Docked mode always stays flat,
|
|
419
|
+
// even when a theme sets an introCard shadow.
|
|
422
420
|
const introCard = createNode(
|
|
423
421
|
"div",
|
|
424
422
|
{
|
|
425
423
|
className: "persona-rounded-2xl persona-p-6",
|
|
426
424
|
attrs: { "data-persona-intro-card": "" },
|
|
427
425
|
style: {
|
|
428
|
-
background:
|
|
429
|
-
"var(--persona-intro-card-bg, var(--persona-surface, #ffffff))",
|
|
426
|
+
background: "var(--persona-intro-card-bg, transparent)",
|
|
430
427
|
boxShadow: isDockedMountMode(config)
|
|
431
428
|
? "none"
|
|
432
|
-
: "var(--persona-intro-card-shadow,
|
|
429
|
+
: "var(--persona-intro-card-shadow, none)",
|
|
433
430
|
},
|
|
434
431
|
},
|
|
435
432
|
introTitle,
|
|
@@ -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~
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Bundle guard for the lazy context-mentions chunk (see
|
|
7
|
+
* `docs/context-mentions-plan.md`, Bundle strategy).
|
|
8
|
+
*
|
|
9
|
+
* The mention runtime (controller/manager/menu) must NOT land in ANY core bundle
|
|
10
|
+
* shipped to consumers — neither the CDN IIFE (`dist/index.global.js`) nor the
|
|
11
|
+
* ESM/CJS bundles (`dist/index.js` / `dist/index.cjs`). It ships as the sibling
|
|
12
|
+
* chunk `dist/context-mentions.{js,cjs}`, loaded on demand: the IIFE via a
|
|
13
|
+
* sibling URL, ESM/CJS via the external `@runtypelabs/persona/context-mentions`
|
|
14
|
+
* subpath. A stray static import would pull the runtime back in; this fails then.
|
|
15
|
+
*
|
|
16
|
+
* Skips when `dist/` hasn't been built (e.g. a test-only CI step).
|
|
17
|
+
*/
|
|
18
|
+
const dist = (f: string) => resolve(__dirname, "..", "dist", f);
|
|
19
|
+
|
|
20
|
+
const RUNTIME_MARKERS = [
|
|
21
|
+
"persona-mention-menu", // menu component class — chunk-only
|
|
22
|
+
"persona-mention-group-header", // menu component class — chunk-only
|
|
23
|
+
// Slash-command dispatch lives in the controller (chunk-only); this literal
|
|
24
|
+
// is unique to the command path and must not leak into a core bundle.
|
|
25
|
+
"context-mention prompt resolve failed",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const SUBPATH = "@runtypelabs/persona/context-mentions";
|
|
29
|
+
|
|
30
|
+
// Inline (contenteditable) runtime markers — unique to the inline chunk. The
|
|
31
|
+
// composer contenteditable class is set only by the inline adapter/entry (in
|
|
32
|
+
// widget.css it's a stylesheet rule, not a JS-bundle string), so its presence in
|
|
33
|
+
// a JS bundle means the contenteditable engine leaked in.
|
|
34
|
+
const INLINE_RUNTIME_MARKERS = ["persona-composer-contenteditable"];
|
|
35
|
+
const INLINE_SUBPATH = "@runtypelabs/persona/context-mentions-inline";
|
|
36
|
+
|
|
37
|
+
describe("context-mentions bundle split", () => {
|
|
38
|
+
const iifeBuilt =
|
|
39
|
+
existsSync(dist("index.global.js")) && existsSync(dist("context-mentions.js"));
|
|
40
|
+
const esmBuilt = existsSync(dist("index.js")) && existsSync(dist("index.cjs"));
|
|
41
|
+
|
|
42
|
+
it.runIf(iifeBuilt)("keeps the mention runtime OUT of the core IIFE bundle", () => {
|
|
43
|
+
const core = readFileSync(dist("index.global.js"), "utf8");
|
|
44
|
+
for (const marker of RUNTIME_MARKERS) {
|
|
45
|
+
expect(core.includes(marker), `IIFE bundle unexpectedly contains "${marker}"`).toBe(
|
|
46
|
+
false
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
// The loader stub (sibling-URL reference) must remain so the chunk can load.
|
|
50
|
+
expect(core).toContain("context-mentions.js");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it.runIf(esmBuilt)("keeps the mention runtime OUT of the ESM/CJS bundles", () => {
|
|
54
|
+
for (const file of ["index.js", "index.cjs"]) {
|
|
55
|
+
const core = readFileSync(dist(file), "utf8");
|
|
56
|
+
for (const marker of RUNTIME_MARKERS) {
|
|
57
|
+
expect(core.includes(marker), `${file} unexpectedly contains "${marker}"`).toBe(
|
|
58
|
+
false
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
// The runtime is reached via the external subpath, not inlined.
|
|
62
|
+
expect(core.includes(SUBPATH), `${file} must import the external subpath`).toBe(
|
|
63
|
+
true
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it.runIf(iifeBuilt)("ships the mention + slash-command runtime in the sibling chunk", () => {
|
|
69
|
+
const chunk = readFileSync(dist("context-mentions.js"), "utf8");
|
|
70
|
+
expect(chunk).toContain("persona-mention-menu");
|
|
71
|
+
// Slash-command dispatch ships in the chunk, not the core.
|
|
72
|
+
expect(chunk).toContain("context-mention prompt resolve failed");
|
|
73
|
+
// The menu CSS rides with the chunk (injected on open), not eager widget.css.
|
|
74
|
+
expect(chunk).toContain(".persona-mention-option[data-active");
|
|
75
|
+
// A CJS twin exists for require()-based consumers.
|
|
76
|
+
expect(existsSync(dist("context-mentions.cjs"))).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// --- inline (contenteditable) chunk guards --------------------------------
|
|
80
|
+
|
|
81
|
+
const inlineBuilt = existsSync(dist("context-mentions-inline.js"));
|
|
82
|
+
|
|
83
|
+
it.runIf(iifeBuilt)(
|
|
84
|
+
"keeps the inline contenteditable engine OUT of the core IIFE bundle",
|
|
85
|
+
() => {
|
|
86
|
+
const core = readFileSync(dist("index.global.js"), "utf8");
|
|
87
|
+
for (const marker of INLINE_RUNTIME_MARKERS) {
|
|
88
|
+
expect(
|
|
89
|
+
core.includes(marker),
|
|
90
|
+
`IIFE bundle unexpectedly contains inline marker "${marker}"`
|
|
91
|
+
).toBe(false);
|
|
92
|
+
}
|
|
93
|
+
// The inline loader stub must remain so the chunk can load on mount.
|
|
94
|
+
expect(core).toContain("context-mentions-inline.js");
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
it.runIf(esmBuilt)(
|
|
99
|
+
"keeps the inline contenteditable engine OUT of the ESM/CJS bundles",
|
|
100
|
+
() => {
|
|
101
|
+
for (const file of ["index.js", "index.cjs"]) {
|
|
102
|
+
const core = readFileSync(dist(file), "utf8");
|
|
103
|
+
for (const marker of INLINE_RUNTIME_MARKERS) {
|
|
104
|
+
expect(
|
|
105
|
+
core.includes(marker),
|
|
106
|
+
`${file} unexpectedly contains inline marker "${marker}"`
|
|
107
|
+
).toBe(false);
|
|
108
|
+
}
|
|
109
|
+
// Reached via the external subpath, not inlined.
|
|
110
|
+
expect(
|
|
111
|
+
core.includes(INLINE_SUBPATH),
|
|
112
|
+
`${file} must import the external inline subpath`
|
|
113
|
+
).toBe(true);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
it.runIf(inlineBuilt)(
|
|
119
|
+
"ships the contenteditable engine in the inline chunk (with a CJS twin)",
|
|
120
|
+
() => {
|
|
121
|
+
const chunk = readFileSync(dist("context-mentions-inline.js"), "utf8");
|
|
122
|
+
expect(chunk).toContain("persona-composer-contenteditable");
|
|
123
|
+
expect(existsSync(dist("context-mentions-inline.cjs"))).toBe(true);
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
it.runIf(iifeBuilt)(
|
|
128
|
+
"keeps the contenteditable engine OUT of the chip chunk (capability-only reach)",
|
|
129
|
+
() => {
|
|
130
|
+
// The chip controller reaches token insertion ONLY via the capability's
|
|
131
|
+
// `insertMentionAtTrigger`, so the chip chunk must never pull the document
|
|
132
|
+
// model / contenteditable adapter. This guard is what keeps that true.
|
|
133
|
+
const chip = readFileSync(dist("context-mentions.js"), "utf8");
|
|
134
|
+
for (const marker of INLINE_RUNTIME_MARKERS) {
|
|
135
|
+
expect(
|
|
136
|
+
chip.includes(marker),
|
|
137
|
+
`chip chunk unexpectedly contains inline marker "${marker}"`
|
|
138
|
+
).toBe(false);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
it.runIf(existsSync(dist("widget.css")))(
|
|
144
|
+
"keeps menu CSS out of the eager stylesheet but keeps chip CSS",
|
|
145
|
+
() => {
|
|
146
|
+
const css = readFileSync(dist("widget.css"), "utf8");
|
|
147
|
+
expect(css).toContain(".persona-mention-chip"); // eager chip pieces stay
|
|
148
|
+
expect(css.includes(".persona-mention-menu")).toBe(false); // menu moved to chunk
|
|
149
|
+
expect(css.includes(".persona-mention-option")).toBe(false);
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
it.runIf(esmBuilt)("ships no lookbehind regex (Safari < 16.4 parse guard)", () => {
|
|
154
|
+
// A lookbehind literal anywhere in a core bundle is a parse-time SyntaxError
|
|
155
|
+
// on older Safari, breaking the whole widget even with mentions disabled.
|
|
156
|
+
for (const file of ["index.js", "index.cjs", "index.global.js"]) {
|
|
157
|
+
if (!existsSync(dist(file))) continue;
|
|
158
|
+
expect(readFileSync(dist(file), "utf8").includes("(?<="), `${file} has a lookbehind`).toBe(
|
|
159
|
+
false
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
});
|