@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
@@ -0,0 +1,1520 @@
1
+ import {
2
+ renderMarkdown
3
+ } from "./chunk-7WSMNAKW.js";
4
+
5
+ // src/AdaptiveChatTrail.ts
6
+ import { css, html, LitElement, nothing } from "lit";
7
+ import { styleMap } from "lit/directives/style-map.js";
8
+ import { unsafeHTML } from "lit/directives/unsafe-html.js";
9
+ var DEFAULT_VISIBLE = 3;
10
+ var TRAIL_MAX_WIDTH = "88%";
11
+ var OPACITY_STEP = 0.22;
12
+ var OPACITY_FLOOR = 0.18;
13
+ var Y_DRIFT_PX = -2;
14
+ var TRAIL_BLUR_VEIL_PX = 72;
15
+ var TRAIL_BLUR_MAX_PX = 5;
16
+ var _AdaptiveChatTrail = class _AdaptiveChatTrail extends LitElement {
17
+ constructor() {
18
+ super(...arguments);
19
+ this.messages = [];
20
+ this.tileReceipts = [];
21
+ this.inlineWidgets = [];
22
+ this.mountInlineWidget = void 0;
23
+ this.visibleCount = DEFAULT_VISIBLE;
24
+ this.expanded = false;
25
+ this.forceExpanded = false;
26
+ this.inFlight = false;
27
+ this.thinkingText = "";
28
+ this.introSuggestion = void 0;
29
+ /**
30
+ * Pre-conversation phantom assistant message. Rendered only when
31
+ * `messages` is empty. NOT injected into chatSession state — the
32
+ * greeting is configuration, not conversation, so it disappears
33
+ * automatically when the first real message arrives.
34
+ */
35
+ this.greeting = void 0;
36
+ // ── Anchor-scroll model (FEAT-1783535427, shared contract with
37
+ // <syntro-chat>) — applies to the SCROLLABLE modes (expanded /
38
+ // forceExpanded). The ambient peek strip keeps its bottom pin: it is
39
+ // overflow-hidden (not user-scrollable) and the pin is what keeps the
40
+ // latest line visible under the blur veil.
41
+ /** Follow the bottom on content growth. On by default (history
42
+ * browsing); off from a new user message until the visitor opts back in
43
+ * (pill tap or a user-gesture scroll that reaches the bottom). */
44
+ this._stickToBottom = true;
45
+ /** The user message the current turn is anchored to. */
46
+ this._anchorMsgId = null;
47
+ /** Whether a turn anchor is live (drives expand-mid-turn landing). */
48
+ this._anchorActive = false;
49
+ /** Keep enforcing chip-at-top on updates (late geometry, mode flips)
50
+ * until the user scrolls or opts into following. */
51
+ this._anchorHold = false;
52
+ /** Number of messages whose BOTTOM edge is still below the viewport —
53
+ * messages the visitor hasn't scrolled past yet. Drives the
54
+ * "⌄ N messages" pill and stays accurate as they scroll (one long
55
+ * streaming reply counts as 1 until its end has been read). Reactive. */
56
+ this._messagesBelow = 0;
57
+ /** Timestamp of the last user scroll gesture (wheel/touch): scroll
58
+ * events outside this window are programmatic (our own pin/anchor) and
59
+ * must not re-engage following — anchoring the newest message clamps AT
60
+ * the bottom in any longer conversation. */
61
+ this._lastUserScrollIntentTs = 0;
62
+ this._markUserScrollIntent = () => {
63
+ this._lastUserScrollIntentTs = Date.now();
64
+ };
65
+ this._onTrailScroll = () => {
66
+ const container = this._trailContainer();
67
+ if (!container) return;
68
+ const atBottom = this._isAtBottom(container);
69
+ this._messagesBelow = this._scrollable && !atBottom ? this._countMessagesBelow(container) : 0;
70
+ const isUserDriven = Date.now() - this._lastUserScrollIntentTs < _AdaptiveChatTrail._USER_SCROLL_INTENT_WINDOW_MS;
71
+ if (isUserDriven) {
72
+ this._anchorHold = false;
73
+ this._stickToBottom = atBottom;
74
+ if (atBottom) this._anchorActive = false;
75
+ }
76
+ };
77
+ /** Pill tap: jump to the newest content and follow for the rest of the turn. */
78
+ this._onScrollToRecent = () => {
79
+ this._stickToBottom = true;
80
+ this._anchorHold = false;
81
+ this._anchorActive = false;
82
+ requestAnimationFrame(() => {
83
+ const container = this._trailContainer();
84
+ if (!container) return;
85
+ container.scrollTop = container.scrollHeight;
86
+ this._syncContentBelow();
87
+ });
88
+ };
89
+ // No `createRenderRoot()` override → default open shadow root. CSS
90
+ // custom properties (--sc-tile-text-color, --sc-content-bubble-*,
91
+ // etc.) inherit through the shadow boundary, so canvas-level theming
92
+ // continues to cascade into the chips without the light-DOM hack.
93
+ // See the `static styles` block above for the in-shadow stylesheet.
94
+ this._onExpand = () => {
95
+ this.expanded = true;
96
+ this.dispatchEvent(new CustomEvent("trail-expand", { bubbles: true, composed: true }));
97
+ };
98
+ /**
99
+ * Approve a pending client tool call. The trail emits a generic
100
+ * event so the host can decide whether to forward to chatSession
101
+ * (the common path) or override. Keeps the trail itself free of
102
+ * direct chatSession coupling — it's a pure view component.
103
+ */
104
+ this._onToolCallClick = (tc) => {
105
+ if (tc.status !== "pending") return;
106
+ this.dispatchEvent(
107
+ new CustomEvent("trail-toolcall-approved", {
108
+ detail: { toolCallId: tc.id, approved: true },
109
+ bubbles: true,
110
+ composed: true
111
+ })
112
+ );
113
+ };
114
+ this._onCollapse = () => {
115
+ this.expanded = false;
116
+ this.dispatchEvent(new CustomEvent("trail-collapse", { bubbles: true, composed: true }));
117
+ };
118
+ /**
119
+ * instanceId → id of the message that was newest when the receipt first
120
+ * appeared. This is what makes a receipt read like a chat bubble: it renders
121
+ * inline right after its anchor message, so newer messages push it up into
122
+ * history instead of it staying pinned below everything. Assigned once per
123
+ * instanceId (in willUpdate, the only place that sees both `messages` and
124
+ * `tileReceipts` at the moment a tile mounts) and never re-homed — a
125
+ * dismiss/bring-back keeps the chip where it was created. `null` = the tile
126
+ * mounted before the first chat message (canvas replay); those fall to the
127
+ * tail strip (see render).
128
+ */
129
+ this._receiptAnchors = /* @__PURE__ */ new Map();
130
+ /**
131
+ * instanceId → anchor message id for inline widgets — same mechanism as
132
+ * `_receiptAnchors`. The inline carousel renders right after the message
133
+ * that was newest when it mounted, so it reads like a rich reply and
134
+ * drifts up into history as later messages append.
135
+ */
136
+ this._inlineAnchors = /* @__PURE__ */ new Map();
137
+ /**
138
+ * instanceId → the cached inline widget entry (render node + props target +
139
+ * optional registry cleanup). Cached so a prop patch (same instanceId, new
140
+ * props) updates the EXISTING element instead of tearing it down and
141
+ * re-creating it — which would reset the carousel's scroll position +
142
+ * internal state. Created lazily the first time we see the widget:
143
+ * `document.createElement(tag)` for a defined tag, or a registry mount for a
144
+ * widget-id. An unresolvable widget caches `null` so the fail-safe renders
145
+ * nothing without re-checking the registry.
146
+ */
147
+ this._inlineElements = /* @__PURE__ */ new Map();
148
+ this._onTileReceiptClick = (instanceId) => {
149
+ this.dispatchEvent(
150
+ new CustomEvent("tile-receipt-activate", {
151
+ detail: { instanceId },
152
+ bubbles: true,
153
+ composed: true
154
+ })
155
+ );
156
+ };
157
+ this._onAlertReceiptClick = (instanceId) => {
158
+ this.dispatchEvent(
159
+ new CustomEvent("alert-receipt-activate", {
160
+ detail: { instanceId },
161
+ bubbles: true,
162
+ composed: true
163
+ })
164
+ );
165
+ };
166
+ this._onIntroSuggestionClick = () => {
167
+ const s = this.introSuggestion;
168
+ if (!s) return;
169
+ this.dispatchEvent(
170
+ new CustomEvent("trail-intro-suggestion", {
171
+ detail: { prompt: s.prompt },
172
+ bubbles: true,
173
+ composed: true
174
+ })
175
+ );
176
+ };
177
+ /**
178
+ * Visitor tapped a `suggest_replies` inline chip. Emit
179
+ * `trail-suggested-reply` carrying the chip text — the parent
180
+ * (AdaptiveChatBar) re-dispatches it as `chat-message-sent`, the
181
+ * SAME channel the input row uses, so the reply rides the existing
182
+ * send path (mountable → chatSession.send → transport). No fabricated
183
+ * AG-UI events; this is exactly what typing the text and hitting
184
+ * Enter would do. The chips clear on their own once the reply lands
185
+ * and a newer assistant message becomes the latest.
186
+ */
187
+ this._onSuggestedReplyClick = (text) => {
188
+ const trimmed = text.trim();
189
+ if (!trimmed) return;
190
+ this.dispatchEvent(
191
+ new CustomEvent("trail-suggested-reply", {
192
+ detail: { text: trimmed },
193
+ bubbles: true,
194
+ composed: true
195
+ })
196
+ );
197
+ };
198
+ }
199
+ get _scrollable() {
200
+ return this.expanded || this.forceExpanded;
201
+ }
202
+ _trailContainer() {
203
+ return this.renderRoot.querySelector("[data-syntro-chat-trail]");
204
+ }
205
+ _isAtBottom(container) {
206
+ return container.scrollHeight - container.scrollTop - container.clientHeight <= _AdaptiveChatTrail._AT_BOTTOM_EPSILON_PX;
207
+ }
208
+ /** Count chips whose bottom edge hasn't entered the viewport yet. */
209
+ _countMessagesBelow(container) {
210
+ const viewportBottom = container.getBoundingClientRect().top + container.clientHeight;
211
+ let count = 0;
212
+ for (const chip of this.renderRoot.querySelectorAll("[data-trail-chip]")) {
213
+ if (chip.getBoundingClientRect().bottom > viewportBottom + 4) count += 1;
214
+ }
215
+ return count > 0 ? count : 1;
216
+ }
217
+ _syncContentBelow() {
218
+ const container = this._trailContainer();
219
+ if (!container) return;
220
+ this._messagesBelow = this._scrollable && !this._isAtBottom(container) ? this._countMessagesBelow(container) : 0;
221
+ }
222
+ /** Scroll so the anchored user chip sits at the top of the trail
223
+ * viewport. Idempotent — safe to re-apply on every update while the
224
+ * hold is live (content grows BELOW the anchor, so the target is
225
+ * stable; re-applying also covers late geometry, e.g. expanding
226
+ * mid-turn from the ambient strip). */
227
+ _applyAnchor() {
228
+ requestAnimationFrame(() => {
229
+ const container = this._trailContainer();
230
+ if (!container || this._anchorMsgId === null) return;
231
+ const chip = this.renderRoot.querySelector(
232
+ `[data-trail-chip-id="${String(this._anchorMsgId)}"]`
233
+ );
234
+ if (!chip) return;
235
+ const top = chip.getBoundingClientRect().top - container.getBoundingClientRect().top + container.scrollTop;
236
+ container.scrollTop = top;
237
+ this._syncContentBelow();
238
+ });
239
+ }
240
+ _pinToBottom() {
241
+ requestAnimationFrame(() => {
242
+ const container = this._trailContainer();
243
+ if (!container) return;
244
+ container.scrollTop = container.scrollHeight;
245
+ this._syncContentBelow();
246
+ });
247
+ }
248
+ /**
249
+ * Whether to render the thinking-dots chip after the last message.
250
+ * True iff a request is in flight AND the assistant hasn't started
251
+ * streaming a reply yet — once the first delta lands, the streaming
252
+ * chip's caret takes over and the thinking indicator hides so the
253
+ * visitor isn't shown both at once. Also hidden when the last
254
+ * message is an error/system message, because the request that
255
+ * "errored" is already over from the user's perspective.
256
+ */
257
+ _shouldShowThinking() {
258
+ if (!this.inFlight) return false;
259
+ const last = this.messages[this.messages.length - 1];
260
+ if (!last) return true;
261
+ if (last.role === "assistant" && last.status === "streaming") {
262
+ return (last.text ?? "").trim().length === 0;
263
+ }
264
+ if (last.role === "system" || last.status === "error") return false;
265
+ return true;
266
+ }
267
+ disconnectedCallback() {
268
+ super.disconnectedCallback();
269
+ for (const entry of this._inlineElements.values()) {
270
+ entry?.cleanup?.();
271
+ }
272
+ this._inlineElements.clear();
273
+ }
274
+ /**
275
+ * The id of the newest message the render will actually PAINT — the only
276
+ * valid anchor target. Anchoring to the raw last message is wrong-by-
277
+ * construction for tool-call turns: mid-turn the newest message is often a
278
+ * whitespace-only assistant shell (some providers emit whitespace text
279
+ * deltas around tool calls) that the empty-bubble filter drops, so an
280
+ * anchor recorded on it can never be in the painted set and the receipt
281
+ * falls to the tail strip forever — resurrecting BUG-1783015943 (seen live
282
+ * on vela desktop, BUG-1783630187). `isSuggestionHost=false` here: a
283
+ * message whose ONLY content is reply chips is too ephemeral to anchor on
284
+ * (the chips clear when the visitor answers), so walk past it too.
285
+ */
286
+ _newestRenderableMessageId() {
287
+ for (let i = this.messages.length - 1; i >= 0; i--) {
288
+ const m = this.messages[i];
289
+ if (m.role !== "assistant" || assistantChipRendersContent(m, false)) return m.id;
290
+ }
291
+ return null;
292
+ }
293
+ willUpdate(changed) {
294
+ if (changed.has("messages")) {
295
+ const lastId = this._newestRenderableMessageId();
296
+ if (lastId !== null) {
297
+ for (const [key, anchor] of this._receiptAnchors) {
298
+ if (anchor === null) this._receiptAnchors.set(key, lastId);
299
+ }
300
+ for (const [key, anchor] of this._inlineAnchors) {
301
+ if (anchor === null) this._inlineAnchors.set(key, lastId);
302
+ }
303
+ }
304
+ }
305
+ if (changed.has("tileReceipts")) {
306
+ const lastId = this._newestRenderableMessageId();
307
+ const live = /* @__PURE__ */ new Set();
308
+ for (const r of this.tileReceipts) {
309
+ live.add(r.instanceId);
310
+ if (!this._receiptAnchors.has(r.instanceId)) {
311
+ this._receiptAnchors.set(r.instanceId, lastId);
312
+ }
313
+ }
314
+ for (const key of this._receiptAnchors.keys()) {
315
+ if (!live.has(key)) this._receiptAnchors.delete(key);
316
+ }
317
+ }
318
+ if (changed.has("inlineWidgets")) {
319
+ const lastId = this._newestRenderableMessageId();
320
+ const live = /* @__PURE__ */ new Set();
321
+ for (const w of this.inlineWidgets) {
322
+ live.add(w.instanceId);
323
+ if (!this._inlineAnchors.has(w.instanceId)) {
324
+ this._inlineAnchors.set(w.instanceId, lastId);
325
+ }
326
+ this._ensureInlineElement(w);
327
+ }
328
+ for (const key of this._inlineAnchors.keys()) {
329
+ if (!live.has(key)) this._inlineAnchors.delete(key);
330
+ }
331
+ for (const key of this._inlineElements.keys()) {
332
+ if (!live.has(key)) {
333
+ this._inlineElements.get(key)?.cleanup?.();
334
+ this._inlineElements.delete(key);
335
+ }
336
+ }
337
+ }
338
+ }
339
+ /**
340
+ * Create (once) or refresh the cached element for an inline widget.
341
+ *
342
+ * Two mount paths, both keeping the chat bundle free of any widget-package
343
+ * import:
344
+ * - Direct tag (self-defining, server-mounted widgets like the reco
345
+ * carousel whose declared `widget` IS its custom-element tag):
346
+ * `document.createElement(tag)`.
347
+ * - Widget-id (admin-template widgets like `adaptive-product:card` or
348
+ * `adaptive-faq:accordion`, whose `widget` is a runtime widget-id, NOT a
349
+ * valid custom-element tag): mount through the runtime widget registry
350
+ * via {@link mountInlineWidget}, which resolves the id to its real
351
+ * element exactly like the deck `SyntroTileCard` path. Without this,
352
+ * `createElement('adaptive-product:card')` yields an inert unknown
353
+ * element and the tile renders permanently blank (the "inline mode
354
+ * shows nothing but the reco" bug).
355
+ *
356
+ * Fail-safe: an id that resolves to neither a defined tag nor a registered
357
+ * widget caches `null` and renders nothing (no broken box).
358
+ */
359
+ _ensureInlineElement(w) {
360
+ let entry = this._inlineElements.get(w.instanceId);
361
+ if (entry === void 0) {
362
+ if (customElements.get(w.tag)) {
363
+ const el = document.createElement(w.tag);
364
+ el.setAttribute("data-inline-widget-el", "");
365
+ entry = { node: el, propsTarget: el };
366
+ } else if (this.mountInlineWidget) {
367
+ const container = document.createElement("div");
368
+ container.setAttribute("data-inline-widget-el", "");
369
+ const mounted = this.mountInlineWidget(w.tag, container, w.props);
370
+ entry = mounted ? { node: container, propsTarget: mounted.element, cleanup: mounted.cleanup } : null;
371
+ } else {
372
+ entry = null;
373
+ }
374
+ this._inlineElements.set(w.instanceId, entry);
375
+ }
376
+ if (!entry) return;
377
+ entry.propsTarget.props = w.props;
378
+ }
379
+ updated(changed) {
380
+ if (!changed.has("expanded") && !changed.has("forceExpanded") && !changed.has("messages")) {
381
+ return;
382
+ }
383
+ if (changed.has("messages")) {
384
+ let lastUserIdx = -1;
385
+ for (let i = this.messages.length - 1; i >= 0; i--) {
386
+ if (this.messages[i].role === "user") {
387
+ lastUserIdx = i;
388
+ break;
389
+ }
390
+ }
391
+ const lastUser = lastUserIdx >= 0 ? this.messages[lastUserIdx] : void 0;
392
+ const isLiveTurn = lastUser !== void 0 && this.messages.slice(lastUserIdx + 1).every((m) => m.role !== "user" && m.status === "streaming");
393
+ if (lastUser && isLiveTurn && lastUser.id !== this._anchorMsgId) {
394
+ this._anchorMsgId = lastUser.id;
395
+ this._anchorActive = true;
396
+ this._anchorHold = true;
397
+ this._stickToBottom = false;
398
+ }
399
+ }
400
+ if (!this._scrollable) {
401
+ this._pinToBottom();
402
+ return;
403
+ }
404
+ if (this._anchorActive && this._anchorHold) {
405
+ this._applyAnchor();
406
+ } else if (this._stickToBottom) {
407
+ this._pinToBottom();
408
+ } else {
409
+ requestAnimationFrame(() => this._syncContentBelow());
410
+ }
411
+ }
412
+ /** Render a set of inline widgets (reco carousel etc.) as
413
+ * `[data-trail-widget]` blocks, anchored to a message position like
414
+ * receipts. Each block hosts the cached custom element for that
415
+ * instanceId — a defined tag mounts inline; an undefined tag renders
416
+ * nothing (fail-safe). Rendered at every trail site (inline per anchor,
417
+ * tail, empty-state) so placement can't diverge. */
418
+ _renderInlineWidgetStrip(list) {
419
+ if (list.length === 0) return nothing;
420
+ const blocks = list.map((w) => {
421
+ const entry = this._inlineElements.get(w.instanceId);
422
+ if (!entry) return nothing;
423
+ return html`<div
424
+ data-trail-widget
425
+ data-instance-id=${w.instanceId}
426
+ style=${styleMap(inlineWidgetBlockStyles())}
427
+ >${entry.node}</div>`;
428
+ }).filter((b) => b !== nothing);
429
+ if (blocks.length === 0) return nothing;
430
+ return html`${blocks}`;
431
+ }
432
+ /** A row of receipt chips, used at every trail render site (inline per
433
+ * anchor message, the tail fallback, and the empty-state branches) so the
434
+ * markup + styling can never diverge. Returns `nothing` for an empty list. */
435
+ _renderReceiptStrip(list) {
436
+ if (list.length === 0) return nothing;
437
+ return html`<div data-trail-tile-receipts style=${styleMap(tileReceiptStripStyles())}>
438
+ ${list.map((tr) => this._renderReceiptChip(tr))}
439
+ </div>`;
440
+ }
441
+ /** One receipt chip — a single, unified, single-line design for both kinds
442
+ * (nav "alert" + mounted "tile"). Same container/title/action style so they
443
+ * read as one chip family; the icon + action label carry the kind. No
444
+ * subtitle: the old "Brought up earlier" line was hardcoded (wrong for
445
+ * same-turn mounts) and made the chip taller — dropping it is the "shorter"
446
+ * chip. Rendered at every trail site so the kinds can't diverge. */
447
+ _renderReceiptChip(tr) {
448
+ if (tr.kind === "alert") {
449
+ return html`<button
450
+ type="button"
451
+ data-trail-alert-receipt
452
+ data-instance-id=${tr.instanceId}
453
+ @click=${() => this._onAlertReceiptClick(tr.instanceId)}
454
+ style=${styleMap(tileReceiptChipStyles())}
455
+ >
456
+ <span style=${styleMap(alertReceiptIconStyles())} aria-hidden="true">⚡</span>
457
+ <span style=${styleMap(receiptChipTitleStyles("alert"))}>${tr.title}</span>
458
+ <span style=${styleMap(tileReceiptActionStyles(false))}>Go →</span>
459
+ </button>`;
460
+ }
461
+ return html`<button
462
+ type="button"
463
+ data-trail-tile-receipt
464
+ data-instance-id=${tr.instanceId}
465
+ @click=${() => this._onTileReceiptClick(tr.instanceId)}
466
+ style=${styleMap(tileReceiptChipStyles())}
467
+ >
468
+ <span style=${styleMap(tileReceiptGlyphStyles())} aria-hidden="true"
469
+ ><i style=${styleMap(tileReceiptGlyphLayerStyles(0))}></i
470
+ ><i style=${styleMap(tileReceiptGlyphLayerStyles(1))}></i
471
+ ></span>
472
+ <span style=${styleMap(receiptChipTitleStyles("tile"))}>${tr.title}</span>
473
+ <span style=${styleMap(tileReceiptActionStyles(tr.dismissed))}
474
+ >${tr.dismissed ? "Bring back \u21BA" : "View \u25B8"}</span
475
+ >
476
+ </button>`;
477
+ }
478
+ render() {
479
+ if (this.messages.length === 0 && !this.greeting && !this.introSuggestion && this.tileReceipts.length === 0 && this.inlineWidgets.length === 0)
480
+ return nothing;
481
+ if (this.messages.length === 0 && (this.greeting || this.introSuggestion)) {
482
+ return html`<div data-syntro-chat-trail data-state="empty" style=${styleMap({
483
+ display: "flex",
484
+ flexDirection: "column",
485
+ alignItems: "center",
486
+ justifyContent: "center",
487
+ textAlign: "center",
488
+ gap: "12px",
489
+ flex: "1 1 auto",
490
+ width: "100%",
491
+ padding: "24px 16px",
492
+ pointerEvents: "auto"
493
+ })}>
494
+ <div data-trail-empty-icon style=${styleMap(emptyIconStyles())} aria-hidden="true">✦</div>
495
+ ${this.greeting ? html`<h2
496
+ data-trail-empty-title
497
+ style=${styleMap(emptyTitleStyles())}
498
+ >${unsafeHTML(renderMarkdown(stripTrailingWhitespace(this.greeting)))}</h2>` : nothing}
499
+ ${this.introSuggestion ? html`<button
500
+ type="button"
501
+ data-trail-intro-suggestion
502
+ @click=${this._onIntroSuggestionClick}
503
+ style=${styleMap(introSuggestionStyles())}
504
+ >${this.introSuggestion.label}</button>` : nothing}
505
+ ${this._renderReceiptStrip(this.tileReceipts)}
506
+ ${this._renderInlineWidgetStrip(this.inlineWidgets)}
507
+ </div>`;
508
+ }
509
+ if (this.messages.length === 0 && (this.tileReceipts.length > 0 || this.inlineWidgets.length > 0)) {
510
+ return html`<div data-syntro-chat-trail data-state="empty" style=${styleMap({
511
+ display: "flex",
512
+ flexDirection: "column",
513
+ alignItems: "flex-start",
514
+ gap: "6px",
515
+ width: "100%",
516
+ padding: "8px 0",
517
+ pointerEvents: "auto"
518
+ })}>
519
+ ${this._renderReceiptStrip(this.tileReceipts)}
520
+ ${this._renderInlineWidgetStrip(this.inlineWidgets)}
521
+ </div>`;
522
+ }
523
+ let suggestionHostId = null;
524
+ for (let i = this.messages.length - 1; i >= 0; i--) {
525
+ const m = this.messages[i];
526
+ if (m.role === "user") break;
527
+ if (m.role === "assistant" && extractSuggestedReplies(m.toolCalls).length > 0) {
528
+ suggestionHostId = m.id;
529
+ break;
530
+ }
531
+ }
532
+ const renderable = this.messages.filter(
533
+ (m) => m.role !== "assistant" || assistantChipRendersContent(m, m.id === suggestionHostId)
534
+ );
535
+ const visible = this.expanded || this.forceExpanded ? renderable : renderable.slice(-this.visibleCount);
536
+ const hidden = renderable.length - visible.length;
537
+ const visibleIds = new Set(visible.map((m) => m.id));
538
+ const receiptsByAnchor = /* @__PURE__ */ new Map();
539
+ const tailReceipts = [];
540
+ for (const r of this.tileReceipts) {
541
+ const anchor = this._receiptAnchors.get(r.instanceId) ?? null;
542
+ if (anchor !== null && visibleIds.has(anchor)) {
543
+ const list = receiptsByAnchor.get(anchor);
544
+ if (list) list.push(r);
545
+ else receiptsByAnchor.set(anchor, [r]);
546
+ } else {
547
+ tailReceipts.push(r);
548
+ }
549
+ }
550
+ const inlineByAnchor = /* @__PURE__ */ new Map();
551
+ const tailInlineWidgets = [];
552
+ for (const w of this.inlineWidgets) {
553
+ const anchor = this._inlineAnchors.get(w.instanceId) ?? null;
554
+ if (anchor !== null && visibleIds.has(anchor)) {
555
+ const list = inlineByAnchor.get(anchor);
556
+ if (list) list.push(w);
557
+ else inlineByAnchor.set(anchor, [w]);
558
+ } else {
559
+ tailInlineWidgets.push(w);
560
+ }
561
+ }
562
+ const baseStyles = {
563
+ display: "flex",
564
+ flexDirection: "column",
565
+ gap: "6px",
566
+ // NO `justifyContent: flex-end` here — combined with `overflow: auto`
567
+ // it's a known cross-browser bug: oversized flex children get
568
+ // pushed ABOVE the container without contributing to scrollHeight,
569
+ // so the scrollbar never appears. We rely on `:first-child {
570
+ // margin-top: auto }` (injected as global CSS) to push short
571
+ // content to the bottom of the box while leaving overflow handling
572
+ // to the native scroll container.
573
+ width: "100%",
574
+ // The trail itself has no chrome — chips sit on the host page bg.
575
+ padding: "0",
576
+ pointerEvents: "auto"
577
+ };
578
+ const modeStyles = this.forceExpanded ? {
579
+ // Full-screen host: fill all available vertical space and
580
+ // scroll the overflow. ``flex: 1 1 auto`` (basis AUTO, not 0) —
581
+ // in a DEFINITE-height parent (full-screen panel) grow:1 still
582
+ // fills all free space and margin-top:auto bottom-anchors, so
583
+ // fill behavior is unchanged; but basis auto also lets the trail
584
+ // report its CONTENT height as its natural size, so in an
585
+ // INDEFINITE-height parent (velvet's shrink-wrap bottom sheet)
586
+ // the trail contributes real content height instead of collapsing
587
+ // to 0 — which is what lets the sheet hug its content with no void.
588
+ // shrink:1 + min-height:0 still lets the trail shrink first when
589
+ // the parent runs out of room; the input row is absolute-pinned
590
+ // (never pushed off) and the header shrinks after the trail.
591
+ flex: "1 1 auto",
592
+ minHeight: "0",
593
+ overflowY: "auto",
594
+ // ``contain`` prevents scroll from bubbling out to the host
595
+ // page when the trail hits top or bottom. Required for the
596
+ // panel-scroll-leak regression test in
597
+ // SyntroBottomSheet.test.ts.
598
+ overscrollBehavior: "contain",
599
+ paddingTop: "8px",
600
+ borderTop: "var(--sc-content-bubble-border, 1px solid rgba(255, 255, 255, 0.18))",
601
+ scrollBehavior: "smooth",
602
+ scrollbarWidth: "thin"
603
+ } : this.expanded ? {
604
+ maxHeight: "min(320px, 40vh)",
605
+ overflowY: "auto",
606
+ paddingTop: "8px",
607
+ borderTop: "var(--sc-content-bubble-border, 1px solid rgba(255, 255, 255, 0.18))",
608
+ scrollBehavior: "smooth",
609
+ scrollbarWidth: "thin"
610
+ } : {
611
+ // Collapsed mode = ambient peek surface. Never a scrollbar:
612
+ // long messages slide up behind the blur veil instead of
613
+ // exposing a scrollbar. The "↑ N more · expand" affordance
614
+ // is the only entry into history; clicking it switches to
615
+ // expanded mode where scroll IS allowed.
616
+ // Height kept short so the trail reads as a peek strip above
617
+ // the chat bar, not a panel — long single replies fade into
618
+ // the blur veil at the top, user expands to read the rest.
619
+ maxHeight: "min(140px, 22vh)",
620
+ overflow: "hidden"
621
+ };
622
+ const containerStyles = { ...baseStyles, ...modeStyles };
623
+ const frameStyles = this.forceExpanded ? {
624
+ position: "relative",
625
+ width: "100%",
626
+ display: "flex",
627
+ flexDirection: "column",
628
+ // basis AUTO (see the scroller's modeStyles note): fills a definite
629
+ // parent, contributes content height in an indefinite one (velvet
630
+ // shrink-wrap). Every fill layer in the chain must be basis-auto or
631
+ // the min-content of a basis-0 layer collapses the whole chain to 0.
632
+ flex: "1 1 auto",
633
+ minHeight: "0"
634
+ } : {
635
+ position: "relative",
636
+ width: "100%"
637
+ };
638
+ return html`
639
+ ${// Single toggle button above the frame. Same DOM node morphs:
640
+ // collapsed → "↑ N more · expand" / "↑ expand" (calls _onExpand)
641
+ // expanded → "⌄ minimize" (calls _onCollapse)
642
+ // Lives OUTSIDE the frame so the absolute blur veil (top:0,
643
+ // z-index:2 inside the frame) doesn't cover it. Shown whenever
644
+ // there's anything in the trail — the user expects this affordance
645
+ // available regardless of how many chips are on screen.
646
+ // When ``forceExpanded`` is set by the host (full-screen panel,
647
+ // etc.), there's nowhere to collapse INTO and the affordance
648
+ // would just confuse the user — suppress it entirely.
649
+ this.messages.length === 0 || this.forceExpanded ? nothing : this.expanded ? html`<button
650
+ type="button"
651
+ data-trail-toggle
652
+ data-trail-collapse
653
+ @click=${this._onCollapse}
654
+ style=${styleMap(moreStyles())}
655
+ >⌄ minimize</button>` : html`<button
656
+ type="button"
657
+ data-trail-toggle
658
+ data-trail-more
659
+ @click=${this._onExpand}
660
+ style=${styleMap(moreStyles())}
661
+ >${hidden > 0 ? html`↑ ${hidden} more · expand` : html`↑ expand`}</button>`}
662
+ <div data-syntro-chat-trail-frame style=${styleMap(frameStyles)}>
663
+ <div
664
+ data-syntro-chat-trail
665
+ style=${styleMap(containerStyles)}
666
+ @scroll=${this._onTrailScroll}
667
+ @wheel=${this._markUserScrollIntent}
668
+ @touchmove=${this._markUserScrollIntent}
669
+ >
670
+ ${visible.map((m, i) => {
671
+ const pos = this.expanded || this.forceExpanded ? 0 : visible.length - 1 - i;
672
+ const state = chipVisualState(m);
673
+ const isStreaming = state.kind === "streaming";
674
+ const {
675
+ pendingToolCalls: toolCalls,
676
+ receipts,
677
+ replyChips
678
+ } = computeAssistantChipParts(m, m.id === suggestionHostId);
679
+ const renderedText = m.role === "assistant" ? (
680
+ // Never paint a degenerate serialized-response dump, even if the
681
+ // turn also carries a legit part (receipt/chip) that keeps it in
682
+ // the renderable set. Mirrors the filter's drop signal.
683
+ looksLikeSerializedModelResponse(m.text ?? "") ? nothing : html`${unsafeHTML(renderMarkdown(stripTrailingWhitespace(m.text)))}`
684
+ ) : html`${m.text}`;
685
+ const chipStyle = isStreaming ? {
686
+ ...chipStyles(state, pos),
687
+ animation: "syntro-trail-chip-enter 220ms cubic-bezier(0.22, 1, 0.36, 1) both"
688
+ } : chipStyles(state, pos);
689
+ return html`<div
690
+ data-trail-chip
691
+ data-trail-chip-id=${String(m.id)}
692
+ data-role=${m.role}
693
+ data-status=${m.status ?? "complete"}
694
+ style=${styleMap(chipStyle)}
695
+ >${renderedText}${isStreaming ? html`<span
696
+ data-trail-caret
697
+ aria-hidden="true"
698
+ style=${styleMap(caretStyles())}
699
+ ></span>` : nothing}${toolCalls.length > 0 ? html`<div data-trail-toolcalls style=${styleMap(toolCallStripStyles())}>
700
+ ${toolCalls.map(
701
+ (tc) => html`<button
702
+ type="button"
703
+ data-trail-toolcall
704
+ data-tool-id=${tc.id}
705
+ data-tool-status=${tc.status}
706
+ @click=${() => this._onToolCallClick(tc)}
707
+ style=${styleMap(pendingToolCallChipStyles())}
708
+ title="${tc.name} · ${tc.status}"
709
+ >? ${tc.name}</button>`
710
+ )}
711
+ </div>` : nothing}${receipts.length > 0 ? html`<div data-trail-receipts style=${styleMap(receiptStripStyles())}>
712
+ ${receipts.map(
713
+ (r) => html`<div
714
+ data-trail-receipt
715
+ data-receipt-type=${r.type}
716
+ style=${styleMap(receiptStyles())}
717
+ ><span aria-hidden="true" style=${styleMap(receiptIconStyles())}>✓</span
718
+ ><span>${receiptText(r)}</span></div>`
719
+ )}
720
+ </div>` : nothing}${replyChips.length > 0 ? html`<div data-trail-reply-chips style=${styleMap(replyChipStripStyles())}>
721
+ ${replyChips.map(
722
+ (reply) => html`<button
723
+ type="button"
724
+ data-trail-reply-chip
725
+ @click=${() => this._onSuggestedReplyClick(reply)}
726
+ style=${styleMap(replyChipStyles())}
727
+ >${reply}</button>`
728
+ )}
729
+ </div>` : nothing}</div>${// Receipt chips that mounted while THIS message was newest render
730
+ // inline right here, so they read as part of the flow and drift up
731
+ // as later messages append below them.
732
+ this._renderReceiptStrip(receiptsByAnchor.get(m.id) ?? [])}${// Inline widgets (reco carousel) anchored to this message render
733
+ // right here in the conversation flow — the whole point of the
734
+ // chat-inline slot: visible in the trail, not behind the panel.
735
+ this._renderInlineWidgetStrip(inlineByAnchor.get(m.id) ?? [])}`;
736
+ })}
737
+ ${// Thinking indicator — fills the gap between the user's
738
+ // most recent message and the assistant's first token.
739
+ // Shows when chatSession.inFlight is true AND the last
740
+ // message is NOT a streaming assistant message (the
741
+ // assistant's own caret takes over once tokens arrive).
742
+ // Styled as an assistant chip with three bouncing dots
743
+ // instead of text, so the thread reads naturally — the
744
+ // visitor sees an "assistant" bubble that's clearly busy.
745
+ this._shouldShowThinking() ? html`<div
746
+ data-trail-thinking
747
+ data-role="assistant"
748
+ data-status="thinking"
749
+ aria-label=${this.thinkingText ? `Assistant is thinking: ${this.thinkingText}` : "Assistant is thinking"}
750
+ style=${styleMap(thinkingSlotStyles())}
751
+ >
752
+ ${// When the backend has streamed reasoning narration into
753
+ // `thinkingText`, render the tail of it as a small italic
754
+ // line — the model's own intent ("Let me check what we have
755
+ // for student athletes…") narrates the gap better than
756
+ // silent dots. Falls back to the bouncing-dot pulse when
757
+ // no narration has arrived yet.
758
+ this.thinkingText ? html`<span
759
+ data-trail-thinking-text
760
+ style=${styleMap(thinkingTextStyles())}
761
+ >${truncateThinkingTail(this.thinkingText)}</span
762
+ >` : html`<span data-trail-thinking-dots style=${styleMap(thinkingDotsStyles())}>
763
+ <span style=${styleMap(thinkingDotStyles(0))}></span>
764
+ <span style=${styleMap(thinkingDotStyles(1))}></span>
765
+ <span style=${styleMap(thinkingDotStyles(2))}></span>
766
+ </span>`}
767
+ </div>` : nothing}
768
+ ${// Tail strip: receipts with no in-slice anchor (mounted pre-chat, or
769
+ // their anchor message scrolled out of the ambient view). Keeps them
770
+ // reachable at the bottom — the old always-pinned position — rather
771
+ // than dropping them. In expanded/maximized mode every message is
772
+ // painted, so this is usually empty and receipts sit inline instead.
773
+ this._renderReceiptStrip(tailReceipts)}
774
+ ${// Inline widgets with no in-slice anchor (mounted pre-chat, or their
775
+ // anchor scrolled out of the ambient view) render at the tail.
776
+ this._renderInlineWidgetStrip(tailInlineWidgets)}
777
+ </div>
778
+ ${// The blur veil only fires in collapsed (ambient) mode.
779
+ // Expanded = the user explicitly asked for history; blurring
780
+ // the top of the panel would hide content (including the
781
+ // minimize button just above the trail's top edge) the user
782
+ // came here to see.
783
+ this.expanded || this.forceExpanded ? nothing : html`<div
784
+ data-trail-blur-veil
785
+ aria-hidden="true"
786
+ style=${styleMap(blurVeilStyles())}
787
+ ></div>`}
788
+ ${// "Scroll to most recent" pill (FEAT-1783535427): shown only in
789
+ // the scrollable modes when content extends below the viewport —
790
+ // the visitor is reading the reply top-down and this is the
791
+ // one-tap opt-in back to the newest content. Absolute inside the
792
+ // positioning frame so it floats over the scroller's bottom edge.
793
+ this._scrollable && this._messagesBelow > 0 ? html`<button
794
+ type="button"
795
+ data-trail-scroll-recent
796
+ aria-label="Scroll to most recent — ${this._messagesBelow} ${this._messagesBelow === 1 ? "message" : "messages"} below"
797
+ @click=${this._onScrollToRecent}
798
+ style=${styleMap(scrollRecentPillStyles())}
799
+ ><span aria-hidden="true" style="font-size:11px;line-height:1;">⌄</span><span>${this._messagesBelow} ${this._messagesBelow === 1 ? "message" : "messages"}</span></button>` : nothing}
800
+ </div>
801
+ `;
802
+ }
803
+ };
804
+ // Trail-local styles. The chat-trail used to render into light DOM
805
+ // (`createRenderRoot() { return this }`) and inject these rules into
806
+ // `document.head` so chip + markdown styles could find their selectors.
807
+ // That broke the moment the trail was hosted inside any shadow root
808
+ // (sc-mount → custom-canvas → velvet-mobile → sc-mount → chat-bar →
809
+ // chat-trail is the production chain): CSS animations require the
810
+ // @keyframes rule to be in the SAME TreeScope as the element using
811
+ // it, and document-level keyframes don't cross a shadow boundary.
812
+ // Result was silent — `animation-name` parsed fine, the property got
813
+ // the right resolved value, but the browser never instantiated an
814
+ // Animation object because no matching keyframe was visible from
815
+ // inside the shadow tree. `getAnimations()` returned `[]`, dots sat
816
+ // frozen at base scale, the typing indicator looked dead.
817
+ //
818
+ // Shadow DOM with `static styles` is the right architecture here.
819
+ // CSS custom properties (--sc-tile-text-color, --sc-content-bubble-*,
820
+ // etc.) inherit through shadow boundaries by default, so the canvas
821
+ // theme cascade still reaches the chips without the light-DOM
822
+ // workaround. As a bonus the markdown reset rules can't leak out
823
+ // onto the host page, and the keyframes are owned by the trail's
824
+ // own tree scope where the chip + dot elements live.
825
+ _AdaptiveChatTrail.styles = css`
826
+ [data-syntro-chat-trail] > :first-child {
827
+ margin-top: auto;
828
+ }
829
+ [data-trail-chip] > p:first-child {
830
+ margin-top: 0;
831
+ }
832
+ [data-trail-chip] > p:last-child {
833
+ margin-bottom: 0;
834
+ }
835
+ [data-trail-chip] p {
836
+ margin: 4px 0;
837
+ }
838
+ [data-trail-chip] br + br {
839
+ display: none;
840
+ }
841
+ [data-trail-chip] ul,
842
+ [data-trail-chip] ol {
843
+ margin: 4px 0;
844
+ padding-left: 1.1em;
845
+ }
846
+ [data-trail-chip] li {
847
+ margin: 0;
848
+ }
849
+ [data-trail-chip] li + li {
850
+ margin-top: 1px;
851
+ }
852
+ [data-trail-chip] pre {
853
+ margin: 6px 0;
854
+ padding: 6px 8px;
855
+ background: var(--sc-content-code-background-block, rgba(0, 0, 0, 0.35));
856
+ border-radius: 6px;
857
+ overflow-x: auto;
858
+ font-size: 11px;
859
+ }
860
+ [data-trail-chip] code {
861
+ font-family: ui-monospace, SF Mono, Menlo, monospace;
862
+ font-size: 11px;
863
+ background: var(--sc-content-code-background, rgba(0, 0, 0, 0.25));
864
+ padding: 1px 4px;
865
+ border-radius: 3px;
866
+ }
867
+ [data-trail-chip] pre code {
868
+ background: transparent;
869
+ padding: 0;
870
+ }
871
+ [data-trail-chip] a {
872
+ color: var(--sc-content-link-color, var(--sc-color-primary, #b72e2a));
873
+ text-decoration: underline;
874
+ }
875
+ [data-trail-chip] strong {
876
+ font-weight: 600;
877
+ }
878
+ /* Streaming-caret blink for in-progress assistant chips. */
879
+ @keyframes syntro-trail-caret {
880
+ 0% {
881
+ opacity: 1;
882
+ }
883
+ 50% {
884
+ opacity: 0;
885
+ }
886
+ 100% {
887
+ opacity: 1;
888
+ }
889
+ }
890
+ /* Thinking-dot scale + opacity pulse. The naked-dots redesign
891
+ relies entirely on this motion to signal "alive" — there's no
892
+ chip frame around the dots, so a subtle animation reads as
893
+ broken. 0.4 → 1.0 scale with 0.25 → 1.0 opacity gives an
894
+ unmistakable breathing cue. */
895
+ @keyframes syntro-trail-thinking-pulse {
896
+ 0%,
897
+ 100% {
898
+ transform: scale(0.4);
899
+ opacity: 0.25;
900
+ }
901
+ 45% {
902
+ transform: scale(1);
903
+ opacity: 1;
904
+ }
905
+ }
906
+ /* Chip entrance — fades a freshly-mounted chip up from below
907
+ so the thinking → first-streaming-chip handoff feels like one
908
+ continuous motion instead of an element swap. */
909
+ @keyframes syntro-trail-chip-enter {
910
+ from {
911
+ opacity: 0;
912
+ transform: translateY(6px) scale(0.97);
913
+ }
914
+ to {
915
+ opacity: 1;
916
+ transform: translateY(0) scale(1);
917
+ }
918
+ }
919
+ `;
920
+ _AdaptiveChatTrail.properties = {
921
+ messages: { attribute: false },
922
+ visibleCount: { type: Number },
923
+ expanded: { type: Boolean },
924
+ greeting: { type: String },
925
+ /** When true: trail renders fully expanded AND the
926
+ * expand/collapse affordance is hidden. Used by hosts that mount
927
+ * the chat in a full-screen surface (the mobile panel) where
928
+ * collapse makes no sense — there's nothing to collapse INTO. */
929
+ forceExpanded: { type: Boolean },
930
+ /**
931
+ * Mirrors `chatSession.inFlight` — true between `send()` and the
932
+ * assistant's first `receiveStart`/`receiveDelta`, OR while a
933
+ * stream is in progress. Drives the "thinking" indicator that
934
+ * fills the gap between the user's just-sent message and the
935
+ * first assistant token. Wired through AdaptiveChatBar (it owns
936
+ * the chatSession subscription and forwards inFlight as a prop).
937
+ */
938
+ inFlight: { type: Boolean },
939
+ /**
940
+ * Live reasoning narration streamed from the model via AG-UI
941
+ * `THINKING_TEXT_MESSAGE_CONTENT`. When non-empty, the thinking
942
+ * chip renders this in place of the bouncing dots so the visitor
943
+ * sees what the model is actually doing instead of staring at
944
+ * silent loading state during a tool-call gap.
945
+ */
946
+ thinkingText: { attribute: false },
947
+ /**
948
+ * Pre-conversation suggestion chip. When the visitor opens the
949
+ * chat and `messages` is empty, the trail renders this as a
950
+ * button beneath the greeting. Clicking it dispatches a
951
+ * `trail-intro-suggestion` event with the configured prompt —
952
+ * the parent (chat-bar) forwards that to chatSession.send().
953
+ * Disappears the moment the first message lands.
954
+ */
955
+ introSuggestion: { attribute: false },
956
+ /**
957
+ * Tappable tile-receipt chips rendered once at the end of the
958
+ * trail. Produced from the store by Task 3 (AdaptiveChatBar).
959
+ * Each chip emits `tile-receipt-activate` with its instanceId.
960
+ */
961
+ tileReceipts: { attribute: false },
962
+ /**
963
+ * Widgets rendered INLINE in the trail (reco carousel etc.),
964
+ * anchored to a message position like receipts. Sourced from the
965
+ * store's `chat-inline` slot mounts via AdaptiveChatBar.
966
+ */
967
+ inlineWidgets: { attribute: false },
968
+ /**
969
+ * Registry-backed mounter for inline widgets whose id is not their
970
+ * custom-element tag. Injected from the chat-bar mountable.
971
+ */
972
+ mountInlineWidget: { attribute: false },
973
+ _messagesBelow: { state: true }
974
+ };
975
+ _AdaptiveChatTrail._AT_BOTTOM_EPSILON_PX = 50;
976
+ _AdaptiveChatTrail._USER_SCROLL_INTENT_WINDOW_MS = 1200;
977
+ var AdaptiveChatTrail = _AdaptiveChatTrail;
978
+ function scrollRecentPillStyles() {
979
+ return {
980
+ position: "absolute",
981
+ bottom: "8px",
982
+ left: "50%",
983
+ transform: "translateX(-50%)",
984
+ display: "flex",
985
+ alignItems: "center",
986
+ gap: "5px",
987
+ height: "26px",
988
+ padding: "0 10px",
989
+ borderRadius: "9999px",
990
+ border: "var(--sc-content-bubble-border, 1px solid rgba(255, 255, 255, 0.18))",
991
+ background: "var(--sc-content-bubble-bg, rgba(20, 22, 28, 0.85))",
992
+ color: "var(--sc-tile-text-color, rgba(255, 255, 255, 0.92))",
993
+ fontSize: "12px",
994
+ fontWeight: "500",
995
+ lineHeight: "1",
996
+ whiteSpace: "nowrap",
997
+ cursor: "pointer",
998
+ boxShadow: "0 2px 8px rgba(0, 0, 0, 0.25)",
999
+ zIndex: "3"
1000
+ };
1001
+ }
1002
+ function blurVeilStyles() {
1003
+ const mask = "linear-gradient(to bottom, black 0%, black 40%, transparent 100%)";
1004
+ return {
1005
+ position: "absolute",
1006
+ top: "0",
1007
+ left: "0",
1008
+ right: "0",
1009
+ height: `${TRAIL_BLUR_VEIL_PX}px`,
1010
+ pointerEvents: "none",
1011
+ backdropFilter: `blur(${TRAIL_BLUR_MAX_PX}px)`,
1012
+ WebkitBackdropFilter: `blur(${TRAIL_BLUR_MAX_PX}px)`,
1013
+ maskImage: mask,
1014
+ WebkitMaskImage: mask,
1015
+ zIndex: "2"
1016
+ };
1017
+ }
1018
+ function chipVisualState(m) {
1019
+ if (m.role === "system") {
1020
+ return { kind: "notice", role: m.role };
1021
+ }
1022
+ if (m.role === "assistant" && m.status === "error") {
1023
+ return { kind: "settled", role: m.role };
1024
+ }
1025
+ if (m.status === "error") {
1026
+ return { kind: "notice", role: m.role };
1027
+ }
1028
+ if (m.role === "assistant" && m.status === "streaming") {
1029
+ return { kind: "streaming" };
1030
+ }
1031
+ return { kind: "settled", role: m.role };
1032
+ }
1033
+ function chipStyles(state, pos) {
1034
+ const opacity = Math.max(OPACITY_FLOOR, 1 - pos * OPACITY_STEP);
1035
+ const yPx = pos * Y_DRIFT_PX;
1036
+ let border;
1037
+ let background;
1038
+ let color;
1039
+ let alignSelf;
1040
+ switch (state.kind) {
1041
+ case "settled":
1042
+ if (state.role === "user") {
1043
+ border = "var(--sc-content-bubble-border-user, 1px solid rgba(255, 255, 255, 0.28))";
1044
+ background = "var(--sc-content-bubble-background-user, rgba(255, 255, 255, 0.10))";
1045
+ alignSelf = "flex-end";
1046
+ } else {
1047
+ border = "var(--sc-content-bubble-border, 1px solid rgba(255, 255, 255, 0.18))";
1048
+ background = "var(--sc-content-bubble-background, rgba(20, 22, 24, 0.35))";
1049
+ alignSelf = "flex-start";
1050
+ }
1051
+ color = "var(--sc-tile-title-color, inherit)";
1052
+ break;
1053
+ case "streaming":
1054
+ case "pending":
1055
+ border = "var(--sc-content-bubble-border, 1px solid rgba(255, 255, 255, 0.18))";
1056
+ background = "var(--sc-content-bubble-background, rgba(20, 22, 24, 0.35))";
1057
+ color = "var(--sc-tile-title-color, inherit)";
1058
+ alignSelf = "flex-start";
1059
+ break;
1060
+ case "notice":
1061
+ border = "var(--sc-content-bubble-border-error, 1px solid rgba(255, 255, 255, 0.22))";
1062
+ background = "var(--sc-content-bubble-background-error, rgba(20, 22, 24, 0.28))";
1063
+ color = "var(--sc-content-bubble-text-error, rgba(255, 255, 255, 0.75))";
1064
+ alignSelf = state.role === "user" ? "flex-end" : "flex-start";
1065
+ break;
1066
+ }
1067
+ return {
1068
+ alignSelf,
1069
+ // Single shared width policy for EVERY bubble (user / assistant /
1070
+ // streaming / pending / error). Short and long replies now read as a
1071
+ // consistent column: `fit-content` sizes each bubble to its text, and
1072
+ // `TRAIL_MAX_WIDTH` caps the wide ones at the same ceiling — no more
1073
+ // per-role or per-path width drift (user feedback: chat tiles had
1074
+ // differing widths).
1075
+ maxWidth: TRAIL_MAX_WIDTH,
1076
+ width: "fit-content",
1077
+ // Bumped from 11px → 13px after the chat lid in production read as
1078
+ // cramped at the smaller size — body chat copy needs to be visitor-
1079
+ // legible on a phone without zoom, and 11px reads as a footnote
1080
+ // rather than the primary conversation surface. Code blocks inside
1081
+ // chip body scale proportionally (10px → 11px) below.
1082
+ fontSize: "13px",
1083
+ lineHeight: "1.45",
1084
+ padding: "5px 11px",
1085
+ borderRadius: "10px",
1086
+ border,
1087
+ background,
1088
+ // Frosted-glass bubbles are OPT-IN via the `--sc-content-bubble-blur`
1089
+ // knob (a host that wants glass sets e.g. `blur(8px)` deliberately); the
1090
+ // DEFAULT is `none`. A backdrop-filter on the bubble composites its OWN
1091
+ // text onto a GPU layer (grayscale AA + the blurred backdrop bleeding
1092
+ // under thin glyphs) — the resting-state compositing contract's
1093
+ // "backdrop-filter behind text" case: soft rasterization everywhere and
1094
+ // fully BLANKED text on some Android GPUs. Glass-by-default shipped that
1095
+ // hazard to every non-velvet surface, so the default flipped to `none`
1096
+ // (GPU/compositing hazard audit, Fix 3). Gate:
1097
+ // AdaptiveChatTrail.compositing.test.ts.
1098
+ backdropFilter: "var(--sc-content-bubble-blur, none)",
1099
+ WebkitBackdropFilter: "var(--sc-content-bubble-blur, none)",
1100
+ color,
1101
+ transition: "opacity 240ms cubic-bezier(0.22, 1, 0.36, 1), transform 240ms cubic-bezier(0.22, 1, 0.36, 1)",
1102
+ // Per-position opacity + y-drift falloff is still per-chip — those
1103
+ // operate naturally per element. Blur is NOT per-chip anymore: it's
1104
+ // applied via an absolute-positioned veil at the top of the scroll
1105
+ // viewport (TRAIL_BLUR_VEIL_PX above), so a single long message
1106
+ // doesn't get uniformly blurred across its full body.
1107
+ opacity: String(toFixedTrim(opacity)),
1108
+ transform: `translateY(${yPx}px)`,
1109
+ // User messages render as raw text (`html` template); pre-wrap preserves
1110
+ // newlines the visitor typed. Assistant messages render as markdown HTML
1111
+ // (`<p>` tags from marked); pre-wrap there would make marked's inter-tag
1112
+ // text nodes (the `\n` between/after `<p>` tags) visible, producing the
1113
+ // trailing/inter-paragraph blank lines visitors reported. Markdown's own
1114
+ // structure carries the paragraph breaks, so assistant chips use the
1115
+ // default `normal` whitespace handling.
1116
+ whiteSpace: (state.kind === "settled" || state.kind === "notice") && state.role === "user" ? "pre-wrap" : "normal",
1117
+ wordBreak: "break-word"
1118
+ };
1119
+ }
1120
+ function toolCallStripStyles() {
1121
+ return {
1122
+ display: "flex",
1123
+ flexWrap: "wrap",
1124
+ gap: "4px",
1125
+ marginTop: "4px",
1126
+ paddingTop: "4px",
1127
+ borderTop: "1px dashed var(--sc-content-divider-color, rgba(255, 255, 255, 0.12))"
1128
+ };
1129
+ }
1130
+ function pendingToolCallChipStyles() {
1131
+ return {
1132
+ display: "inline-flex",
1133
+ alignItems: "center",
1134
+ gap: "4px",
1135
+ padding: "2px 7px",
1136
+ border: "var(--sc-content-bubble-border-user, 1px solid rgba(255, 255, 255, 0.28))",
1137
+ background: "var(--sc-content-bubble-background-user, rgba(255, 255, 255, 0.10))",
1138
+ color: "var(--sc-tile-text-color, inherit)",
1139
+ borderRadius: "999px",
1140
+ fontSize: "10px",
1141
+ fontWeight: "500",
1142
+ fontFamily: "ui-monospace, SF Mono, Menlo, monospace",
1143
+ cursor: "pointer",
1144
+ opacity: "1"
1145
+ };
1146
+ }
1147
+ function deriveActivityReceipt(tc) {
1148
+ if (tc.name !== "primary_response_action") return null;
1149
+ const args = tc.args;
1150
+ if (!args || typeof args !== "object") return null;
1151
+ const templateId = typeof args.template_id === "string" ? args.template_id : "";
1152
+ const content = args.content ?? null;
1153
+ const title = content && typeof content.title === "string" && content.title.trim() ? content.title.trim() : "";
1154
+ const id = templateId.toLowerCase();
1155
+ let type;
1156
+ if (id.includes("faq")) type = "FAQ";
1157
+ else if (id.includes("chart") || id.includes("graph")) type = "chart";
1158
+ else if (id.includes("card") || id.includes("product")) type = "card";
1159
+ else if (id.includes("alert") || id.includes("navigation") || id.includes("nav")) type = "alert";
1160
+ else type = "added to your view";
1161
+ const label = title || templateId || "something";
1162
+ return { type, label };
1163
+ }
1164
+ function receiptText(r) {
1165
+ switch (r.type) {
1166
+ case "FAQ":
1167
+ return "Opened the FAQ for you";
1168
+ case "chart":
1169
+ return `Put together ${r.label} for you`;
1170
+ case "card":
1171
+ return "Pulled up the product for you";
1172
+ case "alert":
1173
+ return `Here's a shortcut: ${r.label}`;
1174
+ default:
1175
+ return `Pulled up ${r.label} for you`;
1176
+ }
1177
+ }
1178
+ function extractSuggestedReplies(toolCalls) {
1179
+ if (!toolCalls) return [];
1180
+ for (const tc of toolCalls) {
1181
+ if (tc.name !== "suggest_replies") continue;
1182
+ const replies = tc.args?.replies;
1183
+ if (!Array.isArray(replies)) return [];
1184
+ return replies.filter((r) => typeof r === "string" && r.trim().length > 0).map((r) => r.trim()).slice(0, 4);
1185
+ }
1186
+ return [];
1187
+ }
1188
+ function computeAssistantChipParts(m, isSuggestionHost) {
1189
+ const toolCalls = m.toolCalls ?? [];
1190
+ return {
1191
+ pendingToolCalls: toolCalls.filter((tc) => tc.status === "pending"),
1192
+ receipts: toolCalls.map(deriveActivityReceipt).filter((r) => r !== null),
1193
+ replyChips: isSuggestionHost ? extractSuggestedReplies(toolCalls) : []
1194
+ };
1195
+ }
1196
+ function looksLikeSerializedModelResponse(text) {
1197
+ const t = text.trimStart();
1198
+ if (t.startsWith("(Empty response")) return true;
1199
+ let hits = 0;
1200
+ if (/['"]stop_reason['"]\s*:/.test(t)) hits++;
1201
+ if (/['"]signature['"]\s*:/.test(t)) hits++;
1202
+ if (/['"]type['"]\s*:\s*['"]thinking['"]/.test(t)) hits++;
1203
+ if (/['"]content['"]\s*:\s*\[/.test(t)) hits++;
1204
+ return hits >= 2;
1205
+ }
1206
+ function assistantChipRendersContent(m, isSuggestionHost) {
1207
+ const text = (m.text ?? "").trim();
1208
+ if (text.length > 0 && !looksLikeSerializedModelResponse(text)) return true;
1209
+ const { pendingToolCalls, receipts, replyChips } = computeAssistantChipParts(m, isSuggestionHost);
1210
+ return pendingToolCalls.length > 0 || receipts.length > 0 || replyChips.length > 0;
1211
+ }
1212
+ function inlineWidgetBlockStyles() {
1213
+ return {
1214
+ alignSelf: "stretch",
1215
+ width: "100%",
1216
+ minWidth: "0",
1217
+ marginTop: "6px",
1218
+ contain: "layout"
1219
+ };
1220
+ }
1221
+ function receiptStripStyles() {
1222
+ return {
1223
+ display: "flex",
1224
+ flexDirection: "column",
1225
+ gap: "2px",
1226
+ marginTop: "4px"
1227
+ };
1228
+ }
1229
+ function receiptStyles() {
1230
+ return {
1231
+ display: "inline-flex",
1232
+ alignItems: "center",
1233
+ gap: "5px",
1234
+ alignSelf: "flex-start",
1235
+ fontSize: "11px",
1236
+ lineHeight: "1.3",
1237
+ fontWeight: "500",
1238
+ letterSpacing: "0.01em",
1239
+ // Inherit the chip's OWN resolved text color (muted via opacity), so the
1240
+ // receipt is legible on every theme by construction. The old
1241
+ // `--sc-content-muted-color` fallback was rgba(255,255,255,…) — white —
1242
+ // invisible on light canvases; a `--sc-tile-text-color` token can likewise
1243
+ // be unset/mismatched on dark canvases. `currentColor` always matches the
1244
+ // chip text the visitor is already reading.
1245
+ color: "currentColor",
1246
+ opacity: "0.7"
1247
+ };
1248
+ }
1249
+ function receiptIconStyles() {
1250
+ return {
1251
+ display: "inline-flex",
1252
+ alignItems: "center",
1253
+ justifyContent: "center",
1254
+ width: "14px",
1255
+ height: "14px",
1256
+ borderRadius: "50%",
1257
+ fontSize: "9px",
1258
+ fontWeight: "700",
1259
+ color: "#fff",
1260
+ background: "hsl(var(--sc-accent-color, 145 40% 45%) / 0.7)"
1261
+ };
1262
+ }
1263
+ function replyChipStripStyles() {
1264
+ return {
1265
+ display: "flex",
1266
+ flexWrap: "wrap",
1267
+ gap: "6px",
1268
+ marginTop: "6px",
1269
+ alignSelf: "flex-start"
1270
+ };
1271
+ }
1272
+ function tileReceiptStripStyles() {
1273
+ return {
1274
+ display: "flex",
1275
+ flexDirection: "column",
1276
+ gap: "6px",
1277
+ marginTop: "6px",
1278
+ width: "100%",
1279
+ alignSelf: "stretch"
1280
+ };
1281
+ }
1282
+ function replyChipStyles() {
1283
+ return {
1284
+ display: "inline-flex",
1285
+ alignItems: "center",
1286
+ padding: "5px 12px",
1287
+ border: "1px solid hsl(var(--sc-accent-color, 145 40% 45%) / 0.40)",
1288
+ borderRadius: "9999px",
1289
+ background: "hsl(var(--sc-accent-color, 145 40% 45%) / 0.12)",
1290
+ color: "var(--sc-tile-text-color, currentColor)",
1291
+ font: "inherit",
1292
+ fontSize: "12px",
1293
+ fontWeight: "500",
1294
+ lineHeight: "1.2",
1295
+ cursor: "pointer",
1296
+ backdropFilter: "var(--sc-content-bubble-blur, none)",
1297
+ WebkitBackdropFilter: "var(--sc-content-bubble-blur, none)",
1298
+ transition: "background 150ms ease, transform 120ms ease"
1299
+ };
1300
+ }
1301
+ function alertReceiptIconStyles() {
1302
+ return {
1303
+ flex: "0 0 auto",
1304
+ width: "26px",
1305
+ height: "26px",
1306
+ borderRadius: "8px",
1307
+ display: "grid",
1308
+ placeItems: "center",
1309
+ background: "hsl(var(--sc-accent-color))",
1310
+ color: "var(--sc-accent-foreground, #fff)",
1311
+ fontSize: "14px"
1312
+ };
1313
+ }
1314
+ function tileReceiptChipStyles() {
1315
+ return {
1316
+ display: "flex",
1317
+ alignItems: "center",
1318
+ gap: "10px",
1319
+ width: "100%",
1320
+ textAlign: "left",
1321
+ cursor: "pointer",
1322
+ padding: "9px 10px 9px 11px",
1323
+ borderRadius: "12px",
1324
+ border: "1px solid hsl(var(--sc-accent-color) / 0.28)",
1325
+ background: "hsl(var(--sc-accent-color) / 0.06)"
1326
+ };
1327
+ }
1328
+ function tileReceiptGlyphStyles() {
1329
+ return { position: "relative", flex: "0 0 auto", width: "28px", height: "28px" };
1330
+ }
1331
+ function tileReceiptGlyphLayerStyles(i) {
1332
+ return {
1333
+ position: "absolute",
1334
+ inset: "0",
1335
+ borderRadius: "7px",
1336
+ background: "hsl(var(--sc-accent-color) / 0.22)",
1337
+ boxShadow: "inset 0 0 0 1px hsl(var(--sc-accent-color) / 0.4)",
1338
+ ...i === 0 ? { transform: "translate(3px,-3px) rotate(6deg)", opacity: "0.5" } : {}
1339
+ };
1340
+ }
1341
+ function receiptChipTitleStyles(_kind) {
1342
+ return {
1343
+ // Grows to fill the space between the icon and the action pill, truncating
1344
+ // long product names on one line (the chip is now single-line).
1345
+ flex: "1",
1346
+ minWidth: "0",
1347
+ fontSize: "13px",
1348
+ fontWeight: "650",
1349
+ lineHeight: "1.2",
1350
+ color: "var(--sc-content-text-color, #0f172a)",
1351
+ whiteSpace: "nowrap",
1352
+ overflow: "hidden",
1353
+ textOverflow: "ellipsis"
1354
+ };
1355
+ }
1356
+ function tileReceiptActionStyles(dismissed) {
1357
+ return {
1358
+ flex: "0 0 auto",
1359
+ fontSize: "11px",
1360
+ fontWeight: "700",
1361
+ borderRadius: "999px",
1362
+ padding: "4px 10px",
1363
+ ...dismissed ? { background: "hsl(var(--sc-accent-color) / 0.14)", color: "hsl(var(--sc-accent-color))" } : {
1364
+ background: "hsl(var(--sc-accent-color) / 0.9)",
1365
+ color: "var(--sc-accent-foreground, #fff)"
1366
+ }
1367
+ };
1368
+ }
1369
+ function caretStyles() {
1370
+ return {
1371
+ display: "inline-block",
1372
+ width: "6px",
1373
+ height: "12px",
1374
+ marginLeft: "3px",
1375
+ verticalAlign: "-1px",
1376
+ background: "hsl(var(--sc-accent-color) / 0.85)",
1377
+ borderRadius: "1px",
1378
+ animation: "syntro-trail-caret 1s steps(2, end) infinite"
1379
+ };
1380
+ }
1381
+ function thinkingSlotStyles() {
1382
+ return {
1383
+ alignSelf: "flex-start",
1384
+ color: "var(--sc-tile-title-color, inherit)",
1385
+ padding: "4px 4px",
1386
+ animation: "syntro-trail-chip-enter 220ms cubic-bezier(0.22, 1, 0.36, 1) both"
1387
+ };
1388
+ }
1389
+ function thinkingDotsStyles() {
1390
+ return {
1391
+ display: "inline-flex",
1392
+ alignItems: "center",
1393
+ gap: "6px",
1394
+ // Line height matches what a single-line text chip would compute
1395
+ // to, so swapping out the slot for the streaming chip doesn't
1396
+ // shift the next chip up or down by a couple of pixels.
1397
+ lineHeight: "14px"
1398
+ };
1399
+ }
1400
+ function thinkingDotStyles(index) {
1401
+ return {
1402
+ display: "inline-block",
1403
+ width: "7px",
1404
+ height: "7px",
1405
+ borderRadius: "50%",
1406
+ background: "currentColor",
1407
+ // The dots are now the entire visual — no bubble framing them —
1408
+ // so the pulse has to do all the work the chip used to do. Scale
1409
+ // 0.4 → 1.0 with opacity 0.25 → 1.0 gives an unmistakable
1410
+ // breathing motion: at the trough each dot reads like a faint
1411
+ // ghost, at the peak like a solid dot. 940ms cycle is a touch
1412
+ // faster than the original 1.05s; without the chip frame the
1413
+ // shorter cycle feels purposeful instead of agitated. Stagger
1414
+ // 130ms = clear rolling cadence across all three.
1415
+ transformOrigin: "center",
1416
+ animation: `syntro-trail-thinking-pulse 940ms cubic-bezier(0.4, 0, 0.6, 1) ${index * 0.13}s infinite both`
1417
+ };
1418
+ }
1419
+ function moreStyles() {
1420
+ return {
1421
+ // Center horizontally regardless of the parent's display model.
1422
+ // ``alignSelf: center`` only works when the parent is a flex/grid
1423
+ // container with cross-axis alignment; the chat-bar's flex column
1424
+ // stretches us full-width instead. ``display: block`` + auto inline
1425
+ // margins centers the auto-sized button without needing the parent
1426
+ // to opt in.
1427
+ display: "block",
1428
+ marginLeft: "auto",
1429
+ marginRight: "auto",
1430
+ marginBottom: "6px",
1431
+ fontSize: "10px",
1432
+ fontWeight: "500",
1433
+ letterSpacing: "0.06em",
1434
+ padding: "4px 10px",
1435
+ border: "1px solid hsl(var(--sc-accent-color) / 0.32)",
1436
+ borderRadius: "999px",
1437
+ background: "hsl(var(--sc-accent-color) / 0.10)",
1438
+ color: "var(--sc-tile-text-color, currentColor)",
1439
+ cursor: "pointer",
1440
+ opacity: "0.85",
1441
+ backdropFilter: "var(--sc-content-bubble-blur, none)",
1442
+ WebkitBackdropFilter: "var(--sc-content-bubble-blur, none)",
1443
+ transition: "opacity 150ms ease, background 150ms ease"
1444
+ };
1445
+ }
1446
+ function stripTrailingWhitespace(text) {
1447
+ return text.replace(/[ \t]+$/gm, "").replace(/\n{3,}/g, "\n\n").trim();
1448
+ }
1449
+ function toFixedTrim(n) {
1450
+ const s = n.toFixed(2);
1451
+ return s.replace(/\.?0+$/, "") || "0";
1452
+ }
1453
+ var THINKING_TAIL_MAX = 120;
1454
+ function truncateThinkingTail(text) {
1455
+ const flat = text.replace(/\s+/g, " ").trim();
1456
+ if (flat.length <= THINKING_TAIL_MAX) return flat;
1457
+ return `\u2026${flat.slice(-THINKING_TAIL_MAX)}`;
1458
+ }
1459
+ function thinkingTextStyles() {
1460
+ return {
1461
+ fontStyle: "italic",
1462
+ opacity: "0.75",
1463
+ fontSize: "13px",
1464
+ lineHeight: "18px",
1465
+ maxWidth: "32ch",
1466
+ overflow: "hidden",
1467
+ textOverflow: "ellipsis",
1468
+ whiteSpace: "nowrap"
1469
+ };
1470
+ }
1471
+ function introSuggestionStyles() {
1472
+ return {
1473
+ display: "inline-flex",
1474
+ alignItems: "center",
1475
+ gap: "6px",
1476
+ marginTop: "4px",
1477
+ padding: "10px 18px",
1478
+ borderRadius: "9999px",
1479
+ border: "0",
1480
+ background: "linear-gradient(135deg, var(--sc-color-primary, #5faf7d) 0%, var(--sc-color-primary-hover, #3d8a5e) 100%)",
1481
+ color: "#fff",
1482
+ cursor: "pointer",
1483
+ font: "inherit",
1484
+ fontSize: "14px",
1485
+ fontWeight: "600",
1486
+ transition: "transform 120ms ease, filter 160ms ease"
1487
+ };
1488
+ }
1489
+ function emptyIconStyles() {
1490
+ return {
1491
+ display: "inline-flex",
1492
+ alignItems: "center",
1493
+ justifyContent: "center",
1494
+ width: "48px",
1495
+ height: "48px",
1496
+ borderRadius: "9999px",
1497
+ background: "var(--sc-tile-icon-background, linear-gradient(135deg, var(--sc-color-primary, #5faf7d) 0%, var(--sc-color-primary-hover, #3d8a5e) 100%))",
1498
+ color: "#fff",
1499
+ fontSize: "22px"
1500
+ };
1501
+ }
1502
+ function emptyTitleStyles() {
1503
+ return {
1504
+ margin: "0",
1505
+ fontSize: "15px",
1506
+ fontWeight: "600",
1507
+ lineHeight: "1.35",
1508
+ color: "var(--sc-content-text-color, inherit)",
1509
+ maxWidth: "32ch"
1510
+ };
1511
+ }
1512
+ if (!customElements.get("adaptive-chat-trail")) {
1513
+ customElements.define("adaptive-chat-trail", AdaptiveChatTrail);
1514
+ }
1515
+
1516
+ export {
1517
+ AdaptiveChatTrail,
1518
+ looksLikeSerializedModelResponse
1519
+ };
1520
+ //# sourceMappingURL=chunk-HVGQJZMP.js.map