@syntrologie/adapt-chatbot 2.26.0 → 2.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/dist/AdaptiveChatBar.d.ts +76 -0
  2. package/dist/AdaptiveChatBar.d.ts.map +1 -0
  3. package/dist/AdaptiveChatBar.js +10 -0
  4. package/dist/AdaptiveChatBar.js.map +7 -0
  5. package/dist/AdaptiveChatBarMountable.d.ts +35 -0
  6. package/dist/AdaptiveChatBarMountable.d.ts.map +1 -0
  7. package/dist/AdaptiveChatTrail.d.ts +77 -0
  8. package/dist/AdaptiveChatTrail.d.ts.map +1 -0
  9. package/dist/AdaptiveChatTrail.js +9 -0
  10. package/dist/AdaptiveChatTrail.js.map +7 -0
  11. package/dist/AdaptiveChipsStrip.d.ts +1150 -0
  12. package/dist/AdaptiveChipsStrip.d.ts.map +1 -0
  13. package/dist/AdaptiveChipsStrip.js +11 -0
  14. package/dist/AdaptiveChipsStrip.js.map +7 -0
  15. package/dist/AdaptiveChipsStripMountable.d.ts +24 -0
  16. package/dist/AdaptiveChipsStripMountable.d.ts.map +1 -0
  17. package/dist/ChatAssistantLit.d.ts +23 -21
  18. package/dist/ChatAssistantLit.d.ts.map +1 -1
  19. package/dist/ChatAssistantLit.js +5 -3
  20. package/dist/ChatSession.d.ts +178 -0
  21. package/dist/ChatSession.d.ts.map +1 -0
  22. package/dist/ChatTransport.d.ts +283 -0
  23. package/dist/ChatTransport.d.ts.map +1 -0
  24. package/dist/NavLinkMountable.d.ts +25 -0
  25. package/dist/NavLinkMountable.d.ts.map +1 -0
  26. package/dist/NavLinkMountable.test.d.ts +2 -0
  27. package/dist/NavLinkMountable.test.d.ts.map +1 -0
  28. package/dist/TextAnswerMountable.d.ts +17 -0
  29. package/dist/TextAnswerMountable.d.ts.map +1 -0
  30. package/dist/Turnstile.d.ts +83 -0
  31. package/dist/Turnstile.d.ts.map +1 -0
  32. package/dist/chunk-435KJD27.js +192 -0
  33. package/dist/chunk-435KJD27.js.map +7 -0
  34. package/dist/chunk-AUER7ZCK.js +634 -0
  35. package/dist/chunk-AUER7ZCK.js.map +7 -0
  36. package/dist/chunk-DOMEUJR7.js +382 -0
  37. package/dist/chunk-DOMEUJR7.js.map +7 -0
  38. package/dist/{chunk-O7RWNUVU.js → chunk-KUO67E2W.js} +1573 -4079
  39. package/dist/chunk-KUO67E2W.js.map +7 -0
  40. package/dist/chunk-QELVKBQV.js +214 -0
  41. package/dist/chunk-QELVKBQV.js.map +7 -0
  42. package/dist/chunk-UC4XU6GH.js +3306 -0
  43. package/dist/chunk-UC4XU6GH.js.map +7 -0
  44. package/dist/elements/ActionHandler.d.ts +34 -0
  45. package/dist/elements/ActionHandler.d.ts.map +1 -0
  46. package/dist/elements/ElementInstanceStore.d.ts +155 -0
  47. package/dist/elements/ElementInstanceStore.d.ts.map +1 -0
  48. package/dist/elements/ElementInstanceStore.test.d.ts +2 -0
  49. package/dist/elements/ElementInstanceStore.test.d.ts.map +1 -0
  50. package/dist/elements/ElementTypeHandler.d.ts +77 -0
  51. package/dist/elements/ElementTypeHandler.d.ts.map +1 -0
  52. package/dist/elements/ItemHandler.d.ts +60 -0
  53. package/dist/elements/ItemHandler.d.ts.map +1 -0
  54. package/dist/elements/ItemHandler.test.d.ts +2 -0
  55. package/dist/elements/ItemHandler.test.d.ts.map +1 -0
  56. package/dist/elements/TileHandler.d.ts +52 -0
  57. package/dist/elements/TileHandler.d.ts.map +1 -0
  58. package/dist/elements/blockRenderer.d.ts +46 -0
  59. package/dist/elements/blockRenderer.d.ts.map +1 -0
  60. package/dist/elements/blockRenderer.test.d.ts +13 -0
  61. package/dist/elements/blockRenderer.test.d.ts.map +1 -0
  62. package/dist/elements/blocks.d.ts +58 -0
  63. package/dist/elements/blocks.d.ts.map +1 -0
  64. package/dist/elements/envelope.d.ts +24 -0
  65. package/dist/elements/envelope.d.ts.map +1 -0
  66. package/dist/elements/fetcher.d.ts +40 -0
  67. package/dist/elements/fetcher.d.ts.map +1 -0
  68. package/dist/elements/index.d.ts +32 -0
  69. package/dist/elements/index.d.ts.map +1 -0
  70. package/dist/elements/types.d.ts +106 -0
  71. package/dist/elements/types.d.ts.map +1 -0
  72. package/dist/observer/__tests__/allowlist.test.d.ts +9 -0
  73. package/dist/observer/__tests__/allowlist.test.d.ts.map +1 -0
  74. package/dist/observer/__tests__/observer-isolation.test.d.ts +13 -0
  75. package/dist/observer/__tests__/observer-isolation.test.d.ts.map +1 -0
  76. package/dist/observer/__tests__/queue.test.d.ts +2 -0
  77. package/dist/observer/__tests__/queue.test.d.ts.map +1 -0
  78. package/dist/observer/__tests__/transport.test.d.ts +2 -0
  79. package/dist/observer/__tests__/transport.test.d.ts.map +1 -0
  80. package/dist/observer/allowlist.d.ts +32 -0
  81. package/dist/observer/allowlist.d.ts.map +1 -0
  82. package/dist/observer/index.d.ts +35 -0
  83. package/dist/observer/index.d.ts.map +1 -0
  84. package/dist/observer/queue.d.ts +57 -0
  85. package/dist/observer/queue.d.ts.map +1 -0
  86. package/dist/observer/transport.d.ts +26 -0
  87. package/dist/observer/transport.d.ts.map +1 -0
  88. package/dist/runtime.d.ts +7 -0
  89. package/dist/runtime.d.ts.map +1 -1
  90. package/dist/runtime.js +1617 -2
  91. package/dist/runtime.js.map +4 -4
  92. package/dist/schema.d.ts +3120 -7
  93. package/dist/schema.d.ts.map +1 -1
  94. package/dist/schema.js +40 -0
  95. package/dist/schema.js.map +2 -2
  96. package/dist/types.d.ts +30 -2
  97. package/dist/types.d.ts.map +1 -1
  98. package/package.json +13 -1
  99. package/dist/chunk-O7RWNUVU.js.map +0 -7
package/dist/runtime.js CHANGED
@@ -1,8 +1,1568 @@
1
1
  import {
2
- ChatAssistantLitMountable
3
- } from "./chunk-O7RWNUVU.js";
2
+ ActionHandler,
3
+ AgUiTransport,
4
+ ChatAssistantLitMountable,
5
+ ElementInstanceStore,
6
+ ItemHandler,
7
+ TileHandler,
8
+ acquireTokenWithChallenge,
9
+ decodeMutationEnvelope,
10
+ fetchMountedElements,
11
+ renderFallbackHtml
12
+ } from "./chunk-KUO67E2W.js";
13
+ import "./chunk-QELVKBQV.js";
14
+ import "./chunk-DOMEUJR7.js";
15
+ import "./chunk-UC4XU6GH.js";
16
+ import "./chunk-AUER7ZCK.js";
17
+ import "./chunk-435KJD27.js";
4
18
  import "./chunk-UVKRO5ER.js";
5
19
 
20
+ // src/ChatSession.ts
21
+ var CHAT_SESSION_STORAGE_KEY = "syntro:chat:v1";
22
+ function isValidMessage(value) {
23
+ if (typeof value !== "object" || value === null) return false;
24
+ const m = value;
25
+ return (typeof m.id === "number" || typeof m.id === "string") && (m.role === "user" || m.role === "assistant" || m.role === "system") && typeof m.text === "string";
26
+ }
27
+ function loadFromStorage() {
28
+ try {
29
+ const raw = globalThis.localStorage?.getItem(CHAT_SESSION_STORAGE_KEY);
30
+ if (!raw) return null;
31
+ const parsed = JSON.parse(raw);
32
+ if (!parsed || !Array.isArray(parsed.messages)) return null;
33
+ const messages = parsed.messages.filter(isValidMessage);
34
+ const nextId = typeof parsed.nextId === "number" ? parsed.nextId : messages.length + 1;
35
+ return { messages, nextId };
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+ var ChatSession = class {
41
+ constructor() {
42
+ this._messages = [];
43
+ this._inFlight = false;
44
+ this._nextId = 1;
45
+ this.subscribers = /* @__PURE__ */ new Set();
46
+ this.sendListeners = /* @__PURE__ */ new Set();
47
+ this.interruptListeners = /* @__PURE__ */ new Set();
48
+ this.toolResultListeners = /* @__PURE__ */ new Set();
49
+ const restored = loadFromStorage();
50
+ if (restored) {
51
+ this._messages = restored.messages;
52
+ this._nextId = restored.nextId;
53
+ }
54
+ }
55
+ /** Snapshot the current state. Always returns a fresh immutable view. */
56
+ getState() {
57
+ return { messages: [...this._messages], inFlight: this._inFlight };
58
+ }
59
+ /**
60
+ * Subscribe to state changes. Called immediately with the current
61
+ * state, then again on every change. Returns an unsubscribe function.
62
+ */
63
+ subscribe(cb) {
64
+ this.subscribers.add(cb);
65
+ cb(this.getState());
66
+ return () => {
67
+ this.subscribers.delete(cb);
68
+ };
69
+ }
70
+ /**
71
+ * User submitted a message. Appends a user-role message, sets
72
+ * inFlight=true, notifies state subscribers, and fires a "send"
73
+ * event so transports can pick it up. Empty/whitespace text is a
74
+ * no-op (matches the chat bar's local guard).
75
+ */
76
+ send(text, opts) {
77
+ const trimmed = text.trim();
78
+ if (!trimmed) return;
79
+ this._messages.push({ id: this._nextId++, role: "user", text: trimmed });
80
+ this._inFlight = true;
81
+ this.notify();
82
+ const event = { text: trimmed };
83
+ if (opts?.activeLidSlot) event.activeLidSlot = opts.activeLidSlot;
84
+ for (const listener of this.sendListeners) listener(event);
85
+ }
86
+ /**
87
+ * Single-shot assistant reply (no streaming). Equivalent to
88
+ * receiveStart + receiveDelta + receiveEnd in one call. Useful for
89
+ * stub transports and tests that don't model streaming.
90
+ */
91
+ receive(text) {
92
+ const id = `m-${this._nextId++}`;
93
+ this._messages.push({ id, role: "assistant", text, status: "complete" });
94
+ this._inFlight = false;
95
+ this.notify();
96
+ }
97
+ /**
98
+ * Begin a streaming assistant message. Appends an empty assistant
99
+ * message with status='streaming'. Caller (transport adapter) feeds
100
+ * deltas via receiveDelta(id, text) and signals completion via
101
+ * receiveEnd(id). `inFlight` stays true through the stream.
102
+ */
103
+ receiveStart(id) {
104
+ this._messages.push({ id, role: "assistant", text: "", status: "streaming" });
105
+ this._inFlight = true;
106
+ this.notify();
107
+ }
108
+ /**
109
+ * Append a delta to a streaming message. No-op when the id is
110
+ * unknown (race between transport events and reset, etc.) — never
111
+ * throws so transports can fire-and-forget.
112
+ */
113
+ receiveDelta(id, delta) {
114
+ const msg = this._messages.find((m) => m.id === id);
115
+ if (!msg || msg.status !== "streaming") return;
116
+ msg.text += delta;
117
+ this.notify();
118
+ }
119
+ /**
120
+ * Mark a streaming message complete and clear inFlight. No-op when
121
+ * the id is unknown (defensive against transport double-fires).
122
+ */
123
+ receiveEnd(id) {
124
+ const msg = this._messages.find((m) => m.id === id);
125
+ if (msg && msg.status === "streaming") {
126
+ msg.status = "complete";
127
+ }
128
+ this._inFlight = false;
129
+ this.notify();
130
+ }
131
+ /**
132
+ * Transport reports a fatal error. Marks any in-flight streaming
133
+ * message as 'error' (so the trail can render a styled error chip
134
+ * instead of pretending the partial text was a complete answer),
135
+ * appends a system-role message with the error text for visibility,
136
+ * and clears inFlight.
137
+ */
138
+ error(message) {
139
+ for (const m of this._messages) {
140
+ if (m.status === "streaming") m.status = "error";
141
+ }
142
+ this._messages.push({
143
+ id: `err-${this._nextId++}`,
144
+ role: "system",
145
+ text: message,
146
+ status: "error"
147
+ });
148
+ this._inFlight = false;
149
+ this.notify();
150
+ }
151
+ /**
152
+ * User clicked the in-flight stop button. Clears inFlight and
153
+ * fires an "interrupt" event so transports can cancel their
154
+ * in-flight request. No-op if not in-flight (idempotent).
155
+ */
156
+ interrupt() {
157
+ if (!this._inFlight) return;
158
+ this._inFlight = false;
159
+ this.notify();
160
+ for (const listener of this.interruptListeners) listener();
161
+ }
162
+ /**
163
+ * Wipe state. Used by the canvas-close path (start a fresh
164
+ * conversation next time) and by tests.
165
+ */
166
+ reset() {
167
+ this._messages = [];
168
+ this._inFlight = false;
169
+ this._nextId = 1;
170
+ this.notify();
171
+ }
172
+ /** Register a transport's send listener. Returns unsubscribe. */
173
+ onSend(listener) {
174
+ this.sendListeners.add(listener);
175
+ return () => {
176
+ this.sendListeners.delete(listener);
177
+ };
178
+ }
179
+ /**
180
+ * True when at least one transport has wired itself to the session's
181
+ * send pipeline. Views can use this to surface "no chat backend
182
+ * connected" affordances instead of hanging in `inFlight` after a
183
+ * send fires into the void.
184
+ */
185
+ hasTransport() {
186
+ return this.sendListeners.size > 0;
187
+ }
188
+ /** Register a transport's interrupt listener. Returns unsubscribe. */
189
+ onInterrupt(listener) {
190
+ this.interruptListeners.add(listener);
191
+ return () => {
192
+ this.interruptListeners.delete(listener);
193
+ };
194
+ }
195
+ /**
196
+ * Register a transport's tool-result listener. The transport
197
+ * forwards `tool-result` actions back to the agent after the user
198
+ * approves or rejects a client-tool call. Returns unsubscribe.
199
+ */
200
+ onToolResult(listener) {
201
+ this.toolResultListeners.add(listener);
202
+ return () => {
203
+ this.toolResultListeners.delete(listener);
204
+ };
205
+ }
206
+ // -------------------------------------------------------------------------
207
+ // Tool calls
208
+ // -------------------------------------------------------------------------
209
+ /**
210
+ * Attach a tool call to a streaming assistant message. No-op when
211
+ * the message id is unknown (race between transport events and
212
+ * reset / late mount).
213
+ */
214
+ addToolCall(messageId, toolCall) {
215
+ const msg = this._messages.find((m) => m.id === messageId);
216
+ if (!msg) return;
217
+ msg.toolCalls = [...msg.toolCalls ?? [], { ...toolCall }];
218
+ this.notify();
219
+ }
220
+ /**
221
+ * Partially update a tool call by id. Used by the transport to
222
+ * advance status (args-streaming → running → done) as AG-UI events
223
+ * arrive. No-op when the id is unknown.
224
+ */
225
+ updateToolCall(toolCallId, patch) {
226
+ for (const msg of this._messages) {
227
+ const tcs = msg.toolCalls;
228
+ if (!tcs) continue;
229
+ const idx = tcs.findIndex((tc) => tc.id === toolCallId);
230
+ if (idx === -1) continue;
231
+ const next = [...tcs];
232
+ next[idx] = { ...next[idx], ...patch };
233
+ msg.toolCalls = next;
234
+ this.notify();
235
+ return;
236
+ }
237
+ }
238
+ /**
239
+ * Resolve a (client-) tool call. Marks the call done, persists, and
240
+ * fires onToolResult so the transport can forward the result + the
241
+ * user's approve/reject decision back to the agent. No-op when the
242
+ * id is unknown.
243
+ */
244
+ resolveToolCall(toolCallId, result, approved) {
245
+ let found = false;
246
+ for (const msg of this._messages) {
247
+ const tcs = msg.toolCalls;
248
+ if (!tcs) continue;
249
+ const idx = tcs.findIndex((tc) => tc.id === toolCallId);
250
+ if (idx === -1) continue;
251
+ const next = [...tcs];
252
+ next[idx] = { ...next[idx], status: "done" };
253
+ msg.toolCalls = next;
254
+ found = true;
255
+ break;
256
+ }
257
+ if (!found) return;
258
+ this.notify();
259
+ for (const listener of this.toolResultListeners) {
260
+ listener({ toolCallId, result, approved });
261
+ }
262
+ }
263
+ notify() {
264
+ const state = this.getState();
265
+ this.persist();
266
+ for (const sub of this.subscribers) sub(state);
267
+ }
268
+ persist() {
269
+ try {
270
+ if (this._messages.length === 0) {
271
+ globalThis.localStorage?.removeItem(CHAT_SESSION_STORAGE_KEY);
272
+ return;
273
+ }
274
+ const payload = { messages: this._messages, nextId: this._nextId };
275
+ globalThis.localStorage?.setItem(CHAT_SESSION_STORAGE_KEY, JSON.stringify(payload));
276
+ } catch {
277
+ }
278
+ }
279
+ };
280
+ var chatSession = new ChatSession();
281
+
282
+ // src/ChatTransport.ts
283
+ var FALLBACK_DEBOUNCE_MS = 1500;
284
+ function readSyntroToken() {
285
+ if (typeof window === "undefined") return void 0;
286
+ const cfg = window.__SYNTRO_CONFIG__;
287
+ const token = cfg?.token;
288
+ return typeof token === "string" && token.length > 0 ? token : void 0;
289
+ }
290
+ function debug(...args) {
291
+ if (typeof window === "undefined") return;
292
+ if (window.__SYNTRO_CHAT_DEBUG__) {
293
+ console.debug("[chat-transport]", ...args);
294
+ }
295
+ }
296
+ var ChatTransport = class {
297
+ constructor() {
298
+ this._config = null;
299
+ this._status = "idle";
300
+ this._agui = null;
301
+ this._transportUnsub = null;
302
+ this._sessionUnsubSend = null;
303
+ this._sessionUnsubInterrupt = null;
304
+ this._sessionUnsubToolResult = null;
305
+ this._connectInFlight = null;
306
+ /** Bounded streaming-message id for the currently-being-typed assistant turn. */
307
+ this._currentAssistantMessageId = null;
308
+ /** Outcome bookkeeping for telemetry. */
309
+ this._hadTurnstileToken = null;
310
+ /** Count of successful round-trips for telemetry payloads. */
311
+ this._messagesSucceeded = 0;
312
+ /** True once a successful assistant message has landed — gates fallback. */
313
+ this._hasSucceeded = false;
314
+ /** True once fallback has fired (one-shot). */
315
+ this._fallbackRendered = false;
316
+ /**
317
+ * Per-send override for ``X-Active-Lid-Slot``. Set in the
318
+ * ``chatSession.onSend`` listener immediately before each
319
+ * ``_forwardUserMessage`` and consumed by ``buildHeaders`` on the
320
+ * outbound request. The singleton transport is shared by every
321
+ * mounted chat-bar (drawer + inline) so cached ``_config.activeLidSlot``
322
+ * follows whichever bar last reconfigured — using that for routing
323
+ * silently misroutes tiles when both bars coexist. Per-send threading
324
+ * pins the header to the bar that actually sent the message.
325
+ */
326
+ this._pendingLidSlot = null;
327
+ /** Active debounce timer; null when no debounce pending. */
328
+ this._errorDebounceTimer = null;
329
+ /** Most recent error payload, captured so debounced fallback can attach it. */
330
+ this._lastErrorPayload = null;
331
+ this._fallbackListeners = /* @__PURE__ */ new Set();
332
+ /**
333
+ * Per-configure-cycle id for correlating telemetry events from a
334
+ * single transport lifetime. Mirrors ChatAssistantLit's mountId
335
+ * but scoped to configure cycles since the transport is a
336
+ * singleton across mounts.
337
+ */
338
+ this._transportId = `tx_${Math.random().toString(36).slice(2, 8)}`;
339
+ this._configuredAt = 0;
340
+ }
341
+ /**
342
+ * Configure the transport. Idempotent — calling again with the same
343
+ * backendUrl is a no-op; calling with a different backendUrl tears
344
+ * the connection down and re-arms.
345
+ */
346
+ configure(config) {
347
+ const same = this._config && this._config.backendUrl === config.backendUrl && this._config.threadId === config.threadId;
348
+ if (same) {
349
+ this._config = { ...this._config, ...config };
350
+ return;
351
+ }
352
+ this._disconnect();
353
+ this._config = config;
354
+ this._status = "configured";
355
+ this._transportId = `tx_${Math.random().toString(36).slice(2, 8)}`;
356
+ this._configuredAt = Date.now();
357
+ this._wireSession();
358
+ debug("configured", {
359
+ transportId: this._transportId,
360
+ backendUrl: config.backendUrl,
361
+ threadId: config.threadId
362
+ });
363
+ }
364
+ /** ms since the most recent configure() call. 0 before any configure. */
365
+ _ageMs() {
366
+ return this._configuredAt === 0 ? 0 : Date.now() - this._configuredAt;
367
+ }
368
+ /** True when configure() has been called and we're ready to lazy-connect on send. */
369
+ get isConfigured() {
370
+ return this._status !== "idle";
371
+ }
372
+ /** True when AgUiTransport is up. */
373
+ get isConnected() {
374
+ return this._status === "connected";
375
+ }
376
+ /**
377
+ * Subscribe to fallback events — fires once per configure cycle
378
+ * when the transport gives up after sustained failure. Hosts
379
+ * (typically AdaptiveChatBarMountable) use this to swap the chat
380
+ * bar for a static "contact support" card.
381
+ */
382
+ onFallback(listener) {
383
+ this._fallbackListeners.add(listener);
384
+ return () => {
385
+ this._fallbackListeners.delete(listener);
386
+ };
387
+ }
388
+ /**
389
+ * Test seam — drive a synthetic error event through the transport's
390
+ * error handling without standing up a real AgUi transport. Production
391
+ * code path uses _onTransportEvent. Exported as a public method to
392
+ * keep the test isolation simple; not part of the documented API.
393
+ */
394
+ simulateError(payload) {
395
+ this._handleErrorEvent(payload);
396
+ }
397
+ /**
398
+ * Test seam — register a synthetic successful message-complete so
399
+ * the hasSucceeded gate flips without a real AgUi round-trip.
400
+ */
401
+ simulateSuccessfulMessage() {
402
+ this._hasSucceeded = true;
403
+ this._messagesSucceeded += 1;
404
+ this._clearDebounceTimer();
405
+ }
406
+ /**
407
+ * Test seam — return the lid slot that the next outbound request
408
+ * WOULD tag ``X-Active-Lid-Slot`` with, given current state. Mirrors
409
+ * the exact resolution buildHeaders uses (per-send slot wins over
410
+ * cached config). Not part of the documented API.
411
+ */
412
+ getActiveLidSlotForTest() {
413
+ return this._pendingLidSlot ?? this._config?.activeLidSlot ?? null;
414
+ }
415
+ /**
416
+ * Tear connection-level state down. Used internally by configure()
417
+ * to swap backends; preserves host-registered fallback listeners
418
+ * because the host UI handler (e.g. AdaptiveChatBarMountable's
419
+ * "swap to fallback card" callback) is configuration-independent.
420
+ */
421
+ _disconnect() {
422
+ if (this._transportUnsub) {
423
+ this._transportUnsub();
424
+ this._transportUnsub = null;
425
+ }
426
+ if (this._sessionUnsubSend) {
427
+ this._sessionUnsubSend();
428
+ this._sessionUnsubSend = null;
429
+ }
430
+ if (this._sessionUnsubInterrupt) {
431
+ this._sessionUnsubInterrupt();
432
+ this._sessionUnsubInterrupt = null;
433
+ }
434
+ if (this._sessionUnsubToolResult) {
435
+ this._sessionUnsubToolResult();
436
+ this._sessionUnsubToolResult = null;
437
+ }
438
+ if (this._agui) {
439
+ this._agui.disconnect();
440
+ this._agui = null;
441
+ }
442
+ this._clearDebounceTimer();
443
+ this._config = null;
444
+ this._status = "idle";
445
+ this._connectInFlight = null;
446
+ this._currentAssistantMessageId = null;
447
+ this._hadTurnstileToken = null;
448
+ this._messagesSucceeded = 0;
449
+ this._hasSucceeded = false;
450
+ this._fallbackRendered = false;
451
+ this._lastErrorPayload = null;
452
+ this._pendingLidSlot = null;
453
+ }
454
+ /**
455
+ * Tear everything down — connection state PLUS host listeners.
456
+ * Use this in test teardown or when fully shutting the transport
457
+ * (page unload, integration test reset). The mountable calls
458
+ * _disconnect indirectly via reconfigure.
459
+ */
460
+ reset() {
461
+ this._disconnect();
462
+ this._fallbackListeners.clear();
463
+ }
464
+ _clearDebounceTimer() {
465
+ if (this._errorDebounceTimer) {
466
+ clearTimeout(this._errorDebounceTimer);
467
+ this._errorDebounceTimer = null;
468
+ }
469
+ }
470
+ /**
471
+ * Shared error-handling kernel — called by the AG-UI subscriber and
472
+ * by the simulateError test seam. Publishes transport_error
473
+ * telemetry, captures lastErrorPayload, and starts the debounce
474
+ * timer that will fire fallback if no successful message arrives
475
+ * before it expires. Gated by hasSucceeded (post-success errors
476
+ * never fallback) and _fallbackRendered (one-shot).
477
+ */
478
+ _handleErrorEvent(payload) {
479
+ const status = payload.status ?? null;
480
+ const body = payload.body ? String(payload.body).slice(0, 200) : null;
481
+ this._lastErrorPayload = {
482
+ message: payload.message,
483
+ status,
484
+ body,
485
+ errorName: payload.errorName ?? null
486
+ };
487
+ this._config?.runtime.events.publish("chatbot.transport_error", {
488
+ source: "chat-transport",
489
+ transportId: this._transportId,
490
+ transportAgeMs: this._ageMs(),
491
+ messagesSucceeded: this._messagesSucceeded,
492
+ hadTurnstileToken: this._hadTurnstileToken,
493
+ hasSucceeded: this._hasSucceeded,
494
+ errorMessage: payload.message ?? null,
495
+ errorStatus: status,
496
+ errorBody: body,
497
+ errorName: payload.errorName ?? null
498
+ });
499
+ if (this._hasSucceeded || this._fallbackRendered || this._errorDebounceTimer) return;
500
+ this._errorDebounceTimer = setTimeout(() => {
501
+ this._errorDebounceTimer = null;
502
+ if (this._hasSucceeded || this._fallbackRendered) return;
503
+ this._renderFallback("connect_failed");
504
+ }, FALLBACK_DEBOUNCE_MS);
505
+ }
506
+ /**
507
+ * Fire the fallback. One-shot per configure cycle. Notifies all
508
+ * fallback listeners with the per-customer card config and the
509
+ * diagnostic snapshot for telemetry/debug.
510
+ */
511
+ _renderFallback(reason) {
512
+ if (this._fallbackRendered) return;
513
+ this._fallbackRendered = true;
514
+ debug("fallback", { reason, transportId: this._transportId, ageMs: this._ageMs() });
515
+ const fallback = this._config?.fallback ?? {};
516
+ const payload = {
517
+ reason,
518
+ fallback,
519
+ transportId: this._transportId,
520
+ transportAgeMs: this._ageMs(),
521
+ messagesSucceeded: this._messagesSucceeded,
522
+ hadTurnstileToken: this._hadTurnstileToken,
523
+ errorStatus: this._lastErrorPayload?.status ?? null,
524
+ errorBody: this._lastErrorPayload?.body ?? null,
525
+ errorMessage: this._lastErrorPayload?.message ?? null,
526
+ errorName: this._lastErrorPayload?.errorName ?? null
527
+ };
528
+ this._config?.runtime.events.publish(
529
+ "chatbot.fallback_rendered",
530
+ payload
531
+ );
532
+ for (const listener of this._fallbackListeners) listener(payload);
533
+ }
534
+ // -------------------------------------------------------------------------
535
+ // Internal: chatSession wiring
536
+ // -------------------------------------------------------------------------
537
+ _wireSession() {
538
+ this._sessionUnsubSend = chatSession.onSend(({ text, activeLidSlot }) => {
539
+ this._pendingLidSlot = typeof activeLidSlot === "string" && activeLidSlot.length > 0 ? activeLidSlot : null;
540
+ void this._forwardUserMessage(text);
541
+ });
542
+ this._sessionUnsubInterrupt = chatSession.onInterrupt(() => {
543
+ this._agui?.send({ type: "stop-generation" });
544
+ });
545
+ this._sessionUnsubToolResult = chatSession.onToolResult(({ toolCallId, result, approved }) => {
546
+ this._agui?.send({ type: "tool-result", toolCallId, result, approved });
547
+ });
548
+ }
549
+ async _forwardUserMessage(text) {
550
+ const ok = await this._ensureConnected();
551
+ if (!ok || !this._agui) {
552
+ chatSession.error("Couldn't connect to chat. Please try again.");
553
+ return;
554
+ }
555
+ this._agui.send({ type: "user-message", text });
556
+ }
557
+ // -------------------------------------------------------------------------
558
+ // Internal: connect (lazy, Turnstile-gated)
559
+ // -------------------------------------------------------------------------
560
+ /**
561
+ * Ensure AgUiTransport is up. Idempotent — multiple concurrent calls
562
+ * dedupe to a single Turnstile acquisition + transport setup.
563
+ * Returns true on success, false on terminal failure.
564
+ */
565
+ async _ensureConnected() {
566
+ if (this._status === "connected" && this._agui) return true;
567
+ if (!this._config) return false;
568
+ if (this._connectInFlight) return this._connectInFlight;
569
+ this._connectInFlight = (async () => {
570
+ this._status = "acquiring";
571
+ const cft = await this._acquireTurnstileWithChallenge();
572
+ this._hadTurnstileToken = cft !== null;
573
+ if (this._config === null) return false;
574
+ const baseUrl = this._config.backendUrl.replace(/\/$/, "");
575
+ const streamUrl = `${baseUrl}/api/adaptive/stream`;
576
+ const token = readSyntroToken();
577
+ const buildHeaders = () => {
578
+ const h = {};
579
+ if (token) h.Authorization = `Bearer ${token}`;
580
+ if (cft) h["CF-Turnstile-Token"] = cft;
581
+ const lidSlot = this._pendingLidSlot ?? this._config?.activeLidSlot;
582
+ if (typeof lidSlot === "string" && lidSlot.length > 0) {
583
+ h["X-Active-Lid-Slot"] = lidSlot;
584
+ }
585
+ try {
586
+ const rt = this._config?.runtime;
587
+ const fromRuntime = rt?.telemetry?.getDistinctId?.();
588
+ const fromWindow = window.posthog?.get_distinct_id?.();
589
+ const did = fromRuntime || fromWindow;
590
+ if (typeof did === "string" && did.length > 0) {
591
+ h["X-Distinct-Id"] = did;
592
+ }
593
+ } catch {
594
+ }
595
+ return h;
596
+ };
597
+ const runtime2 = this._config.runtime;
598
+ const resolveForwardedProps = () => {
599
+ const raw = this._config?.forwardedProps;
600
+ return typeof raw === "function" ? raw() : raw;
601
+ };
602
+ this._agui = new AgUiTransport({
603
+ url: streamUrl,
604
+ headers: buildHeaders,
605
+ threadId: this._config.threadId,
606
+ clientTools: this._config.clientTools,
607
+ // Adaptive runtime SDK needs the `syntro_chat_session` cookie to
608
+ // round-trip cross-origin so subsequent boot fetches can rehydrate
609
+ // LLM-authored UI elements. Editor / action-plan chat surfaces
610
+ // auth via `?token=` and intentionally leave this unset (see
611
+ // AgUiTransportOptions.credentials docstring).
612
+ credentials: "include",
613
+ forwardedProps: resolveForwardedProps,
614
+ onA2UIEvent: (payload) => {
615
+ const mutations = decodeMutationEnvelope(payload);
616
+ if (mutations !== null) {
617
+ const handler = this._config?.onElementMutation;
618
+ if (handler) {
619
+ try {
620
+ handler(mutations);
621
+ runtime2.events.publish("chatbot.element_mutation_applied", {
622
+ source: "chat-transport",
623
+ count: mutations.length
624
+ });
625
+ } catch (err) {
626
+ const msg = err instanceof Error ? err.message : String(err);
627
+ console.error("[chat-transport] element mutation apply failed:", msg);
628
+ }
629
+ return;
630
+ }
631
+ return;
632
+ }
633
+ runtime2.actions.applyBatch([payload]).then(() => {
634
+ runtime2.events.publish("chatbot.a2ui_applied", { source: "chat-transport" });
635
+ }).catch((err) => {
636
+ const msg = err instanceof Error ? err.message : String(err);
637
+ console.error("[chat-transport] A2UI apply failed:", msg);
638
+ });
639
+ }
640
+ });
641
+ this._agui.connect();
642
+ this._transportUnsub = this._agui.subscribe((event) => this._onTransportEvent(event));
643
+ this._status = "connected";
644
+ return true;
645
+ })();
646
+ try {
647
+ return await this._connectInFlight;
648
+ } finally {
649
+ this._connectInFlight = null;
650
+ }
651
+ }
652
+ /**
653
+ * Acquire a Turnstile token via the managed-challenge flow.
654
+ * Delegates to the shared Turnstile helper that owns the verify
655
+ * panel lifecycle. Returns null when Turnstile is disabled at
656
+ * build time or acquisition fails.
657
+ */
658
+ async _acquireTurnstileWithChallenge() {
659
+ const { token } = await acquireTokenWithChallenge();
660
+ return token;
661
+ }
662
+ // -------------------------------------------------------------------------
663
+ // Internal: AgUi event → chatSession
664
+ // -------------------------------------------------------------------------
665
+ /**
666
+ * Look up a tool call's current chatSession-side status by id.
667
+ * Used to decide between addToolCall (first sighting) and
668
+ * updateToolCall (subsequent updates) when AG-UI re-emits
669
+ * `tool-call` events for client tools transitioning to 'pending'.
670
+ */
671
+ _findToolCallStatus(toolCallId) {
672
+ const state = chatSession.getState();
673
+ for (const msg of state.messages) {
674
+ const tc = msg.toolCalls?.find((t) => t.id === toolCallId);
675
+ if (tc) return tc.status;
676
+ }
677
+ return null;
678
+ }
679
+ /** Map AG-UI ToolCallStatus to TrailToolCall status. */
680
+ _mapToolCallStatus(status) {
681
+ switch (status) {
682
+ case "args-streaming":
683
+ return "args-streaming";
684
+ case "pending":
685
+ return "pending";
686
+ case "running":
687
+ return "running";
688
+ case "done":
689
+ return "done";
690
+ case "error":
691
+ return "error";
692
+ default:
693
+ return "running";
694
+ }
695
+ }
696
+ _onTransportEvent(event) {
697
+ const runtime2 = this._config?.runtime;
698
+ switch (event.type) {
699
+ case "session-ready":
700
+ case "messages-snapshot":
701
+ case "typing":
702
+ return;
703
+ case "message-append": {
704
+ this._currentAssistantMessageId = event.message.id;
705
+ chatSession.receiveStart(event.message.id);
706
+ if (event.message.content && event.message.content.length > 0) {
707
+ chatSession.receiveDelta(event.message.id, event.message.content);
708
+ }
709
+ return;
710
+ }
711
+ case "message-delta": {
712
+ const id = event.messageId ?? this._currentAssistantMessageId;
713
+ if (!id || !event.delta) return;
714
+ chatSession.receiveDelta(id, event.delta);
715
+ return;
716
+ }
717
+ case "message-complete": {
718
+ const id = event.messageId ?? this._currentAssistantMessageId;
719
+ if (!id) return;
720
+ chatSession.receiveEnd(id);
721
+ this._currentAssistantMessageId = null;
722
+ this._messagesSucceeded += 1;
723
+ this._hasSucceeded = true;
724
+ this._clearDebounceTimer();
725
+ return;
726
+ }
727
+ case "tool-call": {
728
+ const targetMessageId = event.messageId ?? this._currentAssistantMessageId;
729
+ if (!targetMessageId) return;
730
+ const existing = this._findToolCallStatus(event.toolCall.id);
731
+ if (!existing) {
732
+ chatSession.addToolCall(targetMessageId, {
733
+ id: event.toolCall.id,
734
+ name: event.toolCall.name,
735
+ status: this._mapToolCallStatus(event.toolCall.status)
736
+ });
737
+ } else {
738
+ chatSession.updateToolCall(event.toolCall.id, {
739
+ name: event.toolCall.name,
740
+ status: this._mapToolCallStatus(event.toolCall.status)
741
+ });
742
+ }
743
+ return;
744
+ }
745
+ case "tool-call-args-delta":
746
+ debug("tool-call-args-delta", event);
747
+ return;
748
+ case "tool-call-done":
749
+ chatSession.updateToolCall(event.toolCallId, { status: "done" });
750
+ return;
751
+ case "a2ui":
752
+ return;
753
+ case "error": {
754
+ const status = event.status ?? null;
755
+ const body = event.body ? String(event.body).slice(0, 200) : null;
756
+ console.warn(
757
+ `[chat-transport] error status=${status ?? "no-status"} succeeded=${this._messagesSucceeded}`,
758
+ event
759
+ );
760
+ this._handleErrorEvent({
761
+ message: event.message,
762
+ status,
763
+ body,
764
+ errorName: event.errorName ?? null
765
+ });
766
+ chatSession.error(event.message ?? "Chat connection failed");
767
+ this._currentAssistantMessageId = null;
768
+ if (this._agui) {
769
+ this._agui.disconnect();
770
+ this._agui = null;
771
+ }
772
+ if (this._transportUnsub) {
773
+ this._transportUnsub();
774
+ this._transportUnsub = null;
775
+ }
776
+ this._status = "error";
777
+ return;
778
+ }
779
+ }
780
+ }
781
+ };
782
+ var chatTransport = new ChatTransport();
783
+
784
+ // src/observer/allowlist.ts
785
+ var MAX_TEXT_LEN = 200;
786
+ var SIGNIFICANT_CLICK_TAGS = /* @__PURE__ */ new Set(["button", "a"]);
787
+ function truncate(s) {
788
+ return s.length <= MAX_TEXT_LEN ? s : `${s.slice(0, MAX_TEXT_LEN - 1)}\u2026`;
789
+ }
790
+ function firstElement(props) {
791
+ const els = props.$elements;
792
+ if (Array.isArray(els) && els.length > 0 && typeof els[0] === "object" && els[0] !== null) {
793
+ return els[0];
794
+ }
795
+ return null;
796
+ }
797
+ function elementText(props) {
798
+ const el = firstElement(props);
799
+ if (!el) return "";
800
+ const t = el.text;
801
+ return typeof t === "string" ? t.trim() : "";
802
+ }
803
+ function elementTag(props) {
804
+ const explicit = props.$element_tag_name;
805
+ if (typeof explicit === "string") return explicit.toLowerCase();
806
+ const el = firstElement(props);
807
+ if (el && typeof el.tag_name === "string") return el.tag_name.toLowerCase();
808
+ return "";
809
+ }
810
+ function elementRole(props) {
811
+ const el = firstElement(props);
812
+ if (!el) return "";
813
+ const role = el.attr__role;
814
+ return typeof role === "string" ? role.toLowerCase() : "";
815
+ }
816
+ function formName(props) {
817
+ const el = firstElement(props);
818
+ if (!el) return "unnamed";
819
+ const name = el.attr__name;
820
+ if (typeof name === "string" && name) return name;
821
+ const id = el.attr__id;
822
+ if (typeof id === "string" && id) return id;
823
+ return "unnamed";
824
+ }
825
+ function isSignificantClickTarget(props) {
826
+ const tag = elementTag(props);
827
+ if (SIGNIFICANT_CLICK_TAGS.has(tag)) return true;
828
+ if (elementRole(props) === "button") return true;
829
+ return false;
830
+ }
831
+ function pathname(props) {
832
+ const pn = props.$pathname;
833
+ return typeof pn === "string" ? pn : "/";
834
+ }
835
+ function renderCustomEvent(raw) {
836
+ const fields = [];
837
+ for (const [k, v] of Object.entries(raw.properties)) {
838
+ if (k.startsWith("$")) continue;
839
+ if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
840
+ fields.push(`${k}=${v}`);
841
+ if (fields.length >= 2) break;
842
+ }
843
+ }
844
+ return fields.length > 0 ? `${raw.event} (${fields.join(", ")})` : raw.event;
845
+ }
846
+ function matchEvent(raw, opts = {}) {
847
+ const props = raw.properties;
848
+ switch (raw.event) {
849
+ case "$pageview": {
850
+ const path = pathname(props);
851
+ return {
852
+ ts: raw.timestamp,
853
+ kind: "nav",
854
+ text: truncate(`navigated to ${path}`),
855
+ ref: path
856
+ };
857
+ }
858
+ case "$autocapture": {
859
+ const eventType = props.$event_type;
860
+ if (eventType === "click") {
861
+ if (!isSignificantClickTarget(props)) return null;
862
+ const text = elementText(props) || elementTag(props);
863
+ return {
864
+ ts: raw.timestamp,
865
+ kind: "click",
866
+ text: truncate(`clicked '${text}'`)
867
+ };
868
+ }
869
+ if (eventType === "submit" && elementTag(props) === "form") {
870
+ return {
871
+ ts: raw.timestamp,
872
+ kind: "form",
873
+ text: truncate(`submitted form '${formName(props)}'`)
874
+ };
875
+ }
876
+ return null;
877
+ }
878
+ case "$rageclick": {
879
+ const text = elementText(props) || elementTag(props);
880
+ return {
881
+ ts: raw.timestamp,
882
+ kind: "rage",
883
+ text: truncate(`rage-clicked '${text}'`)
884
+ };
885
+ }
886
+ case "$dead_click": {
887
+ const text = elementText(props) || elementTag(props);
888
+ return {
889
+ ts: raw.timestamp,
890
+ kind: "dead",
891
+ text: truncate(`clicked '${text}' (no response)`)
892
+ };
893
+ }
894
+ // ── Canonical runtime-bus events (sources: `'canvas'` instrumentation
895
+ // or the event-processor's rrweb detectors). These don't go through
896
+ // PostHog; the chat-bar's bus translator forwards them as-is with
897
+ // the canonical event name, and matchEvent picks the right slot
898
+ // here. Keeps a single allowlist as the source of truth for what
899
+ // the agent's observation tail can carry.
900
+ case "nav.section_viewed": {
901
+ const section = typeof props.section === "string" ? props.section : "";
902
+ if (!section) return null;
903
+ return {
904
+ ts: raw.timestamp,
905
+ kind: "view",
906
+ text: truncate(`viewed the '${section}' section`),
907
+ ref: section
908
+ };
909
+ }
910
+ case "nav.scroll_depth": {
911
+ const pct = typeof props.percent === "number" ? props.percent : null;
912
+ if (pct === null) return null;
913
+ return {
914
+ ts: raw.timestamp,
915
+ kind: "scroll",
916
+ text: truncate(`scrolled to ${pct}% of the page`)
917
+ };
918
+ }
919
+ case "ui.scroll_thrash": {
920
+ return {
921
+ ts: raw.timestamp,
922
+ kind: "scroll",
923
+ text: "scrolled up and down repeatedly (looking for something)"
924
+ };
925
+ }
926
+ case "ui.idle": {
927
+ const ms = typeof props.durationMs === "number" ? props.durationMs : null;
928
+ const secs = ms !== null ? Math.round(ms / 1e3) : null;
929
+ return {
930
+ ts: raw.timestamp,
931
+ kind: "idle",
932
+ text: secs !== null ? `idle for ${secs}s` : "idle"
933
+ };
934
+ }
935
+ case "ui.hover": {
936
+ const text = elementText(props) || elementTag(props);
937
+ return {
938
+ ts: raw.timestamp,
939
+ kind: "hover",
940
+ text: truncate(`hovered on '${text}'`)
941
+ };
942
+ }
943
+ case "ui.hesitation": {
944
+ const text = elementText(props) || elementTag(props);
945
+ return {
946
+ ts: raw.timestamp,
947
+ kind: "hesitation",
948
+ text: truncate(`hesitated near '${text}'`)
949
+ };
950
+ }
951
+ case "ui.focus_bounce": {
952
+ const text = elementText(props) || elementTag(props);
953
+ return {
954
+ ts: raw.timestamp,
955
+ kind: "form",
956
+ text: truncate(`focused '${text}' but didn't fill it`)
957
+ };
958
+ }
959
+ default: {
960
+ const allow = opts.observableEvents ?? [];
961
+ if (allow.includes(raw.event)) {
962
+ return {
963
+ ts: raw.timestamp,
964
+ kind: "custom",
965
+ text: truncate(renderCustomEvent(raw))
966
+ };
967
+ }
968
+ return null;
969
+ }
970
+ }
971
+ }
972
+
973
+ // src/observer/queue.ts
974
+ var ObserverQueue = class {
975
+ constructor(opts) {
976
+ this._buffer = [];
977
+ this._sent = 0;
978
+ this._dropped = 0;
979
+ this._lastError = null;
980
+ this._disabled = false;
981
+ this._intervalTimer = null;
982
+ this._backoffTimer = null;
983
+ this._currentBackoffMs = 0;
984
+ this._inFlight = false;
985
+ this._opts = {
986
+ backoffStartMs: 1e3,
987
+ backoffCapMs: 3e4,
988
+ ...opts
989
+ };
990
+ }
991
+ push(event) {
992
+ if (this._disabled) return;
993
+ if (this._buffer.length >= this._opts.queueCap) {
994
+ this._buffer.shift();
995
+ this._dropped += 1;
996
+ }
997
+ this._buffer.push(event);
998
+ if (this._buffer.length >= this._opts.batchSize) {
999
+ this._scheduleImmediate();
1000
+ } else {
1001
+ this._scheduleInterval();
1002
+ }
1003
+ }
1004
+ async flushNow() {
1005
+ this._clearInterval();
1006
+ await this._flush();
1007
+ }
1008
+ stats() {
1009
+ return {
1010
+ queued: this._buffer.length,
1011
+ sent: this._sent,
1012
+ dropped: this._dropped,
1013
+ lastError: this._lastError,
1014
+ disabled: this._disabled
1015
+ };
1016
+ }
1017
+ _scheduleImmediate() {
1018
+ queueMicrotask(() => {
1019
+ void this._flush();
1020
+ });
1021
+ }
1022
+ _scheduleInterval() {
1023
+ if (this._intervalTimer != null) return;
1024
+ this._intervalTimer = setTimeout(() => {
1025
+ this._intervalTimer = null;
1026
+ void this._flush();
1027
+ }, this._opts.flushIntervalMs);
1028
+ }
1029
+ _clearInterval() {
1030
+ if (this._intervalTimer != null) {
1031
+ clearTimeout(this._intervalTimer);
1032
+ this._intervalTimer = null;
1033
+ }
1034
+ }
1035
+ async _flush() {
1036
+ if (this._disabled || this._inFlight || this._buffer.length === 0) return;
1037
+ this._clearInterval();
1038
+ const batch = this._buffer.splice(0, this._opts.batchSize);
1039
+ this._inFlight = true;
1040
+ try {
1041
+ await this._opts.flush(batch);
1042
+ this._sent += batch.length;
1043
+ this._currentBackoffMs = 0;
1044
+ this._lastError = null;
1045
+ } catch (err) {
1046
+ this._buffer.unshift(...batch);
1047
+ const perm = err?.permanent === true;
1048
+ this._lastError = err instanceof Error ? err.message : String(err?.message ?? err);
1049
+ if (perm) {
1050
+ this._disabled = true;
1051
+ } else {
1052
+ this._scheduleBackoff();
1053
+ }
1054
+ } finally {
1055
+ this._inFlight = false;
1056
+ }
1057
+ }
1058
+ _scheduleBackoff() {
1059
+ const next = this._currentBackoffMs === 0 ? this._opts.backoffStartMs : Math.min(this._currentBackoffMs * 2, this._opts.backoffCapMs);
1060
+ this._currentBackoffMs = next;
1061
+ if (this._backoffTimer != null) clearTimeout(this._backoffTimer);
1062
+ this._backoffTimer = setTimeout(() => {
1063
+ this._backoffTimer = null;
1064
+ void this._flush();
1065
+ }, next);
1066
+ }
1067
+ };
1068
+
1069
+ // src/observer/transport.ts
1070
+ function buildBody(distinctId, batch) {
1071
+ const body = { distinct_id: distinctId, batch };
1072
+ return JSON.stringify(body);
1073
+ }
1074
+ function createTransport(opts) {
1075
+ return {
1076
+ async send(batch) {
1077
+ const body = buildBody(opts.getDistinctId(), batch);
1078
+ const resp = await fetch(opts.url, {
1079
+ method: "POST",
1080
+ headers: {
1081
+ Authorization: `Bearer ${opts.token()}`,
1082
+ "Content-Type": "application/json"
1083
+ },
1084
+ body,
1085
+ keepalive: true,
1086
+ credentials: "include"
1087
+ });
1088
+ if (!resp.ok) {
1089
+ const err = new Error(`observation POST ${resp.status}`);
1090
+ if (resp.status >= 400 && resp.status < 500 && resp.status !== 429) {
1091
+ err.permanent = true;
1092
+ }
1093
+ throw err;
1094
+ }
1095
+ },
1096
+ sendBeacon(batch) {
1097
+ const body = buildBody(opts.getDistinctId(), batch);
1098
+ const blob = new Blob([body], { type: "application/json" });
1099
+ const beacon = navigator?.sendBeacon;
1100
+ if (typeof beacon !== "function") return false;
1101
+ return beacon.call(navigator, opts.url, blob);
1102
+ }
1103
+ };
1104
+ }
1105
+
1106
+ // src/observer/index.ts
1107
+ var _active = null;
1108
+ function _attachPostHog(ingest, _observableEvents) {
1109
+ const ph = globalThis.posthog;
1110
+ if (!ph) return () => {
1111
+ };
1112
+ const addHook = ph._addCaptureHook;
1113
+ if (typeof addHook !== "function") return () => {
1114
+ };
1115
+ const handler = (eventName, properties) => {
1116
+ if (typeof eventName !== "string") return;
1117
+ ingest({
1118
+ event: eventName,
1119
+ timestamp: Date.now(),
1120
+ properties: properties ?? {}
1121
+ });
1122
+ };
1123
+ addHook.call(ph, handler);
1124
+ return () => {
1125
+ };
1126
+ }
1127
+ function _attachPageHide(_transport, queue) {
1128
+ const handler = () => {
1129
+ const stats = queue.stats();
1130
+ if (stats.queued > 0) {
1131
+ void queue.flushNow();
1132
+ }
1133
+ };
1134
+ window.addEventListener("pagehide", handler, { capture: true });
1135
+ return () => window.removeEventListener("pagehide", handler, { capture: true });
1136
+ }
1137
+ function startObserver(opts) {
1138
+ if (_active) {
1139
+ _active.refCount += 1;
1140
+ return _makeHandle();
1141
+ }
1142
+ const transport = opts.transport ?? createTransport(opts);
1143
+ const queue = new ObserverQueue({
1144
+ batchSize: 10,
1145
+ flushIntervalMs: 2e3,
1146
+ queueCap: 200,
1147
+ flush: (batch) => transport.send(batch)
1148
+ });
1149
+ const ingest = (raw) => {
1150
+ const ev = matchEvent(raw, { observableEvents: opts.observableEvents });
1151
+ if (ev !== null) {
1152
+ queue.push(ev);
1153
+ }
1154
+ };
1155
+ const unsubscribePh = _attachPostHog(ingest, opts.observableEvents ?? []);
1156
+ const unsubscribePageHide = _attachPageHide(transport, queue);
1157
+ _active = {
1158
+ refCount: 1,
1159
+ queue,
1160
+ unsubscribe: () => {
1161
+ unsubscribePh();
1162
+ unsubscribePageHide();
1163
+ },
1164
+ options: opts
1165
+ };
1166
+ return _makeHandle();
1167
+ }
1168
+ function _makeHandle() {
1169
+ return {
1170
+ stop() {
1171
+ if (!_active) return;
1172
+ _active.refCount -= 1;
1173
+ if (_active.refCount <= 0) {
1174
+ _active.unsubscribe();
1175
+ _active = null;
1176
+ }
1177
+ },
1178
+ stats() {
1179
+ return _active?.queue.stats() ?? {
1180
+ queued: 0,
1181
+ sent: 0,
1182
+ dropped: 0,
1183
+ lastError: null,
1184
+ disabled: true
1185
+ };
1186
+ },
1187
+ ingest(raw) {
1188
+ if (!_active) return;
1189
+ const ev = matchEvent(raw, {
1190
+ observableEvents: _active.options.observableEvents
1191
+ });
1192
+ if (ev !== null) _active.queue.push(ev);
1193
+ },
1194
+ flushNow() {
1195
+ return _active?.queue.flushNow() ?? Promise.resolve();
1196
+ }
1197
+ };
1198
+ }
1199
+
1200
+ // src/AdaptiveChatBarMountable.ts
1201
+ var STATE_KEY = "__syntroChatBarMount";
1202
+ function getState(container) {
1203
+ return container[STATE_KEY] ?? null;
1204
+ }
1205
+ function setState(container, state) {
1206
+ container[STATE_KEY] = state;
1207
+ }
1208
+ function applyPlaceholder(bar, cfg) {
1209
+ if (cfg.placeholder !== void 0) bar.placeholder = cfg.placeholder;
1210
+ if (cfg.greeting !== void 0) bar.greeting = cfg.greeting;
1211
+ }
1212
+ var _elementStore = null;
1213
+ var _templateWidgetMap = /* @__PURE__ */ new Map();
1214
+ function resolveTileWidget(templateId) {
1215
+ return _templateWidgetMap.get(templateId);
1216
+ }
1217
+ function refreshTemplateWidgetMap(uiTemplates) {
1218
+ _templateWidgetMap.clear();
1219
+ if (!uiTemplates) return;
1220
+ const tiles = uiTemplates.tiles;
1221
+ if (!Array.isArray(tiles)) return;
1222
+ for (const t of tiles) {
1223
+ const id = typeof t?.id === "string" ? t.id : null;
1224
+ const widget = typeof t?.widget === "string" ? t.widget : null;
1225
+ if (id && widget) _templateWidgetMap.set(id, widget);
1226
+ }
1227
+ }
1228
+ function getOrCreateElementStore(runtime2) {
1229
+ if (_elementStore) return _elementStore;
1230
+ _elementStore = new ElementInstanceStore({
1231
+ actions: runtime2.actions,
1232
+ // Pass the runtime's event bus so ItemHandler can broadcast
1233
+ // `element.compositional_append` / `_patch` / `_remove` events
1234
+ // to container widgets (chips strip, FAQ accordion, nav tips).
1235
+ events: {
1236
+ publish: runtime2.events.publish.bind(runtime2.events),
1237
+ subscribe: runtime2.events.subscribe?.bind(runtime2.events)
1238
+ },
1239
+ handlers: [new TileHandler(resolveTileWidget), new ActionHandler(), new ItemHandler()],
1240
+ resolveTileWidget
1241
+ });
1242
+ return _elementStore;
1243
+ }
1244
+ var _hydrationStarted = false;
1245
+ function hydrateOnce(runtime2, backendUrl) {
1246
+ if (_hydrationStarted) return;
1247
+ _hydrationStarted = true;
1248
+ const store = getOrCreateElementStore(runtime2);
1249
+ const trimmed = backendUrl.replace(/\/$/, "");
1250
+ const endpoint = trimmed ? `${trimmed}/api/adaptive/mounted_elements` : "/api/adaptive/mounted_elements";
1251
+ fetchMountedElements({ endpoint }).then((response) => {
1252
+ if (!response) return;
1253
+ void store.hydrate(response.mounted_elements);
1254
+ });
1255
+ }
1256
+ function configureTransportIfPossible(cfg) {
1257
+ if (cfg.backendUrl === void 0 || !cfg.runtime) return;
1258
+ const elementsActive = cfg.elementsEnabled === true && cfg.uiTemplates != null;
1259
+ refreshTemplateWidgetMap(elementsActive ? cfg.uiTemplates : void 0);
1260
+ const forwardedProps = elementsActive ? { elementsEnabled: true, uiTemplates: cfg.uiTemplates } : void 0;
1261
+ const runtime2 = cfg.runtime;
1262
+ const onElementMutation = elementsActive ? (mutations) => {
1263
+ void getOrCreateElementStore(runtime2).apply(mutations);
1264
+ } : void 0;
1265
+ if (elementsActive) {
1266
+ hydrateOnce(runtime2, cfg.backendUrl);
1267
+ }
1268
+ chatTransport.configure({
1269
+ ...cfg,
1270
+ forwardedProps,
1271
+ onElementMutation,
1272
+ activeLidSlot: cfg._syntroSlotName
1273
+ });
1274
+ startObserverIfPossible(cfg);
1275
+ }
1276
+ var _observerStarted = false;
1277
+ function startObserverIfPossible(cfg) {
1278
+ if (_observerStarted) return;
1279
+ if (typeof window === "undefined") return;
1280
+ const runtime2 = cfg.runtime;
1281
+ if (!runtime2?.events?.subscribe) return;
1282
+ const trimmed = (cfg.backendUrl ?? "").replace(/\/$/, "");
1283
+ const url = trimmed ? `${trimmed}/api/adaptive/observation` : "/api/adaptive/observation";
1284
+ const getDistinctId = () => {
1285
+ const ph = window.posthog;
1286
+ try {
1287
+ const v = ph?.get_distinct_id?.();
1288
+ return typeof v === "string" && v.length > 0 ? v : null;
1289
+ } catch {
1290
+ return null;
1291
+ }
1292
+ };
1293
+ const token = () => {
1294
+ const c = window.__SYNTRO_CONFIG__;
1295
+ return typeof c?.token === "string" ? c.token : "";
1296
+ };
1297
+ const handle = startObserver({ url, token, getDistinctId });
1298
+ window.__syntroObserverStats = () => handle.stats();
1299
+ runtime2.events.subscribe({}, (evt) => {
1300
+ const e = evt;
1301
+ if (!e?.name) return;
1302
+ const ts = typeof e.ts === "number" ? e.ts : Date.now();
1303
+ if (e.source === "posthog") {
1304
+ const original = e.props?.originalEvent;
1305
+ if (typeof original !== "string" || original.length === 0) return;
1306
+ const properties = { ...e.props ?? {} };
1307
+ if (typeof e.props?.pathname === "string") properties.$pathname = e.props.pathname;
1308
+ if (typeof e.props?.url === "string") properties.$current_url = e.props.url;
1309
+ handle.ingest({ event: original, timestamp: ts, properties });
1310
+ return;
1311
+ }
1312
+ handle.ingest({
1313
+ event: e.name,
1314
+ timestamp: ts,
1315
+ properties: { ...e.props ?? {} }
1316
+ });
1317
+ });
1318
+ _observerStarted = true;
1319
+ }
1320
+ function wireListeners(bar, state) {
1321
+ const onMessageSent = (e) => {
1322
+ const text = e.detail.text;
1323
+ chatSession.send(text, { activeLidSlot: state.cfg._syntroSlotName });
1324
+ if (!chatSession.hasTransport()) {
1325
+ chatSession.error("Chat backend not configured \u2014 set backendUrl in the canvas config.");
1326
+ }
1327
+ };
1328
+ const onInterrupt = () => {
1329
+ chatSession.interrupt();
1330
+ };
1331
+ const onToolCallApproved = (e) => {
1332
+ const detail = e.detail;
1333
+ chatSession.resolveToolCall(detail.toolCallId, {}, detail.approved);
1334
+ };
1335
+ const onClose = () => {
1336
+ state.cfg.onClose?.();
1337
+ };
1338
+ bar.addEventListener("chat-message-sent", onMessageSent);
1339
+ bar.addEventListener("chat-interrupt", onInterrupt);
1340
+ bar.addEventListener("canvas-close", onClose);
1341
+ bar.addEventListener("trail-toolcall-approved", onToolCallApproved);
1342
+ return () => {
1343
+ bar.removeEventListener("chat-message-sent", onMessageSent);
1344
+ bar.removeEventListener("chat-interrupt", onInterrupt);
1345
+ bar.removeEventListener("canvas-close", onClose);
1346
+ bar.removeEventListener("trail-toolcall-approved", onToolCallApproved);
1347
+ };
1348
+ }
1349
+ var AdaptiveChatBarMountable = {
1350
+ mount(container, mountConfig) {
1351
+ const cfg = mountConfig ?? {};
1352
+ configureTransportIfPossible(cfg);
1353
+ const bar = document.createElement("adaptive-chat-bar");
1354
+ applyPlaceholder(bar, cfg);
1355
+ const unsubSession = chatSession.subscribe((s) => {
1356
+ bar.messages = [...s.messages];
1357
+ bar.inFlight = s.inFlight;
1358
+ });
1359
+ const state = { bar, cleanup: () => {
1360
+ }, cfg };
1361
+ const unwireListeners = wireListeners(bar, state);
1362
+ container.appendChild(bar);
1363
+ const unsubFallback = chatTransport.onFallback(() => {
1364
+ bar.remove();
1365
+ container.innerHTML = renderFallbackHtml(state.cfg.fallback);
1366
+ });
1367
+ state.cleanup = () => {
1368
+ unsubSession();
1369
+ unsubFallback();
1370
+ unwireListeners();
1371
+ bar.remove();
1372
+ setState(container, null);
1373
+ };
1374
+ setState(container, state);
1375
+ return state.cleanup;
1376
+ },
1377
+ update(container, mountConfig) {
1378
+ const state = getState(container);
1379
+ if (!state) return;
1380
+ const cfg = mountConfig ?? {};
1381
+ configureTransportIfPossible(cfg);
1382
+ applyPlaceholder(state.bar, cfg);
1383
+ state.cfg = cfg;
1384
+ }
1385
+ };
1386
+
1387
+ // src/AdaptiveChipsStripMountable.ts
1388
+ var STATE_KEY2 = "__syntroChipsStripMount";
1389
+ function getState2(container) {
1390
+ return container[STATE_KEY2] ?? null;
1391
+ }
1392
+ function setState2(container, state) {
1393
+ container[STATE_KEY2] = state;
1394
+ }
1395
+ function applyProps(strip, cfg) {
1396
+ if (cfg.chips !== void 0) strip.chips = cfg.chips;
1397
+ if (cfg.runtime !== void 0) {
1398
+ strip.runtimeRef = cfg.runtime;
1399
+ }
1400
+ if (cfg.chromeless !== void 0) strip.chromeless = cfg.chromeless;
1401
+ }
1402
+ var AdaptiveChipsStripMountable = {
1403
+ mount(container, mountConfig) {
1404
+ const cfg = mountConfig ?? {};
1405
+ const strip = document.createElement("adaptive-chips-strip");
1406
+ applyProps(strip, cfg);
1407
+ const state = {
1408
+ strip,
1409
+ listeners: {
1410
+ revealed: (e) => {
1411
+ const cb = cfg.onChipRevealed;
1412
+ if (cb) {
1413
+ cb(
1414
+ e.detail
1415
+ );
1416
+ }
1417
+ },
1418
+ dismissed: (e) => {
1419
+ const cb = cfg.onChipDismissed;
1420
+ if (cb) cb(e.detail);
1421
+ }
1422
+ }
1423
+ };
1424
+ strip.addEventListener("chip-revealed", state.listeners.revealed);
1425
+ strip.addEventListener("chip-dismissed", state.listeners.dismissed);
1426
+ container.appendChild(strip);
1427
+ setState2(container, state);
1428
+ return () => {
1429
+ strip.removeEventListener("chip-revealed", state.listeners.revealed);
1430
+ strip.removeEventListener("chip-dismissed", state.listeners.dismissed);
1431
+ strip.remove();
1432
+ setState2(container, null);
1433
+ };
1434
+ },
1435
+ update(container, mountConfig) {
1436
+ const state = getState2(container);
1437
+ if (!state) return;
1438
+ const cfg = mountConfig ?? {};
1439
+ applyProps(state.strip, cfg);
1440
+ state.strip.removeEventListener("chip-revealed", state.listeners.revealed);
1441
+ state.strip.removeEventListener("chip-dismissed", state.listeners.dismissed);
1442
+ state.listeners.revealed = (e) => {
1443
+ const cb = cfg.onChipRevealed;
1444
+ if (cb) {
1445
+ cb(e.detail);
1446
+ }
1447
+ };
1448
+ state.listeners.dismissed = (e) => {
1449
+ const cb = cfg.onChipDismissed;
1450
+ if (cb) cb(e.detail);
1451
+ };
1452
+ state.strip.addEventListener("chip-revealed", state.listeners.revealed);
1453
+ state.strip.addEventListener("chip-dismissed", state.listeners.dismissed);
1454
+ }
1455
+ };
1456
+
1457
+ // src/NavLinkMountable.ts
1458
+ var STATE_KEY3 = "__syntroNavLinkMount";
1459
+ function getState3(container) {
1460
+ return container[STATE_KEY3] ?? null;
1461
+ }
1462
+ function setState3(container, state) {
1463
+ container[STATE_KEY3] = state;
1464
+ }
1465
+ function navigateTo(rawUrl) {
1466
+ if (!rawUrl) return;
1467
+ let resolved;
1468
+ try {
1469
+ resolved = new URL(rawUrl, window.location.origin);
1470
+ } catch {
1471
+ return;
1472
+ }
1473
+ if (resolved.origin !== window.location.origin) return;
1474
+ if (resolved.protocol !== "http:" && resolved.protocol !== "https:") return;
1475
+ window.history.pushState(null, "", resolved.toString());
1476
+ window.dispatchEvent(new PopStateEvent("popstate"));
1477
+ }
1478
+ function buildButton(cfg) {
1479
+ const button = document.createElement("button");
1480
+ button.type = "button";
1481
+ button.dataset.syntroNavLink = "";
1482
+ button.style.display = "inline-flex";
1483
+ button.style.alignItems = "center";
1484
+ button.style.gap = "6px";
1485
+ button.style.padding = "6px 12px";
1486
+ button.style.borderRadius = "8px";
1487
+ button.style.fontFamily = "var(--sc-font-family, 'system-ui')";
1488
+ button.style.fontSize = "12px";
1489
+ button.style.fontWeight = "600";
1490
+ button.style.cursor = "pointer";
1491
+ button.style.color = "var(--sc-accent-foreground, currentColor)";
1492
+ button.style.background = "hsl(var(--sc-accent-color) / 0.85)";
1493
+ button.style.border = "1px solid hsl(var(--sc-accent-color) / 0.30)";
1494
+ applyLabel(button, cfg);
1495
+ return button;
1496
+ }
1497
+ function applyLabel(button, cfg) {
1498
+ const url = String(cfg.url ?? "");
1499
+ const label = cfg.label ?? (url ? `Go to ${url}` : "Go");
1500
+ button.textContent = label;
1501
+ }
1502
+ var NavLinkMountable = {
1503
+ mount(container, mountConfig) {
1504
+ const cfg = mountConfig ?? {};
1505
+ const button = buildButton(cfg);
1506
+ const onClick = (_e) => {
1507
+ const current = getState3(container);
1508
+ const url = current?.cfg.url ?? cfg.url;
1509
+ navigateTo(String(url ?? ""));
1510
+ };
1511
+ button.addEventListener("click", onClick);
1512
+ container.appendChild(button);
1513
+ setState3(container, { button, cfg, onClick });
1514
+ return () => {
1515
+ button.removeEventListener("click", onClick);
1516
+ button.remove();
1517
+ setState3(container, null);
1518
+ };
1519
+ },
1520
+ update(container, mountConfig) {
1521
+ const state = getState3(container);
1522
+ if (!state) return;
1523
+ const cfg = mountConfig ?? {};
1524
+ applyLabel(state.button, cfg);
1525
+ state.cfg = cfg;
1526
+ }
1527
+ };
1528
+
1529
+ // src/TextAnswerMountable.ts
1530
+ var STATE_KEY4 = "__syntroTextAnswerMount";
1531
+ function getState4(container) {
1532
+ return container[STATE_KEY4] ?? null;
1533
+ }
1534
+ function setState4(container, state) {
1535
+ container[STATE_KEY4] = state;
1536
+ }
1537
+ function applyText(p, text) {
1538
+ p.textContent = text;
1539
+ }
1540
+ var TextAnswerMountable = {
1541
+ mount(container, mountConfig) {
1542
+ const cfg = mountConfig ?? {};
1543
+ const paragraph = document.createElement("p");
1544
+ paragraph.dataset.syntroTextAnswer = "";
1545
+ paragraph.style.margin = "0";
1546
+ paragraph.style.fontSize = "12px";
1547
+ paragraph.style.lineHeight = "1.55";
1548
+ paragraph.style.color = "var(--sc-tile-text-color, currentColor)";
1549
+ applyText(paragraph, String(cfg.text ?? ""));
1550
+ container.appendChild(paragraph);
1551
+ setState4(container, { paragraph, cfg });
1552
+ return () => {
1553
+ paragraph.remove();
1554
+ setState4(container, null);
1555
+ };
1556
+ },
1557
+ update(container, mountConfig) {
1558
+ const state = getState4(container);
1559
+ if (!state) return;
1560
+ const cfg = mountConfig ?? {};
1561
+ applyText(state.paragraph, String(cfg.text ?? ""));
1562
+ state.cfg = cfg;
1563
+ }
1564
+ };
1565
+
6
1566
  // src/runtime.ts
7
1567
  var runtime = {
8
1568
  id: "adaptive-chatbot",
@@ -19,6 +1579,61 @@ var runtime = {
19
1579
  description: "AI-powered chat assistant with SSE streaming and A2UI support",
20
1580
  icon: "\u{1F4AC}"
21
1581
  }
1582
+ },
1583
+ // PRD §3 chat lid: the standalone chat bar UI shell. Used as a slot
1584
+ // `lid` widget (where it replaces the launcher) or as a regular
1585
+ // tile widget (renders inside the drawer). Headless of any chat
1586
+ // transport — emits chat-message-sent / chat-interrupt / canvas-close
1587
+ // for the parent to wire to whatever pipeline it owns.
1588
+ {
1589
+ id: "adaptive-chatbot:chat-bar",
1590
+ component: AdaptiveChatBarMountable,
1591
+ metadata: {
1592
+ name: "Chat Bar",
1593
+ description: "Always-visible chat input row with bubble-up trail. Headless \u2014 wire to your own transport.",
1594
+ icon: "\u2726"
1595
+ }
1596
+ },
1597
+ // PRD §4.5 suggested-chips tile: horizontally-wrapping chip strip
1598
+ // with click-to-reveal payload drawer. Each chip is a
1599
+ // `suggestions:chip` action.
1600
+ {
1601
+ id: "adaptive-chatbot:chips-strip",
1602
+ component: AdaptiveChipsStripMountable,
1603
+ metadata: {
1604
+ name: "Suggested Chips",
1605
+ description: "Wrapping chip strip with click-to-reveal payloads.",
1606
+ icon: "\u2728"
1607
+ }
1608
+ },
1609
+ // Built-in chip payload — a single paragraph of body text. Chip
1610
+ // payloads are widget references (same {widget,props} shape as
1611
+ // tile/lid configs); this is the simplest one shipped out of the
1612
+ // box so the chip pattern works without requiring a downstream
1613
+ // adaptive for a "just answer with text" case.
1614
+ {
1615
+ id: "adaptive-chatbot:text-answer",
1616
+ component: TextAnswerMountable,
1617
+ metadata: {
1618
+ name: "Text Answer",
1619
+ description: "Single paragraph payload \u2014 the default chip-drawer content.",
1620
+ icon: "\xB6"
1621
+ }
1622
+ },
1623
+ // Chip payload for LLM-authored navigation suggestions. Backed by
1624
+ // the `suggest-navigation` ItemTemplate: the backend wraps a curated
1625
+ // URL into props.url, the chip's payload-drawer mounts this widget,
1626
+ // user clicks "Go to <url>" → same-origin SPA navigation. Kept here
1627
+ // (not in adaptive-nav) so chip payloads ship with the chatbot
1628
+ // bundle that already provides chips-strip + text-answer.
1629
+ {
1630
+ id: "adaptive-chatbot:nav-link",
1631
+ component: NavLinkMountable,
1632
+ metadata: {
1633
+ name: "Nav Link",
1634
+ description: "Same-origin navigation button \u2014 used as a chip payload for LLM-authored nav suggestions.",
1635
+ icon: "\u2192"
1636
+ }
22
1637
  }
23
1638
  ]
24
1639
  };