@syntrologie/adapt-chatbot 2.8.0-canary.43 → 2.8.0-canary.430

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