@runtypelabs/persona 3.16.0 → 3.18.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.
Files changed (71) hide show
  1. package/README.md +142 -0
  2. package/dist/animations/glyph-cycle.cjs +279 -0
  3. package/dist/animations/glyph-cycle.d.cts +5 -0
  4. package/dist/animations/glyph-cycle.d.ts +5 -0
  5. package/dist/animations/glyph-cycle.js +252 -0
  6. package/dist/animations/types-cwY5HaFD.d.cts +307 -0
  7. package/dist/animations/types-cwY5HaFD.d.ts +307 -0
  8. package/dist/animations/wipe.cjs +107 -0
  9. package/dist/animations/wipe.d.cts +5 -0
  10. package/dist/animations/wipe.d.ts +5 -0
  11. package/dist/animations/wipe.js +80 -0
  12. package/dist/index.cjs +49 -48
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.cts +504 -1
  15. package/dist/index.d.ts +504 -1
  16. package/dist/index.global.js +143 -88
  17. package/dist/index.global.js.map +1 -1
  18. package/dist/index.js +49 -48
  19. package/dist/index.js.map +1 -1
  20. package/dist/testing.cjs +85 -0
  21. package/dist/testing.d.cts +39 -0
  22. package/dist/testing.d.ts +39 -0
  23. package/dist/testing.js +56 -0
  24. package/dist/theme-editor.cjs +2095 -207
  25. package/dist/theme-editor.d.cts +432 -2
  26. package/dist/theme-editor.d.ts +432 -2
  27. package/dist/theme-editor.js +2093 -207
  28. package/dist/theme-reference.cjs +1 -1
  29. package/dist/theme-reference.d.cts +14 -0
  30. package/dist/theme-reference.d.ts +14 -0
  31. package/dist/widget.css +565 -0
  32. package/package.json +20 -3
  33. package/src/animations/glyph-cycle.ts +332 -0
  34. package/src/animations/wipe.ts +66 -0
  35. package/src/client.test.ts +275 -0
  36. package/src/client.ts +99 -0
  37. package/src/components/ask-user-question-bubble.test.ts +583 -0
  38. package/src/components/ask-user-question-bubble.ts +924 -0
  39. package/src/components/composer-builder.ts +61 -10
  40. package/src/components/message-bubble.test.ts +181 -2
  41. package/src/components/message-bubble.ts +209 -14
  42. package/src/components/messages.ts +33 -1
  43. package/src/components/panel.ts +45 -5
  44. package/src/defaults.ts +37 -0
  45. package/src/index-global.ts +31 -0
  46. package/src/index.ts +34 -1
  47. package/src/plugins/types.ts +57 -0
  48. package/src/session.test.ts +276 -1
  49. package/src/session.ts +247 -3
  50. package/src/styles/widget.css +565 -0
  51. package/src/testing/index.ts +11 -0
  52. package/src/testing/mock-stream.test.ts +80 -0
  53. package/src/testing/mock-stream.ts +94 -0
  54. package/src/testing.ts +2 -0
  55. package/src/theme-editor/index.ts +4 -0
  56. package/src/theme-editor/preview-utils.test.ts +60 -0
  57. package/src/theme-editor/preview-utils.ts +129 -0
  58. package/src/theme-editor/sections.test.ts +19 -0
  59. package/src/theme-editor/sections.ts +84 -1
  60. package/src/types/theme.ts +15 -0
  61. package/src/types.ts +360 -0
  62. package/src/ui.ask-user-question-plugin.test.ts +649 -0
  63. package/src/ui.stop-button.test.ts +165 -0
  64. package/src/ui.ts +706 -11
  65. package/src/utils/message-fingerprint.ts +2 -0
  66. package/src/utils/morph.ts +7 -0
  67. package/src/utils/storage.ts +10 -2
  68. package/src/utils/stream-animation.test.ts +417 -0
  69. package/src/utils/stream-animation.ts +449 -0
  70. package/src/utils/theme.test.ts +36 -0
  71. package/src/utils/tokens.ts +23 -0
package/README.md CHANGED
@@ -939,6 +939,148 @@ Indicators are resolved in this order:
939
939
  2. **Config function** (`loadingIndicator.render` / `loadingIndicator.renderIdle`)
940
940
  3. **Default** (3-dot bouncing animation for loading, `null` for idle)
941
941
 
942
+ ### Ask User Question
943
+
944
+ The `ask_user_question` feature turns a LOCAL agent tool into an interactive prompt with tappable option pills. When the agent calls the `ask_user_question` tool, the server pauses execution and emits a `step_await` event; the widget renders an answer-pill sheet over the composer; the user picks / types / dismisses; the widget POSTs the answer to `/v1/dispatch/resume` and the paused execution continues with a structured `tool_result`.
945
+
946
+ This is the recommended pattern for human-in-the-loop clarifying questions. The agent-side setup (declare `ask_user_question` as a `runtimeTools` LOCAL tool and instruct the model to call it) lives in your `RuntypeFlowConfig` in the proxy — pair it with a `POST` handler that forwards to the upstream `/resume` endpoint (see `@runtypelabs/persona-proxy` and your deployment’s `resume` route).
947
+
948
+ #### Configuration
949
+
950
+ ```ts
951
+ features: {
952
+ askUserQuestion: {
953
+ enabled: true, // default: true. When false, the tool falls through to the normal tool-bubble path.
954
+ dismissible: true, // default: true. Shows a × close button on the sheet.
955
+ slideInMs: 180, // slide-in animation duration.
956
+ freeTextLabel: 'Other…',
957
+ freeTextPlaceholder: 'Type your answer…',
958
+ submitLabel: 'Send',
959
+ styles: {
960
+ sheetBackground: '#ffffff',
961
+ sheetBorder: '#e5e7eb',
962
+ sheetShadow: '0 12px 28px -10px rgba(0,0,0,0.15)',
963
+ pillBackground: 'transparent',
964
+ pillBackgroundSelected: '#0f0f0f',
965
+ pillTextColor: '#1f2937',
966
+ pillTextColorSelected: '#fafafa',
967
+ pillBorderRadius: '999px',
968
+ customInputBackground: '#ffffff'
969
+ }
970
+ }
971
+ }
972
+ ```
973
+
974
+ The composer-overlay sheet is the only question UI — no transcript stub is rendered. After the user picks, the picked answer appears as a normal user bubble so the transcript reads naturally.
975
+
976
+ #### DOM events
977
+
978
+ The widget dispatches two events on the mount element so the host page can react without touching the plugin API:
979
+
980
+ | Event | Detail |
981
+ |---|---|
982
+ | `persona:askUserQuestion:answered` | `{ toolUseId, answer, values, isFreeText, source }` where `source` is `'pick' \| 'multi' \| 'free-text'` |
983
+ | `persona:askUserQuestion:dismissed` | `{ toolUseId }` |
984
+
985
+ ```ts
986
+ mount.addEventListener('persona:askUserQuestion:answered', (event) => {
987
+ const { answer, source } = event.detail;
988
+ console.log('User picked', answer, 'via', source);
989
+ });
990
+ ```
991
+
992
+ #### Custom UI via the `renderAskUserQuestion` plugin hook
993
+
994
+ For full control over the question UI — a modal, a sidebar form, a command palette, whatever — register a plugin with `renderAskUserQuestion`. Returning a non-null `HTMLElement` renders inline in the transcript and suppresses the built-in overlay sheet. Returning `null` falls through to the default sheet.
995
+
996
+ ```ts
997
+ import type { AgentWidgetPlugin } from '@runtypelabs/persona';
998
+
999
+ const customAskPlugin: AgentWidgetPlugin = {
1000
+ id: 'custom-ask',
1001
+ renderAskUserQuestion: ({ payload, complete, resolve, dismiss }) => {
1002
+ const prompt = payload?.questions?.[0];
1003
+ if (!prompt) return null; // streaming — wait for more data, or show a skeleton
1004
+
1005
+ const root = document.createElement('div');
1006
+ root.className = 'my-question-card';
1007
+
1008
+ const q = document.createElement('p');
1009
+ q.textContent = prompt.question ?? '';
1010
+ root.appendChild(q);
1011
+
1012
+ (prompt.options ?? []).forEach((option) => {
1013
+ const btn = document.createElement('button');
1014
+ btn.textContent = option.label;
1015
+ btn.addEventListener('click', () => resolve(option.label));
1016
+ root.appendChild(btn);
1017
+ });
1018
+
1019
+ if (prompt.allowFreeText !== false) {
1020
+ const input = document.createElement('input');
1021
+ input.placeholder = 'Other…';
1022
+ input.addEventListener('keydown', (e) => {
1023
+ if (e.key === 'Enter' && input.value.trim()) resolve(input.value.trim());
1024
+ });
1025
+ root.appendChild(input);
1026
+ }
1027
+
1028
+ const close = document.createElement('button');
1029
+ close.textContent = '×';
1030
+ close.addEventListener('click', () => dismiss());
1031
+ root.appendChild(close);
1032
+
1033
+ return root;
1034
+ }
1035
+ };
1036
+
1037
+ initAgentWidget({
1038
+ target: '#app',
1039
+ config: {
1040
+ plugins: [customAskPlugin]
1041
+ }
1042
+ });
1043
+ ```
1044
+
1045
+ #### Type Definitions
1046
+
1047
+ ```ts
1048
+ type AskUserQuestionOption = {
1049
+ label: string;
1050
+ description?: string;
1051
+ };
1052
+
1053
+ type AskUserQuestionPrompt = {
1054
+ question: string;
1055
+ header?: string; // short chip label, ≤12 chars
1056
+ options: AskUserQuestionOption[];
1057
+ multiSelect?: boolean; // allow multiple picks with a Submit button
1058
+ allowFreeText?: boolean; // show an "Other…" free-text pill
1059
+ };
1060
+
1061
+ type AskUserQuestionPayload = {
1062
+ questions: AskUserQuestionPrompt[];
1063
+ };
1064
+
1065
+ // Plugin hook signature
1066
+ renderAskUserQuestion?: (context: {
1067
+ message: AgentWidgetMessage;
1068
+ payload: Partial<AskUserQuestionPayload> | null; // may be partial mid-stream
1069
+ complete: boolean; // true once tool-call args fully stream
1070
+ resolve: (answer: string) => void; // posts /resume with structured toolOutput
1071
+ dismiss: () => void; // sends "(dismissed)" sentinel
1072
+ config: AgentWidgetConfig;
1073
+ }) => HTMLElement | null;
1074
+ ```
1075
+
1076
+ For plugins that want to re-parse a tool message outside the hook context, the widget also exports a `parseAskUserQuestionPayload(message)` helper that returns `{ payload, complete }` using the same partial-JSON logic the built-in sheet uses.
1077
+
1078
+ #### Priority chain
1079
+
1080
+ 1. **Plugin hook** (`renderAskUserQuestion` returning a non-null element) — fully owns the UI; built-in overlay is suppressed.
1081
+ 2. **Built-in overlay sheet** — when the feature is enabled and no plugin handles it.
1082
+ 3. **Generic tool bubble** — when `features.askUserQuestion.enabled` is `false`, the tool call renders through the normal `renderToolCall` path.
1083
+
942
1084
  ### Dropdown Menu
943
1085
 
944
1086
  A reusable dropdown menu utility for building custom menus in plugins, custom components, or host-page UI that matches the widget's theme.
@@ -0,0 +1,279 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/animations/glyph-cycle.ts
21
+ var glyph_cycle_exports = {};
22
+ __export(glyph_cycle_exports, {
23
+ default: () => glyph_cycle_default,
24
+ glyphCycle: () => glyphCycle
25
+ });
26
+ module.exports = __toCommonJS(glyph_cycle_exports);
27
+
28
+ // src/utils/stream-animation.ts
29
+ var BUILTIN_PLUGINS = [
30
+ {
31
+ name: "typewriter",
32
+ containerClass: "persona-stream-typewriter",
33
+ wrap: "char",
34
+ useCaret: true
35
+ },
36
+ {
37
+ name: "pop-bubble",
38
+ bubbleClass: "persona-stream-pop",
39
+ wrap: "none"
40
+ },
41
+ {
42
+ name: "letter-rise",
43
+ containerClass: "persona-stream-letter-rise",
44
+ wrap: "char"
45
+ },
46
+ {
47
+ name: "word-fade",
48
+ containerClass: "persona-stream-word-fade",
49
+ wrap: "word"
50
+ }
51
+ ];
52
+ var globalRegistry = /* @__PURE__ */ new Map();
53
+ for (const plugin of BUILTIN_PLUGINS) globalRegistry.set(plugin.name, plugin);
54
+ var registerStreamAnimationPlugin = (plugin) => {
55
+ globalRegistry.set(plugin.name, plugin);
56
+ };
57
+
58
+ // src/animations/glyph-cycle.ts
59
+ var STYLES = `
60
+ [data-persona-root] .persona-stream-glyph-cycle .persona-stream-char {
61
+ animation: persona-stream-glyph-cycle-fade
62
+ calc(var(--persona-stream-step, 120ms) * 1.5) ease-out both;
63
+ }
64
+ [data-persona-root] .persona-stream-glyph-cycle .persona-stream-char[data-glyph-cycle-final] {
65
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
66
+ }
67
+ @keyframes persona-stream-glyph-cycle-fade {
68
+ from { opacity: 0.35; }
69
+ to { opacity: 1; }
70
+ }
71
+ @media (prefers-reduced-motion: reduce) {
72
+ [data-persona-root] .persona-stream-glyph-cycle .persona-stream-char {
73
+ animation: none !important;
74
+ opacity: 1 !important;
75
+ }
76
+ }
77
+ `.trim();
78
+ var GLYPHS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#$%&@";
79
+ var TICK_COUNT = 10;
80
+ var BASE_TICK_MS = 120;
81
+ var DEFAULT_STEP_MS = 120;
82
+ var CROSS_FLICKER_PROBABILITY = 0.4;
83
+ var BUFFER_THRESHOLD = 50;
84
+ var getStepMs = (container) => {
85
+ var _a;
86
+ if (!container) return DEFAULT_STEP_MS;
87
+ const raw = (_a = container.style.getPropertyValue("--persona-stream-step")) == null ? void 0 : _a.trim();
88
+ const match = raw.match(/([\d.]+)\s*ms/);
89
+ return match ? parseFloat(match[1]) : DEFAULT_STEP_MS;
90
+ };
91
+ var getTickMs = (span) => {
92
+ const container = span.closest(".persona-stream-glyph-cycle");
93
+ const step = getStepMs(container);
94
+ return BASE_TICK_MS * step / DEFAULT_STEP_MS;
95
+ };
96
+ var randomGlyph = (avoid) => {
97
+ let ch = GLYPHS[Math.floor(Math.random() * GLYPHS.length)];
98
+ if (avoid && ch === avoid) {
99
+ ch = GLYPHS[(GLYPHS.indexOf(ch) + 1) % GLYPHS.length];
100
+ }
101
+ return ch;
102
+ };
103
+ var flickerLaterSiblings = (span) => {
104
+ const container = span.closest(".persona-stream-glyph-cycle");
105
+ if (!container) return;
106
+ const unsettled = container.querySelectorAll(
107
+ ".persona-stream-char[data-glyph-cycle-final]"
108
+ );
109
+ let seenSelf = false;
110
+ for (const other of Array.from(unsettled)) {
111
+ if (other === span) {
112
+ seenSelf = true;
113
+ continue;
114
+ }
115
+ if (!seenSelf) continue;
116
+ if (Math.random() < CROSS_FLICKER_PROBABILITY) {
117
+ other.textContent = randomGlyph();
118
+ }
119
+ }
120
+ };
121
+ var containerNextSettleAt = /* @__PURE__ */ new WeakMap();
122
+ var DEFAULT_ARRIVAL_MS = 25;
123
+ var STAGGER_DRAIN_FACTOR = 0.5;
124
+ var MIN_STAGGER_MS = 6;
125
+ var EMA_ALPHA = 0.25;
126
+ var containerLastScheduleAt = /* @__PURE__ */ new WeakMap();
127
+ var containerArrivalMs = /* @__PURE__ */ new WeakMap();
128
+ var observeStagger = (container, now) => {
129
+ var _a;
130
+ if (!container) return DEFAULT_ARRIVAL_MS * STAGGER_DRAIN_FACTOR;
131
+ const last = containerLastScheduleAt.get(container);
132
+ containerLastScheduleAt.set(container, now);
133
+ let observed = (_a = containerArrivalMs.get(container)) != null ? _a : DEFAULT_ARRIVAL_MS;
134
+ if (last !== void 0) {
135
+ const interval = now - last;
136
+ if (interval > 1) {
137
+ observed = observed * (1 - EMA_ALPHA) + interval * EMA_ALPHA;
138
+ containerArrivalMs.set(container, observed);
139
+ }
140
+ }
141
+ return Math.max(MIN_STAGGER_MS, observed * STAGGER_DRAIN_FACTOR);
142
+ };
143
+ var activeCyclesByMessage = /* @__PURE__ */ new Map();
144
+ var getMessageId = (span) => {
145
+ var _a;
146
+ const bubble = span.closest("[data-message-id]");
147
+ return (_a = bubble == null ? void 0 : bubble.dataset.messageId) != null ? _a : null;
148
+ };
149
+ var incrementActive = (messageId) => {
150
+ var _a;
151
+ if (!messageId) return;
152
+ activeCyclesByMessage.set(messageId, ((_a = activeCyclesByMessage.get(messageId)) != null ? _a : 0) + 1);
153
+ };
154
+ var decrementActive = (messageId) => {
155
+ var _a;
156
+ if (!messageId) return;
157
+ const count = (_a = activeCyclesByMessage.get(messageId)) != null ? _a : 0;
158
+ if (count <= 1) activeCyclesByMessage.delete(messageId);
159
+ else activeCyclesByMessage.set(messageId, count - 1);
160
+ };
161
+ var scheduleCycle = (span) => {
162
+ var _a;
163
+ if (span.dataset.glyphCycleScheduled === "true") return;
164
+ const finalChar = (_a = span.textContent) != null ? _a : "";
165
+ if (!finalChar || /\s/.test(finalChar)) return;
166
+ span.dataset.glyphCycleScheduled = "true";
167
+ span.dataset.glyphCycleFinal = finalChar;
168
+ span.setAttribute("data-preserve-runtime", "stream-glyph-cycle");
169
+ span.textContent = randomGlyph();
170
+ const messageId = getMessageId(span);
171
+ if (messageId) span.dataset.glyphCycleMessageId = messageId;
172
+ incrementActive(messageId);
173
+ const container = span.closest(".persona-stream-glyph-cycle");
174
+ const now = Date.now();
175
+ const staggerMs = observeStagger(container, now);
176
+ const tickMs = getTickMs(span);
177
+ const baseDurationMs = TICK_COUNT * tickMs;
178
+ let settleAt = now + baseDurationMs;
179
+ if (container) {
180
+ const prev = containerNextSettleAt.get(container);
181
+ if (prev !== void 0) settleAt = Math.max(settleAt, prev);
182
+ containerNextSettleAt.set(container, settleAt + staggerMs);
183
+ }
184
+ startCycle(span, finalChar, settleAt);
185
+ };
186
+ var startCycle = (span, finalChar, settleAt) => {
187
+ var _a;
188
+ if (span.dataset.glyphCycleStarted === "true") return;
189
+ span.dataset.glyphCycleStarted = "true";
190
+ const tickMs = getTickMs(span);
191
+ let lastGlyph = (_a = span.textContent) != null ? _a : void 0;
192
+ const step = () => {
193
+ var _a2;
194
+ if (!span.isConnected) return;
195
+ if (Date.now() >= settleAt) {
196
+ span.textContent = finalChar;
197
+ span.removeAttribute("data-preserve-runtime");
198
+ delete span.dataset.glyphCycleStarted;
199
+ delete span.dataset.glyphCycleFinal;
200
+ decrementActive((_a2 = span.dataset.glyphCycleMessageId) != null ? _a2 : null);
201
+ delete span.dataset.glyphCycleMessageId;
202
+ return;
203
+ }
204
+ const glyph = randomGlyph(lastGlyph);
205
+ span.textContent = glyph;
206
+ lastGlyph = glyph;
207
+ flickerLaterSiblings(span);
208
+ setTimeout(step, tickMs);
209
+ };
210
+ setTimeout(step, tickMs);
211
+ };
212
+ var processCharSpans = (root) => {
213
+ var _a;
214
+ const spans = (_a = root.querySelectorAll) == null ? void 0 : _a.call(
215
+ root,
216
+ ".persona-stream-glyph-cycle .persona-stream-char:not([data-glyph-cycle-scheduled])"
217
+ );
218
+ if (!spans) return;
219
+ for (const span of Array.from(spans)) {
220
+ scheduleCycle(span);
221
+ }
222
+ };
223
+ var isElement = (node) => node.nodeType === 1;
224
+ var glyphCycle = {
225
+ name: "glyph-cycle",
226
+ containerClass: "persona-stream-glyph-cycle",
227
+ wrap: "char",
228
+ // Narrow the default skip list so inline `<code>` and fenced `<pre>`
229
+ // code blocks both render as cycling glyphs along with everything else.
230
+ // Links stay clickable; <script>/<style> stay untouched.
231
+ skipTags: ["a", "script", "style"],
232
+ styles: STYLES,
233
+ bufferContent(content) {
234
+ if (content.length < BUFFER_THRESHOLD) return "";
235
+ let boldPairs = 0;
236
+ let lastSafe = -1;
237
+ let i = 0;
238
+ while (i < content.length) {
239
+ if (content[i] === "*" && content[i + 1] === "*") {
240
+ boldPairs += 1;
241
+ i += 2;
242
+ continue;
243
+ }
244
+ if (/\s/.test(content[i]) && boldPairs % 2 === 0) {
245
+ lastSafe = i;
246
+ }
247
+ i += 1;
248
+ }
249
+ if (lastSafe < 0) return "";
250
+ return content.slice(0, lastSafe);
251
+ },
252
+ isAnimating(message) {
253
+ var _a;
254
+ return ((_a = activeCyclesByMessage.get(message.id)) != null ? _a : 0) > 0;
255
+ },
256
+ onAttach(root) {
257
+ processCharSpans(root);
258
+ const observer = new MutationObserver((mutations) => {
259
+ for (const mutation of mutations) {
260
+ for (const node of Array.from(mutation.addedNodes)) {
261
+ if (!isElement(node)) continue;
262
+ if (node.classList.contains("persona-stream-char") && node.closest(".persona-stream-glyph-cycle")) {
263
+ scheduleCycle(node);
264
+ } else {
265
+ processCharSpans(node);
266
+ }
267
+ }
268
+ }
269
+ });
270
+ observer.observe(root, { childList: true, subtree: true });
271
+ return () => observer.disconnect();
272
+ }
273
+ };
274
+ registerStreamAnimationPlugin(glyphCycle);
275
+ var glyph_cycle_default = glyphCycle;
276
+ // Annotate the CommonJS export names for ESM import in node:
277
+ 0 && (module.exports = {
278
+ glyphCycle
279
+ });
@@ -0,0 +1,5 @@
1
+ import { S as StreamAnimationPlugin } from './types-cwY5HaFD.cjs';
2
+
3
+ declare const glyphCycle: StreamAnimationPlugin;
4
+
5
+ export { glyphCycle as default, glyphCycle };
@@ -0,0 +1,5 @@
1
+ import { S as StreamAnimationPlugin } from './types-cwY5HaFD.js';
2
+
3
+ declare const glyphCycle: StreamAnimationPlugin;
4
+
5
+ export { glyphCycle as default, glyphCycle };
@@ -0,0 +1,252 @@
1
+ // src/utils/stream-animation.ts
2
+ var BUILTIN_PLUGINS = [
3
+ {
4
+ name: "typewriter",
5
+ containerClass: "persona-stream-typewriter",
6
+ wrap: "char",
7
+ useCaret: true
8
+ },
9
+ {
10
+ name: "pop-bubble",
11
+ bubbleClass: "persona-stream-pop",
12
+ wrap: "none"
13
+ },
14
+ {
15
+ name: "letter-rise",
16
+ containerClass: "persona-stream-letter-rise",
17
+ wrap: "char"
18
+ },
19
+ {
20
+ name: "word-fade",
21
+ containerClass: "persona-stream-word-fade",
22
+ wrap: "word"
23
+ }
24
+ ];
25
+ var globalRegistry = /* @__PURE__ */ new Map();
26
+ for (const plugin of BUILTIN_PLUGINS) globalRegistry.set(plugin.name, plugin);
27
+ var registerStreamAnimationPlugin = (plugin) => {
28
+ globalRegistry.set(plugin.name, plugin);
29
+ };
30
+
31
+ // src/animations/glyph-cycle.ts
32
+ var STYLES = `
33
+ [data-persona-root] .persona-stream-glyph-cycle .persona-stream-char {
34
+ animation: persona-stream-glyph-cycle-fade
35
+ calc(var(--persona-stream-step, 120ms) * 1.5) ease-out both;
36
+ }
37
+ [data-persona-root] .persona-stream-glyph-cycle .persona-stream-char[data-glyph-cycle-final] {
38
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
39
+ }
40
+ @keyframes persona-stream-glyph-cycle-fade {
41
+ from { opacity: 0.35; }
42
+ to { opacity: 1; }
43
+ }
44
+ @media (prefers-reduced-motion: reduce) {
45
+ [data-persona-root] .persona-stream-glyph-cycle .persona-stream-char {
46
+ animation: none !important;
47
+ opacity: 1 !important;
48
+ }
49
+ }
50
+ `.trim();
51
+ var GLYPHS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#$%&@";
52
+ var TICK_COUNT = 10;
53
+ var BASE_TICK_MS = 120;
54
+ var DEFAULT_STEP_MS = 120;
55
+ var CROSS_FLICKER_PROBABILITY = 0.4;
56
+ var BUFFER_THRESHOLD = 50;
57
+ var getStepMs = (container) => {
58
+ var _a;
59
+ if (!container) return DEFAULT_STEP_MS;
60
+ const raw = (_a = container.style.getPropertyValue("--persona-stream-step")) == null ? void 0 : _a.trim();
61
+ const match = raw.match(/([\d.]+)\s*ms/);
62
+ return match ? parseFloat(match[1]) : DEFAULT_STEP_MS;
63
+ };
64
+ var getTickMs = (span) => {
65
+ const container = span.closest(".persona-stream-glyph-cycle");
66
+ const step = getStepMs(container);
67
+ return BASE_TICK_MS * step / DEFAULT_STEP_MS;
68
+ };
69
+ var randomGlyph = (avoid) => {
70
+ let ch = GLYPHS[Math.floor(Math.random() * GLYPHS.length)];
71
+ if (avoid && ch === avoid) {
72
+ ch = GLYPHS[(GLYPHS.indexOf(ch) + 1) % GLYPHS.length];
73
+ }
74
+ return ch;
75
+ };
76
+ var flickerLaterSiblings = (span) => {
77
+ const container = span.closest(".persona-stream-glyph-cycle");
78
+ if (!container) return;
79
+ const unsettled = container.querySelectorAll(
80
+ ".persona-stream-char[data-glyph-cycle-final]"
81
+ );
82
+ let seenSelf = false;
83
+ for (const other of Array.from(unsettled)) {
84
+ if (other === span) {
85
+ seenSelf = true;
86
+ continue;
87
+ }
88
+ if (!seenSelf) continue;
89
+ if (Math.random() < CROSS_FLICKER_PROBABILITY) {
90
+ other.textContent = randomGlyph();
91
+ }
92
+ }
93
+ };
94
+ var containerNextSettleAt = /* @__PURE__ */ new WeakMap();
95
+ var DEFAULT_ARRIVAL_MS = 25;
96
+ var STAGGER_DRAIN_FACTOR = 0.5;
97
+ var MIN_STAGGER_MS = 6;
98
+ var EMA_ALPHA = 0.25;
99
+ var containerLastScheduleAt = /* @__PURE__ */ new WeakMap();
100
+ var containerArrivalMs = /* @__PURE__ */ new WeakMap();
101
+ var observeStagger = (container, now) => {
102
+ var _a;
103
+ if (!container) return DEFAULT_ARRIVAL_MS * STAGGER_DRAIN_FACTOR;
104
+ const last = containerLastScheduleAt.get(container);
105
+ containerLastScheduleAt.set(container, now);
106
+ let observed = (_a = containerArrivalMs.get(container)) != null ? _a : DEFAULT_ARRIVAL_MS;
107
+ if (last !== void 0) {
108
+ const interval = now - last;
109
+ if (interval > 1) {
110
+ observed = observed * (1 - EMA_ALPHA) + interval * EMA_ALPHA;
111
+ containerArrivalMs.set(container, observed);
112
+ }
113
+ }
114
+ return Math.max(MIN_STAGGER_MS, observed * STAGGER_DRAIN_FACTOR);
115
+ };
116
+ var activeCyclesByMessage = /* @__PURE__ */ new Map();
117
+ var getMessageId = (span) => {
118
+ var _a;
119
+ const bubble = span.closest("[data-message-id]");
120
+ return (_a = bubble == null ? void 0 : bubble.dataset.messageId) != null ? _a : null;
121
+ };
122
+ var incrementActive = (messageId) => {
123
+ var _a;
124
+ if (!messageId) return;
125
+ activeCyclesByMessage.set(messageId, ((_a = activeCyclesByMessage.get(messageId)) != null ? _a : 0) + 1);
126
+ };
127
+ var decrementActive = (messageId) => {
128
+ var _a;
129
+ if (!messageId) return;
130
+ const count = (_a = activeCyclesByMessage.get(messageId)) != null ? _a : 0;
131
+ if (count <= 1) activeCyclesByMessage.delete(messageId);
132
+ else activeCyclesByMessage.set(messageId, count - 1);
133
+ };
134
+ var scheduleCycle = (span) => {
135
+ var _a;
136
+ if (span.dataset.glyphCycleScheduled === "true") return;
137
+ const finalChar = (_a = span.textContent) != null ? _a : "";
138
+ if (!finalChar || /\s/.test(finalChar)) return;
139
+ span.dataset.glyphCycleScheduled = "true";
140
+ span.dataset.glyphCycleFinal = finalChar;
141
+ span.setAttribute("data-preserve-runtime", "stream-glyph-cycle");
142
+ span.textContent = randomGlyph();
143
+ const messageId = getMessageId(span);
144
+ if (messageId) span.dataset.glyphCycleMessageId = messageId;
145
+ incrementActive(messageId);
146
+ const container = span.closest(".persona-stream-glyph-cycle");
147
+ const now = Date.now();
148
+ const staggerMs = observeStagger(container, now);
149
+ const tickMs = getTickMs(span);
150
+ const baseDurationMs = TICK_COUNT * tickMs;
151
+ let settleAt = now + baseDurationMs;
152
+ if (container) {
153
+ const prev = containerNextSettleAt.get(container);
154
+ if (prev !== void 0) settleAt = Math.max(settleAt, prev);
155
+ containerNextSettleAt.set(container, settleAt + staggerMs);
156
+ }
157
+ startCycle(span, finalChar, settleAt);
158
+ };
159
+ var startCycle = (span, finalChar, settleAt) => {
160
+ var _a;
161
+ if (span.dataset.glyphCycleStarted === "true") return;
162
+ span.dataset.glyphCycleStarted = "true";
163
+ const tickMs = getTickMs(span);
164
+ let lastGlyph = (_a = span.textContent) != null ? _a : void 0;
165
+ const step = () => {
166
+ var _a2;
167
+ if (!span.isConnected) return;
168
+ if (Date.now() >= settleAt) {
169
+ span.textContent = finalChar;
170
+ span.removeAttribute("data-preserve-runtime");
171
+ delete span.dataset.glyphCycleStarted;
172
+ delete span.dataset.glyphCycleFinal;
173
+ decrementActive((_a2 = span.dataset.glyphCycleMessageId) != null ? _a2 : null);
174
+ delete span.dataset.glyphCycleMessageId;
175
+ return;
176
+ }
177
+ const glyph = randomGlyph(lastGlyph);
178
+ span.textContent = glyph;
179
+ lastGlyph = glyph;
180
+ flickerLaterSiblings(span);
181
+ setTimeout(step, tickMs);
182
+ };
183
+ setTimeout(step, tickMs);
184
+ };
185
+ var processCharSpans = (root) => {
186
+ var _a;
187
+ const spans = (_a = root.querySelectorAll) == null ? void 0 : _a.call(
188
+ root,
189
+ ".persona-stream-glyph-cycle .persona-stream-char:not([data-glyph-cycle-scheduled])"
190
+ );
191
+ if (!spans) return;
192
+ for (const span of Array.from(spans)) {
193
+ scheduleCycle(span);
194
+ }
195
+ };
196
+ var isElement = (node) => node.nodeType === 1;
197
+ var glyphCycle = {
198
+ name: "glyph-cycle",
199
+ containerClass: "persona-stream-glyph-cycle",
200
+ wrap: "char",
201
+ // Narrow the default skip list so inline `<code>` and fenced `<pre>`
202
+ // code blocks both render as cycling glyphs along with everything else.
203
+ // Links stay clickable; <script>/<style> stay untouched.
204
+ skipTags: ["a", "script", "style"],
205
+ styles: STYLES,
206
+ bufferContent(content) {
207
+ if (content.length < BUFFER_THRESHOLD) return "";
208
+ let boldPairs = 0;
209
+ let lastSafe = -1;
210
+ let i = 0;
211
+ while (i < content.length) {
212
+ if (content[i] === "*" && content[i + 1] === "*") {
213
+ boldPairs += 1;
214
+ i += 2;
215
+ continue;
216
+ }
217
+ if (/\s/.test(content[i]) && boldPairs % 2 === 0) {
218
+ lastSafe = i;
219
+ }
220
+ i += 1;
221
+ }
222
+ if (lastSafe < 0) return "";
223
+ return content.slice(0, lastSafe);
224
+ },
225
+ isAnimating(message) {
226
+ var _a;
227
+ return ((_a = activeCyclesByMessage.get(message.id)) != null ? _a : 0) > 0;
228
+ },
229
+ onAttach(root) {
230
+ processCharSpans(root);
231
+ const observer = new MutationObserver((mutations) => {
232
+ for (const mutation of mutations) {
233
+ for (const node of Array.from(mutation.addedNodes)) {
234
+ if (!isElement(node)) continue;
235
+ if (node.classList.contains("persona-stream-char") && node.closest(".persona-stream-glyph-cycle")) {
236
+ scheduleCycle(node);
237
+ } else {
238
+ processCharSpans(node);
239
+ }
240
+ }
241
+ }
242
+ });
243
+ observer.observe(root, { childList: true, subtree: true });
244
+ return () => observer.disconnect();
245
+ }
246
+ };
247
+ registerStreamAnimationPlugin(glyphCycle);
248
+ var glyph_cycle_default = glyphCycle;
249
+ export {
250
+ glyph_cycle_default as default,
251
+ glyphCycle
252
+ };