@syntrologie/adapt-chatbot 2.27.0 → 2.29.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 (131) hide show
  1. package/dist/AdaptiveChatBar.composer.test.d.ts +2 -0
  2. package/dist/AdaptiveChatBar.composer.test.d.ts.map +1 -0
  3. package/dist/AdaptiveChatBar.d.ts +212 -0
  4. package/dist/AdaptiveChatBar.d.ts.map +1 -0
  5. package/dist/AdaptiveChatBar.js +11 -0
  6. package/dist/AdaptiveChatBar.js.map +7 -0
  7. package/dist/AdaptiveChatBarMountable.d.ts +82 -0
  8. package/dist/AdaptiveChatBarMountable.d.ts.map +1 -0
  9. package/dist/AdaptiveChatTrail.anchor-scroll.test.d.ts +2 -0
  10. package/dist/AdaptiveChatTrail.anchor-scroll.test.d.ts.map +1 -0
  11. package/dist/AdaptiveChatTrail.d.ts +469 -0
  12. package/dist/AdaptiveChatTrail.d.ts.map +1 -0
  13. package/dist/AdaptiveChatTrail.js +11 -0
  14. package/dist/AdaptiveChatTrail.js.map +7 -0
  15. package/dist/AdaptiveChatTrail.test.d.ts +2 -0
  16. package/dist/AdaptiveChatTrail.test.d.ts.map +1 -0
  17. package/dist/AdaptiveChipsStrip.d.ts +1156 -0
  18. package/dist/AdaptiveChipsStrip.d.ts.map +1 -0
  19. package/dist/AdaptiveChipsStrip.js +11 -0
  20. package/dist/AdaptiveChipsStrip.js.map +7 -0
  21. package/dist/AdaptiveChipsStripMountable.d.ts +24 -0
  22. package/dist/AdaptiveChipsStripMountable.d.ts.map +1 -0
  23. package/dist/ChatAssistantLit.d.ts +22 -50
  24. package/dist/ChatAssistantLit.d.ts.map +1 -1
  25. package/dist/ChatAssistantLit.js +6 -3
  26. package/dist/ChatSession.d.ts +239 -0
  27. package/dist/ChatSession.d.ts.map +1 -0
  28. package/dist/ChatTransport.d.ts +390 -0
  29. package/dist/ChatTransport.d.ts.map +1 -0
  30. package/dist/InlineRegistryWidgetMount.test.d.ts +2 -0
  31. package/dist/InlineRegistryWidgetMount.test.d.ts.map +1 -0
  32. package/dist/InlineWidgetLiveMount.test.d.ts +2 -0
  33. package/dist/InlineWidgetLiveMount.test.d.ts.map +1 -0
  34. package/dist/NavLinkMountable.d.ts +25 -0
  35. package/dist/NavLinkMountable.d.ts.map +1 -0
  36. package/dist/NavLinkMountable.test.d.ts +2 -0
  37. package/dist/NavLinkMountable.test.d.ts.map +1 -0
  38. package/dist/TextAnswerMountable.d.ts +17 -0
  39. package/dist/TextAnswerMountable.d.ts.map +1 -0
  40. package/dist/TextAnswerMountable.test.d.ts +2 -0
  41. package/dist/TextAnswerMountable.test.d.ts.map +1 -0
  42. package/dist/Turnstile.d.ts +83 -0
  43. package/dist/Turnstile.d.ts.map +1 -0
  44. package/dist/chunk-A7RBZLIX.js +1678 -0
  45. package/dist/chunk-A7RBZLIX.js.map +7 -0
  46. package/dist/chunk-EACGDSJI.js +237 -0
  47. package/dist/chunk-EACGDSJI.js.map +7 -0
  48. package/dist/chunk-EWPPVPJ4.js +14 -0
  49. package/dist/chunk-EWPPVPJ4.js.map +7 -0
  50. package/dist/chunk-FXHYE6FY.js +632 -0
  51. package/dist/chunk-FXHYE6FY.js.map +7 -0
  52. package/dist/chunk-OIGVZGSX.js +641 -0
  53. package/dist/chunk-OIGVZGSX.js.map +7 -0
  54. package/dist/chunk-VZIUXXAM.js +4015 -0
  55. package/dist/chunk-VZIUXXAM.js.map +7 -0
  56. package/dist/{chunk-W457NMGD.js → chunk-ZXV4GXMO.js} +2149 -4254
  57. package/dist/chunk-ZXV4GXMO.js.map +7 -0
  58. package/dist/elements/ActionHandler.d.ts +34 -0
  59. package/dist/elements/ActionHandler.d.ts.map +1 -0
  60. package/dist/elements/ElementInstanceStore.d.ts +217 -0
  61. package/dist/elements/ElementInstanceStore.d.ts.map +1 -0
  62. package/dist/elements/ElementInstanceStore.test.d.ts +2 -0
  63. package/dist/elements/ElementInstanceStore.test.d.ts.map +1 -0
  64. package/dist/elements/ElementTypeHandler.d.ts +77 -0
  65. package/dist/elements/ElementTypeHandler.d.ts.map +1 -0
  66. package/dist/elements/ItemHandler.d.ts +60 -0
  67. package/dist/elements/ItemHandler.d.ts.map +1 -0
  68. package/dist/elements/ItemHandler.test.d.ts +2 -0
  69. package/dist/elements/ItemHandler.test.d.ts.map +1 -0
  70. package/dist/elements/TileHandler.d.ts +52 -0
  71. package/dist/elements/TileHandler.d.ts.map +1 -0
  72. package/dist/elements/TileHandler.test.d.ts +2 -0
  73. package/dist/elements/TileHandler.test.d.ts.map +1 -0
  74. package/dist/elements/__tests__/ElementInstanceStore.recall.test.d.ts +2 -0
  75. package/dist/elements/__tests__/ElementInstanceStore.recall.test.d.ts.map +1 -0
  76. package/dist/elements/blockRenderer.d.ts +46 -0
  77. package/dist/elements/blockRenderer.d.ts.map +1 -0
  78. package/dist/elements/blockRenderer.test.d.ts +13 -0
  79. package/dist/elements/blockRenderer.test.d.ts.map +1 -0
  80. package/dist/elements/blocks.d.ts +58 -0
  81. package/dist/elements/blocks.d.ts.map +1 -0
  82. package/dist/elements/envelope.d.ts +24 -0
  83. package/dist/elements/envelope.d.ts.map +1 -0
  84. package/dist/elements/fetcher.d.ts +40 -0
  85. package/dist/elements/fetcher.d.ts.map +1 -0
  86. package/dist/elements/index.d.ts +33 -0
  87. package/dist/elements/index.d.ts.map +1 -0
  88. package/dist/elements/store-singleton.d.ts +4 -0
  89. package/dist/elements/store-singleton.d.ts.map +1 -0
  90. package/dist/elements/types.d.ts +110 -0
  91. package/dist/elements/types.d.ts.map +1 -0
  92. package/dist/observer/__tests__/allowlist.test.d.ts +9 -0
  93. package/dist/observer/__tests__/allowlist.test.d.ts.map +1 -0
  94. package/dist/observer/__tests__/bus-translate.test.d.ts +16 -0
  95. package/dist/observer/__tests__/bus-translate.test.d.ts.map +1 -0
  96. package/dist/observer/__tests__/lifecycle.test.d.ts +2 -0
  97. package/dist/observer/__tests__/lifecycle.test.d.ts.map +1 -0
  98. package/dist/observer/__tests__/observer-enrich.test.d.ts +10 -0
  99. package/dist/observer/__tests__/observer-enrich.test.d.ts.map +1 -0
  100. package/dist/observer/__tests__/observer-isolation.test.d.ts +13 -0
  101. package/dist/observer/__tests__/observer-isolation.test.d.ts.map +1 -0
  102. package/dist/observer/__tests__/queue.test.d.ts +2 -0
  103. package/dist/observer/__tests__/queue.test.d.ts.map +1 -0
  104. package/dist/observer/__tests__/transport.test.d.ts +2 -0
  105. package/dist/observer/__tests__/transport.test.d.ts.map +1 -0
  106. package/dist/observer/allowlist.d.ts +30 -0
  107. package/dist/observer/allowlist.d.ts.map +1 -0
  108. package/dist/observer/bus-translate.d.ts +33 -0
  109. package/dist/observer/bus-translate.d.ts.map +1 -0
  110. package/dist/observer/index.d.ts +44 -0
  111. package/dist/observer/index.d.ts.map +1 -0
  112. package/dist/observer/queue.d.ts +63 -0
  113. package/dist/observer/queue.d.ts.map +1 -0
  114. package/dist/observer/transport.d.ts +27 -0
  115. package/dist/observer/transport.d.ts.map +1 -0
  116. package/dist/personas.d.ts +30 -0
  117. package/dist/personas.d.ts.map +1 -0
  118. package/dist/runtime.d.ts +9 -0
  119. package/dist/runtime.d.ts.map +1 -1
  120. package/dist/runtime.js +2345 -3
  121. package/dist/runtime.js.map +4 -4
  122. package/dist/schema.d.ts +3426 -17
  123. package/dist/schema.d.ts.map +1 -1
  124. package/dist/schema.js +213 -0
  125. package/dist/schema.js.map +2 -2
  126. package/dist/test/setup.d.ts +1 -0
  127. package/dist/test/setup.d.ts.map +1 -0
  128. package/dist/types.d.ts +46 -2
  129. package/dist/types.d.ts.map +1 -1
  130. package/package.json +17 -1
  131. package/dist/chunk-W457NMGD.js.map +0 -7
package/dist/runtime.js CHANGED
@@ -1,8 +1,2291 @@
1
1
  import {
2
- ChatAssistantLitMountable
3
- } from "./chunk-W457NMGD.js";
2
+ ActionHandler,
3
+ AgUiTransport,
4
+ ChatAssistantLitMountable,
5
+ ElementInstanceStore,
6
+ ItemHandler,
7
+ TileHandler,
8
+ acquireTokenWithChallenge,
9
+ decodeMutationEnvelope,
10
+ fetchMountedElements,
11
+ renderFallbackHtml
12
+ } from "./chunk-ZXV4GXMO.js";
13
+ import {
14
+ pickPersona
15
+ } from "./chunk-FXHYE6FY.js";
16
+ import {
17
+ getElementInstanceStore,
18
+ setElementInstanceStore
19
+ } from "./chunk-EWPPVPJ4.js";
20
+ import "./chunk-A7RBZLIX.js";
21
+ import "./chunk-VZIUXXAM.js";
22
+ import "./chunk-OIGVZGSX.js";
23
+ import {
24
+ DIVE_DEEPER_EVENT,
25
+ TILE_MOUNTED_EVENT,
26
+ TILE_UNMOUNTED_EVENT
27
+ } from "./chunk-EACGDSJI.js";
4
28
  import "./chunk-UVKRO5ER.js";
5
29
 
30
+ // src/ChatSession.ts
31
+ var _userIdCounter = 0;
32
+ function generateUserMessageId() {
33
+ return `user-${Date.now()}-${++_userIdCounter}`;
34
+ }
35
+ var ChatSession = class {
36
+ constructor() {
37
+ this._messages = [];
38
+ this._inFlight = false;
39
+ this._runActive = false;
40
+ this._thinkingText = "";
41
+ this._nextId = 1;
42
+ this.subscribers = /* @__PURE__ */ new Set();
43
+ this.sendListeners = /* @__PURE__ */ new Set();
44
+ this.interruptListeners = /* @__PURE__ */ new Set();
45
+ this.toolResultListeners = /* @__PURE__ */ new Set();
46
+ }
47
+ /** Snapshot the current state. Always returns a fresh immutable view. */
48
+ getState() {
49
+ return {
50
+ messages: [...this._messages],
51
+ inFlight: this._inFlight || this._runActive,
52
+ thinkingText: this._thinkingText
53
+ };
54
+ }
55
+ /**
56
+ * Append a delta to the active thinking turn's narration buffer.
57
+ * Forwarded from the AG-UI THINKING_TEXT_MESSAGE_CONTENT event so
58
+ * the chat trail can render the model's own reasoning instead of
59
+ * silent loading dots between turns.
60
+ */
61
+ appendThinkingDelta(delta) {
62
+ if (!delta) return;
63
+ this._thinkingText += delta;
64
+ this.notify();
65
+ }
66
+ /**
67
+ * Replace the active thinking narration outright (vs. appendThinkingDelta's
68
+ * streaming accumulation). Used to surface tool-call progress — e.g.
69
+ * "Searching…" while the agent works a tool — so a long tool phase shows
70
+ * what's happening instead of silent dots. No-op if unchanged.
71
+ */
72
+ setThinkingText(text) {
73
+ if (this._thinkingText === text) return;
74
+ this._thinkingText = text;
75
+ this.notify();
76
+ }
77
+ /**
78
+ * End the active thinking turn — drop any narration so the trail
79
+ * stops showing it. Fires on real text-message-start and on
80
+ * tool-call-start, the moment the model switches from reasoning
81
+ * to visible output.
82
+ */
83
+ clearThinking() {
84
+ if (!this._thinkingText) return;
85
+ this._thinkingText = "";
86
+ this.notify();
87
+ }
88
+ setRunActive(active) {
89
+ if (this._runActive === active) return;
90
+ this._runActive = active;
91
+ if (!active) this._thinkingText = "";
92
+ this.notify();
93
+ }
94
+ /**
95
+ * Subscribe to state changes. Called immediately with the current
96
+ * state, then again on every change. Returns an unsubscribe function.
97
+ */
98
+ subscribe(cb) {
99
+ this.subscribers.add(cb);
100
+ cb(this.getState());
101
+ return () => {
102
+ this.subscribers.delete(cb);
103
+ };
104
+ }
105
+ /**
106
+ * User submitted a message. Appends a user-role message, sets
107
+ * inFlight=true, notifies state subscribers, and fires a "send"
108
+ * event so transports can pick it up. Empty/whitespace text is a
109
+ * no-op (matches the chat bar's local guard).
110
+ */
111
+ send(text, opts) {
112
+ const trimmed = text.trim();
113
+ if (!trimmed) return;
114
+ const id = generateUserMessageId();
115
+ this._messages.push({ id, role: "user", text: trimmed });
116
+ this._inFlight = true;
117
+ this.notify();
118
+ const event = { text: trimmed, id };
119
+ if (opts?.activeLidSlot) event.activeLidSlot = opts.activeLidSlot;
120
+ if (opts?.forwardedProps) event.forwardedProps = opts.forwardedProps;
121
+ for (const listener of this.sendListeners) listener(event);
122
+ }
123
+ /**
124
+ * Single-shot assistant reply (no streaming). Equivalent to
125
+ * receiveStart + receiveDelta + receiveEnd in one call. Useful for
126
+ * stub transports and tests that don't model streaming.
127
+ */
128
+ receive(text) {
129
+ const id = `m-${this._nextId++}`;
130
+ this._messages.push({ id, role: "assistant", text, status: "complete" });
131
+ this._inFlight = false;
132
+ this.notify();
133
+ }
134
+ /**
135
+ * Begin a streaming assistant message. Appends an empty assistant
136
+ * message with status='streaming'. Caller (transport adapter) feeds
137
+ * deltas via receiveDelta(id, text) and signals completion via
138
+ * receiveEnd(id). `inFlight` stays true through the stream.
139
+ */
140
+ receiveStart(id) {
141
+ this._messages.push({ id, role: "assistant", text: "", status: "streaming" });
142
+ this._inFlight = true;
143
+ this.notify();
144
+ }
145
+ /**
146
+ * Append a delta to a streaming message. No-op when the id is
147
+ * unknown (race between transport events and reset, etc.) — never
148
+ * throws so transports can fire-and-forget.
149
+ */
150
+ receiveDelta(id, delta) {
151
+ const msg = this._messages.find((m) => m.id === id);
152
+ if (!msg || msg.status !== "streaming") return;
153
+ msg.text += delta;
154
+ this.notify();
155
+ }
156
+ /**
157
+ * Mark a streaming message complete and clear inFlight. No-op when
158
+ * the id is unknown (defensive against transport double-fires).
159
+ */
160
+ receiveEnd(id) {
161
+ const msg = this._messages.find((m) => m.id === id);
162
+ if (msg && msg.status === "streaming") {
163
+ msg.status = "complete";
164
+ }
165
+ this._inFlight = false;
166
+ this.notify();
167
+ }
168
+ /**
169
+ * Transport reports a fatal error. Marks any in-flight streaming
170
+ * message as 'error' (so the trail can render a styled error chip
171
+ * instead of pretending the partial text was a complete answer),
172
+ * appends a system-role message with the error text for visibility,
173
+ * and clears inFlight. Also drops any pending thinking narration
174
+ * — leaving stale "Let me check…" alongside an error chip reads
175
+ * as if the assistant is still working when it isn't.
176
+ */
177
+ error(message) {
178
+ for (const m of this._messages) {
179
+ if (m.status === "streaming") m.status = "error";
180
+ }
181
+ this._messages.push({
182
+ id: `err-${this._nextId++}`,
183
+ role: "system",
184
+ text: message,
185
+ status: "error"
186
+ });
187
+ this._inFlight = false;
188
+ this._runActive = false;
189
+ this._thinkingText = "";
190
+ this.notify();
191
+ }
192
+ /**
193
+ * User clicked the in-flight stop button. Clears inFlight and
194
+ * fires an "interrupt" event so transports can cancel their
195
+ * in-flight request. No-op if not in-flight (idempotent). Drops
196
+ * any pending thinking narration for the same reason as error():
197
+ * the visitor explicitly asked the assistant to stop, so it
198
+ * shouldn't keep narrating in the trail.
199
+ */
200
+ interrupt() {
201
+ if (!this._inFlight && !this._runActive) return;
202
+ this._inFlight = false;
203
+ this._runActive = false;
204
+ this._thinkingText = "";
205
+ this.notify();
206
+ for (const listener of this.interruptListeners) listener();
207
+ }
208
+ /**
209
+ * Wipe state. Used by the canvas-close path (start a fresh
210
+ * conversation next time) and by tests.
211
+ */
212
+ reset() {
213
+ this._messages = [];
214
+ this._inFlight = false;
215
+ this._runActive = false;
216
+ this._thinkingText = "";
217
+ this._nextId = 1;
218
+ this.notify();
219
+ }
220
+ /** Register a transport's send listener. Returns unsubscribe. */
221
+ onSend(listener) {
222
+ this.sendListeners.add(listener);
223
+ return () => {
224
+ this.sendListeners.delete(listener);
225
+ };
226
+ }
227
+ /**
228
+ * True when at least one transport has wired itself to the session's
229
+ * send pipeline. Views can use this to surface "no chat backend
230
+ * connected" affordances instead of hanging in `inFlight` after a
231
+ * send fires into the void.
232
+ */
233
+ hasTransport() {
234
+ return this.sendListeners.size > 0;
235
+ }
236
+ /** Register a transport's interrupt listener. Returns unsubscribe. */
237
+ onInterrupt(listener) {
238
+ this.interruptListeners.add(listener);
239
+ return () => {
240
+ this.interruptListeners.delete(listener);
241
+ };
242
+ }
243
+ /**
244
+ * Register a transport's tool-result listener. The transport
245
+ * forwards `tool-result` actions back to the agent after the user
246
+ * approves or rejects a client-tool call. Returns unsubscribe.
247
+ */
248
+ onToolResult(listener) {
249
+ this.toolResultListeners.add(listener);
250
+ return () => {
251
+ this.toolResultListeners.delete(listener);
252
+ };
253
+ }
254
+ // -------------------------------------------------------------------------
255
+ // Tool calls
256
+ // -------------------------------------------------------------------------
257
+ /**
258
+ * Attach a tool call to a streaming assistant message. No-op when
259
+ * the message id is unknown (race between transport events and
260
+ * reset / late mount).
261
+ */
262
+ addToolCall(messageId, toolCall) {
263
+ const msg = this._messages.find((m) => m.id === messageId);
264
+ if (!msg) return;
265
+ msg.toolCalls = [...msg.toolCalls ?? [], { ...toolCall }];
266
+ this.notify();
267
+ }
268
+ /**
269
+ * Partially update a tool call by id. Used by the transport to
270
+ * advance status (args-streaming → running → done) as AG-UI events
271
+ * arrive. No-op when the id is unknown.
272
+ */
273
+ updateToolCall(toolCallId, patch) {
274
+ for (const msg of this._messages) {
275
+ const tcs = msg.toolCalls;
276
+ if (!tcs) continue;
277
+ const idx = tcs.findIndex((tc) => tc.id === toolCallId);
278
+ if (idx === -1) continue;
279
+ const next = [...tcs];
280
+ next[idx] = { ...next[idx], ...patch };
281
+ msg.toolCalls = next;
282
+ this.notify();
283
+ return;
284
+ }
285
+ }
286
+ /**
287
+ * Resolve a (client-) tool call. Marks the call done, persists, and
288
+ * fires onToolResult so the transport can forward the result + the
289
+ * user's approve/reject decision back to the agent. No-op when the
290
+ * id is unknown.
291
+ */
292
+ resolveToolCall(toolCallId, result, approved) {
293
+ let found = false;
294
+ for (const msg of this._messages) {
295
+ const tcs = msg.toolCalls;
296
+ if (!tcs) continue;
297
+ const idx = tcs.findIndex((tc) => tc.id === toolCallId);
298
+ if (idx === -1) continue;
299
+ const next = [...tcs];
300
+ next[idx] = { ...next[idx], status: "done" };
301
+ msg.toolCalls = next;
302
+ found = true;
303
+ break;
304
+ }
305
+ if (!found) return;
306
+ this.notify();
307
+ for (const listener of this.toolResultListeners) {
308
+ listener({ toolCallId, result, approved });
309
+ }
310
+ }
311
+ /**
312
+ * Hydrate from the server transcript (the /session SSE's
313
+ * messages-snapshot) — the server is the transcript of record
314
+ * (FEAT-1783701440). Snapshot order is truth: shared ids keep the
315
+ * LOCAL object (it may carry richer live tool-call state) but take the
316
+ * snapshot's position; snapshot-only messages (history on reload, a
317
+ * nudge missed during an SSE gap) are inserted; local-only messages
318
+ * (an in-flight turn, the greeting) float after in local order.
319
+ *
320
+ * Deploy-boundary guard: a user message persisted under a pre-Phase-2
321
+ * transport id (before ids were threaded) would arrive as
322
+ * "snapshot-only" next to its local twin — skip snapshot USER messages
323
+ * whose exact text already exists locally under a different id.
324
+ */
325
+ hydrate(snapshotMessages) {
326
+ if (!snapshotMessages.length) return;
327
+ const localById = new Map(this._messages.map((m) => [String(m.id), m]));
328
+ const twinBudget = /* @__PURE__ */ new Map();
329
+ for (const m of this._messages) {
330
+ if (m.role === "user") twinBudget.set(m.text, (twinBudget.get(m.text) ?? 0) + 1);
331
+ }
332
+ const seen = /* @__PURE__ */ new Set();
333
+ const merged = [];
334
+ for (const sm of snapshotMessages) {
335
+ const key = String(sm.id);
336
+ const local = localById.get(key);
337
+ if (local) {
338
+ seen.add(key);
339
+ merged.push(local);
340
+ continue;
341
+ }
342
+ if (sm.role === "user") {
343
+ const budget = twinBudget.get(sm.text) ?? 0;
344
+ if (budget > 0) {
345
+ twinBudget.set(sm.text, budget - 1);
346
+ continue;
347
+ }
348
+ }
349
+ merged.push(sm);
350
+ }
351
+ for (const m of this._messages) {
352
+ if (!seen.has(String(m.id))) merged.push(m);
353
+ }
354
+ this._messages = merged;
355
+ this.notify();
356
+ }
357
+ notify() {
358
+ const state = this.getState();
359
+ for (const sub of this.subscribers) sub(state);
360
+ }
361
+ };
362
+ var chatSession = new ChatSession();
363
+
364
+ // src/ChatTransport.ts
365
+ var SESSION_READ_DEADLINE_MS = 6e4;
366
+ var FALLBACK_DEBOUNCE_MS = 1500;
367
+ function readSyntroToken() {
368
+ if (typeof window === "undefined") return void 0;
369
+ const cfg = window.__SYNTRO_CONFIG__;
370
+ const token = cfg?.token;
371
+ return typeof token === "string" && token.length > 0 ? token : void 0;
372
+ }
373
+ function debug(...args) {
374
+ if (typeof window === "undefined") return;
375
+ if (window.__SYNTRO_CHAT_DEBUG__) {
376
+ console.debug("[chat-transport]", ...args);
377
+ }
378
+ }
379
+ function parseToolCallArgs(toolCall) {
380
+ if (toolCall.args && typeof toolCall.args === "object") return toolCall.args;
381
+ const buf = toolCall.argsBuffer;
382
+ if (!buf) return null;
383
+ try {
384
+ const parsed = JSON.parse(buf);
385
+ return parsed && typeof parsed === "object" ? parsed : null;
386
+ } catch {
387
+ return null;
388
+ }
389
+ }
390
+ function toolNarrationVariants(name) {
391
+ const n = (name || "").toLowerCase();
392
+ if (n.includes("search") || n.includes("retrieve"))
393
+ return ["Assembling relevant information", "Pulling together what matters for you"];
394
+ if (n.includes("route"))
395
+ return ["Diving deeper on specific products for you", "Exploring the best options for you"];
396
+ if (n.includes("read") || n.includes("section") || n.includes("overview"))
397
+ return ["Buttoning up details to point the right way", "Lining up the details for you"];
398
+ if (n.includes("selector"))
399
+ return ["Finalizing where to guide you", "Pinpointing the right spot for you"];
400
+ if (n.includes("wiki"))
401
+ return ["Looking that up for you", "Checking the knowledge base for you"];
402
+ if (n.includes("mount") || n.includes("unmount") || n.includes("element") || n.includes("response") || n.includes("primary") || n.includes("action"))
403
+ return ["Materializing the information for you", "Bringing it onto your screen"];
404
+ return ["Putting your answer together for you", "Almost there"];
405
+ }
406
+ function mergeForwardedProps(prev, next) {
407
+ if (next.forwardedProps !== void 0) return next;
408
+ if (!prev?.forwardedProps) return next;
409
+ return { ...next, forwardedProps: prev.forwardedProps };
410
+ }
411
+ var ChatTransport = class {
412
+ constructor() {
413
+ this._config = null;
414
+ this._status = "idle";
415
+ this._agui = null;
416
+ this._transportUnsub = null;
417
+ this._sessionUnsubSend = null;
418
+ this._sessionUnsubInterrupt = null;
419
+ this._sessionUnsubToolResult = null;
420
+ this._connectInFlight = null;
421
+ /** Bounded streaming-message id for the currently-being-typed assistant turn. */
422
+ this._currentAssistantMessageId = null;
423
+ /** Intent messageIds this transport actually dispatched (BUG-1784021805).
424
+ * The server's `suppressToast` hint assumes the intent frame reached us —
425
+ * but that frame is fire-and-forget, and a fresh visitor's connection gap
426
+ * loses it. Only the transport knows what it dispatched, so suppression is
427
+ * honored ONLY when the sibling intent (`{id}-intent`) went through here. */
428
+ this._dispatchedNudgeIntentIds = /* @__PURE__ */ new Set();
429
+ /** Outcome bookkeeping for telemetry. */
430
+ this._hadTurnstileToken = null;
431
+ /** Count of successful round-trips for telemetry payloads. */
432
+ this._messagesSucceeded = 0;
433
+ /** True once a successful assistant message has landed — gates fallback. */
434
+ this._hasSucceeded = false;
435
+ /** True once fallback has fired (one-shot). */
436
+ this._fallbackRendered = false;
437
+ /**
438
+ * Per-send override for ``X-Active-Lid-Slot``. Set in the
439
+ * ``chatSession.onSend`` listener immediately before each
440
+ * ``_forwardUserMessage`` and consumed by ``buildHeaders`` on the
441
+ * outbound request. The singleton transport is shared by every
442
+ * mounted chat-bar (drawer + inline) so cached ``_config.activeLidSlot``
443
+ * follows whichever bar last reconfigured — using that for routing
444
+ * silently misroutes tiles when both bars coexist. Per-send threading
445
+ * pins the header to the bar that actually sent the message.
446
+ */
447
+ this._pendingLidSlot = null;
448
+ /**
449
+ * Per-send forwardedProps override. Set in the ``chatSession.onSend``
450
+ * listener before each ``_forwardUserMessage`` and spread OVER the
451
+ * config-level forwardedProps for that one request (see
452
+ * ``_resolveForwardedProps``). Overwritten on EVERY send — ``null`` when
453
+ * the send carried no envelope — so turn provenance can never leak onto a
454
+ * later typed message and make it look synthetic. Same lifecycle as
455
+ * ``_pendingLidSlot``.
456
+ */
457
+ this._pendingForwardedProps = null;
458
+ /** Active debounce timer; null when no debounce pending. */
459
+ this._errorDebounceTimer = null;
460
+ /** Rotates thinking-chip phrasings during a long/looping tool phase (C9). */
461
+ this._thinkingCycleTimer = null;
462
+ /**
463
+ * Per-class rotation counter, keyed by the narration set's first variant.
464
+ * A NEW tool class shows its primary (variant A) so it's recognizable; a
465
+ * REPEATED same-class call advances to the next variant, so a research loop
466
+ * of (e.g.) several searches reads as different thinking, not one repeated
467
+ * label. Bounded to ~6 keys (one per tool class).
468
+ */
469
+ this._thinkingClassSeq = /* @__PURE__ */ new Map();
470
+ /** Most recent error payload, captured so debounced fallback can attach it. */
471
+ this._lastErrorPayload = null;
472
+ this._fallbackListeners = /* @__PURE__ */ new Set();
473
+ /**
474
+ * Per-configure-cycle id for correlating telemetry events from a
475
+ * single transport lifetime. Mirrors ChatAssistantLit's mountId
476
+ * but scoped to configure cycles since the transport is a
477
+ * singleton across mounts.
478
+ */
479
+ this._transportId = `tx_${Math.random().toString(36).slice(2, 8)}`;
480
+ this._configuredAt = 0;
481
+ // -------------------------------------------------------------------------
482
+ // Session stream — server-initiated events (proactive nudges)
483
+ // -------------------------------------------------------------------------
484
+ /** AbortController for the live /session SSE reader; null = not running. */
485
+ this._sessionStreamAbort = null;
486
+ // (read-deadline constant lives at module scope: SESSION_READ_DEADLINE_MS)
487
+ /** Capped reconnect backoff for the session stream. */
488
+ this._sessionStreamBackoffMs = 2e3;
489
+ }
490
+ /**
491
+ * Configure the transport. Idempotent — calling again with the same
492
+ * backendUrl is a no-op; calling with a different backendUrl tears
493
+ * the connection down and re-arms.
494
+ */
495
+ configure(config) {
496
+ const same = this._config && this._config.backendUrl === config.backendUrl && this._config.threadId === config.threadId;
497
+ if (same) {
498
+ this._config = { ...this._config, ...mergeForwardedProps(this._config, config) };
499
+ return;
500
+ }
501
+ this._disconnect();
502
+ this._config = config;
503
+ this._status = "configured";
504
+ this._transportId = `tx_${Math.random().toString(36).slice(2, 8)}`;
505
+ this._configuredAt = Date.now();
506
+ this._wireSession();
507
+ debug("configured", {
508
+ transportId: this._transportId,
509
+ backendUrl: config.backendUrl,
510
+ threadId: config.threadId
511
+ });
512
+ }
513
+ /** ms since the most recent configure() call. 0 before any configure. */
514
+ _ageMs() {
515
+ return this._configuredAt === 0 ? 0 : Date.now() - this._configuredAt;
516
+ }
517
+ /** True when configure() has been called and we're ready to lazy-connect on send. */
518
+ get isConfigured() {
519
+ return this._status !== "idle";
520
+ }
521
+ /** True when AgUiTransport is up. */
522
+ get isConnected() {
523
+ return this._status === "connected";
524
+ }
525
+ /**
526
+ * Subscribe to fallback events — fires once per configure cycle
527
+ * when the transport gives up after sustained failure. Hosts
528
+ * (typically AdaptiveChatBarMountable) use this to swap the chat
529
+ * bar for a static "contact support" card.
530
+ */
531
+ onFallback(listener) {
532
+ this._fallbackListeners.add(listener);
533
+ return () => {
534
+ this._fallbackListeners.delete(listener);
535
+ };
536
+ }
537
+ /**
538
+ * Test seam — drive a synthetic error event through the transport's
539
+ * error handling without standing up a real AgUi transport. Production
540
+ * code path uses _onTransportEvent. Exported as a public method to
541
+ * keep the test isolation simple; not part of the documented API.
542
+ */
543
+ simulateError(payload) {
544
+ this._handleErrorEvent(payload);
545
+ }
546
+ /**
547
+ * Test seam — drive the FULL server-error path (fallback kernel + the
548
+ * visitor-facing chip), exactly what the `case 'error'` transport-event
549
+ * branch does. `midAnswer` simulates an assistant message having been
550
+ * mid-stream when the connection died.
551
+ */
552
+ simulateServerError(payload) {
553
+ if (payload.midAnswer) this._currentAssistantMessageId = "test-mid-answer";
554
+ this._onTransportEvent({
555
+ type: "error",
556
+ message: payload.message ?? "boom",
557
+ status: payload.status ?? null,
558
+ body: payload.body ?? null,
559
+ errorName: payload.errorName ?? null
560
+ });
561
+ }
562
+ /**
563
+ * Test seam — register a synthetic successful message-complete so
564
+ * the hasSucceeded gate flips without a real AgUi round-trip.
565
+ */
566
+ simulateSuccessfulMessage() {
567
+ this._hasSucceeded = true;
568
+ this._messagesSucceeded += 1;
569
+ this._clearDebounceTimer();
570
+ }
571
+ /**
572
+ * Test seam — return the lid slot that the next outbound request
573
+ * WOULD tag ``X-Active-Lid-Slot`` with, given current state. Mirrors
574
+ * the exact resolution buildHeaders uses (per-send slot wins over
575
+ * cached config). Not part of the documented API.
576
+ */
577
+ getActiveLidSlotForTest() {
578
+ return this._pendingLidSlot ?? this._config?.activeLidSlot ?? null;
579
+ }
580
+ /**
581
+ * Tear connection-level state down. Used internally by configure()
582
+ * to swap backends; preserves host-registered fallback listeners
583
+ * because the host UI handler (e.g. AdaptiveChatBarMountable's
584
+ * "swap to fallback card" callback) is configuration-independent.
585
+ */
586
+ _disconnect() {
587
+ if (this._transportUnsub) {
588
+ this._transportUnsub();
589
+ this._transportUnsub = null;
590
+ }
591
+ if (this._sessionUnsubSend) {
592
+ this._sessionUnsubSend();
593
+ this._sessionUnsubSend = null;
594
+ }
595
+ if (this._sessionUnsubInterrupt) {
596
+ this._sessionUnsubInterrupt();
597
+ this._sessionUnsubInterrupt = null;
598
+ }
599
+ if (this._sessionUnsubToolResult) {
600
+ this._sessionUnsubToolResult();
601
+ this._sessionUnsubToolResult = null;
602
+ }
603
+ if (this._agui) {
604
+ this._agui.disconnect();
605
+ this._agui = null;
606
+ }
607
+ this._clearDebounceTimer();
608
+ this._stopThinkingCycle();
609
+ this._thinkingClassSeq.clear();
610
+ this._config = null;
611
+ this._status = "idle";
612
+ this._connectInFlight = null;
613
+ this._currentAssistantMessageId = null;
614
+ this._hadTurnstileToken = null;
615
+ this._messagesSucceeded = 0;
616
+ this._hasSucceeded = false;
617
+ this._fallbackRendered = false;
618
+ this._lastErrorPayload = null;
619
+ this._pendingLidSlot = null;
620
+ this._pendingForwardedProps = null;
621
+ }
622
+ /**
623
+ * Tear everything down — connection state PLUS host listeners.
624
+ * Use this in test teardown or when fully shutting the transport
625
+ * (page unload, integration test reset). The mountable calls
626
+ * _disconnect indirectly via reconfigure.
627
+ */
628
+ reset() {
629
+ this._disconnect();
630
+ this._fallbackListeners.clear();
631
+ this._dispatchedNudgeIntentIds.clear();
632
+ this.stopSessionStream();
633
+ }
634
+ /**
635
+ * Open the persistent `/api/adaptive/session` SSE and feed its frames
636
+ * through the SAME `_onTransportEvent` switch the AG-UI path uses
637
+ * (FEAT-1783640628 — proactive nudges).
638
+ *
639
+ * The AG-UI cutover left this endpoint with no client consumer, so
640
+ * server-INITIATED events had nowhere to land. A fetch-based reader —
641
+ * unlike EventSource — can send the SDK token, and the visitor ids that
642
+ * write the server's nudge identity bridge. Idempotent; reconnects with
643
+ * capped backoff; `reset()`/`stopSessionStream()` tears it down.
644
+ */
645
+ async startSessionStream() {
646
+ if (this._sessionStreamAbort || !this._config) return;
647
+ const abort = new AbortController();
648
+ this._sessionStreamAbort = abort;
649
+ const baseUrl = this._config.backendUrl.replace(/\/$/, "");
650
+ const url = `${baseUrl}/api/adaptive/session`;
651
+ const headers = {};
652
+ const token = readSyntroToken();
653
+ if (token) headers.Authorization = `Bearer ${token}`;
654
+ const cft = await this._acquireTurnstileWithChallenge();
655
+ if (cft) headers["CF-Turnstile-Token"] = cft;
656
+ if (abort.signal.aborted) return;
657
+ const readIds = () => {
658
+ try {
659
+ const rt = this._config?.runtime;
660
+ const did = rt?.telemetry?.getDistinctId?.();
661
+ const sid = rt?.telemetry?.getSessionId?.();
662
+ return {
663
+ did: typeof did === "string" && did.length > 0 ? did : void 0,
664
+ sid: typeof sid === "string" && sid.length > 0 ? sid : void 0
665
+ };
666
+ } catch {
667
+ return {};
668
+ }
669
+ };
670
+ const hasTelemetryAccessor = () => {
671
+ try {
672
+ const rt = this._config?.runtime;
673
+ return typeof rt?.telemetry?.getDistinctId === "function";
674
+ } catch {
675
+ return false;
676
+ }
677
+ };
678
+ let ids = readIds();
679
+ for (let i = 0; i < 80 && hasTelemetryAccessor() && !ids.did && !abort.signal.aborted; i++) {
680
+ await new Promise((r) => setTimeout(r, 250));
681
+ ids = readIds();
682
+ }
683
+ if (abort.signal.aborted) return;
684
+ if (ids.did) headers["X-Distinct-Id"] = ids.did;
685
+ if (ids.sid) headers["X-Syntro-Session"] = ids.sid;
686
+ const runReader = async () => {
687
+ const connAbort = new AbortController();
688
+ const onOuterAbort = () => connAbort.abort();
689
+ abort.signal.addEventListener("abort", onOuterAbort, { once: true });
690
+ const abortRejection = new Promise((_, reject) => {
691
+ connAbort.signal.addEventListener(
692
+ "abort",
693
+ () => reject(new Error("session stream read deadline exceeded")),
694
+ { once: true }
695
+ );
696
+ });
697
+ abortRejection.catch(() => {
698
+ });
699
+ try {
700
+ const response = await fetch(url, {
701
+ headers,
702
+ credentials: "include",
703
+ signal: connAbort.signal
704
+ });
705
+ if (!response.ok || !response.body) {
706
+ throw new Error(`session stream connect failed: ${response.status}`);
707
+ }
708
+ this._sessionStreamBackoffMs = 2e3;
709
+ const reader = response.body.getReader();
710
+ const decoder = new TextDecoder();
711
+ let buffer = "";
712
+ for (; ; ) {
713
+ const deadline = setTimeout(() => connAbort.abort(), SESSION_READ_DEADLINE_MS);
714
+ let result;
715
+ try {
716
+ result = await Promise.race([reader.read(), abortRejection]);
717
+ } catch (err) {
718
+ void reader.cancel().catch(() => {
719
+ });
720
+ throw err;
721
+ } finally {
722
+ clearTimeout(deadline);
723
+ }
724
+ const { done, value } = result;
725
+ if (done) return;
726
+ buffer += decoder.decode(value, { stream: true });
727
+ let sep = buffer.indexOf("\n\n");
728
+ while (sep >= 0) {
729
+ const frame = buffer.slice(0, sep);
730
+ buffer = buffer.slice(sep + 2);
731
+ sep = buffer.indexOf("\n\n");
732
+ const data = frame.split("\n").filter((l) => l.startsWith("data: ")).map((l) => l.slice(6)).join("");
733
+ if (!data) continue;
734
+ try {
735
+ this._onTransportEvent(JSON.parse(data));
736
+ } catch (err) {
737
+ console.warn("[adaptive-chatbot] session-stream frame parse failed", err);
738
+ }
739
+ }
740
+ }
741
+ } finally {
742
+ abort.signal.removeEventListener("abort", onOuterAbort);
743
+ }
744
+ };
745
+ void (async () => {
746
+ while (!abort.signal.aborted) {
747
+ try {
748
+ await runReader();
749
+ } catch (err) {
750
+ if (abort.signal.aborted) return;
751
+ console.warn("[adaptive-chatbot] session stream dropped; reconnecting", err);
752
+ }
753
+ if (abort.signal.aborted) return;
754
+ await new Promise((r) => setTimeout(r, this._sessionStreamBackoffMs));
755
+ this._sessionStreamBackoffMs = Math.min(this._sessionStreamBackoffMs * 2, 3e4);
756
+ const fresh = readIds();
757
+ if (fresh.did) headers["X-Distinct-Id"] = fresh.did;
758
+ if (fresh.sid) headers["X-Syntro-Session"] = fresh.sid;
759
+ }
760
+ })();
761
+ await Promise.resolve();
762
+ }
763
+ stopSessionStream() {
764
+ this._sessionStreamAbort?.abort();
765
+ this._sessionStreamAbort = null;
766
+ this._sessionStreamBackoffMs = 2e3;
767
+ }
768
+ _clearDebounceTimer() {
769
+ if (this._errorDebounceTimer) {
770
+ clearTimeout(this._errorDebounceTimer);
771
+ this._errorDebounceTimer = null;
772
+ }
773
+ }
774
+ /**
775
+ * Show a per-call rotating variant in the thinking chip immediately (so
776
+ * consecutive tool calls differ), then rotate through the remaining phrasings
777
+ * every 4s while the same tool phase runs (C9). A
778
+ * single tool call can sit for tens of seconds (the model reasoning about the
779
+ * next step, or a slow retrieval); a static chip reads as frozen and hides a
780
+ * loop. Cycling makes a long/looping phase read as live progress. Restarting
781
+ * (a new tool), any output (message/thinking), or teardown clears the cycle.
782
+ */
783
+ _startThinkingCycle(variants) {
784
+ this._stopThinkingCycle();
785
+ const key = variants[0] ?? "";
786
+ const seq = this._thinkingClassSeq.get(key) ?? 0;
787
+ this._thinkingClassSeq.set(key, seq + 1);
788
+ let i = variants.length > 0 ? seq % variants.length : 0;
789
+ chatSession.setThinkingText(variants[i] ?? "Working on it\u2026");
790
+ if (variants.length < 2) return;
791
+ const id = setInterval(() => {
792
+ if (this._thinkingCycleTimer !== id) return;
793
+ i = (i + 1) % variants.length;
794
+ chatSession.setThinkingText(variants[i]);
795
+ }, 4e3);
796
+ this._thinkingCycleTimer = id;
797
+ }
798
+ _stopThinkingCycle() {
799
+ if (this._thinkingCycleTimer) {
800
+ clearInterval(this._thinkingCycleTimer);
801
+ this._thinkingCycleTimer = null;
802
+ }
803
+ }
804
+ /**
805
+ * Shared error-handling kernel — called by the AG-UI subscriber and
806
+ * by the simulateError test seam. Publishes transport_error
807
+ * telemetry, captures lastErrorPayload, and starts the debounce
808
+ * timer that will fire fallback if no successful message arrives
809
+ * before it expires. Gated by hasSucceeded (post-success errors
810
+ * never fallback) and _fallbackRendered (one-shot).
811
+ */
812
+ _handleErrorEvent(payload) {
813
+ const status = payload.status ?? null;
814
+ const body = payload.body ? String(payload.body).slice(0, 200) : null;
815
+ this._lastErrorPayload = {
816
+ message: payload.message,
817
+ status,
818
+ body,
819
+ errorName: payload.errorName ?? null
820
+ };
821
+ this._stopThinkingCycle();
822
+ this._config?.runtime.events.publish("chatbot.transport_error", {
823
+ source: "chat-transport",
824
+ transportId: this._transportId,
825
+ transportAgeMs: this._ageMs(),
826
+ messagesSucceeded: this._messagesSucceeded,
827
+ hadTurnstileToken: this._hadTurnstileToken,
828
+ hasSucceeded: this._hasSucceeded,
829
+ errorMessage: payload.message ?? null,
830
+ errorStatus: status,
831
+ errorBody: body,
832
+ errorName: payload.errorName ?? null
833
+ });
834
+ if (this._hasSucceeded || this._fallbackRendered || this._errorDebounceTimer) return;
835
+ this._errorDebounceTimer = setTimeout(() => {
836
+ this._errorDebounceTimer = null;
837
+ if (this._hasSucceeded || this._fallbackRendered) return;
838
+ this._renderFallback("connect_failed");
839
+ }, FALLBACK_DEBOUNCE_MS);
840
+ }
841
+ /**
842
+ * Fire the fallback. One-shot per configure cycle. Notifies all
843
+ * fallback listeners with the per-customer card config and the
844
+ * diagnostic snapshot for telemetry/debug.
845
+ */
846
+ _renderFallback(reason) {
847
+ if (this._fallbackRendered) return;
848
+ this._fallbackRendered = true;
849
+ debug("fallback", { reason, transportId: this._transportId, ageMs: this._ageMs() });
850
+ const fallback = this._config?.fallback ?? {};
851
+ const payload = {
852
+ reason,
853
+ fallback,
854
+ transportId: this._transportId,
855
+ transportAgeMs: this._ageMs(),
856
+ messagesSucceeded: this._messagesSucceeded,
857
+ hadTurnstileToken: this._hadTurnstileToken,
858
+ errorStatus: this._lastErrorPayload?.status ?? null,
859
+ errorBody: this._lastErrorPayload?.body ?? null,
860
+ errorMessage: this._lastErrorPayload?.message ?? null,
861
+ errorName: this._lastErrorPayload?.errorName ?? null
862
+ };
863
+ this._config?.runtime.events.publish(
864
+ "chatbot.fallback_rendered",
865
+ payload
866
+ );
867
+ for (const listener of this._fallbackListeners) listener(payload);
868
+ }
869
+ // -------------------------------------------------------------------------
870
+ // Internal: chatSession wiring
871
+ // -------------------------------------------------------------------------
872
+ /**
873
+ * forwardedProps for the NEXT outbound AG-UI request: the config-level
874
+ * value (late-bound — `configure()` can update uiTemplates /
875
+ * elementsEnabled mid-session without a reconnect, so this must be
876
+ * resolved per request, not captured at construction) with the per-send
877
+ * envelope spread over it. Shallow merge; per-send wins per top-level key.
878
+ */
879
+ _resolveForwardedProps() {
880
+ const raw = this._config?.forwardedProps;
881
+ const base = typeof raw === "function" ? raw() : raw;
882
+ const pending = this._pendingForwardedProps;
883
+ if (!pending) return base;
884
+ return { ...base ?? {}, ...pending };
885
+ }
886
+ _wireSession() {
887
+ this._sessionUnsubSend = chatSession.onSend(({ text, id, activeLidSlot, forwardedProps }) => {
888
+ this._pendingLidSlot = typeof activeLidSlot === "string" && activeLidSlot.length > 0 ? activeLidSlot : null;
889
+ this._pendingForwardedProps = forwardedProps ?? null;
890
+ void this._forwardUserMessage(text, id);
891
+ });
892
+ this._sessionUnsubInterrupt = chatSession.onInterrupt(() => {
893
+ this._agui?.send({ type: "stop-generation" });
894
+ });
895
+ this._sessionUnsubToolResult = chatSession.onToolResult(({ toolCallId, result, approved }) => {
896
+ this._agui?.send({ type: "tool-result", toolCallId, result, approved });
897
+ });
898
+ }
899
+ async _forwardUserMessage(text, id) {
900
+ let ok = false;
901
+ try {
902
+ ok = await this._ensureConnected();
903
+ } catch (err) {
904
+ this._status = "error";
905
+ this._handleErrorEvent({
906
+ message: err instanceof Error ? err.message : String(err),
907
+ errorName: err instanceof Error ? err.name : null
908
+ });
909
+ }
910
+ if (!ok || !this._agui) {
911
+ chatSession.error("Couldn't connect to chat. Please try again.");
912
+ return;
913
+ }
914
+ this._agui.send({ type: "user-message", text, ...id ? { id } : {} });
915
+ }
916
+ // -------------------------------------------------------------------------
917
+ // Internal: connect (lazy, Turnstile-gated)
918
+ // -------------------------------------------------------------------------
919
+ /**
920
+ * Ensure AgUiTransport is up. Idempotent — multiple concurrent calls
921
+ * dedupe to a single Turnstile acquisition + transport setup.
922
+ * Returns true on success, false on terminal failure.
923
+ */
924
+ async _ensureConnected() {
925
+ if (this._status === "connected" && this._agui) return true;
926
+ if (!this._config) return false;
927
+ if (this._connectInFlight) return this._connectInFlight;
928
+ this._connectInFlight = (async () => {
929
+ this._status = "acquiring";
930
+ const cft = await this._acquireTurnstileWithChallenge();
931
+ this._hadTurnstileToken = cft !== null;
932
+ if (this._config === null) return false;
933
+ const baseUrl = this._config.backendUrl.replace(/\/$/, "");
934
+ const streamUrl = `${baseUrl}/api/adaptive/stream`;
935
+ const token = readSyntroToken();
936
+ const buildHeaders = () => {
937
+ const h = {};
938
+ if (token) h.Authorization = `Bearer ${token}`;
939
+ if (cft) h["CF-Turnstile-Token"] = cft;
940
+ const lidSlot = this._pendingLidSlot ?? this._config?.activeLidSlot;
941
+ if (typeof lidSlot === "string" && lidSlot.length > 0) {
942
+ h["X-Active-Lid-Slot"] = lidSlot;
943
+ }
944
+ const asciiSafe = (v, max) => (v ?? "").replace(/[^\x20-\x7e]/g, "").slice(0, max).trim();
945
+ const pid = asciiSafe(this._config?.personaId, 32);
946
+ const pname = asciiSafe(this._config?.personaName, 24);
947
+ if (pid && pname) {
948
+ h["X-Syntro-Persona-Id"] = pid;
949
+ h["X-Syntro-Persona-Name"] = pname;
950
+ const prole = asciiSafe(this._config?.personaRole, 48);
951
+ if (prole) h["X-Syntro-Persona-Role"] = prole;
952
+ }
953
+ try {
954
+ const rt = this._config?.runtime;
955
+ const did = rt?.telemetry?.getDistinctId?.();
956
+ if (typeof did === "string" && did.length > 0) {
957
+ h["X-Distinct-Id"] = did;
958
+ }
959
+ const sid = rt?.telemetry?.getSessionId?.();
960
+ if (typeof sid === "string" && sid.length > 0) {
961
+ h["X-Syntro-Session"] = sid;
962
+ }
963
+ } catch {
964
+ }
965
+ return h;
966
+ };
967
+ const resolveForwardedProps = () => this._resolveForwardedProps();
968
+ this._agui = new AgUiTransport({
969
+ url: streamUrl,
970
+ headers: buildHeaders,
971
+ threadId: this._config.threadId,
972
+ clientTools: this._config.clientTools,
973
+ // Adaptive runtime SDK needs the `syntro_chat_session` cookie to
974
+ // round-trip cross-origin so subsequent boot fetches can rehydrate
975
+ // LLM-authored UI elements. Editor / action-plan chat surfaces
976
+ // auth via `?token=` and intentionally leave this unset (see
977
+ // AgUiTransportOptions.credentials docstring).
978
+ credentials: "include",
979
+ forwardedProps: resolveForwardedProps,
980
+ onA2UIEvent: (payload) => {
981
+ const runtime2 = this._config?.runtime;
982
+ if (!runtime2) return;
983
+ const mutations = decodeMutationEnvelope(payload);
984
+ if (mutations !== null) {
985
+ const handler = this._config?.onElementMutation;
986
+ if (handler) {
987
+ try {
988
+ handler(mutations);
989
+ runtime2.events.publish("chatbot.element_mutation_applied", {
990
+ source: "chat-transport",
991
+ count: mutations.length
992
+ });
993
+ } catch (err) {
994
+ const msg = err instanceof Error ? err.message : String(err);
995
+ console.error("[chat-transport] element mutation apply failed:", msg);
996
+ }
997
+ return;
998
+ }
999
+ return;
1000
+ }
1001
+ runtime2.actions.applyBatch([payload]).then(() => {
1002
+ runtime2.events.publish("chatbot.a2ui_applied", { source: "chat-transport" });
1003
+ }).catch((err) => {
1004
+ const msg = err instanceof Error ? err.message : String(err);
1005
+ console.error("[chat-transport] A2UI apply failed:", msg);
1006
+ });
1007
+ }
1008
+ });
1009
+ this._agui.connect();
1010
+ this._transportUnsub = this._agui.subscribe((event) => this._onTransportEvent(event));
1011
+ this._status = "connected";
1012
+ return true;
1013
+ })();
1014
+ try {
1015
+ return await this._connectInFlight;
1016
+ } finally {
1017
+ this._connectInFlight = null;
1018
+ }
1019
+ }
1020
+ /**
1021
+ * Acquire a Turnstile token via the managed-challenge flow.
1022
+ * Delegates to the shared Turnstile helper that owns the verify
1023
+ * panel lifecycle. Returns null when Turnstile is disabled at
1024
+ * build time or acquisition fails.
1025
+ */
1026
+ async _acquireTurnstileWithChallenge() {
1027
+ const { token } = await acquireTokenWithChallenge();
1028
+ return token;
1029
+ }
1030
+ // -------------------------------------------------------------------------
1031
+ // Internal: AgUi event → chatSession
1032
+ // -------------------------------------------------------------------------
1033
+ /**
1034
+ * Look up a tool call's current chatSession-side status by id.
1035
+ * Used to decide between addToolCall (first sighting) and
1036
+ * updateToolCall (subsequent updates) when AG-UI re-emits
1037
+ * `tool-call` events for client tools transitioning to 'pending'.
1038
+ */
1039
+ _findToolCallStatus(toolCallId) {
1040
+ const state = chatSession.getState();
1041
+ for (const msg of state.messages) {
1042
+ const tc = msg.toolCalls?.find((t) => t.id === toolCallId);
1043
+ if (tc) return tc.status;
1044
+ }
1045
+ return null;
1046
+ }
1047
+ /**
1048
+ * Convert one server-transcript message (the session store shape:
1049
+ * ``{id, role, content, timestamp, status, toolCalls?}`` with toolCalls
1050
+ * in AG-UI wire shape) into a TrailMessage. Returns null for entries the
1051
+ * trail can't render (missing id, system/tool roles, empty content with
1052
+ * no tool receipts) — the transcript may legitimately contain them.
1053
+ */
1054
+ _snapshotToTrailMessage(raw) {
1055
+ if (typeof raw !== "object" || raw === null) return null;
1056
+ const m = raw;
1057
+ const id = m.id;
1058
+ const role = m.role;
1059
+ if (typeof id !== "string" || !id) return null;
1060
+ if (role !== "user" && role !== "assistant") return null;
1061
+ const text = String(m.content ?? m.text ?? "");
1062
+ const toolCalls = this._snapshotToolCalls(m.toolCalls);
1063
+ if (!text && toolCalls.length === 0) return null;
1064
+ const message = { id, role, text, status: "complete" };
1065
+ if (toolCalls.length) message.toolCalls = toolCalls;
1066
+ return message;
1067
+ }
1068
+ /** AG-UI wire-shape toolCalls → settled TrailToolCall receipts. */
1069
+ _snapshotToolCalls(raw) {
1070
+ if (!Array.isArray(raw)) return [];
1071
+ const out = [];
1072
+ for (const entry of raw) {
1073
+ if (typeof entry !== "object" || entry === null) continue;
1074
+ const tc = entry;
1075
+ const fn = tc.function ?? {};
1076
+ if (typeof tc.id !== "string" || typeof fn.name !== "string" || !fn.name) continue;
1077
+ let args;
1078
+ if (typeof fn.arguments === "string" && fn.arguments) {
1079
+ try {
1080
+ const parsed = JSON.parse(fn.arguments);
1081
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1082
+ args = parsed;
1083
+ }
1084
+ } catch {
1085
+ }
1086
+ }
1087
+ out.push({ id: tc.id, name: fn.name, status: "done", ...args ? { args } : {} });
1088
+ }
1089
+ return out;
1090
+ }
1091
+ /** Map AG-UI ToolCallStatus to TrailToolCall status. */
1092
+ _mapToolCallStatus(status) {
1093
+ switch (status) {
1094
+ case "args-streaming":
1095
+ return "args-streaming";
1096
+ case "pending":
1097
+ return "pending";
1098
+ case "running":
1099
+ return "running";
1100
+ case "done":
1101
+ return "done";
1102
+ case "error":
1103
+ return "error";
1104
+ default:
1105
+ return "running";
1106
+ }
1107
+ }
1108
+ _onTransportEvent(event) {
1109
+ switch (event.type) {
1110
+ case "session-ready":
1111
+ return;
1112
+ case "messages-snapshot": {
1113
+ const converted = (event.messages ?? []).map((m) => this._snapshotToTrailMessage(m)).filter((m) => m !== null);
1114
+ if (converted.length) chatSession.hydrate(converted);
1115
+ return;
1116
+ }
1117
+ case "typing":
1118
+ if (!event.isTyping) this._stopThinkingCycle();
1119
+ chatSession.setRunActive(event.isTyping);
1120
+ return;
1121
+ case "message-append": {
1122
+ this._stopThinkingCycle();
1123
+ chatSession.clearThinking();
1124
+ this._currentAssistantMessageId = event.message.id;
1125
+ chatSession.receiveStart(event.message.id);
1126
+ if (event.message.content && event.message.content.length > 0) {
1127
+ chatSession.receiveDelta(event.message.id, event.message.content);
1128
+ }
1129
+ if (event.message.status === "complete") {
1130
+ chatSession.receiveEnd(event.message.id);
1131
+ }
1132
+ return;
1133
+ }
1134
+ case "message-delta": {
1135
+ const id = event.messageId ?? this._currentAssistantMessageId;
1136
+ if (!id || !event.delta) return;
1137
+ chatSession.receiveDelta(id, event.delta);
1138
+ return;
1139
+ }
1140
+ case "nudge-intent": {
1141
+ try {
1142
+ if (event.messageId) this._dispatchedNudgeIntentIds.add(event.messageId);
1143
+ document.dispatchEvent(
1144
+ new CustomEvent("syntro:nudge", {
1145
+ detail: {
1146
+ pattern: event.pattern,
1147
+ confidence: event.confidence,
1148
+ messageId: event.messageId,
1149
+ phase: "intent"
1150
+ }
1151
+ })
1152
+ );
1153
+ } catch {
1154
+ }
1155
+ return;
1156
+ }
1157
+ case "nudge": {
1158
+ const id = event.messageId ?? this._currentAssistantMessageId;
1159
+ if (id) chatSession.receiveEnd(id);
1160
+ try {
1161
+ const intentDispatched = id != null && this._dispatchedNudgeIntentIds.has(`${id}-intent`);
1162
+ document.dispatchEvent(
1163
+ new CustomEvent("syntro:nudge", {
1164
+ detail: {
1165
+ pattern: event.pattern,
1166
+ confidence: event.confidence,
1167
+ messageId: id,
1168
+ suppressToast: event.suppressToast === true && intentDispatched
1169
+ }
1170
+ })
1171
+ );
1172
+ } catch {
1173
+ }
1174
+ return;
1175
+ }
1176
+ case "thinking-delta": {
1177
+ return;
1178
+ }
1179
+ case "thinking-clear": {
1180
+ this._stopThinkingCycle();
1181
+ chatSession.clearThinking();
1182
+ return;
1183
+ }
1184
+ case "message-complete": {
1185
+ const id = event.messageId ?? this._currentAssistantMessageId;
1186
+ if (!id) return;
1187
+ this._stopThinkingCycle();
1188
+ chatSession.receiveEnd(id);
1189
+ this._currentAssistantMessageId = null;
1190
+ this._messagesSucceeded += 1;
1191
+ this._hasSucceeded = true;
1192
+ this._clearDebounceTimer();
1193
+ return;
1194
+ }
1195
+ case "tool-call": {
1196
+ const existing = this._findToolCallStatus(event.toolCall.id);
1197
+ if (!existing) {
1198
+ this._startThinkingCycle(toolNarrationVariants(event.toolCall.name));
1199
+ }
1200
+ const targetMessageId = event.messageId ?? this._currentAssistantMessageId;
1201
+ if (!targetMessageId) return;
1202
+ const parsedArgs = parseToolCallArgs(event.toolCall);
1203
+ if (!existing) {
1204
+ chatSession.addToolCall(targetMessageId, {
1205
+ id: event.toolCall.id,
1206
+ name: event.toolCall.name,
1207
+ status: this._mapToolCallStatus(event.toolCall.status),
1208
+ ...parsedArgs ? { args: parsedArgs } : {}
1209
+ });
1210
+ } else {
1211
+ chatSession.updateToolCall(event.toolCall.id, {
1212
+ name: event.toolCall.name,
1213
+ status: this._mapToolCallStatus(event.toolCall.status),
1214
+ ...parsedArgs ? { args: parsedArgs } : {}
1215
+ });
1216
+ }
1217
+ return;
1218
+ }
1219
+ case "tool-call-args-delta":
1220
+ debug("tool-call-args-delta", event);
1221
+ return;
1222
+ case "tool-call-done": {
1223
+ const doneArgs = parseToolCallArgs(event);
1224
+ chatSession.updateToolCall(event.toolCallId, {
1225
+ status: "done",
1226
+ ...doneArgs ? { args: doneArgs } : {}
1227
+ });
1228
+ return;
1229
+ }
1230
+ case "a2ui":
1231
+ return;
1232
+ case "error": {
1233
+ const status = event.status ?? null;
1234
+ const body = event.body ? String(event.body).slice(0, 200) : null;
1235
+ console.warn(
1236
+ `[chat-transport] error status=${status ?? "no-status"} succeeded=${this._messagesSucceeded}`,
1237
+ event
1238
+ );
1239
+ this._handleErrorEvent({
1240
+ message: event.message,
1241
+ status,
1242
+ body,
1243
+ errorName: event.errorName ?? null
1244
+ });
1245
+ const wasMidAnswer = this._currentAssistantMessageId != null;
1246
+ chatSession.error(
1247
+ wasMidAnswer ? "The connection dropped partway through \u2014 ask again and I'll pick it back up." : "Hmm, that didn't go through. Please try again."
1248
+ );
1249
+ this._currentAssistantMessageId = null;
1250
+ if (this._agui) {
1251
+ this._agui.disconnect();
1252
+ this._agui = null;
1253
+ }
1254
+ if (this._transportUnsub) {
1255
+ this._transportUnsub();
1256
+ this._transportUnsub = null;
1257
+ }
1258
+ this._status = "error";
1259
+ return;
1260
+ }
1261
+ }
1262
+ }
1263
+ };
1264
+ var chatTransport = new ChatTransport();
1265
+
1266
+ // src/observer/allowlist.ts
1267
+ var MAX_TEXT_LEN = 200;
1268
+ var SIGNIFICANT_CLICK_TAGS = /* @__PURE__ */ new Set(["button", "a"]);
1269
+ function truncate(s) {
1270
+ return s.length <= MAX_TEXT_LEN ? s : `${s.slice(0, MAX_TEXT_LEN - 1)}\u2026`;
1271
+ }
1272
+ function firstElement(props) {
1273
+ const els = props.$elements;
1274
+ if (Array.isArray(els) && els.length > 0 && typeof els[0] === "object" && els[0] !== null) {
1275
+ return els[0];
1276
+ }
1277
+ return null;
1278
+ }
1279
+ function elementText(props) {
1280
+ const el = firstElement(props);
1281
+ if (!el) return "";
1282
+ const t = el.text;
1283
+ return typeof t === "string" ? t.trim() : "";
1284
+ }
1285
+ function elementTag(props) {
1286
+ const explicit = props.$element_tag_name;
1287
+ if (typeof explicit === "string") return explicit.toLowerCase();
1288
+ const el = firstElement(props);
1289
+ if (el && typeof el.tag_name === "string") return el.tag_name.toLowerCase();
1290
+ return "";
1291
+ }
1292
+ function elementRole(props) {
1293
+ const el = firstElement(props);
1294
+ if (!el) return "";
1295
+ const role = el.attr__role;
1296
+ return typeof role === "string" ? role.toLowerCase() : "";
1297
+ }
1298
+ function formName(props) {
1299
+ const el = firstElement(props);
1300
+ if (!el) return "unnamed";
1301
+ const name = el.attr__name;
1302
+ if (typeof name === "string" && name) return name;
1303
+ const id = el.attr__id;
1304
+ if (typeof id === "string" && id) return id;
1305
+ return "unnamed";
1306
+ }
1307
+ function isSignificantClickTarget(props) {
1308
+ const tag = elementTag(props);
1309
+ if (SIGNIFICANT_CLICK_TAGS.has(tag)) return true;
1310
+ if (elementRole(props) === "button") return true;
1311
+ return false;
1312
+ }
1313
+ function pathname(props) {
1314
+ const pn = props.$pathname;
1315
+ return typeof pn === "string" ? pn : "/";
1316
+ }
1317
+ function renderCustomEvent(raw) {
1318
+ const fields = [];
1319
+ for (const [k, v] of Object.entries(raw.properties)) {
1320
+ if (k.startsWith("$")) continue;
1321
+ if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
1322
+ fields.push(`${k}=${v}`);
1323
+ if (fields.length >= 2) break;
1324
+ }
1325
+ }
1326
+ return fields.length > 0 ? `${raw.event} (${fields.join(", ")})` : raw.event;
1327
+ }
1328
+ function matchEvent(raw, opts = {}) {
1329
+ const props = raw.properties;
1330
+ switch (raw.event) {
1331
+ case "$pageview": {
1332
+ const path = pathname(props);
1333
+ return {
1334
+ ts: raw.timestamp,
1335
+ kind: "nav",
1336
+ text: truncate(`navigated to ${path}`),
1337
+ ref: truncate(path)
1338
+ };
1339
+ }
1340
+ case "$autocapture": {
1341
+ const eventType = props.$event_type;
1342
+ if (eventType === "click") {
1343
+ if (!isSignificantClickTarget(props)) return null;
1344
+ const text = elementText(props) || elementTag(props);
1345
+ return {
1346
+ ts: raw.timestamp,
1347
+ kind: "click",
1348
+ text: truncate(`clicked '${text}'`)
1349
+ };
1350
+ }
1351
+ if (eventType === "submit" && elementTag(props) === "form") {
1352
+ return {
1353
+ ts: raw.timestamp,
1354
+ kind: "form",
1355
+ text: truncate(`submitted form '${formName(props)}'`)
1356
+ };
1357
+ }
1358
+ return null;
1359
+ }
1360
+ case "$rageclick": {
1361
+ const text = elementText(props) || elementTag(props);
1362
+ return {
1363
+ ts: raw.timestamp,
1364
+ kind: "rage",
1365
+ text: truncate(`rage-clicked '${text}'`)
1366
+ };
1367
+ }
1368
+ case "$dead_click": {
1369
+ const text = elementText(props) || elementTag(props);
1370
+ return {
1371
+ ts: raw.timestamp,
1372
+ kind: "dead",
1373
+ text: truncate(`clicked '${text}' (no response)`)
1374
+ };
1375
+ }
1376
+ // ── Canonical runtime-bus events (sources: `'canvas'` instrumentation
1377
+ // or the event-processor's rrweb detectors). These don't go through
1378
+ // PostHog; the chat-bar's bus translator forwards them as-is with
1379
+ // the canonical event name, and matchEvent picks the right slot
1380
+ // here. Keeps a single allowlist as the source of truth for what
1381
+ // the agent's observation tail can carry.
1382
+ case "nav.section_viewed": {
1383
+ const section = typeof props.section === "string" ? props.section : "";
1384
+ if (!section) return null;
1385
+ return {
1386
+ ts: raw.timestamp,
1387
+ kind: "view",
1388
+ text: truncate(`viewed the '${section}' section`),
1389
+ ref: truncate(section)
1390
+ };
1391
+ }
1392
+ case "nav.scroll_depth": {
1393
+ const pct = typeof props.percent === "number" ? props.percent : null;
1394
+ if (pct === null) return null;
1395
+ return {
1396
+ ts: raw.timestamp,
1397
+ kind: "scroll",
1398
+ text: truncate(`scrolled to ${pct}% of the page`)
1399
+ };
1400
+ }
1401
+ case "ui.scroll_thrash": {
1402
+ return {
1403
+ ts: raw.timestamp,
1404
+ kind: "scroll",
1405
+ text: "scrolled up and down repeatedly (looking for something)"
1406
+ };
1407
+ }
1408
+ case "ui.idle": {
1409
+ const ms = typeof props.durationMs === "number" ? props.durationMs : null;
1410
+ const secs = ms !== null ? Math.round(ms / 1e3) : null;
1411
+ return {
1412
+ ts: raw.timestamp,
1413
+ kind: "idle",
1414
+ text: secs !== null ? `idle for ${secs}s` : "idle"
1415
+ };
1416
+ }
1417
+ case "ui.hover": {
1418
+ const text = elementText(props) || elementTag(props);
1419
+ return {
1420
+ ts: raw.timestamp,
1421
+ kind: "hover",
1422
+ text: truncate(`hovered on '${text}'`)
1423
+ };
1424
+ }
1425
+ // Canonical name is 'ui.hesitate' (StandardEvents.UI_HESITATE) — the rrweb
1426
+ // hesitation detector emits coordinates + duration_ms, no element chain, so
1427
+ // the summary is duration-based. (BUG-1784146789: this case was spelled
1428
+ // 'ui.hesitation', a name nothing emits, so the signal never arrived.)
1429
+ case "ui.hesitate": {
1430
+ const ms = typeof props.duration_ms === "number" ? props.duration_ms : null;
1431
+ const secs = ms !== null ? Math.round(ms / 1e3) : null;
1432
+ return {
1433
+ ts: raw.timestamp,
1434
+ kind: "hesitation",
1435
+ text: secs !== null ? `hesitated in place for ${secs}s` : "hesitated in place"
1436
+ };
1437
+ }
1438
+ case "ui.focus_bounce": {
1439
+ const text = elementText(props) || elementTag(props);
1440
+ return {
1441
+ ts: raw.timestamp,
1442
+ kind: "form",
1443
+ text: truncate(`focused '${text}' but didn't fill it`)
1444
+ };
1445
+ }
1446
+ default: {
1447
+ const allow = opts.observableEvents ?? [];
1448
+ if (allow.includes(raw.event)) {
1449
+ return {
1450
+ ts: raw.timestamp,
1451
+ kind: "custom",
1452
+ text: truncate(renderCustomEvent(raw))
1453
+ };
1454
+ }
1455
+ return null;
1456
+ }
1457
+ }
1458
+ }
1459
+
1460
+ // src/observer/queue.ts
1461
+ var ObserverQueue = class {
1462
+ constructor(opts) {
1463
+ this._buffer = [];
1464
+ this._sent = 0;
1465
+ this._dropped = 0;
1466
+ this._lastError = null;
1467
+ this._disabled = false;
1468
+ this._intervalTimer = null;
1469
+ this._backoffTimer = null;
1470
+ this._currentBackoffMs = 0;
1471
+ this._inFlight = false;
1472
+ this._opts = {
1473
+ backoffStartMs: 1e3,
1474
+ backoffCapMs: 3e4,
1475
+ ...opts
1476
+ };
1477
+ }
1478
+ push(event) {
1479
+ if (this._disabled) return;
1480
+ if (this._buffer.length >= this._opts.queueCap) {
1481
+ this._buffer.shift();
1482
+ this._dropped += 1;
1483
+ }
1484
+ this._buffer.push(event);
1485
+ if (this._buffer.length >= this._opts.batchSize) {
1486
+ this._scheduleImmediate();
1487
+ } else {
1488
+ this._scheduleInterval();
1489
+ }
1490
+ }
1491
+ async flushNow() {
1492
+ this._clearInterval();
1493
+ await this._flush();
1494
+ }
1495
+ /**
1496
+ * Synchronously hand the newest backend-sized batch to an unload-safe sender.
1497
+ * Returns false when the sender rejected delivery and restores the batch so
1498
+ * the caller can fall back to fetch({ keepalive: true }).
1499
+ */
1500
+ flushOnPageHide(send) {
1501
+ if (this._disabled || this._buffer.length === 0) return true;
1502
+ this._clearInterval();
1503
+ const overflow = Math.max(0, this._buffer.length - 50);
1504
+ if (overflow > 0) {
1505
+ this._buffer.splice(0, overflow);
1506
+ this._dropped += overflow;
1507
+ }
1508
+ const batch = this._buffer.splice(0, 50);
1509
+ if (!send(batch)) {
1510
+ this._buffer.unshift(...batch);
1511
+ return false;
1512
+ }
1513
+ this._sent += batch.length;
1514
+ this._lastError = null;
1515
+ return true;
1516
+ }
1517
+ stats() {
1518
+ return {
1519
+ queued: this._buffer.length,
1520
+ sent: this._sent,
1521
+ dropped: this._dropped,
1522
+ lastError: this._lastError,
1523
+ disabled: this._disabled
1524
+ };
1525
+ }
1526
+ _scheduleImmediate() {
1527
+ queueMicrotask(() => {
1528
+ void this._flush();
1529
+ });
1530
+ }
1531
+ _scheduleInterval() {
1532
+ if (this._intervalTimer != null) return;
1533
+ this._intervalTimer = setTimeout(() => {
1534
+ this._intervalTimer = null;
1535
+ void this._flush();
1536
+ }, this._opts.flushIntervalMs);
1537
+ }
1538
+ _clearInterval() {
1539
+ if (this._intervalTimer != null) {
1540
+ clearTimeout(this._intervalTimer);
1541
+ this._intervalTimer = null;
1542
+ }
1543
+ }
1544
+ async _flush() {
1545
+ if (this._disabled || this._inFlight || this._buffer.length === 0) return;
1546
+ this._clearInterval();
1547
+ const batch = this._buffer.splice(0, this._opts.batchSize);
1548
+ this._inFlight = true;
1549
+ try {
1550
+ await this._opts.flush(batch);
1551
+ this._sent += batch.length;
1552
+ this._currentBackoffMs = 0;
1553
+ this._lastError = null;
1554
+ } catch (err) {
1555
+ this._buffer.unshift(...batch);
1556
+ const perm = err?.permanent === true;
1557
+ this._lastError = err instanceof Error ? err.message : String(err?.message ?? err);
1558
+ if (perm) {
1559
+ this._disabled = true;
1560
+ } else {
1561
+ this._scheduleBackoff();
1562
+ }
1563
+ } finally {
1564
+ this._inFlight = false;
1565
+ }
1566
+ }
1567
+ _scheduleBackoff() {
1568
+ const next = this._currentBackoffMs === 0 ? this._opts.backoffStartMs : Math.min(this._currentBackoffMs * 2, this._opts.backoffCapMs);
1569
+ this._currentBackoffMs = next;
1570
+ if (this._backoffTimer != null) clearTimeout(this._backoffTimer);
1571
+ this._backoffTimer = setTimeout(() => {
1572
+ this._backoffTimer = null;
1573
+ void this._flush();
1574
+ }, next);
1575
+ }
1576
+ };
1577
+
1578
+ // src/observer/transport.ts
1579
+ function buildBody(distinctId, sessionId, batch) {
1580
+ if (!distinctId || !sessionId) return null;
1581
+ const body = { distinct_id: distinctId, session_id: sessionId, batch };
1582
+ return JSON.stringify(body);
1583
+ }
1584
+ function createTransport(opts) {
1585
+ return {
1586
+ async send(batch) {
1587
+ const body = buildBody(opts.getDistinctId(), opts.getSessionId(), batch);
1588
+ if (body === null) return;
1589
+ const resp = await fetch(opts.url, {
1590
+ method: "POST",
1591
+ headers: {
1592
+ Authorization: `Bearer ${opts.token()}`,
1593
+ "Content-Type": "application/json"
1594
+ },
1595
+ body,
1596
+ keepalive: true,
1597
+ credentials: "include"
1598
+ });
1599
+ if (!resp.ok) {
1600
+ const err = new Error(`observation POST ${resp.status}`);
1601
+ if (resp.status >= 400 && resp.status < 500 && resp.status !== 429) {
1602
+ err.permanent = true;
1603
+ }
1604
+ throw err;
1605
+ }
1606
+ },
1607
+ sendBeacon(batch) {
1608
+ const body = buildBody(opts.getDistinctId(), opts.getSessionId(), batch);
1609
+ if (body === null) return false;
1610
+ const blob = new Blob([body], { type: "application/json" });
1611
+ const beacon = navigator?.sendBeacon;
1612
+ if (typeof beacon !== "function") return false;
1613
+ const separator = opts.url.includes("?") ? "&" : "?";
1614
+ const url = `${opts.url}${separator}syn=${encodeURIComponent(opts.token())}`;
1615
+ return beacon.call(navigator, url, blob);
1616
+ }
1617
+ };
1618
+ }
1619
+
1620
+ // src/observer/index.ts
1621
+ var _active = null;
1622
+ function _attachPostHog(ingest, _observableEvents) {
1623
+ const ph = globalThis.posthog;
1624
+ if (!ph) return () => {
1625
+ };
1626
+ const addHook = ph._addCaptureHook;
1627
+ if (typeof addHook !== "function") return () => {
1628
+ };
1629
+ const handler = (eventName, properties) => {
1630
+ if (typeof eventName !== "string") return;
1631
+ ingest({
1632
+ event: eventName,
1633
+ timestamp: Date.now(),
1634
+ properties: properties ?? {}
1635
+ });
1636
+ };
1637
+ addHook.call(ph, handler);
1638
+ return () => {
1639
+ };
1640
+ }
1641
+ function _attachPageHide(transport, queue) {
1642
+ const handler = () => {
1643
+ const stats = queue.stats();
1644
+ if (stats.queued > 0) {
1645
+ queue.flushOnPageHide((batch) => {
1646
+ if (transport.sendBeacon(batch)) return true;
1647
+ void transport.send(batch);
1648
+ return true;
1649
+ });
1650
+ }
1651
+ };
1652
+ window.addEventListener("pagehide", handler, { capture: true });
1653
+ return () => window.removeEventListener("pagehide", handler, { capture: true });
1654
+ }
1655
+ function startObserver(opts) {
1656
+ if (_active) {
1657
+ _active.refCount += 1;
1658
+ return _makeHandle(_active);
1659
+ }
1660
+ const transport = opts.transport ?? createTransport(opts);
1661
+ const queue = new ObserverQueue({
1662
+ batchSize: 10,
1663
+ flushIntervalMs: 2e3,
1664
+ queueCap: 200,
1665
+ flush: (batch) => transport.send(batch)
1666
+ });
1667
+ const ingest = (raw) => {
1668
+ const matched = matchEvent(raw, { observableEvents: opts.observableEvents });
1669
+ if (matched !== null) {
1670
+ queue.push(opts.enrich ? opts.enrich(matched, raw) : matched);
1671
+ }
1672
+ };
1673
+ const unsubscribePh = _attachPostHog(ingest, opts.observableEvents ?? []);
1674
+ const unsubscribePageHide = _attachPageHide(transport, queue);
1675
+ const active = {
1676
+ refCount: 1,
1677
+ queue,
1678
+ unsubscribe: () => {
1679
+ unsubscribePh();
1680
+ unsubscribePageHide();
1681
+ },
1682
+ options: opts
1683
+ };
1684
+ _active = active;
1685
+ return _makeHandle(active);
1686
+ }
1687
+ function _makeHandle(owner) {
1688
+ return {
1689
+ stop() {
1690
+ if (_active !== owner) return;
1691
+ owner.refCount -= 1;
1692
+ if (owner.refCount <= 0) {
1693
+ owner.unsubscribe();
1694
+ _active = null;
1695
+ }
1696
+ },
1697
+ stats() {
1698
+ return (_active === owner ? owner.queue.stats() : null) ?? {
1699
+ queued: 0,
1700
+ sent: 0,
1701
+ dropped: 0,
1702
+ lastError: null,
1703
+ disabled: true
1704
+ };
1705
+ },
1706
+ ingest(raw) {
1707
+ if (_active !== owner) return;
1708
+ const matched = matchEvent(raw, {
1709
+ observableEvents: owner.options.observableEvents
1710
+ });
1711
+ if (matched !== null) {
1712
+ owner.queue.push(owner.options.enrich ? owner.options.enrich(matched, raw) : matched);
1713
+ }
1714
+ },
1715
+ flushNow() {
1716
+ return _active === owner ? owner.queue.flushNow() : Promise.resolve();
1717
+ }
1718
+ };
1719
+ }
1720
+
1721
+ // src/observer/bus-translate.ts
1722
+ function eventTypeFromName(name) {
1723
+ switch (name) {
1724
+ case "ui.click":
1725
+ return "click";
1726
+ case "ui.submit":
1727
+ return "submit";
1728
+ case "ui.change":
1729
+ return "change";
1730
+ default:
1731
+ return void 0;
1732
+ }
1733
+ }
1734
+ function busEventToRawEvent(e) {
1735
+ if (!e?.name) return null;
1736
+ const ts = typeof e.ts === "number" ? e.ts : Date.now();
1737
+ if (e.source === "posthog") {
1738
+ const original = e.props?.originalEvent;
1739
+ if (typeof original !== "string" || original.length === 0) return null;
1740
+ const src = e.props ?? {};
1741
+ const properties = { ...src };
1742
+ if (Array.isArray(src.elements)) properties.$elements = src.elements;
1743
+ if (typeof src.tagName === "string") properties.$element_tag_name = src.tagName;
1744
+ if (typeof src.pathname === "string") properties.$pathname = src.pathname;
1745
+ if (typeof src.url === "string") properties.$current_url = src.url;
1746
+ const eventType = eventTypeFromName(e.name);
1747
+ if (eventType) properties.$event_type = eventType;
1748
+ return { event: original, timestamp: ts, properties };
1749
+ }
1750
+ return { event: e.name, timestamp: ts, properties: { ...e.props ?? {} } };
1751
+ }
1752
+
1753
+ // src/AdaptiveChatBarMountable.ts
1754
+ var STATE_KEY = "__syntroChatBarMount";
1755
+ function getState(container) {
1756
+ return container[STATE_KEY] ?? null;
1757
+ }
1758
+ function setState(container, state) {
1759
+ container[STATE_KEY] = state;
1760
+ }
1761
+ function applyPlaceholder(bar, cfg) {
1762
+ if (cfg.placeholder !== void 0) bar.placeholder = cfg.placeholder;
1763
+ if (cfg.assistantName !== void 0) bar.assistantName = cfg.assistantName;
1764
+ if (cfg.personas !== void 0) bar.personas = cfg.personas;
1765
+ if (cfg.greeting !== void 0) bar.greeting = cfg.greeting;
1766
+ if (cfg.voiceInput !== void 0) bar.voiceInput = cfg.voiceInput;
1767
+ bar.introSuggestion = cfg.introSuggestion;
1768
+ bar.forceExpanded = cfg.forceExpanded === true;
1769
+ if (typeof cfg.maximized === "boolean") {
1770
+ bar.maximized = cfg.maximized;
1771
+ }
1772
+ }
1773
+ var _elementStore = null;
1774
+ var _templateWidgetMap = /* @__PURE__ */ new Map();
1775
+ var SERVER_MOUNTED_TILE_WIDGETS = ["syntro-product-reco-carousel"];
1776
+ function resolveTileWidget(templateId) {
1777
+ return _templateWidgetMap.get(templateId);
1778
+ }
1779
+ function refreshTemplateWidgetMap(uiTemplates) {
1780
+ _templateWidgetMap.clear();
1781
+ for (const tag of SERVER_MOUNTED_TILE_WIDGETS) {
1782
+ _templateWidgetMap.set(tag, tag);
1783
+ }
1784
+ if (!uiTemplates) return;
1785
+ const tiles = uiTemplates.tiles;
1786
+ if (!Array.isArray(tiles)) return;
1787
+ for (const t of tiles) {
1788
+ const id = typeof t?.id === "string" ? t.id : null;
1789
+ const widget = typeof t?.widget === "string" ? t.widget : null;
1790
+ if (id && widget) _templateWidgetMap.set(id, widget);
1791
+ }
1792
+ }
1793
+ function getOrCreateElementStore(runtime2) {
1794
+ if (_elementStore) return _elementStore;
1795
+ _elementStore = new ElementInstanceStore({
1796
+ actions: runtime2.actions,
1797
+ // Pass the runtime's event bus so ItemHandler can broadcast
1798
+ // `element.compositional_append` / `_patch` / `_remove` events
1799
+ // to container widgets (chips strip, FAQ accordion, nav tips).
1800
+ events: {
1801
+ publish: runtime2.events.publish.bind(runtime2.events),
1802
+ subscribe: runtime2.events.subscribe?.bind(runtime2.events)
1803
+ },
1804
+ handlers: [new TileHandler(resolveTileWidget), new ActionHandler(), new ItemHandler()],
1805
+ resolveTileWidget
1806
+ });
1807
+ setElementInstanceStore(_elementStore);
1808
+ return _elementStore;
1809
+ }
1810
+ var _hydrationStarted = false;
1811
+ function hydrateOnce(runtime2, backendUrl) {
1812
+ if (_hydrationStarted) return;
1813
+ _hydrationStarted = true;
1814
+ const store = getOrCreateElementStore(runtime2);
1815
+ const trimmed = backendUrl.replace(/\/$/, "");
1816
+ const endpoint = trimmed ? `${trimmed}/api/adaptive/mounted_elements` : "/api/adaptive/mounted_elements";
1817
+ fetchMountedElements({ endpoint }).then((response) => {
1818
+ if (!response) return;
1819
+ void store.hydrate(response.mounted_elements);
1820
+ });
1821
+ }
1822
+ var PAGE_LEVEL_KINDS = /* @__PURE__ */ new Set(["view", "scroll", "idle", "hesitation", "hover", "nav"]);
1823
+ function buildObserverEnrich(runtimeRef) {
1824
+ return (ev, raw) => {
1825
+ const extract = runtimeRef.getProductIdentity;
1826
+ if (typeof extract !== "function") return ev;
1827
+ if (ev.kind === "click") {
1828
+ const chain = raw.properties.$elements;
1829
+ if (!Array.isArray(chain)) return ev;
1830
+ const id = extract(chain);
1831
+ if (!id) return ev;
1832
+ return {
1833
+ ...ev,
1834
+ product: {
1835
+ handle: id.productHandle,
1836
+ url: id.productUrl,
1837
+ ...id.productTitle ? { title: id.productTitle } : {}
1838
+ }
1839
+ };
1840
+ }
1841
+ if (!ev.product && PAGE_LEVEL_KINDS.has(ev.kind) && typeof location !== "undefined") {
1842
+ const id = extract([{ href: location.pathname }]);
1843
+ if (!id) return ev;
1844
+ return {
1845
+ ...ev,
1846
+ product: {
1847
+ handle: id.productHandle,
1848
+ url: id.productUrl,
1849
+ ...id.productTitle ? { title: id.productTitle } : {}
1850
+ }
1851
+ };
1852
+ }
1853
+ return ev;
1854
+ };
1855
+ }
1856
+ var _nudgeSurfaceWired = false;
1857
+ var _nudgeListener = null;
1858
+ function wireNudgeSurface(runtime2, backendUrl) {
1859
+ if (_nudgeSurfaceWired || typeof document === "undefined") return;
1860
+ _nudgeSurfaceWired = true;
1861
+ _nudgeListener = (e) => {
1862
+ try {
1863
+ const detail = e.detail;
1864
+ const isIntent = detail?.phase === "intent";
1865
+ if (!isIntent) {
1866
+ const store = getOrCreateElementStore(runtime2);
1867
+ const trimmed = backendUrl.replace(/\/$/, "");
1868
+ const endpoint = trimmed ? `${trimmed}/api/adaptive/mounted_elements` : "/api/adaptive/mounted_elements";
1869
+ void fetchMountedElements({ endpoint }).then((response) => {
1870
+ if (!response) return;
1871
+ void store.hydrate(response.mounted_elements);
1872
+ });
1873
+ }
1874
+ if (detail?.suppressToast === true) return;
1875
+ const findCanvas = (root) => {
1876
+ const direct = root.querySelector("smart-canvas");
1877
+ if (direct) return direct;
1878
+ for (const el of root.querySelectorAll("*")) {
1879
+ const sr = el.shadowRoot;
1880
+ if (sr) {
1881
+ const found = findCanvas(sr);
1882
+ if (found) return found;
1883
+ }
1884
+ }
1885
+ return null;
1886
+ };
1887
+ const canvas = findCanvas(document);
1888
+ const messageId = e.detail?.messageId;
1889
+ const nudgeText = messageId ? (chatSession.getState().messages.find((m) => m.id === messageId)?.text ?? "").trim() : "";
1890
+ const title = nudgeText ? nudgeText.length > 140 ? `${nudgeText.slice(0, 137)}\u2026` : nudgeText : "I put something together for you";
1891
+ canvas?.previewNotification?.({
1892
+ title,
1893
+ body: "Tap to take a look"
1894
+ });
1895
+ } catch (err) {
1896
+ console.warn("[adaptive-chatbot] nudge surface reaction failed", err);
1897
+ }
1898
+ };
1899
+ document.addEventListener("syntro:nudge", _nudgeListener);
1900
+ }
1901
+ function configureTransportIfPossible(cfg) {
1902
+ if (!cfg.runtime) return;
1903
+ const backendUrl = cfg.runtime.backendUrl;
1904
+ if (!backendUrl) return;
1905
+ const elementsActive = cfg.elementsEnabled === true && cfg.uiTemplates != null;
1906
+ refreshTemplateWidgetMap(elementsActive ? cfg.uiTemplates : void 0);
1907
+ const forwardedProps = elementsActive ? { elementsEnabled: true, uiTemplates: cfg.uiTemplates } : void 0;
1908
+ if (elementsActive) publishTemplatesIfPossible(backendUrl, cfg.uiTemplates);
1909
+ const runtime2 = cfg.runtime;
1910
+ const onElementMutation = elementsActive ? (mutations) => {
1911
+ void getOrCreateElementStore(runtime2).apply(mutations);
1912
+ } : void 0;
1913
+ if (elementsActive) {
1914
+ hydrateOnce(runtime2, backendUrl);
1915
+ }
1916
+ const persona = pickPersona(cfg.personas ?? null);
1917
+ chatTransport.configure({
1918
+ ...cfg,
1919
+ backendUrl,
1920
+ forwardedProps,
1921
+ onElementMutation,
1922
+ activeLidSlot: cfg._syntroSlotName,
1923
+ personaId: persona?.id,
1924
+ personaName: persona?.name,
1925
+ personaRole: persona?.role_title
1926
+ });
1927
+ void chatTransport.startSessionStream();
1928
+ wireNudgeSurface(runtime2, backendUrl);
1929
+ startObserverIfPossible(cfg);
1930
+ }
1931
+ var _templatesPublished = false;
1932
+ function publishTemplatesIfPossible(backendUrl, uiTemplates) {
1933
+ if (_templatesPublished || typeof window === "undefined") return;
1934
+ if (!uiTemplates || typeof uiTemplates !== "object") return;
1935
+ _templatesPublished = true;
1936
+ const cfg = window.__SYNTRO_CONFIG__;
1937
+ const token = typeof cfg?.token === "string" && cfg.token.length > 0 ? cfg.token : void 0;
1938
+ const headers = { "Content-Type": "application/json" };
1939
+ if (token) headers.Authorization = `Bearer ${token}`;
1940
+ const trimmed = backendUrl.replace(/\/$/, "");
1941
+ void fetch(`${trimmed}/api/adaptive/templates`, {
1942
+ method: "POST",
1943
+ headers,
1944
+ credentials: "include",
1945
+ body: JSON.stringify({ ui_templates: uiTemplates })
1946
+ }).catch(() => {
1947
+ });
1948
+ }
1949
+ var _diveDeeperWired = false;
1950
+ function wireDiveDeeperListener() {
1951
+ if (_diveDeeperWired || typeof window === "undefined") return;
1952
+ _diveDeeperWired = true;
1953
+ window.addEventListener(DIVE_DEEPER_EVENT, (e) => {
1954
+ const detail = e.detail ?? {};
1955
+ const prompt = typeof detail.prompt === "string" && detail.prompt.trim() ? detail.prompt : detail.title ? "I'd like to dive deeper on this, I didn't see what I needed." : "";
1956
+ if (!prompt) return;
1957
+ const origin = { kind: "dive-deeper" };
1958
+ if (detail.title) origin.title = detail.title;
1959
+ if (detail.context) origin.context = detail.context;
1960
+ chatSession.send(prompt, { forwardedProps: { origin } });
1961
+ if (!chatSession.hasTransport()) {
1962
+ chatSession.error("Chat backend not configured \u2014 set backendUrl in the canvas config.");
1963
+ }
1964
+ });
1965
+ }
1966
+ var _observerStarted = false;
1967
+ var _observerRuntime = null;
1968
+ var _observerHandle = null;
1969
+ var _observerBusUnsubscribe = null;
1970
+ function startObserverIfPossible(cfg) {
1971
+ if (typeof window === "undefined") return;
1972
+ const runtimeRef = cfg.runtime;
1973
+ if (!runtimeRef?.events?.subscribe) return;
1974
+ if (_observerStarted && _observerRuntime === runtimeRef) return;
1975
+ if (_observerStarted) {
1976
+ _observerBusUnsubscribe?.();
1977
+ _observerHandle?.stop();
1978
+ _observerBusUnsubscribe = null;
1979
+ _observerHandle = null;
1980
+ _observerStarted = false;
1981
+ }
1982
+ const trimmed = (runtimeRef.backendUrl ?? "").replace(/\/$/, "");
1983
+ const url = trimmed ? `${trimmed}/api/adaptive/observation` : "/api/adaptive/observation";
1984
+ const runtimeId = (method) => {
1985
+ try {
1986
+ const v = runtimeRef.telemetry?.[method]?.();
1987
+ return typeof v === "string" && v.length > 0 ? v : null;
1988
+ } catch {
1989
+ return null;
1990
+ }
1991
+ };
1992
+ const getDistinctId = () => runtimeId("getDistinctId");
1993
+ const getSessionId = () => runtimeId("getSessionId");
1994
+ const token = () => {
1995
+ const c = window.__SYNTRO_CONFIG__;
1996
+ return typeof c?.token === "string" ? c.token : "";
1997
+ };
1998
+ const enrich = buildObserverEnrich(runtimeRef);
1999
+ const handle = startObserver({ url, token, getDistinctId, getSessionId, enrich });
2000
+ window.__syntroObserverStats = () => handle.stats();
2001
+ const unsubscribe = runtimeRef.events.subscribe({}, (evt) => {
2002
+ const raw = busEventToRawEvent(evt);
2003
+ if (raw) handle.ingest(raw);
2004
+ });
2005
+ _observerRuntime = runtimeRef;
2006
+ _observerHandle = handle;
2007
+ _observerBusUnsubscribe = unsubscribe;
2008
+ _observerStarted = true;
2009
+ }
2010
+ function wireListeners(bar, state) {
2011
+ const onMessageSent = (e) => {
2012
+ const text = e.detail.text;
2013
+ chatSession.send(text, { activeLidSlot: state.cfg._syntroSlotName });
2014
+ if (!chatSession.hasTransport()) {
2015
+ chatSession.error("Chat backend not configured \u2014 set backendUrl in the canvas config.");
2016
+ }
2017
+ };
2018
+ const onInterrupt = () => {
2019
+ chatSession.interrupt();
2020
+ };
2021
+ const onToolCallApproved = (e) => {
2022
+ const detail = e.detail;
2023
+ chatSession.resolveToolCall(detail.toolCallId, {}, detail.approved);
2024
+ };
2025
+ const onClose = () => {
2026
+ state.cfg.onClose?.();
2027
+ };
2028
+ bar.addEventListener("chat-message-sent", onMessageSent);
2029
+ bar.addEventListener("chat-interrupt", onInterrupt);
2030
+ bar.addEventListener("canvas-close", onClose);
2031
+ bar.addEventListener("trail-toolcall-approved", onToolCallApproved);
2032
+ return () => {
2033
+ bar.removeEventListener("chat-message-sent", onMessageSent);
2034
+ bar.removeEventListener("chat-interrupt", onInterrupt);
2035
+ bar.removeEventListener("canvas-close", onClose);
2036
+ bar.removeEventListener("trail-toolcall-approved", onToolCallApproved);
2037
+ };
2038
+ }
2039
+ var AdaptiveChatBarMountable = {
2040
+ mount(container, mountConfig) {
2041
+ const cfg = mountConfig ?? {};
2042
+ configureTransportIfPossible(cfg);
2043
+ wireDiveDeeperListener();
2044
+ const bar = document.createElement("adaptive-chat-bar");
2045
+ applyPlaceholder(bar, cfg);
2046
+ const widgets = cfg.runtime?.widgets;
2047
+ if (widgets) {
2048
+ bar.mountInlineWidget = (widgetId, container2, props) => {
2049
+ if (!widgets.has(widgetId)) return null;
2050
+ const handle = widgets.mount(widgetId, container2, props);
2051
+ const element = container2.firstElementChild;
2052
+ if (!element) {
2053
+ handle.unmount?.();
2054
+ return null;
2055
+ }
2056
+ return { element, cleanup: () => handle.unmount?.() };
2057
+ };
2058
+ }
2059
+ const unsubSession = chatSession.subscribe((s) => {
2060
+ bar.messages = [...s.messages];
2061
+ bar.inFlight = s.inFlight;
2062
+ bar.thinkingText = s.thinkingText;
2063
+ });
2064
+ const runtimeEvents = cfg.runtime ? cfg.runtime.events : void 0;
2065
+ const refreshReceipts = () => {
2066
+ const store = _elementStore;
2067
+ if (store) {
2068
+ bar.tileReceipts = store.listTileReceipts();
2069
+ bar.inlineWidgets = store.listInlineWidgets();
2070
+ }
2071
+ };
2072
+ let unsubReceipts;
2073
+ if (runtimeEvents?.subscribe) {
2074
+ unsubReceipts = runtimeEvents.subscribe(
2075
+ { names: [TILE_MOUNTED_EVENT, TILE_UNMOUNTED_EVENT] },
2076
+ refreshReceipts
2077
+ );
2078
+ }
2079
+ refreshReceipts();
2080
+ const state = { bar, cleanup: () => {
2081
+ }, cfg };
2082
+ const unwireListeners = wireListeners(bar, state);
2083
+ container.appendChild(bar);
2084
+ const unsubFallback = chatTransport.onFallback(() => {
2085
+ bar.remove();
2086
+ container.innerHTML = renderFallbackHtml(state.cfg.fallback);
2087
+ });
2088
+ state.cleanup = () => {
2089
+ unsubSession();
2090
+ unsubFallback();
2091
+ unsubReceipts?.();
2092
+ unwireListeners();
2093
+ bar.remove();
2094
+ setState(container, null);
2095
+ };
2096
+ setState(container, state);
2097
+ return state.cleanup;
2098
+ },
2099
+ update(container, mountConfig) {
2100
+ const state = getState(container);
2101
+ if (!state) return;
2102
+ const cfg = mountConfig ?? {};
2103
+ configureTransportIfPossible(cfg);
2104
+ applyPlaceholder(state.bar, cfg);
2105
+ state.cfg = cfg;
2106
+ }
2107
+ };
2108
+
2109
+ // src/AdaptiveChipsStripMountable.ts
2110
+ var STATE_KEY2 = "__syntroChipsStripMount";
2111
+ function getState2(container) {
2112
+ return container[STATE_KEY2] ?? null;
2113
+ }
2114
+ function setState2(container, state) {
2115
+ container[STATE_KEY2] = state;
2116
+ }
2117
+ function applyProps(strip, cfg) {
2118
+ if (cfg.chips !== void 0) strip.chips = cfg.chips;
2119
+ if (cfg.runtime !== void 0) {
2120
+ strip.runtimeRef = cfg.runtime;
2121
+ }
2122
+ if (cfg.chromeless !== void 0) strip.chromeless = cfg.chromeless;
2123
+ }
2124
+ var AdaptiveChipsStripMountable = {
2125
+ mount(container, mountConfig) {
2126
+ const cfg = mountConfig ?? {};
2127
+ const strip = document.createElement("adaptive-chips-strip");
2128
+ applyProps(strip, cfg);
2129
+ const state = {
2130
+ strip,
2131
+ listeners: {
2132
+ revealed: (e) => {
2133
+ const cb = cfg.onChipRevealed;
2134
+ if (cb) {
2135
+ cb(
2136
+ e.detail
2137
+ );
2138
+ }
2139
+ },
2140
+ dismissed: (e) => {
2141
+ const cb = cfg.onChipDismissed;
2142
+ if (cb) cb(e.detail);
2143
+ }
2144
+ }
2145
+ };
2146
+ strip.addEventListener("chip-revealed", state.listeners.revealed);
2147
+ strip.addEventListener("chip-dismissed", state.listeners.dismissed);
2148
+ container.appendChild(strip);
2149
+ setState2(container, state);
2150
+ return () => {
2151
+ strip.removeEventListener("chip-revealed", state.listeners.revealed);
2152
+ strip.removeEventListener("chip-dismissed", state.listeners.dismissed);
2153
+ strip.remove();
2154
+ setState2(container, null);
2155
+ };
2156
+ },
2157
+ update(container, mountConfig) {
2158
+ const state = getState2(container);
2159
+ if (!state) return;
2160
+ const cfg = mountConfig ?? {};
2161
+ applyProps(state.strip, cfg);
2162
+ state.strip.removeEventListener("chip-revealed", state.listeners.revealed);
2163
+ state.strip.removeEventListener("chip-dismissed", state.listeners.dismissed);
2164
+ state.listeners.revealed = (e) => {
2165
+ const cb = cfg.onChipRevealed;
2166
+ if (cb) {
2167
+ cb(e.detail);
2168
+ }
2169
+ };
2170
+ state.listeners.dismissed = (e) => {
2171
+ const cb = cfg.onChipDismissed;
2172
+ if (cb) cb(e.detail);
2173
+ };
2174
+ state.strip.addEventListener("chip-revealed", state.listeners.revealed);
2175
+ state.strip.addEventListener("chip-dismissed", state.listeners.dismissed);
2176
+ }
2177
+ };
2178
+
2179
+ // src/NavLinkMountable.ts
2180
+ var STATE_KEY3 = "__syntroNavLinkMount";
2181
+ var SAFE_NAVIGATION_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
2182
+ function getState3(container) {
2183
+ return container[STATE_KEY3] ?? null;
2184
+ }
2185
+ function setState3(container, state) {
2186
+ container[STATE_KEY3] = state;
2187
+ }
2188
+ function navigateTo(rawUrl) {
2189
+ if (!rawUrl) return;
2190
+ let resolved;
2191
+ try {
2192
+ resolved = new URL(rawUrl, window.location.origin);
2193
+ } catch {
2194
+ return;
2195
+ }
2196
+ if (resolved.origin !== window.location.origin) return;
2197
+ if (!SAFE_NAVIGATION_PROTOCOLS.has(resolved.protocol)) return;
2198
+ window.history.pushState(null, "", resolved.toString());
2199
+ window.dispatchEvent(new PopStateEvent("popstate"));
2200
+ }
2201
+ function buildButton(cfg) {
2202
+ const button = document.createElement("button");
2203
+ button.type = "button";
2204
+ button.dataset.syntroNavLink = "";
2205
+ button.style.display = "inline-flex";
2206
+ button.style.alignItems = "center";
2207
+ button.style.gap = "6px";
2208
+ button.style.padding = "6px 12px";
2209
+ button.style.borderRadius = "8px";
2210
+ button.style.fontFamily = "var(--sc-font-family, 'system-ui')";
2211
+ button.style.fontSize = "12px";
2212
+ button.style.fontWeight = "600";
2213
+ button.style.cursor = "pointer";
2214
+ button.style.color = "var(--sc-accent-foreground, currentColor)";
2215
+ button.style.background = "hsl(var(--sc-accent-color) / 0.85)";
2216
+ button.style.border = "1px solid hsl(var(--sc-accent-color) / 0.30)";
2217
+ applyLabel(button, cfg);
2218
+ return button;
2219
+ }
2220
+ function applyLabel(button, cfg) {
2221
+ const url = String(cfg.url ?? "");
2222
+ const label = cfg.label ?? (url ? `Go to ${url}` : "Go");
2223
+ button.textContent = label;
2224
+ }
2225
+ var NavLinkMountable = {
2226
+ mount(container, mountConfig) {
2227
+ const cfg = mountConfig ?? {};
2228
+ const button = buildButton(cfg);
2229
+ const onClick = (_e) => {
2230
+ const current = getState3(container);
2231
+ const url = current?.cfg.url ?? cfg.url;
2232
+ navigateTo(String(url ?? ""));
2233
+ };
2234
+ button.addEventListener("click", onClick);
2235
+ container.appendChild(button);
2236
+ setState3(container, { button, cfg, onClick });
2237
+ return () => {
2238
+ button.removeEventListener("click", onClick);
2239
+ button.remove();
2240
+ setState3(container, null);
2241
+ };
2242
+ },
2243
+ update(container, mountConfig) {
2244
+ const state = getState3(container);
2245
+ if (!state) return;
2246
+ const cfg = mountConfig ?? {};
2247
+ applyLabel(state.button, cfg);
2248
+ state.cfg = cfg;
2249
+ }
2250
+ };
2251
+
2252
+ // src/TextAnswerMountable.ts
2253
+ var STATE_KEY4 = "__syntroTextAnswerMount";
2254
+ function getState4(container) {
2255
+ return container[STATE_KEY4] ?? null;
2256
+ }
2257
+ function setState4(container, state) {
2258
+ container[STATE_KEY4] = state;
2259
+ }
2260
+ function applyText(p, text) {
2261
+ p.textContent = text;
2262
+ }
2263
+ var TextAnswerMountable = {
2264
+ mount(container, mountConfig) {
2265
+ const cfg = mountConfig ?? {};
2266
+ const paragraph = document.createElement("p");
2267
+ paragraph.dataset.syntroTextAnswer = "";
2268
+ paragraph.style.margin = "0";
2269
+ paragraph.style.fontSize = "12px";
2270
+ paragraph.style.lineHeight = "1.55";
2271
+ paragraph.style.color = "var(--sc-tile-text-color, currentColor)";
2272
+ applyText(paragraph, String(cfg.text ?? ""));
2273
+ container.appendChild(paragraph);
2274
+ setState4(container, { paragraph, cfg });
2275
+ return () => {
2276
+ paragraph.remove();
2277
+ setState4(container, null);
2278
+ };
2279
+ },
2280
+ update(container, mountConfig) {
2281
+ const state = getState4(container);
2282
+ if (!state) return;
2283
+ const cfg = mountConfig ?? {};
2284
+ applyText(state.paragraph, String(cfg.text ?? ""));
2285
+ state.cfg = cfg;
2286
+ }
2287
+ };
2288
+
6
2289
  // src/runtime.ts
7
2290
  var runtime = {
8
2291
  id: "adaptive-chatbot",
@@ -19,12 +2302,71 @@ var runtime = {
19
2302
  description: "AI-powered chat assistant with SSE streaming and A2UI support",
20
2303
  icon: "\u{1F4AC}"
21
2304
  }
2305
+ },
2306
+ // PRD §3 chat lid: the standalone chat bar UI shell. Used as a slot
2307
+ // `lid` widget (where it replaces the launcher) or as a regular
2308
+ // tile widget (renders inside the drawer). Headless of any chat
2309
+ // transport — emits chat-message-sent / chat-interrupt / canvas-close
2310
+ // for the parent to wire to whatever pipeline it owns.
2311
+ {
2312
+ id: "adaptive-chatbot:chat-bar",
2313
+ component: AdaptiveChatBarMountable,
2314
+ metadata: {
2315
+ name: "Chat Bar",
2316
+ description: "Always-visible chat input row with bubble-up trail. Headless \u2014 wire to your own transport.",
2317
+ icon: "\u2726"
2318
+ }
2319
+ },
2320
+ // PRD §4.5 suggested-chips tile: horizontally-wrapping chip strip
2321
+ // with click-to-reveal payload drawer. Each chip is a
2322
+ // `suggestions:chip` action.
2323
+ {
2324
+ id: "adaptive-chatbot:chips-strip",
2325
+ component: AdaptiveChipsStripMountable,
2326
+ metadata: {
2327
+ name: "Suggested Chips",
2328
+ description: "Wrapping chip strip with click-to-reveal payloads.",
2329
+ icon: "\u2728"
2330
+ }
2331
+ },
2332
+ // Built-in chip payload — a single paragraph of body text. Chip
2333
+ // payloads are widget references (same {widget,props} shape as
2334
+ // tile/lid configs); this is the simplest one shipped out of the
2335
+ // box so the chip pattern works without requiring a downstream
2336
+ // adaptive for a "just answer with text" case.
2337
+ {
2338
+ id: "adaptive-chatbot:text-answer",
2339
+ component: TextAnswerMountable,
2340
+ metadata: {
2341
+ name: "Text Answer",
2342
+ description: "Single paragraph payload \u2014 the default chip-drawer content.",
2343
+ icon: "\xB6"
2344
+ }
2345
+ },
2346
+ // Chip payload for LLM-authored navigation suggestions. Backed by
2347
+ // the `suggest-navigation` ItemTemplate: the backend wraps a curated
2348
+ // URL into props.url, the chip's payload-drawer mounts this widget,
2349
+ // user clicks "Go to <url>" → same-origin SPA navigation. Kept here
2350
+ // (not in adaptive-nav) so chip payloads ship with the chatbot
2351
+ // bundle that already provides chips-strip + text-answer.
2352
+ {
2353
+ id: "adaptive-chatbot:nav-link",
2354
+ component: NavLinkMountable,
2355
+ metadata: {
2356
+ name: "Nav Link",
2357
+ description: "Same-origin navigation button \u2014 used as a chip payload for LLM-authored nav suggestions.",
2358
+ icon: "\u2192"
2359
+ }
22
2360
  }
23
2361
  ]
24
2362
  };
25
2363
  var runtime_default = runtime;
26
2364
  export {
2365
+ ElementInstanceStore,
2366
+ TileHandler,
27
2367
  runtime_default as default,
28
- runtime
2368
+ getElementInstanceStore,
2369
+ runtime,
2370
+ setElementInstanceStore
29
2371
  };
30
2372
  //# sourceMappingURL=runtime.js.map