@syntrologie/adapt-chatbot 2.8.0-canary.62 → 2.8.0-canary.621

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 (250) hide show
  1. package/dist/AdaptiveChatBar.composer.test.d.ts +2 -0
  2. package/dist/AdaptiveChatBar.composer.test.d.ts.map +1 -0
  3. package/dist/AdaptiveChatBar.d.ts +248 -0
  4. package/dist/AdaptiveChatBar.d.ts.map +1 -0
  5. package/dist/AdaptiveChatBar.interleave.test.d.ts +9 -0
  6. package/dist/AdaptiveChatBar.interleave.test.d.ts.map +1 -0
  7. package/dist/AdaptiveChatBar.js +12 -0
  8. package/dist/AdaptiveChatBar.js.map +7 -0
  9. package/dist/AdaptiveChatBarMountable.d.ts +93 -0
  10. package/dist/AdaptiveChatBarMountable.d.ts.map +1 -0
  11. package/dist/AdaptiveChatTrail.anchor-scroll.test.d.ts +2 -0
  12. package/dist/AdaptiveChatTrail.anchor-scroll.test.d.ts.map +1 -0
  13. package/dist/AdaptiveChatTrail.d.ts +539 -0
  14. package/dist/AdaptiveChatTrail.d.ts.map +1 -0
  15. package/dist/AdaptiveChatTrail.follow-bottom.test.d.ts +2 -0
  16. package/dist/AdaptiveChatTrail.follow-bottom.test.d.ts.map +1 -0
  17. package/dist/AdaptiveChatTrail.js +12 -0
  18. package/dist/AdaptiveChatTrail.js.map +7 -0
  19. package/dist/AdaptiveChatTrail.live-region.test.d.ts +2 -0
  20. package/dist/AdaptiveChatTrail.live-region.test.d.ts.map +1 -0
  21. package/dist/AdaptiveChatTrail.test.d.ts +2 -0
  22. package/dist/AdaptiveChatTrail.test.d.ts.map +1 -0
  23. package/dist/AdaptiveChipsStrip.d.ts +1156 -0
  24. package/dist/AdaptiveChipsStrip.d.ts.map +1 -0
  25. package/dist/AdaptiveChipsStrip.js +11 -0
  26. package/dist/AdaptiveChipsStrip.js.map +7 -0
  27. package/dist/AdaptiveChipsStripMountable.d.ts +24 -0
  28. package/dist/AdaptiveChipsStripMountable.d.ts.map +1 -0
  29. package/dist/ChatAssistantLit.d.ts +64 -0
  30. package/dist/ChatAssistantLit.d.ts.map +1 -0
  31. package/dist/ChatAssistantLit.js +14 -0
  32. package/dist/ChatAssistantLit.js.map +7 -0
  33. package/dist/ChatSession.d.ts +275 -0
  34. package/dist/ChatSession.d.ts.map +1 -0
  35. package/dist/ChatTransport.d.ts +448 -0
  36. package/dist/ChatTransport.d.ts.map +1 -0
  37. package/dist/InlineRegistryWidgetMount.test.d.ts +2 -0
  38. package/dist/InlineRegistryWidgetMount.test.d.ts.map +1 -0
  39. package/dist/InlineWidgetLiveMount.test.d.ts +2 -0
  40. package/dist/InlineWidgetLiveMount.test.d.ts.map +1 -0
  41. package/dist/LeaveAMessage.leadconnector.integration.test.d.ts +2 -0
  42. package/dist/LeaveAMessage.leadconnector.integration.test.d.ts.map +1 -0
  43. package/dist/LeaveAMessageLit.d.ts +329 -0
  44. package/dist/LeaveAMessageLit.d.ts.map +1 -0
  45. package/dist/LeaveAMessageLit.test.d.ts +2 -0
  46. package/dist/LeaveAMessageLit.test.d.ts.map +1 -0
  47. package/dist/LeaveAMessageMountable.test.d.ts +2 -0
  48. package/dist/LeaveAMessageMountable.test.d.ts.map +1 -0
  49. package/dist/NavLinkMountable.d.ts +25 -0
  50. package/dist/NavLinkMountable.d.ts.map +1 -0
  51. package/dist/NavLinkMountable.test.d.ts +2 -0
  52. package/dist/NavLinkMountable.test.d.ts.map +1 -0
  53. package/dist/TextAnswerMountable.d.ts +17 -0
  54. package/dist/TextAnswerMountable.d.ts.map +1 -0
  55. package/dist/TextAnswerMountable.test.d.ts +2 -0
  56. package/dist/TextAnswerMountable.test.d.ts.map +1 -0
  57. package/dist/Turnstile.d.ts +101 -0
  58. package/dist/Turnstile.d.ts.map +1 -0
  59. package/dist/cdn.d.ts +16 -8
  60. package/dist/cdn.d.ts.map +1 -1
  61. package/dist/chunk-4V5L7IRV.js +4711 -0
  62. package/dist/chunk-4V5L7IRV.js.map +7 -0
  63. package/dist/chunk-EWPPVPJ4.js +14 -0
  64. package/dist/chunk-EWPPVPJ4.js.map +7 -0
  65. package/dist/chunk-HD2NXGYO.js +8445 -0
  66. package/dist/chunk-HD2NXGYO.js.map +7 -0
  67. package/dist/chunk-HV52LYST.js +641 -0
  68. package/dist/chunk-HV52LYST.js.map +7 -0
  69. package/dist/chunk-LY5AGXVD.js +21 -0
  70. package/dist/chunk-LY5AGXVD.js.map +7 -0
  71. package/dist/chunk-T6VEDUXD.js +694 -0
  72. package/dist/chunk-T6VEDUXD.js.map +7 -0
  73. package/dist/chunk-UFTSEIQP.js +1877 -0
  74. package/dist/chunk-UFTSEIQP.js.map +7 -0
  75. package/dist/chunk-VIRIVP7H.js +898 -0
  76. package/dist/chunk-VIRIVP7H.js.map +7 -0
  77. package/dist/chunk-YFZCMLV6.js +4078 -0
  78. package/dist/chunk-YFZCMLV6.js.map +7 -0
  79. package/dist/editor-lit.d.ts +37 -0
  80. package/dist/editor-lit.d.ts.map +1 -0
  81. package/dist/editor-lit.js +134 -0
  82. package/dist/editor-lit.js.map +7 -0
  83. package/dist/editorToken.d.ts +23 -0
  84. package/dist/editorToken.d.ts.map +1 -0
  85. package/dist/element-session.d.ts +3 -0
  86. package/dist/element-session.d.ts.map +1 -0
  87. package/dist/element-session.js +9 -0
  88. package/dist/element-session.js.map +7 -0
  89. package/dist/elements/ActionHandler.d.ts +34 -0
  90. package/dist/elements/ActionHandler.d.ts.map +1 -0
  91. package/dist/elements/ElementInstanceStore.d.ts +232 -0
  92. package/dist/elements/ElementInstanceStore.d.ts.map +1 -0
  93. package/dist/elements/ElementInstanceStore.test.d.ts +2 -0
  94. package/dist/elements/ElementInstanceStore.test.d.ts.map +1 -0
  95. package/dist/elements/ElementSessionController.d.ts +37 -0
  96. package/dist/elements/ElementSessionController.d.ts.map +1 -0
  97. package/dist/elements/ElementSessionController.test.d.ts +2 -0
  98. package/dist/elements/ElementSessionController.test.d.ts.map +1 -0
  99. package/dist/elements/ElementTypeHandler.d.ts +77 -0
  100. package/dist/elements/ElementTypeHandler.d.ts.map +1 -0
  101. package/dist/elements/ItemHandler.d.ts +60 -0
  102. package/dist/elements/ItemHandler.d.ts.map +1 -0
  103. package/dist/elements/ItemHandler.test.d.ts +2 -0
  104. package/dist/elements/ItemHandler.test.d.ts.map +1 -0
  105. package/dist/elements/TileHandler.d.ts +52 -0
  106. package/dist/elements/TileHandler.d.ts.map +1 -0
  107. package/dist/elements/TileHandler.test.d.ts +2 -0
  108. package/dist/elements/TileHandler.test.d.ts.map +1 -0
  109. package/dist/elements/__tests__/ElementInstanceStore.recall.test.d.ts +2 -0
  110. package/dist/elements/__tests__/ElementInstanceStore.recall.test.d.ts.map +1 -0
  111. package/dist/elements/blockRenderer.d.ts +46 -0
  112. package/dist/elements/blockRenderer.d.ts.map +1 -0
  113. package/dist/elements/blockRenderer.test.d.ts +13 -0
  114. package/dist/elements/blockRenderer.test.d.ts.map +1 -0
  115. package/dist/elements/blocks.d.ts +58 -0
  116. package/dist/elements/blocks.d.ts.map +1 -0
  117. package/dist/elements/createElementStore.d.ts +11 -0
  118. package/dist/elements/createElementStore.d.ts.map +1 -0
  119. package/dist/elements/envelope.d.ts +24 -0
  120. package/dist/elements/envelope.d.ts.map +1 -0
  121. package/dist/elements/fetcher.d.ts +43 -0
  122. package/dist/elements/fetcher.d.ts.map +1 -0
  123. package/dist/elements/index.d.ts +33 -0
  124. package/dist/elements/index.d.ts.map +1 -0
  125. package/dist/elements/store-singleton.d.ts +4 -0
  126. package/dist/elements/store-singleton.d.ts.map +1 -0
  127. package/dist/elements/tileLifecycleProps.d.ts +13 -0
  128. package/dist/elements/tileLifecycleProps.d.ts.map +1 -0
  129. package/dist/elements/types.d.ts +117 -0
  130. package/dist/elements/types.d.ts.map +1 -0
  131. package/dist/findSmartCanvas.d.ts +19 -0
  132. package/dist/findSmartCanvas.d.ts.map +1 -0
  133. package/dist/hostAgentController.d.ts +65 -0
  134. package/dist/hostAgentController.d.ts.map +1 -0
  135. package/dist/hostAgentController.test.d.ts +2 -0
  136. package/dist/hostAgentController.test.d.ts.map +1 -0
  137. package/dist/observer/__tests__/allowlist.test.d.ts +9 -0
  138. package/dist/observer/__tests__/allowlist.test.d.ts.map +1 -0
  139. package/dist/observer/__tests__/bus-translate.test.d.ts +16 -0
  140. package/dist/observer/__tests__/bus-translate.test.d.ts.map +1 -0
  141. package/dist/observer/__tests__/detector-observation-contract.test.d.ts +25 -0
  142. package/dist/observer/__tests__/detector-observation-contract.test.d.ts.map +1 -0
  143. package/dist/observer/__tests__/lifecycle.test.d.ts +2 -0
  144. package/dist/observer/__tests__/lifecycle.test.d.ts.map +1 -0
  145. package/dist/observer/__tests__/observer-enrich.test.d.ts +10 -0
  146. package/dist/observer/__tests__/observer-enrich.test.d.ts.map +1 -0
  147. package/dist/observer/__tests__/observer-isolation.test.d.ts +13 -0
  148. package/dist/observer/__tests__/observer-isolation.test.d.ts.map +1 -0
  149. package/dist/observer/__tests__/queue.test.d.ts +2 -0
  150. package/dist/observer/__tests__/queue.test.d.ts.map +1 -0
  151. package/dist/observer/__tests__/transport.test.d.ts +2 -0
  152. package/dist/observer/__tests__/transport.test.d.ts.map +1 -0
  153. package/dist/observer/allowlist.d.ts +32 -0
  154. package/dist/observer/allowlist.d.ts.map +1 -0
  155. package/dist/observer/bus-translate.d.ts +33 -0
  156. package/dist/observer/bus-translate.d.ts.map +1 -0
  157. package/dist/observer/index.d.ts +44 -0
  158. package/dist/observer/index.d.ts.map +1 -0
  159. package/dist/observer/queue.d.ts +63 -0
  160. package/dist/observer/queue.d.ts.map +1 -0
  161. package/dist/observer/transport.d.ts +27 -0
  162. package/dist/observer/transport.d.ts.map +1 -0
  163. package/dist/observer/wire.d.ts +54 -0
  164. package/dist/observer/wire.d.ts.map +1 -0
  165. package/dist/observer/wire.test.d.ts +2 -0
  166. package/dist/observer/wire.test.d.ts.map +1 -0
  167. package/dist/personas.d.ts +30 -0
  168. package/dist/personas.d.ts.map +1 -0
  169. package/dist/runtime-lit.d.ts +8 -0
  170. package/dist/runtime-lit.d.ts.map +1 -0
  171. package/dist/runtime.d.ts +63 -5
  172. package/dist/runtime.d.ts.map +1 -1
  173. package/dist/runtime.js +4439 -28
  174. package/dist/runtime.js.map +7 -0
  175. package/dist/schema.d.ts +3688 -11
  176. package/dist/schema.d.ts.map +1 -1
  177. package/dist/schema.js +578 -30
  178. package/dist/schema.js.map +7 -0
  179. package/dist/support-chat/__tests__/lc-fetch-stub.d.ts +52 -0
  180. package/dist/support-chat/__tests__/lc-fetch-stub.d.ts.map +1 -0
  181. package/dist/support-chat/boot.d.ts +47 -0
  182. package/dist/support-chat/boot.d.ts.map +1 -0
  183. package/dist/support-chat/boot.test.d.ts +2 -0
  184. package/dist/support-chat/boot.test.d.ts.map +1 -0
  185. package/dist/support-chat/crisp.d.ts +4 -0
  186. package/dist/support-chat/crisp.d.ts.map +1 -0
  187. package/dist/support-chat/crisp.test.d.ts +2 -0
  188. package/dist/support-chat/crisp.test.d.ts.map +1 -0
  189. package/dist/support-chat/inbox.d.ts +57 -0
  190. package/dist/support-chat/inbox.d.ts.map +1 -0
  191. package/dist/support-chat/inbox.test.d.ts +2 -0
  192. package/dist/support-chat/inbox.test.d.ts.map +1 -0
  193. package/dist/support-chat/leadconnector.d.ts +13 -0
  194. package/dist/support-chat/leadconnector.d.ts.map +1 -0
  195. package/dist/support-chat/leadconnector.test.d.ts +2 -0
  196. package/dist/support-chat/leadconnector.test.d.ts.map +1 -0
  197. package/dist/support-chat/registry.d.ts +5 -0
  198. package/dist/support-chat/registry.d.ts.map +1 -0
  199. package/dist/support-chat/registry.test.d.ts +2 -0
  200. package/dist/support-chat/registry.test.d.ts.map +1 -0
  201. package/dist/support-chat/schema-widgetid.test.d.ts +2 -0
  202. package/dist/support-chat/schema-widgetid.test.d.ts.map +1 -0
  203. package/dist/support-chat/types.d.ts +19 -0
  204. package/dist/support-chat/types.d.ts.map +1 -0
  205. package/dist/test/setup.d.ts +1 -0
  206. package/dist/test/setup.d.ts.map +1 -0
  207. package/dist/types.d.ts +89 -40
  208. package/dist/types.d.ts.map +1 -1
  209. package/package.json +49 -24
  210. package/dist/ChatAssistant.d.ts +0 -18
  211. package/dist/ChatAssistant.d.ts.map +0 -1
  212. package/dist/ChatAssistant.js +0 -178
  213. package/dist/SyntroChatbot.d.ts +0 -14
  214. package/dist/SyntroChatbot.d.ts.map +0 -1
  215. package/dist/SyntroChatbot.js +0 -56
  216. package/dist/actionParser.d.ts +0 -15
  217. package/dist/actionParser.d.ts.map +0 -1
  218. package/dist/actionParser.js +0 -52
  219. package/dist/apiClient.d.ts +0 -23
  220. package/dist/apiClient.d.ts.map +0 -1
  221. package/dist/apiClient.js +0 -51
  222. package/dist/cdn.js +0 -37
  223. package/dist/editor.d.ts +0 -17
  224. package/dist/editor.d.ts.map +0 -1
  225. package/dist/editor.js +0 -36
  226. package/dist/tools/bridge.d.ts +0 -26
  227. package/dist/tools/bridge.d.ts.map +0 -1
  228. package/dist/tools/bridge.js +0 -25
  229. package/dist/tools/expanders.d.ts +0 -17
  230. package/dist/tools/expanders.d.ts.map +0 -1
  231. package/dist/tools/expanders.js +0 -41
  232. package/dist/tools/index.d.ts +0 -14
  233. package/dist/tools/index.d.ts.map +0 -1
  234. package/dist/tools/index.js +0 -11
  235. package/dist/tools/registrations/GetContextTool.d.ts +0 -7
  236. package/dist/tools/registrations/GetContextTool.d.ts.map +0 -1
  237. package/dist/tools/registrations/GetContextTool.js +0 -18
  238. package/dist/tools/registrations/ScrollToTool.d.ts +0 -7
  239. package/dist/tools/registrations/ScrollToTool.d.ts.map +0 -1
  240. package/dist/tools/registrations/ScrollToTool.js +0 -38
  241. package/dist/tools/registrations/ShowOverlayTool.d.ts +0 -7
  242. package/dist/tools/registrations/ShowOverlayTool.d.ts.map +0 -1
  243. package/dist/tools/registrations/ShowOverlayTool.js +0 -47
  244. package/dist/tools/registrations/index.d.ts +0 -19
  245. package/dist/tools/registrations/index.d.ts.map +0 -1
  246. package/dist/tools/registrations/index.js +0 -7
  247. package/dist/types.js +0 -6
  248. package/dist/useChat.d.ts +0 -25
  249. package/dist/useChat.d.ts.map +0 -1
  250. package/dist/useChat.js +0 -106
@@ -0,0 +1,1877 @@
1
+ import {
2
+ renderMarkdown
3
+ } from "./chunk-YFZCMLV6.js";
4
+ import {
5
+ IMAGE_STANDIN_BACKGROUND,
6
+ standinInitial
7
+ } from "./chunk-4V5L7IRV.js";
8
+
9
+ // src/AdaptiveChatTrail.ts
10
+ import { css, html, LitElement, nothing } from "lit";
11
+ import { repeat } from "lit/directives/repeat.js";
12
+ import { styleMap } from "lit/directives/style-map.js";
13
+ import { unsafeHTML } from "lit/directives/unsafe-html.js";
14
+ var DEFAULT_VISIBLE = 3;
15
+ var TRAIL_MAX_WIDTH = "88%";
16
+ var OPACITY_STEP = 0.22;
17
+ var OPACITY_FLOOR = 0.18;
18
+ var Y_DRIFT_PX = -2;
19
+ var TRAIL_BLUR_VEIL_PX = 72;
20
+ var TRAIL_BLUR_MAX_PX = 5;
21
+ var _AdaptiveChatTrail = class _AdaptiveChatTrail extends LitElement {
22
+ constructor() {
23
+ super(...arguments);
24
+ this.messages = [];
25
+ this.tileReceipts = [];
26
+ this.inlineWidgets = [];
27
+ this.mountInlineWidget = void 0;
28
+ this.visibleCount = DEFAULT_VISIBLE;
29
+ this.expanded = false;
30
+ this.forceExpanded = false;
31
+ this.hideTopBorder = false;
32
+ this.followBottom = false;
33
+ this.inFlight = false;
34
+ this.thinkingText = "";
35
+ /** Persona identity (FEAT-1784917430) — name + small avatar for assistant
36
+ * turn-start chips and the thinking indicator. Empty = legacy rendering. */
37
+ this.personaName = "";
38
+ this.personaAvatarSm = "";
39
+ this.introSuggestion = void 0;
40
+ /**
41
+ * Pre-conversation phantom assistant message. Rendered only when
42
+ * `messages` is empty. NOT injected into chatSession state — the
43
+ * greeting is configuration, not conversation, so it disappears
44
+ * automatically when the first real message arrives.
45
+ */
46
+ this.greeting = void 0;
47
+ // ── Anchor-scroll model (FEAT-1783535427, shared contract with
48
+ // <syntro-chat>) — applies to the SCROLLABLE modes (expanded /
49
+ // forceExpanded). The ambient peek strip keeps its bottom pin: it is
50
+ // overflow-hidden (not user-scrollable) and the pin is what keeps the
51
+ // latest line visible under the blur veil.
52
+ /** Follow the bottom on content growth. On by default (history
53
+ * browsing); off from a new user message until the visitor opts back in
54
+ * (pill tap or a user-gesture scroll that reaches the bottom). */
55
+ this._stickToBottom = true;
56
+ /** The user message the current turn is anchored to. */
57
+ this._anchorMsgId = null;
58
+ /** Whether a turn anchor is live (drives expand-mid-turn landing). */
59
+ this._anchorActive = false;
60
+ /** Keep enforcing chip-at-top on updates (late geometry, mode flips)
61
+ * until the user scrolls or opts into following. */
62
+ this._anchorHold = false;
63
+ /** Number of messages whose BOTTOM edge is still below the viewport —
64
+ * messages the visitor hasn't scrolled past yet. Drives the
65
+ * "⌄ N messages" pill and stays accurate as they scroll (one long
66
+ * streaming reply counts as 1 until its end has been read). Reactive. */
67
+ this._messagesBelow = 0;
68
+ /** Timestamp of the last user scroll gesture (wheel/touch): scroll
69
+ * events outside this window are programmatic (our own pin/anchor) and
70
+ * must not re-engage following — anchoring the newest message clamps AT
71
+ * the bottom in any longer conversation. */
72
+ this._lastUserScrollIntentTs = 0;
73
+ this._markUserScrollIntent = () => {
74
+ this._lastUserScrollIntentTs = Date.now();
75
+ };
76
+ this._onTrailScroll = () => {
77
+ const container = this._trailContainer();
78
+ if (!container) return;
79
+ const atBottom = this._isAtBottom(container);
80
+ this._messagesBelow = this._scrollable && !atBottom ? this._countMessagesBelow(container) : 0;
81
+ const isUserDriven = Date.now() - this._lastUserScrollIntentTs < _AdaptiveChatTrail._USER_SCROLL_INTENT_WINDOW_MS;
82
+ if (isUserDriven) {
83
+ this._anchorHold = false;
84
+ this._stickToBottom = atBottom;
85
+ if (atBottom) this._anchorActive = false;
86
+ }
87
+ };
88
+ /** Pill tap: jump to the newest content and follow for the rest of the turn. */
89
+ this._onScrollToRecent = () => {
90
+ this._stickToBottom = true;
91
+ this._anchorHold = false;
92
+ this._anchorActive = false;
93
+ requestAnimationFrame(() => {
94
+ const container = this._trailContainer();
95
+ if (!container) return;
96
+ container.scrollTop = container.scrollHeight;
97
+ this._syncContentBelow();
98
+ });
99
+ };
100
+ // No `createRenderRoot()` override → default open shadow root. CSS
101
+ // custom properties (--sc-tile-text-color, --sc-content-bubble-*,
102
+ // etc.) inherit through the shadow boundary, so canvas-level theming
103
+ // continues to cascade into the chips without the light-DOM hack.
104
+ // See the `static styles` block above for the in-shadow stylesheet.
105
+ this._onExpand = () => {
106
+ this.expanded = true;
107
+ this.dispatchEvent(new CustomEvent("trail-expand", { bubbles: true, composed: true }));
108
+ };
109
+ /**
110
+ * Approve a pending client tool call. The trail emits a generic
111
+ * event so the host can decide whether to forward to chatSession
112
+ * (the common path) or override. Keeps the trail itself free of
113
+ * direct chatSession coupling — it's a pure view component.
114
+ */
115
+ this._onToolCallClick = (tc) => {
116
+ if (tc.status !== "pending") return;
117
+ this.dispatchEvent(
118
+ new CustomEvent("trail-toolcall-approved", {
119
+ detail: { toolCallId: tc.id, approved: true },
120
+ bubbles: true,
121
+ composed: true
122
+ })
123
+ );
124
+ };
125
+ this._onCollapse = () => {
126
+ this.expanded = false;
127
+ this.dispatchEvent(new CustomEvent("trail-collapse", { bubbles: true, composed: true }));
128
+ };
129
+ /**
130
+ * instanceId → id of the message that was newest when the receipt first
131
+ * appeared. This is what makes a receipt read like a chat bubble: it renders
132
+ * inline right after its anchor message, so newer messages push it up into
133
+ * history instead of it staying pinned below everything. Assigned once per
134
+ * instanceId (in willUpdate, the only place that sees both `messages` and
135
+ * `tileReceipts` at the moment a tile mounts) and never re-homed — a
136
+ * dismiss/bring-back keeps the chip where it was created. `null` = the tile
137
+ * mounted before the first chat message (canvas replay); those fall to the
138
+ * tail strip (see render).
139
+ */
140
+ this._receiptAnchors = /* @__PURE__ */ new Map();
141
+ /**
142
+ * instanceId → anchor message id for inline widgets — same mechanism as
143
+ * `_receiptAnchors`. The inline carousel renders right after the message
144
+ * that was newest when it mounted, so it reads like a rich reply and
145
+ * drifts up into history as later messages append.
146
+ */
147
+ this._inlineAnchors = /* @__PURE__ */ new Map();
148
+ /**
149
+ * instanceId → the cached inline widget entry (render node + props target +
150
+ * optional registry cleanup). Cached so a prop patch (same instanceId, new
151
+ * props) updates the EXISTING element instead of tearing it down and
152
+ * re-creating it — which would reset the carousel's scroll position +
153
+ * internal state. Created lazily the first time we see the widget:
154
+ * `document.createElement(tag)` for a defined tag, or a registry mount for a
155
+ * widget-id. An unresolvable widget caches `null` so the fail-safe renders
156
+ * nothing without re-checking the registry.
157
+ */
158
+ this._inlineElements = /* @__PURE__ */ new Map();
159
+ this._onTileReceiptClick = (instanceId) => {
160
+ this.dispatchEvent(
161
+ new CustomEvent("tile-receipt-activate", {
162
+ detail: { instanceId },
163
+ bubbles: true,
164
+ composed: true
165
+ })
166
+ );
167
+ };
168
+ this._onAlertReceiptClick = (instanceId) => {
169
+ this.dispatchEvent(
170
+ new CustomEvent("alert-receipt-activate", {
171
+ detail: { instanceId },
172
+ bubbles: true,
173
+ composed: true
174
+ })
175
+ );
176
+ };
177
+ this._onIntroSuggestionClick = () => {
178
+ const s = this.introSuggestion;
179
+ if (!s) return;
180
+ this.dispatchEvent(
181
+ new CustomEvent("trail-intro-suggestion", {
182
+ detail: { prompt: s.prompt },
183
+ bubbles: true,
184
+ composed: true
185
+ })
186
+ );
187
+ };
188
+ /**
189
+ * Visitor tapped a `suggest_replies` inline chip. Emit
190
+ * `trail-suggested-reply` carrying the chip text — the parent
191
+ * (AdaptiveChatBar) re-dispatches it as `chat-message-sent`, the
192
+ * SAME channel the input row uses, so the reply rides the existing
193
+ * send path (mountable → chatSession.send → transport). No fabricated
194
+ * AG-UI events; this is exactly what typing the text and hitting
195
+ * Enter would do. The chips clear on their own once the reply lands
196
+ * and a newer assistant message becomes the latest.
197
+ */
198
+ this._onSuggestedReplyClick = (text) => {
199
+ const trimmed = text.trim();
200
+ if (!trimmed) return;
201
+ this.dispatchEvent(
202
+ new CustomEvent("trail-suggested-reply", {
203
+ detail: { text: trimmed },
204
+ bubbles: true,
205
+ composed: true
206
+ })
207
+ );
208
+ };
209
+ }
210
+ /** True when `m` opens an assistant run (previous visible message is not
211
+ * assistant) — the persona byline renders once per turn, not per chip. */
212
+ _isAssistantTurnStart(m) {
213
+ if (m.role !== "assistant") return false;
214
+ const i = this.messages.findIndex((x) => x.id === m.id);
215
+ if (i <= 0) return true;
216
+ return this.messages[i - 1].role !== "assistant";
217
+ }
218
+ get _scrollable() {
219
+ return this.expanded || this.forceExpanded;
220
+ }
221
+ _trailContainer() {
222
+ return this.renderRoot.querySelector("[data-syntro-chat-trail]");
223
+ }
224
+ _isAtBottom(container) {
225
+ return container.scrollHeight - container.scrollTop - container.clientHeight <= _AdaptiveChatTrail._AT_BOTTOM_EPSILON_PX;
226
+ }
227
+ /** Count chips whose bottom edge hasn't entered the viewport yet. */
228
+ _countMessagesBelow(container) {
229
+ const viewportBottom = container.getBoundingClientRect().top + container.clientHeight;
230
+ let count = 0;
231
+ for (const chip of this.renderRoot.querySelectorAll("[data-trail-chip]")) {
232
+ if (chip.getBoundingClientRect().bottom > viewportBottom + 4) count += 1;
233
+ }
234
+ return count > 0 ? count : 1;
235
+ }
236
+ _syncContentBelow() {
237
+ const container = this._trailContainer();
238
+ if (!container) return;
239
+ this._messagesBelow = this._scrollable && !this._isAtBottom(container) ? this._countMessagesBelow(container) : 0;
240
+ }
241
+ /** Scroll so the anchored user chip sits at the top of the trail
242
+ * viewport. Idempotent — safe to re-apply on every update while the
243
+ * hold is live (content grows BELOW the anchor, so the target is
244
+ * stable; re-applying also covers late geometry, e.g. expanding
245
+ * mid-turn from the ambient strip). */
246
+ _applyAnchor() {
247
+ requestAnimationFrame(() => {
248
+ const container = this._trailContainer();
249
+ if (!container || this._anchorMsgId === null) return;
250
+ const chip = this.renderRoot.querySelector(
251
+ `[data-trail-chip-id="${String(this._anchorMsgId)}"]`
252
+ );
253
+ if (!chip) return;
254
+ const top = chip.getBoundingClientRect().top - container.getBoundingClientRect().top + container.scrollTop;
255
+ container.scrollTop = top;
256
+ this._syncContentBelow();
257
+ });
258
+ }
259
+ _pinToBottom() {
260
+ requestAnimationFrame(() => {
261
+ const container = this._trailContainer();
262
+ if (!container) return;
263
+ container.scrollTop = container.scrollHeight;
264
+ this._syncContentBelow();
265
+ });
266
+ }
267
+ /**
268
+ * Whether to render the thinking-dots chip after the last message.
269
+ * True iff a request is in flight AND the assistant hasn't started
270
+ * streaming a reply yet — once the first delta lands, the streaming
271
+ * chip's caret takes over and the thinking indicator hides so the
272
+ * visitor isn't shown both at once. Also hidden when the last
273
+ * message is an error/system message, because the request that
274
+ * "errored" is already over from the user's perspective.
275
+ */
276
+ _shouldShowThinking() {
277
+ if (!this.inFlight) return false;
278
+ const last = this.messages[this.messages.length - 1];
279
+ if (!last) return true;
280
+ if (last.role === "assistant" && last.status === "streaming") {
281
+ return (last.text ?? "").trim().length === 0;
282
+ }
283
+ if (last.role === "system" || last.status === "error") return false;
284
+ return true;
285
+ }
286
+ /**
287
+ * The "assistant is busy" indicator — ONE status line, never a stack.
288
+ *
289
+ * Two facts can be live at once here: WHO is answering (`personaName`, a
290
+ * static sticky pick, fixed for the whole turn) and WHAT is happening
291
+ * (`thinkingText`, rewritten by `ChatTransport`'s narration cycle every ~4s).
292
+ * They used to render as two independently-gated sibling rows, so a
293
+ * persona-enabled workspace showed "Jordan is typing…" stacked directly
294
+ * above the phase copy — the same claim twice, in two different verbs, and
295
+ * "Jordan is typing…" above three bouncing dots before any phase arrived.
296
+ * `thinkingStatusLine()` merges them into one sentence instead, and the
297
+ * dots remain the sole fallback when neither fact is known.
298
+ *
299
+ * The row's own height must NOT change when the phase text rotates, or the
300
+ * whole trail below it jumps every 4 seconds. `thinkingTextStyles()` pins
301
+ * that with a fixed `line-height` plus `white-space: nowrap` +
302
+ * `text-overflow: ellipsis`, so a short and a long phase paint the same box.
303
+ */
304
+ _renderThinking() {
305
+ const status = thinkingStatusLine(this.personaName, this.thinkingText);
306
+ return html`<div
307
+ data-trail-thinking
308
+ data-role="assistant"
309
+ data-status="thinking"
310
+ aria-live="off"
311
+ aria-label=${// The accessible name is the sentence on screen. When the dots are the
312
+ // only visual there is no sentence to mirror, so name the state with
313
+ // the SAME verb the visible copy uses ("typing", not "thinking").
314
+ // NOTE: a workspace with a configured `assistantName` but no persona
315
+ // roster still lands on this hardcoded 'Assistant' — `AdaptiveChatTrail`
316
+ // is never handed `assistantName` (see BUG-1786919868 report).
317
+ status || "Assistant is typing\u2026"}
318
+ style=${styleMap(thinkingSlotStyles())}
319
+ >
320
+ ${status ? html`<span data-trail-thinking-status style=${styleMap(thinkingStatusRowStyles())}>
321
+ ${this.personaAvatarSm ? html`<img
322
+ data-trail-persona-avatar
323
+ src=${this.personaAvatarSm}
324
+ alt=""
325
+ style=${styleMap(personaAvatarStyles())}
326
+ />` : nothing}<span data-trail-thinking-text style=${styleMap(thinkingTextStyles())}
327
+ >${status}</span
328
+ >
329
+ </span>` : html`<span data-trail-thinking-dots style=${styleMap(thinkingDotsStyles())}>
330
+ <span style=${styleMap(thinkingDotStyles(0))}></span>
331
+ <span style=${styleMap(thinkingDotStyles(1))}></span>
332
+ <span style=${styleMap(thinkingDotStyles(2))}></span>
333
+ </span>`}
334
+ </div>`;
335
+ }
336
+ connectedCallback() {
337
+ super.connectedCallback();
338
+ this.classList.add("ph-no-capture");
339
+ }
340
+ disconnectedCallback() {
341
+ super.disconnectedCallback();
342
+ for (const entry of this._inlineElements.values()) {
343
+ entry?.cleanup?.();
344
+ }
345
+ this._inlineElements.clear();
346
+ }
347
+ /**
348
+ * The id of the newest message the render will actually PAINT — the only
349
+ * valid anchor target. Anchoring to the raw last message is wrong-by-
350
+ * construction for tool-call turns: mid-turn the newest message is often a
351
+ * whitespace-only assistant shell (some providers emit whitespace text
352
+ * deltas around tool calls) that the empty-bubble filter drops, so an
353
+ * anchor recorded on it can never be in the painted set and the receipt
354
+ * falls to the tail strip forever — resurrecting BUG-1783015943 (seen live
355
+ * on vela desktop, BUG-1783630187). `isSuggestionHost=false` here: a
356
+ * message whose ONLY content is reply chips is too ephemeral to anchor on
357
+ * (the chips clear when the visitor answers), so walk past it too.
358
+ */
359
+ _newestRenderableMessageId() {
360
+ for (let i = this.messages.length - 1; i >= 0; i--) {
361
+ const m = this.messages[i];
362
+ if (m.role !== "assistant" || assistantChipRendersContent(m, [])) return m.id;
363
+ }
364
+ return null;
365
+ }
366
+ /**
367
+ * Server-authoritative anchor for a restored element: `turnId` is the
368
+ * persisted `mounted_by.turn_id` — the id of the user message that
369
+ * triggered the mounting turn. The element anchors to the first
370
+ * content-bearing assistant message of that turn (where it appeared live:
371
+ * intro text, then the mount, then any follow-up), falling back to the
372
+ * turn's user message when the assistant hasn't produced renderable text.
373
+ * Returns null when `turnId` is absent or not in the transcript (nudge
374
+ * mounts carry a runId) — the caller then uses the legacy
375
+ * newest-renderable anchor. Without this, a reload assigned every element
376
+ * to the newest message of the fully restored transcript, dumping all
377
+ * inline views below the last message (the arrival-timing bug).
378
+ */
379
+ /**
380
+ * Exact restore anchor: `toolCallId` is the persisted id of the tool call
381
+ * that mounted the element, and the transcript message hosting that call
382
+ * carries it in `toolCalls`. Anchor to the host message itself when it
383
+ * renders content, else walk BACK to the nearest renderable message — a
384
+ * standalone tool-call message persists with empty content and must not
385
+ * strand the element. This survives every turn shape (text→mount,
386
+ * text→text→mount, mount→text); `_anchorFromTurn` is the coarser fallback
387
+ * for elements persisted before tool_call_id existed.
388
+ */
389
+ _anchorFromToolCall(toolCallId, turnId) {
390
+ if (!toolCallId) return null;
391
+ let start = 0;
392
+ let end = this.messages.length;
393
+ const turnIdx = turnId ? this.messages.findIndex((m) => String(m.id) === turnId) : -1;
394
+ if (turnIdx >= 0) {
395
+ start = turnIdx + 1;
396
+ end = this.messages.findIndex((m, i) => i > turnIdx && m.role === "user");
397
+ if (end < 0) end = this.messages.length;
398
+ }
399
+ let idx = -1;
400
+ for (let i = start; i < end; i++) {
401
+ if ((this.messages[i].toolCalls ?? []).some((tc) => tc.id === toolCallId)) {
402
+ idx = i;
403
+ break;
404
+ }
405
+ }
406
+ if (idx < 0) return null;
407
+ for (let i = idx; i >= 0; i--) {
408
+ const m = this.messages[i];
409
+ if (m.role !== "assistant" || assistantChipRendersContent(m, [])) return m.id;
410
+ }
411
+ return null;
412
+ }
413
+ _anchorFromTurn(turnId) {
414
+ if (!turnId) return null;
415
+ const idx = this.messages.findIndex((m) => String(m.id) === turnId);
416
+ if (idx < 0) return null;
417
+ for (let i = idx + 1; i < this.messages.length; i++) {
418
+ const m = this.messages[i];
419
+ if (m.role === "user") break;
420
+ if (m.role === "assistant" && assistantChipRendersContent(m, [])) return m.id;
421
+ }
422
+ return this.messages[idx].id;
423
+ }
424
+ willUpdate(changed) {
425
+ if (changed.has("messages")) {
426
+ const lastId = this._newestRenderableMessageId();
427
+ if (lastId !== null) {
428
+ const receiptTurns = new Map(this.tileReceipts.map((r) => [r.instanceId, r.turnId]));
429
+ const widgetTurns = new Map(this.inlineWidgets.map((w) => [w.instanceId, w.turnId]));
430
+ const receiptCalls = new Map(this.tileReceipts.map((r) => [r.instanceId, r.toolCallId]));
431
+ const widgetCalls = new Map(this.inlineWidgets.map((w) => [w.instanceId, w.toolCallId]));
432
+ for (const [key, anchor] of this._receiptAnchors) {
433
+ if (anchor === null) {
434
+ this._receiptAnchors.set(
435
+ key,
436
+ this._anchorFromToolCall(receiptCalls.get(key), receiptTurns.get(key)) ?? this._anchorFromTurn(receiptTurns.get(key)) ?? lastId
437
+ );
438
+ }
439
+ }
440
+ for (const [key, anchor] of this._inlineAnchors) {
441
+ if (anchor === null) {
442
+ this._inlineAnchors.set(
443
+ key,
444
+ this._anchorFromToolCall(widgetCalls.get(key), widgetTurns.get(key)) ?? this._anchorFromTurn(widgetTurns.get(key)) ?? lastId
445
+ );
446
+ }
447
+ }
448
+ }
449
+ }
450
+ if (changed.has("tileReceipts")) {
451
+ const lastId = this._newestRenderableMessageId();
452
+ const live = /* @__PURE__ */ new Set();
453
+ for (const r of this.tileReceipts) {
454
+ live.add(r.instanceId);
455
+ if (!this._receiptAnchors.has(r.instanceId)) {
456
+ this._receiptAnchors.set(
457
+ r.instanceId,
458
+ this._anchorFromToolCall(r.toolCallId, r.turnId) ?? this._anchorFromTurn(r.turnId) ?? lastId
459
+ );
460
+ }
461
+ }
462
+ for (const key of this._receiptAnchors.keys()) {
463
+ if (!live.has(key)) this._receiptAnchors.delete(key);
464
+ }
465
+ }
466
+ if (changed.has("inlineWidgets")) {
467
+ const lastId = this._newestRenderableMessageId();
468
+ const live = /* @__PURE__ */ new Set();
469
+ for (const w of this.inlineWidgets) {
470
+ live.add(w.instanceId);
471
+ if (!this._inlineAnchors.has(w.instanceId)) {
472
+ this._inlineAnchors.set(
473
+ w.instanceId,
474
+ this._anchorFromToolCall(w.toolCallId, w.turnId) ?? this._anchorFromTurn(w.turnId) ?? lastId
475
+ );
476
+ }
477
+ this._ensureInlineElement(w);
478
+ }
479
+ for (const key of this._inlineAnchors.keys()) {
480
+ if (!live.has(key)) this._inlineAnchors.delete(key);
481
+ }
482
+ for (const key of this._inlineElements.keys()) {
483
+ if (!live.has(key)) {
484
+ this._inlineElements.get(key)?.cleanup?.();
485
+ this._inlineElements.delete(key);
486
+ }
487
+ }
488
+ }
489
+ }
490
+ /**
491
+ * Create (once) or refresh the cached element for an inline widget.
492
+ *
493
+ * Two mount paths, both keeping the chat bundle free of any widget-package
494
+ * import:
495
+ * - Direct tag (self-defining, server-mounted widgets like the reco
496
+ * carousel whose declared `widget` IS its custom-element tag):
497
+ * `document.createElement(tag)`.
498
+ * - Widget-id (admin-template widgets like `adaptive-product:card` or
499
+ * `adaptive-faq:accordion`, whose `widget` is a runtime widget-id, NOT a
500
+ * valid custom-element tag): mount through the runtime widget registry
501
+ * via {@link mountInlineWidget}, which resolves the id to its real
502
+ * element exactly like the deck `SyntroTileCard` path. Without this,
503
+ * `createElement('adaptive-product:card')` yields an inert unknown
504
+ * element and the tile renders permanently blank (the "inline mode
505
+ * shows nothing but the reco" bug).
506
+ *
507
+ * Fail-safe: an id that resolves to neither a defined tag nor a registered
508
+ * widget caches `null` and renders nothing (no broken box).
509
+ */
510
+ _ensureInlineElement(w) {
511
+ let entry = this._inlineElements.get(w.instanceId);
512
+ if (entry === void 0) {
513
+ if (customElements.get(w.tag)) {
514
+ const el = document.createElement(w.tag);
515
+ el.setAttribute("data-inline-widget-el", "");
516
+ entry = { node: el, propsTarget: el };
517
+ } else if (this.mountInlineWidget) {
518
+ const container = document.createElement("div");
519
+ container.setAttribute("data-inline-widget-el", "");
520
+ const mounted = this.mountInlineWidget(w.tag, container, w.props);
521
+ entry = mounted ? { node: container, propsTarget: mounted.element, cleanup: mounted.cleanup } : null;
522
+ } else {
523
+ entry = null;
524
+ }
525
+ this._inlineElements.set(w.instanceId, entry);
526
+ }
527
+ if (!entry) return;
528
+ entry.propsTarget.props = w.props;
529
+ }
530
+ updated(changed) {
531
+ if (!changed.has("expanded") && !changed.has("forceExpanded") && !changed.has("messages")) {
532
+ return;
533
+ }
534
+ if (changed.has("messages") && !this.followBottom) {
535
+ let lastUserIdx = -1;
536
+ for (let i = this.messages.length - 1; i >= 0; i--) {
537
+ if (this.messages[i].role === "user") {
538
+ lastUserIdx = i;
539
+ break;
540
+ }
541
+ }
542
+ const lastUser = lastUserIdx >= 0 ? this.messages[lastUserIdx] : void 0;
543
+ const isLiveTurn = lastUser !== void 0 && this.messages.slice(lastUserIdx + 1).every((m) => m.role !== "user" && m.status === "streaming");
544
+ if (lastUser && isLiveTurn && lastUser.id !== this._anchorMsgId) {
545
+ this._anchorMsgId = lastUser.id;
546
+ this._anchorActive = true;
547
+ this._anchorHold = true;
548
+ this._stickToBottom = false;
549
+ }
550
+ }
551
+ if (!this._scrollable) {
552
+ this._pinToBottom();
553
+ return;
554
+ }
555
+ if (this._anchorActive && this._anchorHold) {
556
+ this._applyAnchor();
557
+ } else if (this._stickToBottom) {
558
+ this._pinToBottom();
559
+ } else {
560
+ requestAnimationFrame(() => this._syncContentBelow());
561
+ }
562
+ }
563
+ /** Render a set of inline widgets (reco carousel etc.) as
564
+ * `[data-trail-widget]` blocks, anchored to a message position like
565
+ * receipts. Each block hosts the cached custom element for that
566
+ * instanceId — a defined tag mounts inline; an undefined tag renders
567
+ * nothing (fail-safe). Rendered at every trail site (inline per anchor,
568
+ * tail, empty-state) so placement can't diverge. */
569
+ _renderInlineWidgetStrip(list) {
570
+ if (list.length === 0) return nothing;
571
+ const blocks = list.map((w) => {
572
+ const entry = this._inlineElements.get(w.instanceId);
573
+ if (!entry) return nothing;
574
+ return html`<div
575
+ data-trail-widget
576
+ data-instance-id=${w.instanceId}
577
+ style=${styleMap(inlineWidgetBlockStyles())}
578
+ >${entry.node}</div>`;
579
+ }).filter((b) => b !== nothing);
580
+ if (blocks.length === 0) return nothing;
581
+ return html`${blocks}`;
582
+ }
583
+ /** A row of receipt chips, used at every trail render site (inline per
584
+ * anchor message, the tail fallback, and the empty-state branches) so the
585
+ * markup + styling can never diverge. Returns `nothing` for an empty list. */
586
+ _renderReceiptStrip(list) {
587
+ if (list.length === 0) return nothing;
588
+ return html`<div data-trail-tile-receipts style=${styleMap(tileReceiptStripStyles())}>
589
+ ${list.map((tr) => this._renderReceiptChip(tr))}
590
+ </div>`;
591
+ }
592
+ /** One receipt chip — a single, unified, single-line design for both kinds
593
+ * (nav "alert" + mounted "tile"). Same container/title/action style so they
594
+ * read as one chip family; the icon + action label carry the kind. No
595
+ * subtitle: the old "Brought up earlier" line was hardcoded (wrong for
596
+ * same-turn mounts) and made the chip taller — dropping it is the "shorter"
597
+ * chip. Rendered at every trail site so the kinds can't diverge. */
598
+ _renderReceiptChip(tr) {
599
+ if (tr.kind === "alert") {
600
+ return html`<button
601
+ type="button"
602
+ data-trail-alert-receipt
603
+ data-instance-id=${tr.instanceId}
604
+ @click=${() => this._onAlertReceiptClick(tr.instanceId)}
605
+ style=${styleMap(tileReceiptChipStyles())}
606
+ >
607
+ <span style=${styleMap(alertReceiptIconStyles())} aria-hidden="true">⚡</span>
608
+ <span style=${styleMap(receiptChipTitleStyles("alert"))}>${tr.title}</span>
609
+ <span style=${styleMap(tileReceiptActionStyles(false))}>Go →</span>
610
+ </button>`;
611
+ }
612
+ return html`<button
613
+ type="button"
614
+ data-trail-tile-receipt
615
+ data-instance-id=${tr.instanceId}
616
+ @click=${() => this._onTileReceiptClick(tr.instanceId)}
617
+ style=${styleMap(tileReceiptChipStyles())}
618
+ >
619
+ <span style=${styleMap(tileReceiptGlyphStyles())} aria-hidden="true"
620
+ >${tr.imageUrl ? (
621
+ // COI-037: a tiny product thumbnail — the decorative stack glyph
622
+ // read as a BROKEN image next to a product name (tester report).
623
+ html`<img
624
+ src=${tr.imageUrl}
625
+ alt=""
626
+ loading="lazy"
627
+ style=${styleMap(tileReceiptThumbStyles())}
628
+ />`
629
+ ) : (
630
+ // No image (a FAQ, a product the store has no photo for): the
631
+ // title's monogram on the brand stand-in, the same treatment the
632
+ // product card gives an imageless product (adaptive-product
633
+ // imageStandin.ts). The old two-layer stack glyph used the
634
+ // accent token, which on light-accent brands (iGlow) painted a
635
+ // pale empty square.
636
+ html`<span data-trail-receipt-monogram style=${styleMap(tileReceiptMonogramStyles())}
637
+ >${standinInitial(tr.title) ?? "\u2022"}</span
638
+ >`
639
+ )}</span
640
+ >
641
+ <span style=${styleMap(receiptChipTitleStyles("tile"))}>${tr.title}</span>
642
+ <span style=${styleMap(tileReceiptActionStyles(tr.dismissed))}
643
+ >${tr.dismissed ? "Bring back \u21BA" : "View \u25B8"}</span
644
+ >
645
+ </button>`;
646
+ }
647
+ /**
648
+ * The conversation live region.
649
+ *
650
+ * Always rendered, in EVERY branch of `render()` — including the ones that
651
+ * paint no transcript at all. A live region must already be in the DOM when
652
+ * its first child arrives; one created in the same mutation as its content
653
+ * is not reliably announced by NVDA or JAWS (adversarial review I3), and the
654
+ * content that would be lost is the assistant's first reply of the session.
655
+ *
656
+ * `display: contents` means it contributes no box and no layout anywhere it
657
+ * is mounted, so it is safe to place in an empty-state hero or in the flex
658
+ * scroll container alike. In the empty states it is rendered LAST so it can
659
+ * never become the `:first-child` any layout rule targets.
660
+ */
661
+ _renderConversationLog(children) {
662
+ return html`<div
663
+ data-syntro-chat-log
664
+ role="log"
665
+ aria-label="Conversation"
666
+ style="display:contents"
667
+ >${children}</div>`;
668
+ }
669
+ render() {
670
+ if (this.messages.length === 0 && !this.greeting && !this.introSuggestion && this.tileReceipts.length === 0 && this.inlineWidgets.length === 0)
671
+ return this._renderConversationLog(nothing);
672
+ if (this.messages.length === 0 && (this.greeting || this.introSuggestion)) {
673
+ return html`<div data-syntro-chat-trail data-state="empty" style=${styleMap({
674
+ display: "flex",
675
+ flexDirection: "column",
676
+ alignItems: "center",
677
+ justifyContent: "center",
678
+ textAlign: "center",
679
+ gap: "12px",
680
+ flex: "1 1 auto",
681
+ width: "100%",
682
+ padding: "24px 16px",
683
+ pointerEvents: "auto"
684
+ })}>
685
+ <div data-trail-empty-icon style=${styleMap(emptyIconStyles())} aria-hidden="true">
686
+ ${this.personaAvatarSm ? html`<img
687
+ data-trail-empty-avatar
688
+ src=${this.personaAvatarSm}
689
+ alt=""
690
+ style="width:44px;height:44px;border-radius:9999px;object-fit:cover;display:block;"
691
+ />` : "\u2726"}
692
+ </div>
693
+ ${this.greeting ? html`<h2
694
+ data-trail-empty-title
695
+ style=${styleMap(emptyTitleStyles())}
696
+ >${unsafeHTML(renderMarkdown(stripTrailingWhitespace(this.greeting)))}</h2>` : nothing}
697
+ ${this.introSuggestion ? html`<button
698
+ type="button"
699
+ data-trail-intro-suggestion
700
+ @click=${this._onIntroSuggestionClick}
701
+ style=${styleMap(introSuggestionStyles())}
702
+ >${this.introSuggestion.label}</button>` : nothing}
703
+ ${this._renderReceiptStrip(this.tileReceipts)}
704
+ ${this._renderInlineWidgetStrip(this.inlineWidgets)}
705
+ ${this._renderConversationLog(nothing)}
706
+ </div>`;
707
+ }
708
+ if (this.messages.length === 0 && (this.tileReceipts.length > 0 || this.inlineWidgets.length > 0)) {
709
+ return html`<div data-syntro-chat-trail data-state="empty" style=${styleMap({
710
+ display: "flex",
711
+ flexDirection: "column",
712
+ alignItems: "flex-start",
713
+ gap: "6px",
714
+ width: "100%",
715
+ padding: "8px 0",
716
+ pointerEvents: "auto"
717
+ })}>
718
+ ${this._renderReceiptStrip(this.tileReceipts)}
719
+ ${this._renderInlineWidgetStrip(this.inlineWidgets)}
720
+ ${this._renderConversationLog(nothing)}
721
+ </div>`;
722
+ }
723
+ let turnReplies = [];
724
+ for (let i = this.messages.length - 1; i >= 0; i--) {
725
+ const m = this.messages[i];
726
+ if (m.role === "user") break;
727
+ if (m.role === "assistant") {
728
+ const replies = extractSuggestedReplies(m.toolCalls);
729
+ if (replies.length > 0) {
730
+ turnReplies = replies;
731
+ break;
732
+ }
733
+ }
734
+ }
735
+ let suggestionHostId = null;
736
+ if (turnReplies.length > 0) {
737
+ for (let i = this.messages.length - 1; i >= 0; i--) {
738
+ const m = this.messages[i];
739
+ if (m.role === "user") break;
740
+ if (m.role === "assistant" && (assistantChipRendersContent(m, []) || extractSuggestedReplies(m.toolCalls).length > 0)) {
741
+ suggestionHostId = m.id;
742
+ break;
743
+ }
744
+ }
745
+ }
746
+ const renderable = this.messages.filter(
747
+ (m) => m.role !== "assistant" || assistantChipRendersContent(m, m.id === suggestionHostId ? turnReplies : [])
748
+ );
749
+ const visible = this.expanded || this.forceExpanded ? renderable : renderable.slice(-this.visibleCount);
750
+ const hidden = renderable.length - visible.length;
751
+ const visibleIds = new Set(visible.map((m) => m.id));
752
+ const receiptsByAnchor = /* @__PURE__ */ new Map();
753
+ const tailReceipts = [];
754
+ for (const r of this.tileReceipts) {
755
+ const anchor = this._receiptAnchors.get(r.instanceId) ?? null;
756
+ if (anchor !== null && visibleIds.has(anchor)) {
757
+ const list = receiptsByAnchor.get(anchor);
758
+ if (list) list.push(r);
759
+ else receiptsByAnchor.set(anchor, [r]);
760
+ } else {
761
+ tailReceipts.push(r);
762
+ }
763
+ }
764
+ const inlineByAnchor = /* @__PURE__ */ new Map();
765
+ const tailInlineWidgets = [];
766
+ for (const w of this.inlineWidgets) {
767
+ const anchor = this._inlineAnchors.get(w.instanceId) ?? null;
768
+ if (anchor !== null && visibleIds.has(anchor)) {
769
+ const list = inlineByAnchor.get(anchor);
770
+ if (list) list.push(w);
771
+ else inlineByAnchor.set(anchor, [w]);
772
+ } else {
773
+ tailInlineWidgets.push(w);
774
+ }
775
+ }
776
+ const baseStyles = {
777
+ display: "flex",
778
+ flexDirection: "column",
779
+ gap: "6px",
780
+ // NO `justifyContent: flex-end` here — combined with `overflow: auto`
781
+ // it's a known cross-browser bug: oversized flex children get
782
+ // pushed ABOVE the container without contributing to scrollHeight,
783
+ // so the scrollbar never appears. We rely on `:first-child {
784
+ // margin-top: auto }` (injected as global CSS) to push short
785
+ // content to the bottom of the box while leaving overflow handling
786
+ // to the native scroll container.
787
+ width: "100%",
788
+ // The trail itself has no chrome — chips sit on the host page bg.
789
+ padding: "0",
790
+ pointerEvents: "auto"
791
+ };
792
+ const modeStyles = this.forceExpanded ? {
793
+ // Full-screen host: fill all available vertical space and
794
+ // scroll the overflow. ``flex: 1 1 auto`` (basis AUTO, not 0) —
795
+ // in a DEFINITE-height parent (full-screen panel) grow:1 still
796
+ // fills all free space and margin-top:auto bottom-anchors, so
797
+ // fill behavior is unchanged; but basis auto also lets the trail
798
+ // report its CONTENT height as its natural size, so in an
799
+ // INDEFINITE-height parent (velvet's shrink-wrap bottom sheet)
800
+ // the trail contributes real content height instead of collapsing
801
+ // to 0 — which is what lets the sheet hug its content with no void.
802
+ // shrink:1 + min-height:0 still lets the trail shrink first when
803
+ // the parent runs out of room; the input row is absolute-pinned
804
+ // (never pushed off) and the header shrinks after the trail.
805
+ flex: "1 1 auto",
806
+ minHeight: "0",
807
+ overflowY: "auto",
808
+ // ``contain`` prevents scroll from bubbling out to the host
809
+ // page when the trail hits top or bottom. Required for the
810
+ // panel-scroll-leak regression test in
811
+ // SyntroBottomSheet.test.ts.
812
+ overscrollBehavior: "contain",
813
+ paddingTop: "8px",
814
+ ...this.hideTopBorder ? {} : {
815
+ borderTop: "var(--sc-content-bubble-border, 1px solid rgba(255, 255, 255, 0.18))"
816
+ },
817
+ scrollBehavior: "smooth",
818
+ scrollbarWidth: "thin"
819
+ } : this.expanded ? {
820
+ maxHeight: "min(320px, 40vh)",
821
+ overflowY: "auto",
822
+ paddingTop: "8px",
823
+ ...this.hideTopBorder ? {} : {
824
+ borderTop: "var(--sc-content-bubble-border, 1px solid rgba(255, 255, 255, 0.18))"
825
+ },
826
+ scrollBehavior: "smooth",
827
+ scrollbarWidth: "thin"
828
+ } : {
829
+ // Collapsed mode = ambient peek surface. Never a scrollbar:
830
+ // long messages slide up behind the blur veil instead of
831
+ // exposing a scrollbar. The "↑ N more · expand" affordance
832
+ // is the only entry into history; clicking it switches to
833
+ // expanded mode where scroll IS allowed.
834
+ // Height kept short so the trail reads as a peek strip above
835
+ // the chat bar, not a panel — long single replies fade into
836
+ // the blur veil at the top, user expands to read the rest.
837
+ maxHeight: "min(140px, 22vh)",
838
+ overflow: "hidden"
839
+ };
840
+ const containerStyles = { ...baseStyles, ...modeStyles };
841
+ const frameStyles = this.forceExpanded ? {
842
+ position: "relative",
843
+ width: "100%",
844
+ display: "flex",
845
+ flexDirection: "column",
846
+ // basis AUTO (see the scroller's modeStyles note): fills a definite
847
+ // parent, contributes content height in an indefinite one (velvet
848
+ // shrink-wrap). Every fill layer in the chain must be basis-auto or
849
+ // the min-content of a basis-0 layer collapses the whole chain to 0.
850
+ flex: "1 1 auto",
851
+ minHeight: "0"
852
+ } : {
853
+ position: "relative",
854
+ width: "100%"
855
+ };
856
+ const isStreamingMsg = (m) => m.role === "assistant" && m.status === "streaming";
857
+ const lastVisible = visible[visible.length - 1];
858
+ const streamingTail = lastVisible !== void 0 && isStreamingMsg(lastVisible);
859
+ const strandedStreaming = (m, i) => isStreamingMsg(m) && !(streamingTail && i === visible.length - 1);
860
+ const renderChip = (m, i) => {
861
+ const pos = this.expanded || this.forceExpanded ? 0 : visible.length - 1 - i;
862
+ const state = chipVisualState(m);
863
+ const isStreaming = state.kind === "streaming";
864
+ const {
865
+ pendingToolCalls: toolCalls,
866
+ receipts,
867
+ replyChips
868
+ } = computeAssistantChipParts(m, m.id === suggestionHostId ? turnReplies : []);
869
+ const renderedText = m.role === "assistant" ? (
870
+ // Never paint a degenerate serialized-response dump, even if the
871
+ // turn also carries a legit part (receipt/chip) that keeps it in
872
+ // the renderable set. Mirrors the filter's drop signal.
873
+ looksLikeSerializedModelResponse(m.text ?? "") ? nothing : html`${unsafeHTML(renderMarkdown(stripTrailingWhitespace(m.text)))}`
874
+ ) : html`${m.text}`;
875
+ const chipStyle = isStreaming ? {
876
+ ...chipStyles(state, pos),
877
+ animation: "syntro-trail-chip-enter 220ms cubic-bezier(0.22, 1, 0.36, 1) both"
878
+ } : chipStyles(state, pos);
879
+ const personaByline = this.personaName && this._isAssistantTurnStart(m) ? html`<div data-trail-persona-byline style=${styleMap(personaBylineStyles())}>
880
+ ${this.personaAvatarSm ? html`<img
881
+ data-trail-persona-avatar
882
+ src=${this.personaAvatarSm}
883
+ alt=""
884
+ style=${styleMap(personaAvatarStyles())}
885
+ />` : nothing}<span>${this.personaName}</span>
886
+ </div>` : nothing;
887
+ return html`<div
888
+ data-trail-chip
889
+ data-trail-chip-id=${String(m.id)}
890
+ data-role=${m.role}
891
+ data-status=${m.status ?? "complete"}
892
+ aria-hidden=${strandedStreaming(m, i) ? "true" : nothing}
893
+ ?inert=${strandedStreaming(m, i)}
894
+ style=${styleMap(chipStyle)}
895
+ >${personaByline}${renderedText}${isStreaming ? html`<span
896
+ data-trail-caret
897
+ aria-hidden="true"
898
+ style=${styleMap(caretStyles())}
899
+ ></span>` : nothing}${toolCalls.length > 0 ? html`<div data-trail-toolcalls style=${styleMap(toolCallStripStyles())}>
900
+ ${toolCalls.map(
901
+ (tc) => html`<button
902
+ type="button"
903
+ data-trail-toolcall
904
+ data-tool-id=${tc.id}
905
+ data-tool-status=${tc.status}
906
+ @click=${() => this._onToolCallClick(tc)}
907
+ style=${styleMap(pendingToolCallChipStyles())}
908
+ title="${tc.name} · ${tc.status}"
909
+ >? ${tc.name}</button>`
910
+ )}
911
+ </div>` : nothing}${receipts.length > 0 ? html`<div data-trail-receipts style=${styleMap(receiptStripStyles())}>
912
+ ${receipts.map(
913
+ (r) => html`<div
914
+ data-trail-receipt
915
+ data-receipt-type=${r.type}
916
+ style=${styleMap(receiptStyles())}
917
+ ><span aria-hidden="true" style=${styleMap(receiptIconStyles())}>✓</span
918
+ ><span>${receiptText(r)}</span></div>`
919
+ )}
920
+ </div>` : nothing}${replyChips.length > 0 ? html`<div data-trail-reply-chips style=${styleMap(replyChipStripStyles())}>
921
+ ${replyChips.map(
922
+ (reply) => html`<button
923
+ type="button"
924
+ data-trail-reply-chip
925
+ @click=${() => this._onSuggestedReplyClick(reply)}
926
+ style=${styleMap(replyChipStyles())}
927
+ >${reply}</button>`
928
+ )}
929
+ </div>` : nothing}</div>${// Receipt chips that mounted while THIS message was newest render
930
+ // inline right here, so they read as part of the flow and drift up
931
+ // as later messages append below them.
932
+ this._renderReceiptStrip(receiptsByAnchor.get(m.id) ?? [])}${// Inline widgets (reco carousel) anchored to this message render
933
+ // right here in the conversation flow — the whole point of the
934
+ // chat-inline slot: visible in the trail, not behind the panel.
935
+ this._renderInlineWidgetStrip(inlineByAnchor.get(m.id) ?? [])}`;
936
+ };
937
+ const committed = streamingTail ? visible.slice(0, -1) : visible;
938
+ const committedChips = repeat(
939
+ committed,
940
+ (m) => m.id,
941
+ (m, i) => renderChip(m, i)
942
+ );
943
+ const streamingChip = streamingTail ? renderChip(lastVisible, visible.length - 1) : nothing;
944
+ return html`
945
+ ${// Single toggle button above the frame. Same DOM node morphs:
946
+ // collapsed → "↑ N more · expand" / "↑ expand" (calls _onExpand)
947
+ // expanded → "⌄ minimize" (calls _onCollapse)
948
+ // Lives OUTSIDE the frame so the absolute blur veil (top:0,
949
+ // z-index:2 inside the frame) doesn't cover it. Shown whenever
950
+ // there's anything in the trail — the user expects this affordance
951
+ // available regardless of how many chips are on screen.
952
+ // When ``forceExpanded`` is set by the host (full-screen panel,
953
+ // etc.), there's nowhere to collapse INTO and the affordance
954
+ // would just confuse the user — suppress it entirely.
955
+ this.messages.length === 0 || this.forceExpanded ? nothing : this.expanded ? html`<button
956
+ type="button"
957
+ data-trail-toggle
958
+ data-trail-collapse
959
+ @click=${this._onCollapse}
960
+ style=${styleMap(moreStyles())}
961
+ >⌄ minimize</button>` : html`<button
962
+ type="button"
963
+ data-trail-toggle
964
+ data-trail-more
965
+ @click=${this._onExpand}
966
+ style=${styleMap(moreStyles())}
967
+ >${hidden > 0 ? html`↑ ${hidden} more · expand` : html`↑ expand`}</button>`}
968
+ <div data-syntro-chat-trail-frame style=${styleMap(frameStyles)}>
969
+ <div
970
+ data-syntro-chat-trail
971
+ style=${styleMap(containerStyles)}
972
+ @scroll=${this._onTrailScroll}
973
+ @wheel=${this._markUserScrollIntent}
974
+ @touchmove=${this._markUserScrollIntent}
975
+ >
976
+ <!-- Committed transcript. Rendered UNCONDITIONALLY, even empty: a live
977
+ region has to be in the DOM before its first child arrives or the
978
+ announcement is dropped (review I3), and the first assistant reply
979
+ of a session is exactly the one that matters. display:contents
980
+ keeps the scroll container's flex layout byte-identical — the
981
+ wrapper is a semantic node only, which is why the bottom-anchor
982
+ rule reaches through it, and past it while it is still empty. -->
983
+ ${this._renderConversationLog(committedChips)}
984
+ ${// The in-flight reply: same slot on screen, outside the log.
985
+ streamingChip}
986
+ ${// Thinking indicator — fills the gap between the user's
987
+ // most recent message and the assistant's first token.
988
+ // Shows when chatSession.inFlight is true AND the last
989
+ // message is NOT a streaming assistant message (the
990
+ // assistant's own caret takes over once tokens arrive).
991
+ // Styled as an assistant chip with three bouncing dots
992
+ // instead of text, so the thread reads naturally — the
993
+ // visitor sees an "assistant" bubble that's clearly busy.
994
+ this._shouldShowThinking() ? this._renderThinking() : nothing}
995
+ ${// Tail strip: receipts with no in-slice anchor (mounted pre-chat, or
996
+ // their anchor message scrolled out of the ambient view). Keeps them
997
+ // reachable at the bottom — the old always-pinned position — rather
998
+ // than dropping them. In expanded/maximized mode every message is
999
+ // painted, so this is usually empty and receipts sit inline instead.
1000
+ this._renderReceiptStrip(tailReceipts)}
1001
+ ${// Inline widgets with no in-slice anchor (mounted pre-chat, or their
1002
+ // anchor scrolled out of the ambient view) render at the tail.
1003
+ this._renderInlineWidgetStrip(tailInlineWidgets)}
1004
+ </div>
1005
+ ${// The blur veil only fires in collapsed (ambient) mode.
1006
+ // Expanded = the user explicitly asked for history; blurring
1007
+ // the top of the panel would hide content (including the
1008
+ // minimize button just above the trail's top edge) the user
1009
+ // came here to see.
1010
+ this.expanded || this.forceExpanded ? nothing : html`<div
1011
+ data-trail-blur-veil
1012
+ aria-hidden="true"
1013
+ style=${styleMap(blurVeilStyles())}
1014
+ ></div>`}
1015
+ ${// "Scroll to most recent" pill (FEAT-1783535427): shown only in
1016
+ // the scrollable modes when content extends below the viewport —
1017
+ // the visitor is reading the reply top-down and this is the
1018
+ // one-tap opt-in back to the newest content. Absolute inside the
1019
+ // positioning frame so it floats over the scroller's bottom edge.
1020
+ this._scrollable && this._messagesBelow > 0 ? html`<button
1021
+ type="button"
1022
+ data-trail-scroll-recent
1023
+ aria-label="Scroll to most recent — ${this._messagesBelow} ${this._messagesBelow === 1 ? "message" : "messages"} below"
1024
+ @click=${this._onScrollToRecent}
1025
+ style=${styleMap(scrollRecentPillStyles())}
1026
+ ><span aria-hidden="true" style="font-size:11px;line-height:1;">⌄</span><span>${this._messagesBelow} ${this._messagesBelow === 1 ? "message" : "messages"}</span></button>` : nothing}
1027
+ </div>
1028
+ `;
1029
+ }
1030
+ };
1031
+ // Trail-local styles. The chat-trail used to render into light DOM
1032
+ // (`createRenderRoot() { return this }`) and inject these rules into
1033
+ // `document.head` so chip + markdown styles could find their selectors.
1034
+ // That broke the moment the trail was hosted inside any shadow root
1035
+ // (sc-mount → custom-canvas → velvet-mobile → sc-mount → chat-bar →
1036
+ // chat-trail is the production chain): CSS animations require the
1037
+ // @keyframes rule to be in the SAME TreeScope as the element using
1038
+ // it, and document-level keyframes don't cross a shadow boundary.
1039
+ // Result was silent — `animation-name` parsed fine, the property got
1040
+ // the right resolved value, but the browser never instantiated an
1041
+ // Animation object because no matching keyframe was visible from
1042
+ // inside the shadow tree. `getAnimations()` returned `[]`, dots sat
1043
+ // frozen at base scale, the typing indicator looked dead.
1044
+ //
1045
+ // Shadow DOM with `static styles` is the right architecture here.
1046
+ // CSS custom properties (--sc-tile-text-color, --sc-content-bubble-*,
1047
+ // etc.) inherit through shadow boundaries by default, so the canvas
1048
+ // theme cascade still reaches the chips without the light-DOM
1049
+ // workaround. As a bonus the markdown reset rules can't leak out
1050
+ // onto the host page, and the keyframes are owned by the trail's
1051
+ // own tree scope where the chip + dot elements live.
1052
+ _AdaptiveChatTrail.styles = css`
1053
+ /* Bottom-anchor for CONVERSATIONS only: a short message list hugs the
1054
+ composer instead of floating at the top. The :not() gate matters —
1055
+ unscoped, the auto margin also grabbed the empty-state hero's first
1056
+ child (the icon disc), ate all free space above it, and pinned the
1057
+ greeting to the bottom of a maximized panel, silently defeating the
1058
+ hero's own justify-content: center. */
1059
+ [data-syntro-chat-trail]:not([data-state="empty"]) > :first-child {
1060
+ margin-top: auto;
1061
+ }
1062
+ /* …and THROUGH the conversation-log wrapper. That wrapper is
1063
+ display: contents (a semantic node carrying role=log, see render), so
1064
+ margin on it does nothing — the auto margin has to land on the first
1065
+ real chip inside it or a short thread floats to the top of a tall
1066
+ panel instead of hugging the composer. */
1067
+ [data-syntro-chat-trail]:not([data-state="empty"]) > [data-syntro-chat-log] > :first-child {
1068
+ margin-top: auto;
1069
+ }
1070
+ /* …and PAST it while it is still empty. The wrapper is always rendered (it
1071
+ is a live region, which must pre-exist its content), so with only an
1072
+ in-flight reply on screen the wrapper IS the :first-child and has no box
1073
+ to carry the margin. :empty ignores comment nodes, so Lit's own markers
1074
+ inside the wrapper do not defeat it. */
1075
+ [data-syntro-chat-trail]:not([data-state="empty"]) > [data-syntro-chat-log]:empty + * {
1076
+ margin-top: auto;
1077
+ }
1078
+ [data-trail-chip] > p:first-child {
1079
+ margin-top: 0;
1080
+ }
1081
+ [data-trail-chip] > p:last-child {
1082
+ margin-bottom: 0;
1083
+ }
1084
+ [data-trail-chip] p {
1085
+ margin: 4px 0;
1086
+ }
1087
+ [data-trail-chip] br + br {
1088
+ display: none;
1089
+ }
1090
+ [data-trail-chip] ul,
1091
+ [data-trail-chip] ol {
1092
+ margin: 4px 0;
1093
+ padding-left: 1.1em;
1094
+ }
1095
+ [data-trail-chip] li {
1096
+ margin: 0;
1097
+ }
1098
+ [data-trail-chip] li + li {
1099
+ margin-top: 1px;
1100
+ }
1101
+ [data-trail-chip] pre {
1102
+ margin: 6px 0;
1103
+ padding: 6px 8px;
1104
+ background: var(--sc-content-code-background-block, rgba(0, 0, 0, 0.35));
1105
+ border-radius: 6px;
1106
+ overflow-x: auto;
1107
+ font-size: 11px;
1108
+ }
1109
+ [data-trail-chip] code {
1110
+ font-family: ui-monospace, SF Mono, Menlo, monospace;
1111
+ font-size: 11px;
1112
+ background: var(--sc-content-code-background, rgba(0, 0, 0, 0.25));
1113
+ padding: 1px 4px;
1114
+ border-radius: 3px;
1115
+ }
1116
+ [data-trail-chip] pre code {
1117
+ background: transparent;
1118
+ padding: 0;
1119
+ }
1120
+ [data-trail-chip] a {
1121
+ color: var(--sc-content-link-color, var(--sc-color-primary, #b72e2a));
1122
+ text-decoration: underline;
1123
+ }
1124
+ [data-trail-chip] strong {
1125
+ font-weight: 600;
1126
+ }
1127
+ /* Streaming-caret blink for in-progress assistant chips. */
1128
+ @keyframes syntro-trail-caret {
1129
+ 0% {
1130
+ opacity: 1;
1131
+ }
1132
+ 50% {
1133
+ opacity: 0;
1134
+ }
1135
+ 100% {
1136
+ opacity: 1;
1137
+ }
1138
+ }
1139
+ /* Thinking-dot scale + opacity pulse. The naked-dots redesign
1140
+ relies entirely on this motion to signal "alive" — there's no
1141
+ chip frame around the dots, so a subtle animation reads as
1142
+ broken. 0.4 → 1.0 scale with 0.25 → 1.0 opacity gives an
1143
+ unmistakable breathing cue. */
1144
+ @keyframes syntro-trail-thinking-pulse {
1145
+ 0%,
1146
+ 100% {
1147
+ transform: scale(0.4);
1148
+ opacity: 0.25;
1149
+ }
1150
+ 45% {
1151
+ transform: scale(1);
1152
+ opacity: 1;
1153
+ }
1154
+ }
1155
+ /* Chip entrance — fades a freshly-mounted chip up from below
1156
+ so the thinking → first-streaming-chip handoff feels like one
1157
+ continuous motion instead of an element swap. */
1158
+ @keyframes syntro-trail-chip-enter {
1159
+ from {
1160
+ opacity: 0;
1161
+ transform: translateY(6px) scale(0.97);
1162
+ }
1163
+ to {
1164
+ opacity: 1;
1165
+ transform: translateY(0) scale(1);
1166
+ }
1167
+ }
1168
+ `;
1169
+ _AdaptiveChatTrail.properties = {
1170
+ messages: { attribute: false },
1171
+ visibleCount: { type: Number },
1172
+ expanded: { type: Boolean },
1173
+ greeting: { type: String },
1174
+ /** When true: trail renders fully expanded AND the
1175
+ * expand/collapse affordance is hidden. Used by hosts that mount
1176
+ * the chat in a full-screen surface (the mobile panel) where
1177
+ * collapse makes no sense — there's nothing to collapse INTO. */
1178
+ forceExpanded: { type: Boolean },
1179
+ /**
1180
+ * BUG (found 2026-08-13, reported live as a stray horizontal rule
1181
+ * floating above the transcript with nothing above it): the
1182
+ * expanded/forceExpanded scroll container always paints a
1183
+ * `borderTop` to mark "the panel's top edge". That reads correctly
1184
+ * in `AdaptiveChatBar`, which ALWAYS renders `renderHeader(...)`
1185
+ * immediately above the trail when `forceExpanded` is true (see
1186
+ * `AdaptiveChatBar.ts`'s `${this.forceExpanded ? renderHeader(...)
1187
+ * : nothing}`) — the border separates header from transcript.
1188
+ * `LeaveAMessageLit` mounts this element directly with no header
1189
+ * above it, so the same border renders as an orphaned rule sitting
1190
+ * in blank card padding. Default `false` preserves the existing
1191
+ * `AdaptiveChatBar` look exactly; hosts with no header above the
1192
+ * trail set this `true` to suppress it. */
1193
+ hideTopBorder: { type: Boolean },
1194
+ /**
1195
+ * Short-thread mode (BUG-1789098964): never engage the turn anchor;
1196
+ * every messages change pins the bottom unless the visitor scrolled up.
1197
+ * The anchor contract exists so a long STREAMED answer reads top-down
1198
+ * from the visitor's message. The human-handoff card shows a 160px
1199
+ * thread whose replies land whole, and there the anchor pinned the
1200
+ * visitor's own chip to the top while the reply sat below the fold
1201
+ * (Parker, live on mainpeptides.com 2026-09-10). Hosts that render a
1202
+ * short thread set this `true`; the concierge leaves it `false`.
1203
+ */
1204
+ followBottom: { type: Boolean },
1205
+ /**
1206
+ * Mirrors `chatSession.inFlight` — true between `send()` and the
1207
+ * assistant's first `receiveStart`/`receiveDelta`, OR while a
1208
+ * stream is in progress. Drives the "thinking" indicator that
1209
+ * fills the gap between the user's just-sent message and the
1210
+ * first assistant token. Wired through AdaptiveChatBar (it owns
1211
+ * the chatSession subscription and forwards inFlight as a prop).
1212
+ */
1213
+ inFlight: { type: Boolean },
1214
+ /**
1215
+ * Live reasoning narration streamed from the model via AG-UI
1216
+ * `THINKING_TEXT_MESSAGE_CONTENT`. When non-empty, the thinking
1217
+ * chip renders this in place of the bouncing dots so the visitor
1218
+ * sees what the model is actually doing instead of staring at
1219
+ * silent loading state during a tool-call gap.
1220
+ */
1221
+ thinkingText: { attribute: false },
1222
+ personaName: { attribute: false },
1223
+ personaAvatarSm: { attribute: false },
1224
+ /**
1225
+ * Pre-conversation suggestion chip. When the visitor opens the
1226
+ * chat and `messages` is empty, the trail renders this as a
1227
+ * button beneath the greeting. Clicking it dispatches a
1228
+ * `trail-intro-suggestion` event with the configured prompt —
1229
+ * the parent (chat-bar) forwards that to chatSession.send().
1230
+ * Disappears the moment the first message lands.
1231
+ */
1232
+ introSuggestion: { attribute: false },
1233
+ /**
1234
+ * Tappable tile-receipt chips rendered once at the end of the
1235
+ * trail. Produced from the store by Task 3 (AdaptiveChatBar).
1236
+ * Each chip emits `tile-receipt-activate` with its instanceId.
1237
+ */
1238
+ tileReceipts: { attribute: false },
1239
+ /**
1240
+ * Widgets rendered INLINE in the trail (reco carousel etc.),
1241
+ * anchored to a message position like receipts. Sourced from the
1242
+ * store's `chat-inline` slot mounts via AdaptiveChatBar.
1243
+ */
1244
+ inlineWidgets: { attribute: false },
1245
+ /**
1246
+ * Registry-backed mounter for inline widgets whose id is not their
1247
+ * custom-element tag. Injected from the chat-bar mountable.
1248
+ */
1249
+ mountInlineWidget: { attribute: false },
1250
+ _messagesBelow: { state: true }
1251
+ };
1252
+ _AdaptiveChatTrail._AT_BOTTOM_EPSILON_PX = 50;
1253
+ _AdaptiveChatTrail._USER_SCROLL_INTENT_WINDOW_MS = 1200;
1254
+ var AdaptiveChatTrail = _AdaptiveChatTrail;
1255
+ function scrollRecentPillStyles() {
1256
+ return {
1257
+ position: "absolute",
1258
+ bottom: "8px",
1259
+ left: "50%",
1260
+ transform: "translateX(-50%)",
1261
+ display: "flex",
1262
+ alignItems: "center",
1263
+ gap: "5px",
1264
+ height: "26px",
1265
+ padding: "0 10px",
1266
+ borderRadius: "9999px",
1267
+ border: "var(--sc-content-bubble-border, 1px solid rgba(255, 255, 255, 0.18))",
1268
+ // REAL token names + correct pairing (Parker 2026-08-28, screenshot:
1269
+ // unreadable dark smear). `--sc-content-bubble-bg` never existed
1270
+ // (flattenThemeConfig emits `--sc-content-bubble-background`), so every
1271
+ // workspace fell through to a translucent dark fallback — and the text
1272
+ // rode `--sc-tile-text-color` (iGlow: #333), the TILE pairing, giving
1273
+ // dark-on-dark. Bubble surface -> bubble background + content text, and
1274
+ // the themeless fallback is OPAQUE: a translucent pill floating over
1275
+ // scrolling trail content is unreadable by construction.
1276
+ background: "var(--sc-content-bubble-background, rgb(20, 22, 28))",
1277
+ color: "var(--sc-content-text-color, rgba(255, 255, 255, 0.92))",
1278
+ fontSize: "12px",
1279
+ fontWeight: "500",
1280
+ lineHeight: "1",
1281
+ whiteSpace: "nowrap",
1282
+ cursor: "pointer",
1283
+ boxShadow: "0 2px 8px rgba(0, 0, 0, 0.25)",
1284
+ zIndex: "3"
1285
+ };
1286
+ }
1287
+ function blurVeilStyles() {
1288
+ const mask = "linear-gradient(to bottom, black 0%, black 40%, transparent 100%)";
1289
+ return {
1290
+ position: "absolute",
1291
+ top: "0",
1292
+ left: "0",
1293
+ right: "0",
1294
+ height: `${TRAIL_BLUR_VEIL_PX}px`,
1295
+ pointerEvents: "none",
1296
+ backdropFilter: `blur(${TRAIL_BLUR_MAX_PX}px)`,
1297
+ WebkitBackdropFilter: `blur(${TRAIL_BLUR_MAX_PX}px)`,
1298
+ maskImage: mask,
1299
+ WebkitMaskImage: mask,
1300
+ zIndex: "2"
1301
+ };
1302
+ }
1303
+ function chipVisualState(m) {
1304
+ if (m.role === "system") {
1305
+ return { kind: "notice", role: m.role };
1306
+ }
1307
+ if (m.role === "assistant" && m.status === "error") {
1308
+ return { kind: "settled", role: m.role };
1309
+ }
1310
+ if (m.status === "error") {
1311
+ return { kind: "notice", role: m.role };
1312
+ }
1313
+ if (m.role === "assistant" && m.status === "streaming") {
1314
+ return { kind: "streaming" };
1315
+ }
1316
+ return { kind: "settled", role: m.role };
1317
+ }
1318
+ function chipStyles(state, pos) {
1319
+ const opacity = Math.max(OPACITY_FLOOR, 1 - pos * OPACITY_STEP);
1320
+ const yPx = pos * Y_DRIFT_PX;
1321
+ let border;
1322
+ let background;
1323
+ let color;
1324
+ let alignSelf;
1325
+ switch (state.kind) {
1326
+ case "settled":
1327
+ if (state.role === "user") {
1328
+ border = "var(--sc-content-bubble-border-user, 1px solid rgba(255, 255, 255, 0.28))";
1329
+ background = "var(--sc-content-bubble-background-user, rgba(255, 255, 255, 0.10))";
1330
+ alignSelf = "flex-end";
1331
+ } else {
1332
+ border = "var(--sc-content-bubble-border, 1px solid rgba(255, 255, 255, 0.18))";
1333
+ background = "var(--sc-content-bubble-background, rgba(20, 22, 24, 0.35))";
1334
+ alignSelf = "flex-start";
1335
+ }
1336
+ color = "var(--sc-tile-title-color, inherit)";
1337
+ break;
1338
+ case "streaming":
1339
+ case "pending":
1340
+ border = "var(--sc-content-bubble-border, 1px solid rgba(255, 255, 255, 0.18))";
1341
+ background = "var(--sc-content-bubble-background, rgba(20, 22, 24, 0.35))";
1342
+ color = "var(--sc-tile-title-color, inherit)";
1343
+ alignSelf = "flex-start";
1344
+ break;
1345
+ case "notice":
1346
+ border = "var(--sc-content-bubble-border-error, 1px solid rgba(255, 255, 255, 0.22))";
1347
+ background = "var(--sc-content-bubble-background-error, rgba(20, 22, 24, 0.28))";
1348
+ color = "var(--sc-content-bubble-text-error, rgba(255, 255, 255, 0.75))";
1349
+ alignSelf = state.role === "user" ? "flex-end" : "flex-start";
1350
+ break;
1351
+ }
1352
+ return {
1353
+ alignSelf,
1354
+ // Single shared width policy for EVERY bubble (user / assistant /
1355
+ // streaming / pending / error). Short and long replies now read as a
1356
+ // consistent column: `fit-content` sizes each bubble to its text, and
1357
+ // `TRAIL_MAX_WIDTH` caps the wide ones at the same ceiling — no more
1358
+ // per-role or per-path width drift (user feedback: chat tiles had
1359
+ // differing widths).
1360
+ maxWidth: TRAIL_MAX_WIDTH,
1361
+ width: "fit-content",
1362
+ // Bumped from 11px → 13px after the chat lid in production read as
1363
+ // cramped at the smaller size — body chat copy needs to be visitor-
1364
+ // legible on a phone without zoom, and 11px reads as a footnote
1365
+ // rather than the primary conversation surface. Code blocks inside
1366
+ // chip body scale proportionally (10px → 11px) below.
1367
+ fontSize: "13px",
1368
+ lineHeight: "1.45",
1369
+ padding: "5px 11px",
1370
+ borderRadius: "10px",
1371
+ border,
1372
+ background,
1373
+ // Frosted-glass bubbles are OPT-IN via the `--sc-content-bubble-blur`
1374
+ // knob (a host that wants glass sets e.g. `blur(8px)` deliberately); the
1375
+ // DEFAULT is `none`. A backdrop-filter on the bubble composites its OWN
1376
+ // text onto a GPU layer (grayscale AA + the blurred backdrop bleeding
1377
+ // under thin glyphs) — the resting-state compositing contract's
1378
+ // "backdrop-filter behind text" case: soft rasterization everywhere and
1379
+ // fully BLANKED text on some Android GPUs. Glass-by-default shipped that
1380
+ // hazard to every non-velvet surface, so the default flipped to `none`
1381
+ // (GPU/compositing hazard audit, Fix 3). Gate:
1382
+ // AdaptiveChatTrail.compositing.test.ts.
1383
+ backdropFilter: "var(--sc-content-bubble-blur, none)",
1384
+ WebkitBackdropFilter: "var(--sc-content-bubble-blur, none)",
1385
+ color,
1386
+ transition: "opacity 240ms cubic-bezier(0.22, 1, 0.36, 1), transform 240ms cubic-bezier(0.22, 1, 0.36, 1)",
1387
+ // Per-position opacity + y-drift falloff is still per-chip — those
1388
+ // operate naturally per element. Blur is NOT per-chip anymore: it's
1389
+ // applied via an absolute-positioned veil at the top of the scroll
1390
+ // viewport (TRAIL_BLUR_VEIL_PX above), so a single long message
1391
+ // doesn't get uniformly blurred across its full body.
1392
+ opacity: String(toFixedTrim(opacity)),
1393
+ transform: `translateY(${yPx}px)`,
1394
+ // User messages render as raw text (`html` template); pre-wrap preserves
1395
+ // newlines the visitor typed. Assistant messages render as markdown HTML
1396
+ // (`<p>` tags from marked); pre-wrap there would make marked's inter-tag
1397
+ // text nodes (the `\n` between/after `<p>` tags) visible, producing the
1398
+ // trailing/inter-paragraph blank lines visitors reported. Markdown's own
1399
+ // structure carries the paragraph breaks, so assistant chips use the
1400
+ // default `normal` whitespace handling.
1401
+ whiteSpace: (state.kind === "settled" || state.kind === "notice") && state.role === "user" ? "pre-wrap" : "normal",
1402
+ wordBreak: "break-word"
1403
+ };
1404
+ }
1405
+ function toolCallStripStyles() {
1406
+ return {
1407
+ display: "flex",
1408
+ flexWrap: "wrap",
1409
+ gap: "4px",
1410
+ marginTop: "4px",
1411
+ paddingTop: "4px",
1412
+ borderTop: "1px dashed var(--sc-content-divider-color, rgba(255, 255, 255, 0.12))"
1413
+ };
1414
+ }
1415
+ function pendingToolCallChipStyles() {
1416
+ return {
1417
+ display: "inline-flex",
1418
+ alignItems: "center",
1419
+ gap: "4px",
1420
+ padding: "2px 7px",
1421
+ border: "var(--sc-content-bubble-border-user, 1px solid rgba(255, 255, 255, 0.28))",
1422
+ background: "var(--sc-content-bubble-background-user, rgba(255, 255, 255, 0.10))",
1423
+ color: "var(--sc-tile-text-color, inherit)",
1424
+ borderRadius: "999px",
1425
+ fontSize: "10px",
1426
+ fontWeight: "500",
1427
+ fontFamily: "ui-monospace, SF Mono, Menlo, monospace",
1428
+ cursor: "pointer",
1429
+ opacity: "1"
1430
+ };
1431
+ }
1432
+ function deriveActivityReceipt(tc) {
1433
+ if (tc.name !== "primary_response_action") return null;
1434
+ const args = tc.args;
1435
+ if (!args || typeof args !== "object") return null;
1436
+ const templateId = typeof args.template_id === "string" ? args.template_id : "";
1437
+ const content = args.content ?? null;
1438
+ const title = content && typeof content.title === "string" && content.title.trim() ? content.title.trim() : "";
1439
+ const id = templateId.toLowerCase();
1440
+ let type;
1441
+ if (id.includes("faq")) type = "FAQ";
1442
+ else if (id.includes("chart") || id.includes("graph")) type = "chart";
1443
+ else if (id.includes("card") || id.includes("product")) type = "card";
1444
+ else if (id.includes("alert") || id.includes("navigation") || id.includes("nav")) type = "alert";
1445
+ else type = "added to your view";
1446
+ const label = title || templateId || "something";
1447
+ return { type, label };
1448
+ }
1449
+ function receiptText(r) {
1450
+ switch (r.type) {
1451
+ case "FAQ":
1452
+ return "Opened the FAQ for you";
1453
+ case "chart":
1454
+ return `Put together ${r.label} for you`;
1455
+ case "card":
1456
+ return "Pulled up the product for you";
1457
+ case "alert":
1458
+ return `Here's a shortcut: ${r.label}`;
1459
+ default:
1460
+ return `Pulled up ${r.label} for you`;
1461
+ }
1462
+ }
1463
+ function extractSuggestedReplies(toolCalls) {
1464
+ if (!toolCalls) return [];
1465
+ for (const tc of toolCalls) {
1466
+ if (tc.name !== "suggest_replies") continue;
1467
+ const replies = tc.args?.replies;
1468
+ if (!Array.isArray(replies)) return [];
1469
+ return replies.filter((r) => typeof r === "string" && r.trim().length > 0).map((r) => r.trim()).slice(0, 4);
1470
+ }
1471
+ return [];
1472
+ }
1473
+ function computeAssistantChipParts(m, hostReplies) {
1474
+ const toolCalls = m.toolCalls ?? [];
1475
+ return {
1476
+ pendingToolCalls: toolCalls.filter((tc) => tc.status === "pending"),
1477
+ receipts: toolCalls.map(deriveActivityReceipt).filter((r) => r !== null),
1478
+ // Threaded in by the render (empty for non-hosts): under pills-last the
1479
+ // hosting message can differ from the message that carried the tool call.
1480
+ replyChips: hostReplies
1481
+ };
1482
+ }
1483
+ function looksLikeSerializedModelResponse(text) {
1484
+ const t = text.trimStart();
1485
+ if (t.startsWith("(Empty response")) return true;
1486
+ let hits = 0;
1487
+ if (/['"]stop_reason['"]\s*:/.test(t)) hits++;
1488
+ if (/['"]signature['"]\s*:/.test(t)) hits++;
1489
+ if (/['"]type['"]\s*:\s*['"]thinking['"]/.test(t)) hits++;
1490
+ if (/['"]content['"]\s*:\s*\[/.test(t)) hits++;
1491
+ return hits >= 2;
1492
+ }
1493
+ function assistantChipRendersContent(m, hostReplies) {
1494
+ const text = (m.text ?? "").trim();
1495
+ if (text.length > 0 && !looksLikeSerializedModelResponse(text)) return true;
1496
+ const { pendingToolCalls, receipts, replyChips } = computeAssistantChipParts(m, hostReplies);
1497
+ return pendingToolCalls.length > 0 || receipts.length > 0 || replyChips.length > 0;
1498
+ }
1499
+ function inlineWidgetBlockStyles() {
1500
+ return {
1501
+ alignSelf: "stretch",
1502
+ width: "100%",
1503
+ minWidth: "0",
1504
+ marginTop: "6px",
1505
+ contain: "layout"
1506
+ };
1507
+ }
1508
+ function receiptStripStyles() {
1509
+ return {
1510
+ display: "flex",
1511
+ flexDirection: "column",
1512
+ gap: "2px",
1513
+ marginTop: "4px"
1514
+ };
1515
+ }
1516
+ function receiptStyles() {
1517
+ return {
1518
+ display: "inline-flex",
1519
+ alignItems: "center",
1520
+ gap: "5px",
1521
+ alignSelf: "flex-start",
1522
+ fontSize: "11px",
1523
+ lineHeight: "1.3",
1524
+ fontWeight: "500",
1525
+ letterSpacing: "0.01em",
1526
+ // Inherit the chip's OWN resolved text color (muted via opacity), so the
1527
+ // receipt is legible on every theme by construction. The old
1528
+ // `--sc-content-muted-color` fallback was rgba(255,255,255,…) — white —
1529
+ // invisible on light canvases; a `--sc-tile-text-color` token can likewise
1530
+ // be unset/mismatched on dark canvases. `currentColor` always matches the
1531
+ // chip text the visitor is already reading.
1532
+ color: "currentColor",
1533
+ opacity: "0.7"
1534
+ };
1535
+ }
1536
+ function receiptIconStyles() {
1537
+ return {
1538
+ display: "inline-flex",
1539
+ alignItems: "center",
1540
+ justifyContent: "center",
1541
+ width: "14px",
1542
+ height: "14px",
1543
+ borderRadius: "50%",
1544
+ fontSize: "9px",
1545
+ fontWeight: "700",
1546
+ color: "#fff",
1547
+ background: "hsl(var(--sc-accent-color, 145 40% 45%) / 0.7)"
1548
+ };
1549
+ }
1550
+ function replyChipStripStyles() {
1551
+ return {
1552
+ display: "flex",
1553
+ flexWrap: "wrap",
1554
+ gap: "6px",
1555
+ marginTop: "6px",
1556
+ alignSelf: "flex-start"
1557
+ };
1558
+ }
1559
+ function tileReceiptStripStyles() {
1560
+ return {
1561
+ display: "flex",
1562
+ flexDirection: "column",
1563
+ gap: "6px",
1564
+ marginTop: "6px",
1565
+ width: "100%",
1566
+ alignSelf: "stretch"
1567
+ };
1568
+ }
1569
+ function replyChipStyles() {
1570
+ return {
1571
+ display: "inline-flex",
1572
+ alignItems: "center",
1573
+ padding: "5px 12px",
1574
+ border: "1px solid hsl(var(--sc-accent-color, 145 40% 45%) / 0.40)",
1575
+ borderRadius: "9999px",
1576
+ background: "hsl(var(--sc-accent-color, 145 40% 45%) / 0.12)",
1577
+ color: "var(--sc-tile-text-color, currentColor)",
1578
+ font: "inherit",
1579
+ fontSize: "12px",
1580
+ fontWeight: "500",
1581
+ lineHeight: "1.2",
1582
+ cursor: "pointer",
1583
+ backdropFilter: "var(--sc-content-bubble-blur, none)",
1584
+ WebkitBackdropFilter: "var(--sc-content-bubble-blur, none)",
1585
+ transition: "background 150ms ease, transform 120ms ease"
1586
+ };
1587
+ }
1588
+ function alertReceiptIconStyles() {
1589
+ return {
1590
+ flex: "0 0 auto",
1591
+ width: "26px",
1592
+ height: "26px",
1593
+ borderRadius: "8px",
1594
+ display: "grid",
1595
+ placeItems: "center",
1596
+ background: "hsl(var(--sc-accent-color))",
1597
+ color: "var(--sc-accent-foreground, #fff)",
1598
+ fontSize: "14px"
1599
+ };
1600
+ }
1601
+ function tileReceiptChipStyles() {
1602
+ return {
1603
+ display: "flex",
1604
+ alignItems: "center",
1605
+ gap: "10px",
1606
+ width: "100%",
1607
+ textAlign: "left",
1608
+ cursor: "pointer",
1609
+ padding: "9px 10px 9px 11px",
1610
+ borderRadius: "12px",
1611
+ border: "1px solid hsl(var(--sc-accent-color) / 0.28)",
1612
+ background: "hsl(var(--sc-accent-color) / 0.06)"
1613
+ };
1614
+ }
1615
+ function tileReceiptGlyphStyles() {
1616
+ return { position: "relative", flex: "0 0 auto", width: "28px", height: "28px" };
1617
+ }
1618
+ function tileReceiptThumbStyles() {
1619
+ return {
1620
+ position: "absolute",
1621
+ inset: "0",
1622
+ width: "100%",
1623
+ height: "100%",
1624
+ objectFit: "cover",
1625
+ borderRadius: "7px",
1626
+ // Same hairline the glyph layers carry, so the thumb sits in the chip
1627
+ // with identical weight.
1628
+ boxShadow: "inset 0 0 0 1px hsl(var(--sc-accent-color) / 0.4)"
1629
+ };
1630
+ }
1631
+ function tileReceiptMonogramStyles() {
1632
+ return {
1633
+ position: "absolute",
1634
+ inset: "0",
1635
+ display: "flex",
1636
+ alignItems: "center",
1637
+ justifyContent: "center",
1638
+ borderRadius: "7px",
1639
+ // The shared stand-in (sdk-contracts image-standin): alpha sheen over the
1640
+ // brand primary, initial in the launcher's paired foreground token.
1641
+ background: IMAGE_STANDIN_BACKGROUND,
1642
+ color: "var(--sc-launcher-color, #ffffff)",
1643
+ font: "700 13px/1 var(--sc-font-family, inherit)",
1644
+ userSelect: "none"
1645
+ };
1646
+ }
1647
+ function receiptChipTitleStyles(_kind) {
1648
+ return {
1649
+ // Grows to fill the space between the icon and the action pill, truncating
1650
+ // long product names on one line (the chip is now single-line).
1651
+ flex: "1",
1652
+ minWidth: "0",
1653
+ fontSize: "13px",
1654
+ fontWeight: "650",
1655
+ lineHeight: "1.2",
1656
+ color: "var(--sc-content-text-color, #0f172a)",
1657
+ whiteSpace: "nowrap",
1658
+ overflow: "hidden",
1659
+ textOverflow: "ellipsis"
1660
+ };
1661
+ }
1662
+ function tileReceiptActionStyles(dismissed) {
1663
+ return {
1664
+ flex: "0 0 auto",
1665
+ fontSize: "11px",
1666
+ fontWeight: "700",
1667
+ borderRadius: "999px",
1668
+ padding: "4px 10px",
1669
+ ...dismissed ? { background: "hsl(var(--sc-accent-color) / 0.14)", color: "hsl(var(--sc-accent-color))" } : {
1670
+ background: "hsl(var(--sc-accent-color) / 0.9)",
1671
+ color: "var(--sc-accent-foreground, #fff)"
1672
+ }
1673
+ };
1674
+ }
1675
+ function caretStyles() {
1676
+ return {
1677
+ display: "inline-block",
1678
+ width: "6px",
1679
+ height: "12px",
1680
+ marginLeft: "3px",
1681
+ verticalAlign: "-1px",
1682
+ background: "hsl(var(--sc-accent-color) / 0.85)",
1683
+ borderRadius: "1px",
1684
+ animation: "syntro-trail-caret 1s steps(2, end) infinite"
1685
+ };
1686
+ }
1687
+ function thinkingSlotStyles() {
1688
+ return {
1689
+ alignSelf: "flex-start",
1690
+ color: "var(--sc-tile-title-color, inherit)",
1691
+ padding: "4px 4px",
1692
+ // The status sentence is `white-space: nowrap`, which makes this slot's
1693
+ // MIN-content width the full untruncated string. A flex item's default
1694
+ // `min-width: auto` refuses to shrink below that, so without these three
1695
+ // the composed line ("<persona> is buttoning up details to point the right
1696
+ // way…") pushes ~65px past a 320px trail instead of ellipsizing inside it.
1697
+ // `min-width: 0` unlocks shrinking, `max-width: 100%` caps it at the
1698
+ // trail's content box, and `box-sizing` keeps the 4px padding inside that
1699
+ // cap. Measured in chat-status-line-height.spec.ts.
1700
+ boxSizing: "border-box",
1701
+ maxWidth: "100%",
1702
+ minWidth: "0",
1703
+ animation: "syntro-trail-chip-enter 220ms cubic-bezier(0.22, 1, 0.36, 1) both"
1704
+ };
1705
+ }
1706
+ function thinkingDotsStyles() {
1707
+ return {
1708
+ display: "inline-flex",
1709
+ alignItems: "center",
1710
+ gap: "6px",
1711
+ // Line height matches what a single-line text chip would compute
1712
+ // to, so swapping out the slot for the streaming chip doesn't
1713
+ // shift the next chip up or down by a couple of pixels.
1714
+ lineHeight: "14px"
1715
+ };
1716
+ }
1717
+ function thinkingDotStyles(index) {
1718
+ return {
1719
+ display: "inline-block",
1720
+ width: "7px",
1721
+ height: "7px",
1722
+ borderRadius: "50%",
1723
+ background: "currentColor",
1724
+ // The dots are now the entire visual — no bubble framing them —
1725
+ // so the pulse has to do all the work the chip used to do. Scale
1726
+ // 0.4 → 1.0 with opacity 0.25 → 1.0 gives an unmistakable
1727
+ // breathing motion: at the trough each dot reads like a faint
1728
+ // ghost, at the peak like a solid dot. 940ms cycle is a touch
1729
+ // faster than the original 1.05s; without the chip frame the
1730
+ // shorter cycle feels purposeful instead of agitated. Stagger
1731
+ // 130ms = clear rolling cadence across all three.
1732
+ transformOrigin: "center",
1733
+ animation: `syntro-trail-thinking-pulse 940ms cubic-bezier(0.4, 0, 0.6, 1) ${index * 0.13}s infinite both`
1734
+ };
1735
+ }
1736
+ function moreStyles() {
1737
+ return {
1738
+ // Center horizontally regardless of the parent's display model.
1739
+ // ``alignSelf: center`` only works when the parent is a flex/grid
1740
+ // container with cross-axis alignment; the chat-bar's flex column
1741
+ // stretches us full-width instead. ``display: block`` + auto inline
1742
+ // margins centers the auto-sized button without needing the parent
1743
+ // to opt in.
1744
+ display: "block",
1745
+ marginLeft: "auto",
1746
+ marginRight: "auto",
1747
+ marginBottom: "6px",
1748
+ fontSize: "10px",
1749
+ fontWeight: "500",
1750
+ letterSpacing: "0.06em",
1751
+ padding: "4px 10px",
1752
+ border: "1px solid hsl(var(--sc-accent-color) / 0.32)",
1753
+ borderRadius: "999px",
1754
+ background: "hsl(var(--sc-accent-color) / 0.10)",
1755
+ color: "var(--sc-tile-text-color, currentColor)",
1756
+ cursor: "pointer",
1757
+ opacity: "0.85",
1758
+ backdropFilter: "var(--sc-content-bubble-blur, none)",
1759
+ WebkitBackdropFilter: "var(--sc-content-bubble-blur, none)",
1760
+ transition: "opacity 150ms ease, background 150ms ease"
1761
+ };
1762
+ }
1763
+ function stripTrailingWhitespace(text) {
1764
+ return text.replace(/[ \t]+$/gm, "").replace(/\n{3,}/g, "\n\n").trim();
1765
+ }
1766
+ function toFixedTrim(n) {
1767
+ const s = n.toFixed(2);
1768
+ return s.replace(/\.?0+$/, "") || "0";
1769
+ }
1770
+ var THINKING_TAIL_MAX = 120;
1771
+ function truncateThinkingTail(text) {
1772
+ const flat = text.replace(/\s+/g, " ").trim();
1773
+ if (flat.length <= THINKING_TAIL_MAX) return flat;
1774
+ return `\u2026${flat.slice(-THINKING_TAIL_MAX)}`;
1775
+ }
1776
+ function thinkingStatusLine(personaName, thinkingText) {
1777
+ const name = (personaName ?? "").trim();
1778
+ const phase = thinkingText ? truncateThinkingTail(thinkingText) : "";
1779
+ if (!name) return phase;
1780
+ if (!phase) return `${name} is typing\u2026`;
1781
+ const predicate = `${phase.charAt(0).toLowerCase()}${phase.slice(1)}`;
1782
+ const tail = /[.!?…]$/.test(predicate) ? "" : "\u2026";
1783
+ return `${name} is ${predicate}${tail}`;
1784
+ }
1785
+ function thinkingStatusRowStyles() {
1786
+ return {
1787
+ display: "flex",
1788
+ alignItems: "center",
1789
+ gap: "5px",
1790
+ maxWidth: "100%",
1791
+ minWidth: "0"
1792
+ };
1793
+ }
1794
+ function thinkingTextStyles() {
1795
+ return {
1796
+ fontStyle: "italic",
1797
+ opacity: "0.75",
1798
+ fontSize: "13px",
1799
+ lineHeight: "18px",
1800
+ maxWidth: "46ch",
1801
+ minWidth: "0",
1802
+ overflow: "hidden",
1803
+ textOverflow: "ellipsis",
1804
+ whiteSpace: "nowrap"
1805
+ };
1806
+ }
1807
+ function introSuggestionStyles() {
1808
+ return {
1809
+ display: "inline-flex",
1810
+ alignItems: "center",
1811
+ gap: "6px",
1812
+ marginTop: "4px",
1813
+ padding: "10px 18px",
1814
+ borderRadius: "9999px",
1815
+ border: "0",
1816
+ background: "linear-gradient(135deg, var(--sc-color-primary, #5faf7d) 0%, var(--sc-color-primary-hover, #3d8a5e) 100%)",
1817
+ color: "#fff",
1818
+ cursor: "pointer",
1819
+ font: "inherit",
1820
+ fontSize: "14px",
1821
+ fontWeight: "600",
1822
+ transition: "transform 120ms ease, filter 160ms ease"
1823
+ };
1824
+ }
1825
+ function emptyIconStyles() {
1826
+ return {
1827
+ display: "inline-flex",
1828
+ alignItems: "center",
1829
+ justifyContent: "center",
1830
+ width: "48px",
1831
+ height: "48px",
1832
+ borderRadius: "9999px",
1833
+ background: "var(--sc-tile-icon-background, linear-gradient(135deg, var(--sc-color-primary, #5faf7d) 0%, var(--sc-color-primary-hover, #3d8a5e) 100%))",
1834
+ color: "#fff",
1835
+ fontSize: "22px"
1836
+ };
1837
+ }
1838
+ function emptyTitleStyles() {
1839
+ return {
1840
+ margin: "0",
1841
+ fontSize: "15px",
1842
+ fontWeight: "600",
1843
+ lineHeight: "1.35",
1844
+ color: "var(--sc-content-text-color, inherit)",
1845
+ maxWidth: "32ch"
1846
+ };
1847
+ }
1848
+ if (!customElements.get("adaptive-chat-trail")) {
1849
+ customElements.define("adaptive-chat-trail", AdaptiveChatTrail);
1850
+ }
1851
+ function personaBylineStyles() {
1852
+ return {
1853
+ display: "flex",
1854
+ alignItems: "center",
1855
+ gap: "5px",
1856
+ fontSize: "0.68rem",
1857
+ fontWeight: "600",
1858
+ opacity: "0.65",
1859
+ marginBottom: "3px",
1860
+ lineHeight: "1"
1861
+ };
1862
+ }
1863
+ function personaAvatarStyles() {
1864
+ return {
1865
+ width: "16px",
1866
+ height: "16px",
1867
+ borderRadius: "9999px",
1868
+ objectFit: "cover",
1869
+ flexShrink: "0"
1870
+ };
1871
+ }
1872
+
1873
+ export {
1874
+ AdaptiveChatTrail,
1875
+ looksLikeSerializedModelResponse
1876
+ };
1877
+ //# sourceMappingURL=chunk-UFTSEIQP.js.map