@runtypelabs/persona 3.34.1 → 3.35.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 +11 -11
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-CthJFfNx.d.cts → types-DgYsuwXL.d.cts} +15 -14
- package/dist/animations/{types-CthJFfNx.d.ts → types-DgYsuwXL.d.ts} +15 -14
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/codegen.cjs +11 -11
- package/dist/codegen.js +10 -10
- package/dist/index.cjs +46 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +514 -158
- package/dist/index.d.ts +514 -158
- package/dist/index.global.js +41 -89
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +46 -44
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/launcher.global.js +2 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/markdown-parsers.js +53 -0
- package/dist/plugin-kit.d.cts +4 -4
- package/dist/plugin-kit.d.ts +4 -4
- package/dist/runtype-tts.js +1 -0
- package/dist/smart-dom-reader.cjs +1 -1
- package/dist/smart-dom-reader.d.cts +352 -103
- package/dist/smart-dom-reader.d.ts +352 -103
- package/dist/smart-dom-reader.js +1 -1
- package/dist/theme-editor-preview.cjs +203 -0
- package/dist/theme-editor-preview.d.cts +5748 -0
- package/dist/theme-editor-preview.d.ts +5748 -0
- package/dist/theme-editor-preview.js +203 -0
- package/dist/theme-editor.cjs +14 -131
- package/dist/theme-editor.d.cts +362 -574
- package/dist/theme-editor.d.ts +362 -574
- package/dist/theme-editor.js +14 -131
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +1 -1
- package/dist/theme-reference.d.ts +1 -1
- package/dist/theme-reference.js +1 -1
- package/dist/voice-worklet-player.cjs +80 -0
- package/dist/voice-worklet-player.d.cts +215 -0
- package/dist/voice-worklet-player.d.ts +215 -0
- package/dist/voice-worklet-player.js +80 -0
- package/dist/widget.css +54 -36
- package/package.json +16 -2
- package/src/animations/glyph-cycle.ts +11 -14
- package/src/animations/wipe.ts +1 -1
- package/src/ask-user-question-tool.test.ts +1 -1
- package/src/ask-user-question-tool.ts +6 -8
- package/src/client.test.ts +12 -12
- package/src/client.ts +30 -31
- package/src/codegen.test.ts +1 -1
- package/src/codegen.ts +1 -1
- package/src/components/approval-bubble.test.ts +1 -1
- package/src/components/approval-bubble.ts +3 -3
- package/src/components/artifact-pane.ts +3 -3
- package/src/components/ask-user-question-bubble.test.ts +14 -14
- package/src/components/ask-user-question-bubble.ts +10 -10
- package/src/components/composer-builder.ts +17 -16
- package/src/components/composer-parts.test.ts +54 -0
- package/src/components/composer-parts.ts +95 -126
- package/src/components/demo-carousel.ts +1 -1
- package/src/components/event-stream-view.test.ts +7 -7
- package/src/components/event-stream-view.ts +5 -5
- package/src/components/header-builder.ts +33 -27
- package/src/components/header-layouts.ts +1 -1
- package/src/components/header-parts.test.ts +90 -0
- package/src/components/header-parts.ts +62 -93
- package/src/components/launcher.test.ts +11 -0
- package/src/components/launcher.ts +1 -1
- package/src/components/message-bubble.test.ts +74 -2
- package/src/components/message-bubble.ts +25 -11
- package/src/components/messages.ts +2 -2
- package/src/components/panel.test.ts +1 -1
- package/src/components/panel.ts +92 -97
- package/src/components/pill-composer-builder.ts +28 -24
- package/src/components/reasoning-bubble.ts +2 -2
- package/src/components/suggestions.ts +1 -1
- package/src/components/tool-bubble.ts +1 -1
- package/src/components/widget-view.test.ts +181 -0
- package/src/components/widget-view.ts +234 -0
- package/src/defaults.ts +54 -51
- package/src/generated/runtype-openapi-contract.ts +5 -0
- package/src/index-core.ts +30 -4
- package/src/index-global.ts +73 -6
- package/src/index.ts +7 -1
- package/src/install.test.ts +11 -11
- package/src/install.ts +28 -18
- package/src/launcher-global.ts +25 -14
- package/src/markdown-parsers-eager.ts +20 -0
- package/src/markdown-parsers-entry.ts +4 -0
- package/src/markdown-parsers-loader.ts +49 -0
- package/src/plugin-kit.test.ts +1 -1
- package/src/plugin-kit.ts +7 -7
- package/src/plugins/types.ts +5 -5
- package/src/postprocessors.ts +27 -14
- package/src/runtime/host-layout.test.ts +1 -1
- package/src/runtime/host-layout.ts +4 -5
- package/src/runtime/init.ts +2 -2
- package/src/session.test.ts +5 -6
- package/src/session.ts +333 -227
- package/src/session.voice.test.ts +146 -0
- package/src/session.webmcp.test.ts +24 -25
- package/src/smart-dom-reader.ts +3 -4
- package/src/styles/widget.css +54 -36
- package/src/suggest-replies-tool.test.ts +3 -3
- package/src/suggest-replies-tool.ts +5 -5
- package/src/testing/index.ts +1 -1
- package/src/theme-editor/color-utils.ts +2 -2
- package/src/theme-editor/index.ts +4 -13
- package/src/theme-editor/presets.ts +1 -1
- package/src/theme-editor/preview-utils.ts +22 -20
- package/src/theme-editor/preview.ts +4 -1
- package/src/theme-editor/role-mappings.ts +3 -3
- package/src/theme-editor/sections.ts +21 -20
- package/src/theme-editor/types.ts +1 -1
- package/src/theme-editor/webmcp/index.ts +1 -1
- package/src/theme-editor/webmcp/summary.ts +1 -2
- package/src/theme-editor/webmcp/tools.ts +30 -30
- package/src/theme-editor/webmcp/types.ts +2 -2
- package/src/theme-editor-preview.ts +10 -0
- package/src/theme-reference.ts +26 -26
- package/src/types.ts +368 -99
- package/src/ui.approval-plugin.test.ts +3 -3
- package/src/ui.ask-user-question-plugin.test.ts +9 -9
- package/src/ui.component-directive.test.ts +4 -4
- package/src/ui.composer-bar.test.ts +10 -11
- package/src/ui.composer-keyboard.test.ts +5 -5
- package/src/ui.header-icon-color.test.ts +59 -0
- package/src/ui.scroll.test.ts +5 -6
- package/src/ui.stop-button.test.ts +3 -4
- package/src/ui.suggest-replies.test.ts +2 -2
- package/src/ui.ts +292 -131
- package/src/utils/buttons.ts +1 -1
- package/src/utils/code-generators.ts +1 -1
- package/src/utils/composer-history.ts +3 -3
- package/src/utils/dom-context.test.ts +2 -2
- package/src/utils/dom-context.ts +7 -7
- package/src/utils/dom.test.ts +67 -0
- package/src/utils/dom.ts +75 -0
- package/src/utils/dropdown.ts +2 -2
- package/src/utils/event-stream-buffer.test.ts +2 -2
- package/src/utils/message-fingerprint.test.ts +9 -0
- package/src/utils/message-fingerprint.ts +5 -0
- package/src/utils/morph.ts +1 -1
- package/src/utils/sanitize.ts +32 -16
- package/src/utils/sequence-buffer.test.ts +8 -8
- package/src/utils/sequence-buffer.ts +4 -4
- package/src/utils/smart-dom-adapter.test.ts +1 -1
- package/src/utils/smart-dom-adapter.ts +4 -5
- package/src/utils/speech-text.test.ts +122 -0
- package/src/utils/speech-text.ts +101 -0
- package/src/utils/stream-animation.test.ts +3 -3
- package/src/utils/stream-animation.ts +6 -6
- package/src/utils/theme.test.ts +1 -1
- package/src/utils/throughput-tracker.test.ts +11 -11
- package/src/utils/throughput-tracker.ts +9 -9
- package/src/utils/tokens.ts +13 -13
- package/src/utils/virtual-scroller.ts +1 -1
- package/src/vendor/smart-dom-reader/README.md +4 -4
- package/src/vendor/smart-dom-reader/index.d.ts +1 -1
- package/src/vendor/smart-dom-reader/index.js +2 -2
- package/src/version.ts +1 -1
- package/src/voice/audio-playback-manager.test.ts +159 -0
- package/src/voice/audio-playback-manager.ts +145 -30
- package/src/voice/browser-speech-engine.ts +130 -0
- package/src/voice/fallback-speech-engine.ts +89 -0
- package/src/voice/index.ts +21 -0
- package/src/voice/read-aloud-controller.test.ts +143 -0
- package/src/voice/read-aloud-controller.ts +136 -0
- package/src/voice/runtype-speech-engine.test.ts +280 -0
- package/src/voice/runtype-speech-engine.ts +211 -0
- package/src/voice/runtype-tts-entry.ts +10 -0
- package/src/voice/runtype-tts-loader.test.ts +47 -0
- package/src/voice/runtype-tts-loader.ts +42 -0
- package/src/voice/runtype-voice-provider.ts +332 -537
- package/src/voice/voice-factory.ts +26 -4
- package/src/voice/voice.test.ts +321 -7
- package/src/voice/worklet-playback-engine.ts +272 -0
- package/src/voice-worklet-player.ts +39 -0
- package/src/webmcp-bridge.test.ts +6 -6
- package/src/webmcp-bridge.ts +24 -25
- package/src/webmcp-polyfill.ts +1 -1
- package/src/voice/voice-activity-detector.ts +0 -90
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { stripMarkdownForSpeech, resolveSpeakableText, extractActionMessageText } from "./speech-text";
|
|
3
|
+
|
|
4
|
+
describe("stripMarkdownForSpeech", () => {
|
|
5
|
+
it("returns empty string for empty/undefined input", () => {
|
|
6
|
+
expect(stripMarkdownForSpeech("")).toBe("");
|
|
7
|
+
// @ts-expect-error testing defensive undefined handling
|
|
8
|
+
expect(stripMarkdownForSpeech(undefined)).toBe("");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("strips bold and italic markers but keeps the words", () => {
|
|
12
|
+
expect(stripMarkdownForSpeech("This is **bold** and *italic* text")).toBe(
|
|
13
|
+
"This is bold and italic text"
|
|
14
|
+
);
|
|
15
|
+
expect(stripMarkdownForSpeech("__strong__ and _em_")).toBe("strong and em");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("keeps inline code text without backticks", () => {
|
|
19
|
+
expect(stripMarkdownForSpeech("Run `npm install` first")).toBe("Run npm install first");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("drops fenced code blocks entirely", () => {
|
|
23
|
+
const md = "Here is code:\n```js\nconst x = 1;\n```\nDone.";
|
|
24
|
+
const out = stripMarkdownForSpeech(md);
|
|
25
|
+
expect(out).not.toContain("const x");
|
|
26
|
+
expect(out).toContain("Here is code:");
|
|
27
|
+
expect(out).toContain("Done.");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("speaks link text, not the URL", () => {
|
|
31
|
+
expect(stripMarkdownForSpeech("See [the docs](https://example.com/x) now")).toBe(
|
|
32
|
+
"See the docs now"
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("speaks image alt text", () => {
|
|
37
|
+
expect(stripMarkdownForSpeech("")).toBe("a red car");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("strips heading, blockquote, and list markers", () => {
|
|
41
|
+
expect(stripMarkdownForSpeech("# Title")).toBe("Title");
|
|
42
|
+
expect(stripMarkdownForSpeech("> quoted")).toBe("quoted");
|
|
43
|
+
expect(stripMarkdownForSpeech("- item one\n- item two")).toBe("item one\nitem two");
|
|
44
|
+
expect(stripMarkdownForSpeech("1. first\n2. second")).toBe("first\nsecond");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("strips raw HTML tags", () => {
|
|
48
|
+
expect(stripMarkdownForSpeech("Hello <strong>world</strong>!")).toBe("Hello world !");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("decodes common HTML entities", () => {
|
|
52
|
+
expect(stripMarkdownForSpeech("Tom & Jerry <3")).toBe("Tom & Jerry <3");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("collapses excess whitespace and trims", () => {
|
|
56
|
+
expect(stripMarkdownForSpeech(" lots of\n\n\nspace ")).toBe("lots of\nspace");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("handles a realistic mixed message", () => {
|
|
60
|
+
const md = [
|
|
61
|
+
"## Summary",
|
|
62
|
+
"",
|
|
63
|
+
"I'd **ship** `approval.choices.alwaysAllow` and [this one](https://x.y).",
|
|
64
|
+
"",
|
|
65
|
+
"```ts",
|
|
66
|
+
"const skip = true;",
|
|
67
|
+
"```",
|
|
68
|
+
"",
|
|
69
|
+
"- point A",
|
|
70
|
+
"- point B",
|
|
71
|
+
].join("\n");
|
|
72
|
+
const out = stripMarkdownForSpeech(md);
|
|
73
|
+
expect(out).toContain("Summary");
|
|
74
|
+
expect(out).toContain("I'd ship approval.choices.alwaysAllow and this one.");
|
|
75
|
+
expect(out).toContain("point A");
|
|
76
|
+
expect(out).not.toContain("```");
|
|
77
|
+
expect(out).not.toContain("const skip");
|
|
78
|
+
expect(out).not.toContain("https://x.y");
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("extractActionMessageText", () => {
|
|
83
|
+
it("extracts text from a bare action envelope", () => {
|
|
84
|
+
expect(extractActionMessageText('{"action":"message","text":"Hello there"}')).toBe("Hello there");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("extracts text from a fenced ```json envelope", () => {
|
|
88
|
+
const raw = '```json\n{\n "action": "message",\n "text": "Why don\'t scientists trust atoms?"\n}\n```';
|
|
89
|
+
expect(extractActionMessageText(raw)).toBe("Why don't scientists trust atoms?");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("returns null for plain prose", () => {
|
|
93
|
+
expect(extractActionMessageText("Just a normal reply")).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("returns null for JSON without a string text field", () => {
|
|
97
|
+
expect(extractActionMessageText('{"action":"navigate","url":"/x"}')).toBeNull();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("returns null for malformed JSON", () => {
|
|
101
|
+
expect(extractActionMessageText('{"action":"message", "text":')).toBeNull();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("resolveSpeakableText", () => {
|
|
106
|
+
it("speaks the action text, not the JSON envelope", () => {
|
|
107
|
+
const raw = '```json\n{"action":"message","text":"Why don\'t scientists trust atoms?"}\n```';
|
|
108
|
+
expect(resolveSpeakableText(raw)).toBe("Why don't scientists trust atoms?");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("strips markdown from action text", () => {
|
|
112
|
+
expect(resolveSpeakableText('{"action":"message","text":"Ship **it** now"}')).toBe("Ship it now");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("falls back to markdown stripping for plain prose", () => {
|
|
116
|
+
expect(resolveSpeakableText("This is **bold**")).toBe("This is bold");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("returns empty string for empty input", () => {
|
|
120
|
+
expect(resolveSpeakableText("")).toBe("");
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the text that should be spoken for an assistant message.
|
|
3
|
+
*
|
|
4
|
+
* Some flows return an action-format envelope as the message body — e.g.
|
|
5
|
+
* `{"action":"message","text":"Hello"}`, often inside a ```json fence — rather
|
|
6
|
+
* than plain prose. Speaking the raw JSON (or, after fenced-code stripping,
|
|
7
|
+
* nothing at all) is useless, so we first try to extract the human `text` field
|
|
8
|
+
* (mirroring the widget's `defaultJsonActionParser`, which also keys on
|
|
9
|
+
* `.text`), and otherwise fall back to stripping Markdown from the raw content.
|
|
10
|
+
*/
|
|
11
|
+
export function resolveSpeakableText(raw: string): string {
|
|
12
|
+
if (!raw) return "";
|
|
13
|
+
const actionText = extractActionMessageText(raw);
|
|
14
|
+
if (actionText !== null) return stripMarkdownForSpeech(actionText);
|
|
15
|
+
return stripMarkdownForSpeech(raw);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* If `raw` is (or wraps, in a ```json fence) a JSON object with a string `text`
|
|
20
|
+
* property, return that text; otherwise return `null`. Matches the action
|
|
21
|
+
* envelope shape `{ action, text, ... }` used by the widget's action system.
|
|
22
|
+
*/
|
|
23
|
+
export function extractActionMessageText(raw: string): string | null {
|
|
24
|
+
let body = raw.trim();
|
|
25
|
+
const fence = body.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
|
|
26
|
+
if (fence) body = fence[1].trim();
|
|
27
|
+
if (!body.startsWith("{")) return null;
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(body);
|
|
30
|
+
if (parsed && typeof parsed === "object" && typeof (parsed as { text?: unknown }).text === "string") {
|
|
31
|
+
return (parsed as { text: string }).text;
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
// Not valid JSON — fall through to the Markdown path.
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Convert Markdown to plain text suitable for text-to-speech.
|
|
41
|
+
*
|
|
42
|
+
* Read-aloud should speak the prose a user sees, not the markup: code fences,
|
|
43
|
+
* backticks, emphasis markers, link/image syntax and raw HTML are all noise
|
|
44
|
+
* when spoken. This is intentionally lightweight (regex-based, no full Markdown
|
|
45
|
+
* parser) — it favours predictable, dependency-free output over perfect
|
|
46
|
+
* fidelity. Speech engines receive the result of this function, never raw
|
|
47
|
+
* Markdown.
|
|
48
|
+
*/
|
|
49
|
+
export function stripMarkdownForSpeech(markdown: string): string {
|
|
50
|
+
if (!markdown) return "";
|
|
51
|
+
let text = markdown;
|
|
52
|
+
|
|
53
|
+
// Fenced code blocks: drop entirely (reading source aloud is noise).
|
|
54
|
+
text = text.replace(/```[\s\S]*?```/g, " ");
|
|
55
|
+
text = text.replace(/~~~[\s\S]*?~~~/g, " ");
|
|
56
|
+
|
|
57
|
+
// Inline code: keep the inner text, drop the backticks.
|
|
58
|
+
text = text.replace(/`([^`]+)`/g, "$1");
|
|
59
|
+
|
|
60
|
+
// Images: speak the alt text only.
|
|
61
|
+
text = text.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1");
|
|
62
|
+
|
|
63
|
+
// Inline links: speak the link text, drop the URL.
|
|
64
|
+
text = text.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1");
|
|
65
|
+
|
|
66
|
+
// Reference-style links/images: [text][ref] -> text.
|
|
67
|
+
text = text.replace(/\[([^\]]+)\]\[[^\]]*\]/g, "$1");
|
|
68
|
+
|
|
69
|
+
// Raw HTML tags.
|
|
70
|
+
text = text.replace(/<\/?[a-zA-Z][^>]*>/g, " ");
|
|
71
|
+
|
|
72
|
+
// Line-leading markers: headings, blockquotes, list bullets, ordered items.
|
|
73
|
+
text = text.replace(/^[ \t]*#{1,6}[ \t]+/gm, "");
|
|
74
|
+
text = text.replace(/^[ \t]*>[ \t]?/gm, "");
|
|
75
|
+
text = text.replace(/^[ \t]*[-*+][ \t]+/gm, "");
|
|
76
|
+
text = text.replace(/^[ \t]*\d+\.[ \t]+/gm, "");
|
|
77
|
+
|
|
78
|
+
// Horizontal rules.
|
|
79
|
+
text = text.replace(/^[ \t]*([-*_])([ \t]*\1){2,}[ \t]*$/gm, " ");
|
|
80
|
+
|
|
81
|
+
// Emphasis / strikethrough markers (bold before italic so ** is consumed).
|
|
82
|
+
text = text.replace(/(\*\*|__)(.*?)\1/g, "$2");
|
|
83
|
+
text = text.replace(/(\*|_)(.*?)\1/g, "$2");
|
|
84
|
+
text = text.replace(/~~(.*?)~~/g, "$1");
|
|
85
|
+
|
|
86
|
+
// Decode a few common HTML entities so they aren't spoken literally.
|
|
87
|
+
text = text
|
|
88
|
+
.replace(/&/g, "&")
|
|
89
|
+
.replace(/</g, "<")
|
|
90
|
+
.replace(/>/g, ">")
|
|
91
|
+
.replace(/"/g, '"')
|
|
92
|
+
.replace(/'/g, "'")
|
|
93
|
+
.replace(/ /g, " ");
|
|
94
|
+
|
|
95
|
+
// Collapse whitespace.
|
|
96
|
+
text = text.replace(/[ \t]+/g, " ");
|
|
97
|
+
text = text.replace(/[ \t]*\n[ \t]*/g, "\n");
|
|
98
|
+
text = text.replace(/\n{2,}/g, "\n");
|
|
99
|
+
|
|
100
|
+
return text.trim();
|
|
101
|
+
}
|
|
@@ -22,7 +22,7 @@ import type { AgentWidgetMessage, StreamAnimationPlugin } from "../types";
|
|
|
22
22
|
import "../animations/wipe";
|
|
23
23
|
import "../animations/glyph-cycle";
|
|
24
24
|
|
|
25
|
-
describe("wrapStreamAnimation
|
|
25
|
+
describe("wrapStreamAnimation: char mode", () => {
|
|
26
26
|
it("wraps every character in a plain paragraph into a stream-char span", () => {
|
|
27
27
|
const out = wrapStreamAnimation("<p>Hi!</p>", "char", "m1");
|
|
28
28
|
const parser = document.createElement("div");
|
|
@@ -145,7 +145,7 @@ describe("wrapStreamAnimation — char mode", () => {
|
|
|
145
145
|
};
|
|
146
146
|
const firstSpans = parse(first);
|
|
147
147
|
const secondSpans = parse(second);
|
|
148
|
-
// First two ids are stable
|
|
148
|
+
// First two ids are stable: idiomorph match contract. The space between
|
|
149
149
|
// "Hi" and "there" is a plain text node, not a span, so the next wrapped
|
|
150
150
|
// char after "Hi" jumps to index 2 for "t" in "there".
|
|
151
151
|
expect(firstSpans[0].id).toBe(secondSpans[0].id);
|
|
@@ -161,7 +161,7 @@ describe("wrapStreamAnimation — char mode", () => {
|
|
|
161
161
|
});
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
-
describe("wrapStreamAnimation
|
|
164
|
+
describe("wrapStreamAnimation: word mode", () => {
|
|
165
165
|
it("splits on whitespace and wraps each non-whitespace token", () => {
|
|
166
166
|
const out = wrapStreamAnimation("<p>Hello brave world</p>", "word", "m1");
|
|
167
167
|
const parser = document.createElement("div");
|
|
@@ -42,11 +42,11 @@ export const resolveStreamAnimation = (
|
|
|
42
42
|
============================================================ */
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
|
-
* Built-in animations ship with the core widget
|
|
45
|
+
* Built-in animations ship with the core widget: CSS lives in widget.css
|
|
46
46
|
* and no subpath import is required. They register automatically.
|
|
47
47
|
*
|
|
48
48
|
* Other animations (`letter-rise`, `word-fade`, `wipe`, `glyph-cycle`) are
|
|
49
|
-
* tree-shakeable subpath plugins
|
|
49
|
+
* tree-shakeable subpath plugins: consumers import them from
|
|
50
50
|
* `@runtypelabs/persona/animations/<name>` and they auto-register on load.
|
|
51
51
|
*/
|
|
52
52
|
const BUILTIN_PLUGINS: StreamAnimationPlugin[] = [
|
|
@@ -268,7 +268,7 @@ const wrapTextNodeWords = (
|
|
|
268
268
|
*
|
|
269
269
|
* Each wrapped span carries a stable `id` (`stream-c-{messageId}-{N}` or
|
|
270
270
|
* `stream-w-{messageId}-{N}`) so idiomorph preserves existing spans across
|
|
271
|
-
* token-by-token re-renders
|
|
271
|
+
* token-by-token re-renders: animations on already-streamed characters never
|
|
272
272
|
* restart.
|
|
273
273
|
*/
|
|
274
274
|
export const wrapStreamAnimation = (
|
|
@@ -297,7 +297,7 @@ export const wrapStreamAnimation = (
|
|
|
297
297
|
// `startIndex` lets callers number spans by their absolute position in a
|
|
298
298
|
// larger string, even when only a slice is being wrapped. The peek banner
|
|
299
299
|
// uses this so per-char span IDs stay stable as the trailing-100-char
|
|
300
|
-
// window shifts each chunk
|
|
300
|
+
// window shifts each chunk: idiomorph then preserves animations on
|
|
301
301
|
// already-revealed chars instead of restarting them.
|
|
302
302
|
const counterRef = { value: options?.startIndex ?? 0 };
|
|
303
303
|
const wrap = mode === "char" ? wrapTextNodeChars : wrapTextNodeWords;
|
|
@@ -326,7 +326,7 @@ export const createStreamCaret = (doc: Document = document): HTMLElement => {
|
|
|
326
326
|
};
|
|
327
327
|
|
|
328
328
|
/**
|
|
329
|
-
* Shimmer placeholder shown before the first token arrives
|
|
329
|
+
* Shimmer placeholder shown before the first token arrives, and, when the
|
|
330
330
|
* `"line"` buffer strategy is active, reshown between lines. A single
|
|
331
331
|
* full-width bar; we don't know ahead of time how wide the next line will be,
|
|
332
332
|
* so committing to one width avoids implying structure the stream won't match.
|
|
@@ -415,7 +415,7 @@ export const detachAllPlugins = (root: HTMLElement | ShadowRoot): void => {
|
|
|
415
415
|
|
|
416
416
|
/**
|
|
417
417
|
* Ensure the plugin's one-time side effects (style injection, onAttach) have
|
|
418
|
-
* run for this widget root. Idempotent
|
|
418
|
+
* run for this widget root. Idempotent: safe to call on every render.
|
|
419
419
|
*/
|
|
420
420
|
export const ensurePluginActive = (
|
|
421
421
|
plugin: StreamAnimationPlugin,
|
package/src/utils/theme.test.ts
CHANGED
|
@@ -297,7 +297,7 @@ describe('theme utils', () => {
|
|
|
297
297
|
toolBubble: { shadow: '0 1px 2px rgba(255,0,0,0.5)' },
|
|
298
298
|
},
|
|
299
299
|
},
|
|
300
|
-
// config.toolCall.shadow no longer rewrites the root variable
|
|
300
|
+
// config.toolCall.shadow no longer rewrites the root variable: the
|
|
301
301
|
// override is applied inline by createToolBubble (see tool-bubble tests).
|
|
302
302
|
toolCall: { shadow: 'none' },
|
|
303
303
|
});
|
|
@@ -35,7 +35,7 @@ describe("estimateOutputTokens", () => {
|
|
|
35
35
|
});
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
-
describe("ThroughputTracker
|
|
38
|
+
describe("ThroughputTracker: live estimate", () => {
|
|
39
39
|
it("estimates output tokens live from visible text deltas", () => {
|
|
40
40
|
const h = makeTracker();
|
|
41
41
|
|
|
@@ -97,7 +97,7 @@ describe("ThroughputTracker — live estimate", () => {
|
|
|
97
97
|
});
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
-
describe("ThroughputTracker
|
|
100
|
+
describe("ThroughputTracker: exact usage finalization", () => {
|
|
101
101
|
it("prefers exact output tokens from the terminal event over the estimate", () => {
|
|
102
102
|
const h = makeTracker();
|
|
103
103
|
h.at(1000).processEvent("step_delta", { type: "step_delta", text: text(40) });
|
|
@@ -160,7 +160,7 @@ describe("ThroughputTracker — exact usage finalization", () => {
|
|
|
160
160
|
});
|
|
161
161
|
});
|
|
162
162
|
|
|
163
|
-
describe("ThroughputTracker
|
|
163
|
+
describe("ThroughputTracker: non-visible deltas ignored", () => {
|
|
164
164
|
it("ignores agent thinking and tool_input deltas", () => {
|
|
165
165
|
const h = makeTracker();
|
|
166
166
|
|
|
@@ -223,7 +223,7 @@ describe("ThroughputTracker — non-visible deltas ignored", () => {
|
|
|
223
223
|
});
|
|
224
224
|
});
|
|
225
225
|
|
|
226
|
-
describe("ThroughputTracker
|
|
226
|
+
describe("ThroughputTracker: intermediate completes do not finalize", () => {
|
|
227
227
|
it("keeps the run running across step_complete / agent_turn_complete", () => {
|
|
228
228
|
const h = makeTracker();
|
|
229
229
|
h.at(1000).processEvent("step_delta", { type: "step_delta", text: text(40) });
|
|
@@ -245,7 +245,7 @@ describe("ThroughputTracker — intermediate completes do not finalize", () => {
|
|
|
245
245
|
});
|
|
246
246
|
});
|
|
247
247
|
|
|
248
|
-
describe("ThroughputTracker
|
|
248
|
+
describe("ThroughputTracker: error handling", () => {
|
|
249
249
|
it.each(["step_error", "flow_error", "agent_error", "error"])(
|
|
250
250
|
"marks the metric unavailable on %s",
|
|
251
251
|
(errorType: string) => {
|
|
@@ -278,7 +278,7 @@ describe("ThroughputTracker — error handling", () => {
|
|
|
278
278
|
});
|
|
279
279
|
});
|
|
280
280
|
|
|
281
|
-
describe("ThroughputTracker
|
|
281
|
+
describe("ThroughputTracker: reset & re-run", () => {
|
|
282
282
|
it("reset() returns to idle", () => {
|
|
283
283
|
const h = makeTracker();
|
|
284
284
|
h.at(1000).processEvent("step_delta", { type: "step_delta", text: text(40) });
|
|
@@ -302,7 +302,7 @@ describe("ThroughputTracker — reset & re-run", () => {
|
|
|
302
302
|
it("resets a stale run on the next request's start event (no bleed)", () => {
|
|
303
303
|
const h = makeTracker();
|
|
304
304
|
// First request streams visible output but never terminates (e.g. the
|
|
305
|
-
// user cancels mid-stream
|
|
305
|
+
// user cancels mid-stream: no flow_complete / error frame is emitted).
|
|
306
306
|
h.at(1000).processEvent("step_delta", { type: "step_delta", text: text(120) });
|
|
307
307
|
expect(h.metric().outputTokens).toBe(30);
|
|
308
308
|
expect(h.metric().status).toBe("running");
|
|
@@ -328,7 +328,7 @@ describe("ThroughputTracker — reset & re-run", () => {
|
|
|
328
328
|
});
|
|
329
329
|
});
|
|
330
330
|
|
|
331
|
-
describe("ThroughputTracker
|
|
331
|
+
describe("ThroughputTracker: exact usage never drops mid-run", () => {
|
|
332
332
|
it("keeps exact tokens as a floor when later steps stream more text", () => {
|
|
333
333
|
const h = makeTracker();
|
|
334
334
|
h.at(1000).processEvent("step_delta", { type: "step_delta", text: text(40) });
|
|
@@ -342,7 +342,7 @@ describe("ThroughputTracker — exact usage never drops mid-run", () => {
|
|
|
342
342
|
expect(m.outputTokens).toBe(50);
|
|
343
343
|
expect(m.source).toBe("usage");
|
|
344
344
|
|
|
345
|
-
// Step 2 streams more visible text
|
|
345
|
+
// Step 2 streams more visible text: the total must grow from 50, not
|
|
346
346
|
// collapse back to a bare 10-token estimate of the new text.
|
|
347
347
|
h.at(2000).processEvent("step_delta", { type: "step_delta", text: text(40) });
|
|
348
348
|
m = h.metric();
|
|
@@ -351,14 +351,14 @@ describe("ThroughputTracker — exact usage never drops mid-run", () => {
|
|
|
351
351
|
});
|
|
352
352
|
});
|
|
353
353
|
|
|
354
|
-
describe("ThroughputTracker
|
|
354
|
+
describe("ThroughputTracker: live rate decays while paused", () => {
|
|
355
355
|
it("recomputes duration/tok-s from the clock between events", () => {
|
|
356
356
|
const h = makeTracker();
|
|
357
357
|
h.at(1000).processEvent("step_delta", { type: "step_delta", text: text(400) });
|
|
358
358
|
// 100 tokens over a 1s window read at t=2000 → ~100 tok/s.
|
|
359
359
|
h.at(2000);
|
|
360
360
|
expect(h.metric().tokensPerSecond).toBeCloseTo(100);
|
|
361
|
-
// Same tokens, but the model has paused
|
|
361
|
+
// Same tokens, but the model has paused: reading at t=5000 (4s window)
|
|
362
362
|
// must decay the displayed rate even though no new event arrived.
|
|
363
363
|
h.at(5000);
|
|
364
364
|
expect(h.metric().tokensPerSecond).toBeCloseTo(25);
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// Derives an output tokens-per-second metric from the widget's existing SSE
|
|
6
6
|
// event stream, for display in the Events diagnostics screen. This is a passive
|
|
7
7
|
// consumer: it never mutates dispatch payloads, never forces debug mode, and
|
|
8
|
-
// never changes the wire contract
|
|
8
|
+
// never changes the wire contract: it only inspects the `(type, payload)`
|
|
9
9
|
// events that already flow through the SSE tap.
|
|
10
10
|
//
|
|
11
11
|
// Throughput is estimated live from visible text deltas while a run streams,
|
|
@@ -62,7 +62,7 @@ const REQUEST_START_EVENTS = new Set([
|
|
|
62
62
|
]);
|
|
63
63
|
|
|
64
64
|
// Per-step markers that fire repeatedly WITHIN a single request (a flow emits
|
|
65
|
-
// one per step). These only lazily begin a run
|
|
65
|
+
// one per step). These only lazily begin a run: they must never reset, or a
|
|
66
66
|
// multi-step response would restart the metric between steps. If no request- or
|
|
67
67
|
// step-start event is emitted, the first visible delta lazily starts the run.
|
|
68
68
|
const STEP_START_EVENTS = new Set(["step_start", "execution_start"]);
|
|
@@ -151,8 +151,8 @@ function getTextDelta(payload: Record<string, unknown>): string {
|
|
|
151
151
|
* other value, and a missing contentType are ignored so throughput never
|
|
152
152
|
* includes deltas the chat UI doesn't render.
|
|
153
153
|
*
|
|
154
|
-
* For `step_delta` / `step_chunk`, skip tool and context steps
|
|
155
|
-
* tool I/O, not model-visible text
|
|
154
|
+
* For `step_delta` / `step_chunk`, skip tool and context steps: those carry
|
|
155
|
+
* tool I/O, not model-visible text: mirroring the widget's own renderer.
|
|
156
156
|
*/
|
|
157
157
|
function isVisibleTextDelta(
|
|
158
158
|
type: string,
|
|
@@ -298,13 +298,13 @@ export class ThroughputTracker {
|
|
|
298
298
|
const now = this.now();
|
|
299
299
|
|
|
300
300
|
if (REQUEST_START_EVENTS.has(type)) {
|
|
301
|
-
// New request
|
|
301
|
+
// New request: start fresh, discarding any incomplete prior run.
|
|
302
302
|
this.startRun(now);
|
|
303
303
|
return;
|
|
304
304
|
}
|
|
305
305
|
|
|
306
306
|
if (STEP_START_EVENTS.has(type)) {
|
|
307
|
-
// Mid-request step marker
|
|
307
|
+
// Mid-request step marker: only begin a run if none is active.
|
|
308
308
|
if (!this.run) this.startRun(now);
|
|
309
309
|
return;
|
|
310
310
|
}
|
|
@@ -323,7 +323,7 @@ export class ThroughputTracker {
|
|
|
323
323
|
|
|
324
324
|
// Add the live char estimate of the CURRENT (not-yet-completed) step on
|
|
325
325
|
// top of any exact usage already booked from completed steps, so the
|
|
326
|
-
// count only grows
|
|
326
|
+
// count only grows: it never drops back to a bare estimate mid-run.
|
|
327
327
|
const outputTokens =
|
|
328
328
|
stats.exactOutputTokens +
|
|
329
329
|
estimateTokensFromCharCount(stats.visibleCharCount);
|
|
@@ -339,7 +339,7 @@ export class ThroughputTracker {
|
|
|
339
339
|
}
|
|
340
340
|
|
|
341
341
|
if (INTERMEDIATE_COMPLETE_EVENTS.has(type)) {
|
|
342
|
-
// Accumulate exact usage but keep the run going
|
|
342
|
+
// Accumulate exact usage but keep the run going: these fire per
|
|
343
343
|
// step/turn, not at the end of the whole run.
|
|
344
344
|
if (!this.run) return;
|
|
345
345
|
const stats = this.run;
|
|
@@ -347,7 +347,7 @@ export class ThroughputTracker {
|
|
|
347
347
|
if (exact !== undefined) {
|
|
348
348
|
stats.exactOutputTokens += exact;
|
|
349
349
|
// This step's visible text is now represented exactly by provider
|
|
350
|
-
// usage
|
|
350
|
+
// usage: drop it from the running char estimate so the two don't
|
|
351
351
|
// double-count once the next step starts streaming.
|
|
352
352
|
stats.visibleCharCount = 0;
|
|
353
353
|
}
|
package/src/utils/tokens.ts
CHANGED
|
@@ -194,23 +194,23 @@ export const DEFAULT_SEMANTIC: SemanticTokens = {
|
|
|
194
194
|
colors: {
|
|
195
195
|
primary: 'palette.colors.primary.500',
|
|
196
196
|
secondary: 'palette.colors.secondary.500',
|
|
197
|
-
// Links/Focus role
|
|
197
|
+
// Links/Focus role: solid primary
|
|
198
198
|
accent: 'palette.colors.primary.600',
|
|
199
|
-
// Surfaces role
|
|
199
|
+
// Surfaces role: soft gray
|
|
200
200
|
surface: 'palette.colors.gray.50',
|
|
201
201
|
background: 'palette.colors.gray.50',
|
|
202
202
|
container: 'palette.colors.gray.50',
|
|
203
203
|
text: 'palette.colors.gray.900',
|
|
204
204
|
textMuted: 'palette.colors.gray.500',
|
|
205
205
|
textInverse: 'palette.colors.gray.50',
|
|
206
|
-
// Borders role
|
|
206
|
+
// Borders role: soft gray
|
|
207
207
|
border: 'palette.colors.gray.200',
|
|
208
208
|
divider: 'palette.colors.gray.200',
|
|
209
209
|
interactive: {
|
|
210
|
-
// Primary Actions role
|
|
210
|
+
// Primary Actions role: solid primary
|
|
211
211
|
default: 'palette.colors.primary.600',
|
|
212
212
|
hover: 'palette.colors.primary.700',
|
|
213
|
-
// Links/Focus role
|
|
213
|
+
// Links/Focus role: solid primary
|
|
214
214
|
focus: 'palette.colors.primary.600',
|
|
215
215
|
active: 'palette.colors.primary.600',
|
|
216
216
|
disabled: 'palette.colors.gray.300',
|
|
@@ -241,7 +241,7 @@ export const DEFAULT_SEMANTIC: SemanticTokens = {
|
|
|
241
241
|
export const DEFAULT_COMPONENTS: ComponentTokens = {
|
|
242
242
|
button: {
|
|
243
243
|
primary: {
|
|
244
|
-
// Primary Actions role
|
|
244
|
+
// Primary Actions role: solid primary
|
|
245
245
|
background: 'palette.colors.primary.500',
|
|
246
246
|
foreground: 'palette.colors.primary.50',
|
|
247
247
|
borderRadius: 'palette.radius.lg',
|
|
@@ -261,7 +261,7 @@ export const DEFAULT_COMPONENTS: ComponentTokens = {
|
|
|
261
261
|
},
|
|
262
262
|
},
|
|
263
263
|
input: {
|
|
264
|
-
// Input role
|
|
264
|
+
// Input role: soft gray
|
|
265
265
|
background: 'palette.colors.gray.50',
|
|
266
266
|
placeholder: 'palette.colors.gray.400',
|
|
267
267
|
borderRadius: 'palette.radius.lg',
|
|
@@ -289,7 +289,7 @@ export const DEFAULT_COMPONENTS: ComponentTokens = {
|
|
|
289
289
|
shadow: 'palette.shadows.xl',
|
|
290
290
|
},
|
|
291
291
|
header: {
|
|
292
|
-
// Header role
|
|
292
|
+
// Header role: solid primary
|
|
293
293
|
background: 'palette.colors.primary.500',
|
|
294
294
|
border: 'palette.colors.primary.600',
|
|
295
295
|
borderRadius: 'palette.radius.xl palette.radius.xl 0 0',
|
|
@@ -302,14 +302,14 @@ export const DEFAULT_COMPONENTS: ComponentTokens = {
|
|
|
302
302
|
},
|
|
303
303
|
message: {
|
|
304
304
|
user: {
|
|
305
|
-
// User Messages role
|
|
305
|
+
// User Messages role: solid primary
|
|
306
306
|
background: 'palette.colors.primary.500',
|
|
307
307
|
text: 'palette.colors.primary.50',
|
|
308
308
|
borderRadius: 'palette.radius.lg',
|
|
309
309
|
shadow: 'palette.shadows.sm',
|
|
310
310
|
},
|
|
311
311
|
assistant: {
|
|
312
|
-
// Assistant Messages role
|
|
312
|
+
// Assistant Messages role: soft gray
|
|
313
313
|
background: 'palette.colors.gray.50',
|
|
314
314
|
text: 'palette.colors.gray.900',
|
|
315
315
|
borderRadius: 'palette.radius.lg',
|
|
@@ -341,7 +341,7 @@ export const DEFAULT_COMPONENTS: ComponentTokens = {
|
|
|
341
341
|
foreground: 'palette.colors.gray.900',
|
|
342
342
|
},
|
|
343
343
|
link: {
|
|
344
|
-
// Links/Focus role
|
|
344
|
+
// Links/Focus role: solid primary
|
|
345
345
|
foreground: 'palette.colors.primary.600',
|
|
346
346
|
},
|
|
347
347
|
prose: {
|
|
@@ -755,7 +755,7 @@ export function themeToCssVariables(theme: PersonaTheme): Record<string, string>
|
|
|
755
755
|
cssVars['--persona-radius-lg'] ??
|
|
756
756
|
'0.5rem';
|
|
757
757
|
|
|
758
|
-
// Component-level color overrides
|
|
758
|
+
// Component-level color overrides: these map component tokens to
|
|
759
759
|
// dedicated CSS variables that the widget CSS reads for individual elements.
|
|
760
760
|
cssVars['--persona-header-bg'] =
|
|
761
761
|
cssVars['--persona-components-header-background'] ?? cssVars['--persona-surface'];
|
|
@@ -776,7 +776,7 @@ export function themeToCssVariables(theme: PersonaTheme): Record<string, string>
|
|
|
776
776
|
if (headerTokens?.shadow) cssVars['--persona-header-shadow'] = headerTokens.shadow;
|
|
777
777
|
if (headerTokens?.borderBottom) cssVars['--persona-header-border-bottom'] = headerTokens.borderBottom;
|
|
778
778
|
|
|
779
|
-
// Intro card aliases
|
|
779
|
+
// Intro card aliases: short names the panel inline-styles read directly.
|
|
780
780
|
// The full-path `--persona-components-introCard-*` variables auto-emit above;
|
|
781
781
|
// these mirror them with sensible fallbacks so existing pages keep their look.
|
|
782
782
|
const introCardTokens = theme.components?.introCard;
|
|
@@ -55,7 +55,7 @@ export class VirtualScroller {
|
|
|
55
55
|
setTotalCount(count: number): void {
|
|
56
56
|
this.totalCount = count;
|
|
57
57
|
this.spacer.style.height = `${count * this.rowHeight}px`;
|
|
58
|
-
// Always invalidate cached rows
|
|
58
|
+
// Always invalidate cached rows: data may have changed even at the same count
|
|
59
59
|
for (const [, el] of this.visibleRows) {
|
|
60
60
|
el.remove();
|
|
61
61
|
}
|
|
@@ -34,15 +34,15 @@ and never touches a package resolver. Consumers who never import the
|
|
|
34
34
|
|
|
35
35
|
## Files
|
|
36
36
|
|
|
37
|
-
- `index.js
|
|
38
|
-
- `index.d.ts
|
|
39
|
-
- `LICENSE
|
|
37
|
+
- `index.js`: upstream `dist/index.mjs`, with two local edits (see header comment).
|
|
38
|
+
- `index.d.ts`: upstream `dist/index.d.mts` verbatim (sourceMappingURL stripped).
|
|
39
|
+
- `LICENSE`: upstream MIT license.
|
|
40
40
|
|
|
41
41
|
## Local modifications to `index.js`
|
|
42
42
|
|
|
43
43
|
The **only** changes from upstream `dist/index.mjs` are:
|
|
44
44
|
|
|
45
|
-
1. Removed the top-level `import { createRequire } from "node:module"
|
|
45
|
+
1. Removed the top-level `import { createRequire } from "node:module";`: a
|
|
46
46
|
Node-only builtin that breaks browser bundling.
|
|
47
47
|
2. Replaced `var __require = createRequire(import.meta.url);` with
|
|
48
48
|
`var __require = void 0;`. `__require` is referenced only inside a guarded
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* VENDORED type declarations
|
|
2
|
+
* VENDORED type declarations: @mcp-b/smart-dom-reader v2.3.1 (MIT).
|
|
3
3
|
* Copied verbatim from upstream dist/index.d.mts (only the trailing sourceMappingURL
|
|
4
4
|
* comment removed). See ./index.js and ./README.md for why this is vendored.
|
|
5
5
|
*/
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* VENDORED
|
|
2
|
+
* VENDORED: @mcp-b/smart-dom-reader v2.3.1 (MIT, (c) 2025 mcp-b contributors).
|
|
3
3
|
* Source: https://github.com/WebMCP-org/npm-packages/tree/main/packages/smart-dom-reader
|
|
4
4
|
*
|
|
5
5
|
* Vendored rather than installed as a dependency because every published version
|
|
@@ -864,7 +864,7 @@ function renderRegionInfo(region) {
|
|
|
864
864
|
if (region.linkCount) stats.push(`${region.linkCount} links`);
|
|
865
865
|
if (region.inputCount) stats.push(`${region.inputCount} inputs`);
|
|
866
866
|
if (region.textPreview) stats.push(`“${truncate(region.textPreview, 80)}”`);
|
|
867
|
-
const statsLine = stats.length ?
|
|
867
|
+
const statsLine = stats.length ? `: ${stats.join(", ")}` : "";
|
|
868
868
|
return `${icon} ${label} → \`${region.selector}\` [${id}]${statsLine}`;
|
|
869
869
|
}
|
|
870
870
|
function wrapXml(body, meta, type = "section") {
|
package/src/version.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Named import (not default) so esbuild tree-shakes the JSON module down to
|
|
2
|
-
// the one field we read
|
|
2
|
+
// the one field we read: a default import inlines the entire package.json
|
|
3
3
|
// (~4.8 kB minified) into every bundle.
|
|
4
4
|
import { version } from "../package.json";
|
|
5
5
|
|