@syntrologie/adapt-chatbot 2.54.0 → 2.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/AdaptiveChatBar.d.ts +15 -3
  2. package/dist/AdaptiveChatBar.d.ts.map +1 -1
  3. package/dist/AdaptiveChatBar.js +2 -2
  4. package/dist/AdaptiveChatBarMountable.d.ts.map +1 -1
  5. package/dist/AdaptiveChatTrail.d.ts +15 -0
  6. package/dist/AdaptiveChatTrail.d.ts.map +1 -1
  7. package/dist/AdaptiveChatTrail.follow-bottom.test.d.ts +2 -0
  8. package/dist/AdaptiveChatTrail.follow-bottom.test.d.ts.map +1 -0
  9. package/dist/AdaptiveChatTrail.js +1 -1
  10. package/dist/AdaptiveChipsStrip.js +2 -2
  11. package/dist/ChatAssistantLit.js +2 -2
  12. package/dist/ChatSession.d.ts +14 -1
  13. package/dist/ChatSession.d.ts.map +1 -1
  14. package/dist/ChatTransport.d.ts +27 -0
  15. package/dist/ChatTransport.d.ts.map +1 -1
  16. package/dist/LeaveAMessageLit.d.ts.map +1 -1
  17. package/dist/{chunk-IALBNWHO.js → chunk-57MPLQOL.js} +2 -2
  18. package/dist/{chunk-A36JVPEF.js → chunk-ETDHHQS3.js} +2 -2
  19. package/dist/{chunk-M7FVDW3M.js → chunk-LJEPZI6Y.js} +18 -2
  20. package/dist/chunk-LJEPZI6Y.js.map +7 -0
  21. package/dist/{chunk-2OS5KLNX.js → chunk-NGV35N4T.js} +99 -1
  22. package/dist/{chunk-2OS5KLNX.js.map → chunk-NGV35N4T.js.map} +4 -4
  23. package/dist/{chunk-74CVNFXS.js → chunk-VFWA5W56.js} +21 -28
  24. package/dist/{chunk-74CVNFXS.js.map → chunk-VFWA5W56.js.map} +2 -2
  25. package/dist/runtime.js +113 -23
  26. package/dist/runtime.js.map +2 -2
  27. package/dist/schema.js +2 -2
  28. package/package.json +1 -1
  29. package/dist/chunk-M7FVDW3M.js.map +0 -7
  30. /package/dist/{chunk-IALBNWHO.js.map → chunk-57MPLQOL.js.map} +0 -0
  31. /package/dist/{chunk-A36JVPEF.js.map → chunk-ETDHHQS3.js.map} +0 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/AdaptiveChatBar.ts", "../src/personas.ts"],
4
- "sourcesContent": ["/**\n * AdaptiveChatBar \u2014 the canvas lid composition for the chat-canvas\n * experience. Renders the bubble-up trail on top and an always-visible\n * input row below.\n *\n * Wiring contract (events out):\n * - `chat-message-sent` ({ text }): the user submitted text via Enter\n * or the send button. The owning code is responsible for pushing\n * this into the conversation history and producing a reply.\n *\n * Wiring contract (props in):\n * - `messages`: the conversation trail, passed straight through to\n * `<adaptive-chat-trail>`.\n * - `placeholder`: input placeholder copy. Defaults to a generic\n * \"Ask, find, or navigate\u2026\" \u2014 the host (typically the slot's lid\n * widget mount) can override to a context-specific string.\n *\n * Light DOM. Glassmorphism is applied at the input-row level so the\n * trail above floats over the host page without chrome. See PRD \u00A74.4.\n */\n\nimport { createDictation, type DictationController, isDictationSupported } from '@syntrologie/chat';\nimport { html, LitElement, nothing, svg } from 'lit';\nimport { styleMap } from 'lit/directives/style-map.js';\nimport { type ChatPersona, pickPersona } from './personas.js';\n\nimport './AdaptiveChatTrail.js';\nimport type {\n InlineTrailWidget,\n InlineWidgetMounter,\n TileReceipt,\n TrailMessage,\n} from './AdaptiveChatTrail.js';\nimport { getElementInstanceStore } from './elements/store-singleton.js';\n\nconst DEFAULT_PLACEHOLDER = 'Ask, find, or navigate\u2026';\nconst DEFAULT_ASSISTANT_NAME = 'Assistant';\n\nexport class AdaptiveChatBar extends LitElement {\n static override properties = {\n personas: { attribute: false },\n _persona: { state: true },\n messages: { attribute: false },\n placeholder: { type: String },\n assistantName: { type: String },\n greeting: { type: String },\n inFlight: { type: Boolean, reflect: true },\n thinkingText: { attribute: false },\n introSuggestion: { attribute: false },\n forceExpanded: { type: Boolean, reflect: true },\n maximized: { type: Boolean },\n tileReceipts: { attribute: false },\n inlineWidgets: { attribute: false },\n mountInlineWidget: { attribute: false },\n voiceInput: { type: Boolean },\n _input: { state: true },\n _composerFocused: { state: true },\n _micListening: { state: true },\n _micDisabled: { state: true },\n };\n\n messages: TrailMessage[] = [];\n maximized = false;\n tileReceipts: TileReceipt[] = [];\n /** Widgets rendered inline in the trail (reco carousel), sourced from\n * the store's `chat-inline` slot mounts. Forwarded to the trail. */\n inlineWidgets: InlineTrailWidget[] = [];\n /** Registry-backed mounter for inline widgets whose id is not their\n * custom-element tag (product card, comparison, FAQ). Set by the\n * mountable from `runtime.widgets`; forwarded to the trail. */\n mountInlineWidget: InlineWidgetMounter | undefined = undefined;\n\n /**\n * Live reasoning narration from the model \u2014 streamed in via\n * AG-UI THINKING_TEXT_MESSAGE_CONTENT events and forwarded by\n * the ChatSession subscription on the host (mountable).\n * Forwarded verbatim to the trail, which renders it in place of\n * the silent loading dots between turns. Empty string when no\n * thinking turn is active.\n */\n thinkingText = '';\n /**\n * Pre-conversation suggestion button \u2014 forwarded to the trail's\n * empty-state pane. When the visitor clicks it the trail emits\n * `trail-intro-suggestion`; we catch that here and submit the\n * configured prompt through the normal send path.\n */\n introSuggestion: { label: string; prompt: string } | undefined = undefined;\n\n /** Pass-through to {@link AdaptiveChatTrail}'s ``forceExpanded`` prop.\n * Hosts that mount the chat-bar in a full-screen surface (mobile\n * panel, agent app, etc.) set this to skip the collapse affordance\n * \u2014 there is no smaller state to fall back to. */\n forceExpanded = false;\n placeholder = DEFAULT_PLACEHOLDER;\n /**\n * Name shown in the (forceExpanded) header. Defaults to a generic\n * \"Assistant\". Hosts can override per-workspace (e.g. Vela sets\n * \"Vela Concierge\") without inventing new SDK surface \u2014 it threads\n * through the same mount config the placeholder does.\n */\n assistantName = DEFAULT_ASSISTANT_NAME;\n /** Persona roster from config (FEAT-1784917430); one is sticky-assigned. */\n personas: ChatPersona[] | null = null;\n /** @internal \u2014 the visitor's sticky persona, resolved from the roster. */\n _persona: ChatPersona | null = null;\n\n /**\n * Initial assistant message shown before any real conversation\n * starts. Renders inside the trail as a phantom assistant chip\n * when messages.length === 0; auto-disappears as soon as the\n * first user message lands. Mirrors ChatAssistantLit's greeting.\n */\n greeting: string | undefined = undefined;\n /**\n * Whether a chat round-trip is in flight (request sent, awaiting\n * reply). The single send/stop button morphs accordingly: \u2191 when\n * ready, \u23F9 when in-flight. Parent owns this state \u2014 set true after\n * `chat-message-sent` fires, false when the reply lands or the\n * request is aborted. Enter is inert while in-flight.\n */\n inFlight = false;\n /** Show a dictation mic when the Web Speech API is available. Config-gated, default off. */\n voiceInput = false;\n _input = '';\n _micListening = false;\n _micDisabled = false;\n private _micBase = '';\n private _micNetworkErrors = 0;\n private _dictation: DictationController | null = null;\n /** True while focus is inside the composer row \u2014 drives the accent\n * focus ring so the active text-entry surface is unmistakable on any\n * theme (the idle pill is deliberately subtle). */\n _composerFocused = false;\n\n override createRenderRoot(): HTMLElement {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n // Listen on the host element so that `tile-receipt-activate` events\n // dispatched directly on this element (as well as bubbling up from the\n // trail child) are handled in one place.\n this.addEventListener('tile-receipt-activate', this._onTileReceiptActivate);\n this.addEventListener('alert-receipt-activate', this._onAlertReceiptActivate);\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback();\n this.removeEventListener('tile-receipt-activate', this._onTileReceiptActivate);\n this.removeEventListener('alert-receipt-activate', this._onAlertReceiptActivate);\n }\n\n // Host-level styling (display, width, height, flex, etc.) is owned\n // by the container \u2014 SDK shadow root provides default rules, panel\n // overrides via design tokens (--sc-chat-bar-host-*). This adaptive\n // never sets inline styles on itself. See SmartCanvasElementLit's\n // static styles for the rule.\n\n private _onInput = (e: Event): void => {\n // Typing wins: the user taking over the input ends dictation so a\n // late transcript can't clobber their edit.\n if (this._micListening) this._dictation?.stop();\n this._input = (e.target as HTMLInputElement).value;\n };\n\n private _onMicClick = (): void => {\n if (this._micListening) {\n this._dictation?.stop();\n return;\n }\n this._micBase = this._input;\n this._dictation = createDictation({\n onTranscript: (text) => {\n this._micNetworkErrors = 0;\n this._input = this._micBase + (this._micBase ? ' ' : '') + text;\n },\n onEnd: () => {\n this._micListening = false;\n },\n onError: (code) => {\n this._micListening = false;\n if (code === 'not-allowed') this._micDisabled = true;\n if (code === 'network' && ++this._micNetworkErrors >= 2) this._micDisabled = true;\n },\n });\n this._dictation.start();\n // Mirror the controller: if start() bailed (support vanished, engine\n // threw), don't paint a listening state that can never end.\n this._micListening = this._dictation.listening;\n };\n\n private _onKeyDown = (e: KeyboardEvent): void => {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n // Enter is the send affordance \u2014 while in-flight there's nothing\n // to send, so it's a no-op. Interrupt requires the explicit\n // \u23F9 click to avoid surprise-aborting on a stray keystroke.\n if (this.inFlight) return;\n this._send();\n }\n };\n\n // Single button. Click delegates to send-or-interrupt based on\n // current state. Same DOM node, different behavior \u2014 discoverability\n // wins from the user not having to scan for which control is active.\n private _onSendOrInterrupt = (): void => {\n if (this.inFlight) {\n // Stop means stop: discard any chip tap queued during this run \u2014\n // otherwise the interrupt flips inFlight false and the queue flush\n // would fire a NEW turn the instant the user pressed stop.\n this._queuedSuggestedReply = null;\n this.dispatchEvent(new CustomEvent('chat-interrupt', { bubbles: true, composed: true }));\n return;\n }\n this._send();\n };\n\n private _onTileReceiptActivate = (e: Event): void => {\n const { instanceId } = (e as CustomEvent<{ instanceId: string }>).detail;\n getElementInstanceStore()?.recreate(instanceId);\n this.dispatchEvent(\n new CustomEvent('tile-recall', { detail: { instanceId }, bubbles: true, composed: true })\n );\n };\n\n private _onAlertReceiptActivate = (e: Event): void => {\n const { instanceId } = (e as CustomEvent<{ instanceId: string }>).detail;\n this.dispatchEvent(\n new CustomEvent('alert-recall', { detail: { instanceId }, bubbles: true, composed: true })\n );\n };\n\n private _onComposerFocusIn = (): void => {\n this._composerFocused = true;\n };\n\n private _onComposerFocusOut = (): void => {\n this._composerFocused = false;\n };\n\n private _onMaximizeToggle = (): void => {\n this.dispatchEvent(new CustomEvent('chat-maximize-toggle', { bubbles: true, composed: true }));\n };\n\n private _send(): void {\n // Sending supersedes dictation \u2014 stop first so a late final transcript\n // can't resurrect the just-sent text into the emptied input.\n if (this._micListening) this._dictation?.stop();\n const text = this._input.trim();\n if (!text) return;\n // A typed message is the user's NEWEST intent \u2014 it supersedes any chip\n // tap still queued from the run tail (defense: Enter is currently inert\n // while inFlight, but this guard must not depend on that staying true).\n this._queuedSuggestedReply = null;\n this.dispatchEvent(\n new CustomEvent<{ text: string }>('chat-message-sent', {\n detail: { text },\n bubbles: true,\n composed: true,\n })\n );\n this._input = '';\n // Drop the input value too \u2014 the input's bound value is `this._input`\n // via the render, but jsdom's two-way binding via property only takes\n // effect after the next render, so set it imperatively for the test\n // and for browser symmetry (most browsers do the right thing here\n // anyway because Lit re-renders on the state change).\n const input = this.querySelector<HTMLInputElement>('input[data-chat-input]');\n if (input) input.value = '';\n }\n\n /**\n * Visitor tapped the intro suggestion button in the empty-state\n * trail. Submit the configured prompt through the same channel as\n * a typed message so listeners (mountable \u2192 chatSession.send) see\n * a uniform shape \u2014 no need for a parallel \"suggestion-sent\" path.\n * The trail's intro pane auto-disappears the moment the first\n * message lands, so this also closes the empty state in one go.\n */\n private _onIntroSuggestion = (e: Event): void => {\n const detail = (e as CustomEvent<{ prompt: string }>).detail;\n if (!detail?.prompt) return;\n e.stopPropagation();\n if (this.inFlight) return;\n // Latch in-flight optimistically. `inFlight` is owned by the parent\n // mountable and only pushed back down after it handles\n // `chat-message-sent` + a Lit render cycle. Two taps in the same frame\n // both see inFlight===false otherwise, double-dispatching one intended\n // reply (C3). Set it true synchronously here; the parent confirms/owns\n // it from the next render and clears it when the reply lands.\n this.inFlight = true;\n this.dispatchEvent(\n new CustomEvent<{ text: string }>('chat-message-sent', {\n detail: { text: detail.prompt },\n bubbles: true,\n composed: true,\n })\n );\n };\n\n /**\n * Reply-chip tap that arrived during the run TAIL. Chips render the\n * moment the reply text lands, but `inFlight` (which includes\n * `runActive` \u2014 typing events, tool tail, RUN_FINISHED) can lag for\n * seconds. The old `if (this.inFlight) return;` guard silently ATE\n * those taps \u2014 the visitor's chosen reply never appeared as their\n * message (live repro, deployed vela 2026-07-08). Now the tap queues\n * (latest wins) and dispatches when `inFlight` clears.\n */\n private _queuedSuggestedReply: string | null = null;\n\n protected override willUpdate(changed: Map<string, unknown>): void {\n super.willUpdate?.(changed);\n if (changed.has('personas')) {\n this._persona = pickPersona(this.personas);\n }\n if (\n changed.has('inFlight') &&\n changed.get('inFlight') === true &&\n !this.inFlight &&\n this._queuedSuggestedReply\n ) {\n const text = this._queuedSuggestedReply;\n this._queuedSuggestedReply = null;\n // Same latch + channel as a direct tap.\n this.inFlight = true;\n this.dispatchEvent(\n new CustomEvent<{ text: string }>('chat-message-sent', {\n detail: { text },\n bubbles: true,\n composed: true,\n })\n );\n }\n }\n\n /**\n * Visitor tapped an inline `suggest_replies` chip in the trail. Submit\n * the chip text through the SAME `chat-message-sent` channel a typed\n * message uses, so the reply rides the existing send path (mountable \u2192\n * chatSession.send \u2192 transport) \u2014 no fabricated AG-UI events. The\n * trail clears the chips on its own once the reply lands and a newer\n * assistant turn becomes the latest.\n *\n * A tap while the previous run is still closing is QUEUED, never\n * dropped \u2014 see `_queuedSuggestedReply`.\n */\n private _onSuggestedReply = (e: Event): void => {\n const detail = (e as CustomEvent<{ text: string }>).detail;\n if (!detail?.text) return;\n e.stopPropagation();\n if (this.inFlight) {\n this._queuedSuggestedReply = detail.text;\n return;\n }\n // Latch in-flight optimistically \u2014 same race as `_onIntroSuggestion`\n // (C3). The parent owns `inFlight` and only re-asserts it after a render\n // cycle, so a same-frame double tap would otherwise fire two\n // `chat-message-sent` events for one chip.\n this.inFlight = true;\n this.dispatchEvent(\n new CustomEvent<{ text: string }>('chat-message-sent', {\n detail: { text: detail.text },\n bubbles: true,\n composed: true,\n })\n );\n };\n\n override render() {\n return html`\n <div style=${styleMap(rootStyles(this.forceExpanded))}>\n ${this.forceExpanded ? renderHeader(this.assistantName, this._persona) : nothing}\n <adaptive-chat-trail\n .messages=${this.messages}\n .greeting=${this._persona?.intro_line ?? this.greeting}\n .forceExpanded=${this.forceExpanded}\n .inFlight=${this.inFlight}\n .thinkingText=${this.thinkingText}\n .personaName=${this._persona?.name ?? ''}\n .personaAvatarSm=${this._persona?.avatar?.sm ?? ''}\n .introSuggestion=${this.introSuggestion}\n .tileReceipts=${this.tileReceipts}\n .inlineWidgets=${this.inlineWidgets}\n .mountInlineWidget=${this.mountInlineWidget}\n @trail-intro-suggestion=${this._onIntroSuggestion}\n @trail-suggested-reply=${this._onSuggestedReply}\n ></adaptive-chat-trail>\n <div\n style=${styleMap(rowStyles(this.forceExpanded, this._composerFocused))}\n data-chat-row\n @focusin=${this._onComposerFocusIn}\n @focusout=${this._onComposerFocusOut}\n >\n <button\n type=\"button\"\n data-chat-maximize\n @click=${this._onMaximizeToggle}\n aria-label=${this.maximized ? 'Exit full screen' : 'Maximize chat'}\n title=${this.maximized ? 'Exit full screen' : 'Maximize'}\n aria-pressed=${this.maximized ? 'true' : 'false'}\n style=${styleMap(maximizeStyles(this.maximized))}\n >${this.maximized ? MINIMIZE_ICON : MAXIMIZE_ICON}</button>\n <input\n data-chat-input\n type=\"text\"\n placeholder=${this._micListening ? 'Listening \u2014 tap the mic to stop' : this.placeholder}\n .value=${this._input}\n @input=${this._onInput}\n @keydown=${this._onKeyDown}\n style=${styleMap(inputStyles())}\n aria-label=\"Chat input\"\n />\n ${\n this.voiceInput && isDictationSupported()\n ? html`${MIC_RING_STYLES}<button\n type=\"button\"\n data-chat-mic\n data-listening=${this._micListening ? 'true' : 'false'}\n ?disabled=${this._micDisabled}\n @click=${this._onMicClick}\n aria-pressed=${this._micListening ? 'true' : 'false'}\n aria-label=${this._micListening ? 'Stop dictation' : 'Start dictation'}\n title=${this._micDisabled ? 'Microphone blocked' : this._micListening ? 'Stop dictation' : 'Dictate'}\n style=${styleMap(micStyles(this._micListening, this._micDisabled))}\n >${MIC_ICON}</button>`\n : nothing\n }\n <button\n type=\"button\"\n data-chat-send\n data-state=${this.inFlight ? 'in-flight' : 'ready'}\n @click=${this._onSendOrInterrupt}\n aria-label=${this.inFlight ? 'Stop' : 'Send message'}\n title=${this.inFlight ? 'Interrupt' : 'Send'}\n style=${styleMap(sendStyles(this.inFlight))}\n >${this.inFlight ? '\u25A0' : '\u2191'}</button>\n </div>\n </div>\n `;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Styles \u2014 kept inline (Lit styleMap) so they participate in light-DOM\n// rendering and pick up host CSS variables (e.g. --sc-tile-background).\n// ---------------------------------------------------------------------------\n\nfunction rootStyles(forceExpanded: boolean): Record<string, string> {\n // When the host expects the chat to fill its container (full-screen\n // panel, agent app), we grow vertically AND wrap the whole widget in\n // its own chrome card \u2014 trail + input read as one cohesive surface\n // instead of a naked trail above a chrome'd input. Floating mounts\n // (bottom-right pinned, etc.) stay chrome-less here because their\n // chrome is owned by the host's positioned wrapper.\n if (!forceExpanded) {\n return {\n display: 'flex',\n flexDirection: 'column',\n gap: '6px',\n width: '100%',\n pointerEvents: 'auto',\n };\n }\n // Input is ABSOLUTELY positioned at the bottom of this chrome card\n // (see rowStyles forceExpanded mode). That guarantees the input is\n // always visible \u2014 even when the lid region is tighter than the\n // chat-bar would naturally want. The flex column above holds\n // header + trail; padding-bottom reserves the input's footprint so\n // trail content can never overlap.\n //\n // Priority of \"what disappears when space is tight\":\n // Trail (shrinks to 0 first via flex: 1 1 0)\n // Header (can shrink via default flex: 0 1 auto)\n // Input (NEVER \u2014 it's positioned absolute, always at the bottom)\n return {\n position: 'relative',\n display: 'flex',\n flexDirection: 'column',\n gap: '8px',\n width: '100%',\n height: '100%',\n boxSizing: 'border-box',\n flex: '1 1 auto',\n minHeight: '0',\n overflow: 'hidden',\n pointerEvents: 'auto',\n background: 'var(--sc-tile-background, rgba(255, 255, 255, 0.92))',\n border: 'var(--sc-tile-border, 1px solid rgba(0, 0, 0, 0.06))',\n borderRadius: 'var(--sc-tile-border-radius, 14px)',\n boxShadow: 'var(--sc-tile-shadow, 0 12px 32px -16px rgba(0, 0, 0, 0.18))',\n // Reserves room for the absolute-positioned input pill (36px high\n // + 12px from bottom) plus a 16px visual gap above the input.\n padding: '0.75rem 0.75rem calc(36px + 1.75rem)',\n color: 'var(--sc-tile-title-color, inherit)',\n fontFamily: 'var(--sc-font-family, inherit)',\n // Override the trail's host-layout tokens so the trail (a flex item\n // of this column) fills a definite-height parent AND contributes its\n // content height in an indefinite one. basis AUTO (not 0) is\n // load-bearing: grow:1 still fills a full-screen panel, but basis auto\n // also lets the trail's content height propagate up the fill chain so a\n // SHRINK-WRAP host (velvet's bottom sheet) hugs the content with no void.\n // A basis-0 layer anywhere in the chain reports 0 min-content and\n // collapses the sheet to header+input. shrink:1 + min-height:0 still lets\n // the trail yield first when space is tight (input is absolute-pinned).\n // The SDK-level rule in SmartCanvasElementLit consumes these tokens on\n // adaptive-chat-trail's host element. Containers OWN host styling\n // via tokens; the adaptive never sets inline styles on its host.\n ['--sc-chat-trail-host-display']: 'flex',\n ['--sc-chat-trail-host-flex-direction']: 'column',\n ['--sc-chat-trail-host-flex']: '1 1 auto',\n ['--sc-chat-trail-host-min-height']: '0',\n ['--sc-chat-trail-host-overflow']: 'hidden',\n };\n}\n\n// Focus ring shared by both composer variants: while focus is inside the\n// row, the border switches to the accent so the active text-entry surface\n// is unmistakable regardless of theme.\nconst COMPOSER_FOCUS_BORDER = '1px solid hsl(var(--sc-accent-color) / 0.6)';\n\nfunction rowStyles(forceExpanded: boolean, focused: boolean): Record<string, string> {\n // forceExpanded \u2014 root container already carries the chrome; row\n // becomes a flat input-pill inside that chrome (subtle border + soft\n // background, no shadow/blur).\n if (forceExpanded) {\n // Absolute-pinned to the chrome card's bottom edge so it never\n // disappears under any container size. The chrome reserves\n // padding-bottom equal to this row's footprint so flex content\n // above (header + trail) can't overlap.\n //\n // The pill's tint derives from currentColor (the theme's text color),\n // NOT a hardcoded black alpha: rgba(0,0,0,.04) over a near-black card\n // is invisible \u2014 on dark themes visitors couldn't find the text entry\n // at all. color-mix keeps one declaration correct on every theme, and\n // the --sc-chat-composer-* tokens let a workspace override the tint.\n return {\n position: 'absolute',\n left: '0.75rem',\n right: '0.75rem',\n bottom: '0.75rem',\n display: 'flex',\n alignItems: 'center',\n gap: '8px',\n padding: '6px 14px',\n borderRadius: '9999px',\n background:\n 'var(--sc-chat-composer-background, color-mix(in srgb, currentColor 8%, transparent))',\n border: focused\n ? COMPOSER_FOCUS_BORDER\n : '1px solid var(--sc-chat-composer-border, color-mix(in srgb, currentColor 22%, transparent))',\n color: 'var(--sc-content-text-color, inherit)',\n fontFamily: 'var(--sc-font-family, inherit)',\n minHeight: '36px',\n };\n }\n // Floating mount \u2014 keeps the original tile-chrome look.\n return {\n display: 'flex',\n alignItems: 'center',\n gap: '8px',\n padding: '10px 12px',\n borderRadius: 'var(--sc-tile-border-radius, 14px)',\n background: 'var(--sc-tile-background, rgba(15, 19, 24, 0.6))',\n backdropFilter: 'var(--sc-chat-bar-blur, blur(24px) saturate(140%))',\n WebkitBackdropFilter: 'var(--sc-chat-bar-blur, blur(24px) saturate(140%))',\n border: focused\n ? COMPOSER_FOCUS_BORDER\n : 'var(--sc-tile-border, 1px solid rgba(255, 255, 255, 0.08))',\n boxShadow: 'var(--sc-tile-shadow, 0 2px 12px rgba(0, 0, 0, 0.3))',\n color: 'var(--sc-tile-title-color, #fafafa)',\n fontFamily: 'var(--sc-font-family, inherit)',\n };\n}\n\n// Maximize / restore toggle in the chat bar. Two hard-won legibility rules\n// live here:\n// 1. The glyph is an inline SVG, never a Unicode arrow (`\u2922`/`\u2921`): glyph\n// metrics vary per platform font, so the character rendered small and\n// visibly off-center inside the circular chip.\n// 2. The chip mirrors the send button's filled-accent language but at\n// 32px, so the expand/collapse control reads as a control \u2014 the ACTIVE\n// (maximized) state is a filled chip, idle is a ringed chip.\n// Expand: arrows pointing out to opposite corners (Lucide maximize-2).\nconst MIC_ICON = svg`<svg viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" fill=\"none\" aria-hidden=\"true\"><rect x=\"6\" y=\"1.5\" width=\"4\" height=\"8\" rx=\"2\" stroke=\"currentColor\" stroke-width=\"1.6\"></rect><path d=\"M3.5 7.5a4.5 4.5 0 0 0 9 0M8 12v2.5\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"></path></svg>`;\n\nfunction micStyles(listening: boolean, disabled: boolean): Record<string, string> {\n return {\n display: 'inline-flex',\n 'align-items': 'center',\n 'justify-content': 'center',\n width: '32px',\n height: '32px',\n border: 'none',\n 'border-radius': '50%',\n // Listening is a MODE: the mic flips to a filled primary chip with a\n // ripple ring (MIC_RING_STYLES) so recording is unmissable, and the\n // input placeholder says how to end it.\n background: listening ? 'var(--sc-color-primary, #6a59ce)' : 'transparent',\n color: listening ? 'var(--sc-color-primary-text, #ffffff)' : 'currentColor',\n opacity: disabled ? '0.3' : listening ? '1' : '0.65',\n cursor: disabled ? 'not-allowed' : 'pointer',\n transition: 'color 150ms ease, background 150ms ease, opacity 150ms ease',\n 'flex-shrink': '0',\n };\n}\n\n// Ripple keyframes can't ride styleMap (light DOM, no stylesheet of our\n// own) \u2014 rendered once next to the mic button. Scoped to [data-chat-mic]\n// inside the canvas shadow root, so nothing leaks to the host page.\nconst MIC_RING_STYLES = html`<style>\n [data-chat-mic][data-listening='true'] {\n animation: sc-chatbar-mic-ring 1.5s ease-out infinite;\n }\n @keyframes sc-chatbar-mic-ring {\n 0% {\n box-shadow: 0 0 0 0 color-mix(in srgb, var(--sc-color-primary, #6a59ce) 45%, transparent);\n transform: scale(1);\n }\n 50% { transform: scale(1.08); }\n 70% { box-shadow: 0 0 0 12px transparent; }\n 100% {\n box-shadow: 0 0 0 0 transparent;\n transform: scale(1);\n }\n }\n @media (prefers-reduced-motion: reduce) {\n [data-chat-mic][data-listening='true'] {\n animation: none;\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--sc-color-primary, #6a59ce) 35%, transparent);\n }\n }\n</style>`;\n\nconst MAXIMIZE_ICON = svg`<svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.25\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><polyline points=\"15 3 21 3 21 9\"></polyline><polyline points=\"9 21 3 21 3 15\"></polyline><line x1=\"21\" y1=\"3\" x2=\"14\" y2=\"10\"></line><line x1=\"3\" y1=\"21\" x2=\"10\" y2=\"14\"></line></svg>`;\n\n// Collapse: arrows pointing in toward the center (Lucide minimize-2).\nconst MINIMIZE_ICON = svg`<svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.25\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><polyline points=\"4 14 10 14 10 20\"></polyline><polyline points=\"20 10 14 10 14 4\"></polyline><line x1=\"14\" y1=\"10\" x2=\"21\" y2=\"3\"></line><line x1=\"3\" y1=\"21\" x2=\"10\" y2=\"14\"></line></svg>`;\n\nfunction maximizeStyles(maximized: boolean): Record<string, string> {\n return {\n display: 'inline-flex',\n alignItems: 'center',\n justifyContent: 'center',\n width: '32px',\n height: '32px',\n borderRadius: '9999px',\n // Active (maximized): filled accent chip. Idle: subtle accent chip with a\n // visible ring. Colour is full-opacity in both states (no 0.95 ghosting).\n background: maximized\n ? 'hsl(var(--sc-accent-color) / 0.85)'\n : 'hsl(var(--sc-accent-color) / 0.15)',\n border: maximized ? 'none' : '1px solid hsl(var(--sc-accent-color) / 0.45)',\n color: maximized ? 'var(--sc-accent-foreground, #fff)' : 'hsl(var(--sc-accent-color))',\n flexShrink: '0',\n lineHeight: '1',\n padding: '0',\n cursor: 'pointer',\n transition: 'background 150ms ease, color 150ms ease, border-color 150ms ease',\n };\n}\n\nfunction inputStyles(): Record<string, string> {\n return {\n flex: '1',\n minWidth: '0',\n background: 'transparent',\n border: 'none',\n outline: 'none',\n color: 'inherit',\n // MUST be >= 16px. iOS Safari auto-zooms the page when a focused input has a\n // font-size below 16px, then does NOT zoom back out on blur \u2014 so the composer\n // zoomed in, the input ended up behind the keyboard (the zoom breaks the\n // visualViewport keyboard-lift math), and tapping \"Done\" left the whole site\n // stuck zoomed in. 16px is the standard, accessibility-safe fix (no\n // user-scalable=no / maximum-scale hacks that would kill pinch-zoom).\n fontSize: '16px',\n // WCAG 2.2 AA, 2.5.8 \u2014 every pointer target is at least 24x24 CSS px, and\n // an input's own box is what gets measured, not the bar it sits inside\n // (BUG-1787284957: the manual keyboard walk measured 278x20 while the bar\n // around it looked perfectly comfortable). A bare 16px input's line box is\n // ~20px, so the floor has to be declared.\n //\n // Painted height is UNCHANGED in both composer variants: the pinned pill\n // is already `min-height: 36px` with 6px block padding (24 + 12 = 36,\n // exactly the floor it was already holding), and the floating row's\n // tallest child is the 32px mic.\n //\n // The fallback is what carries the guarantee, and only for the UNSET case:\n // a workspace that never publishes --sc-chat-input-min-height gets 24px and\n // meets AA. A workspace that DOES publish it can set anything, including a\n // value below 24 \u2014 CSS has no floor to enforce here. Catching that belongs\n // at authoring time, in `validate-config.mjs` alongside the contrast\n // warnings, per the repo's validators-at-authoring-time law; tracked\n // follow-up, not something this declaration can promise.\n minHeight: 'var(--sc-chat-input-min-height, 24px)',\n };\n}\n\nfunction sendStyles(inFlight: boolean): Record<string, string> {\n return {\n width: '28px',\n height: '28px',\n display: 'inline-flex',\n alignItems: 'center',\n justifyContent: 'center',\n borderRadius: '9999px',\n // Ready: filled accent (send is the affordance). In-flight: hollow\n // ring + accent ring (stop is a one-of-a-kind disruptive action,\n // styled differently so the user reads \"this isn't just send\").\n background: inFlight ? 'transparent' : 'hsl(var(--sc-accent-color) / 0.85)',\n color: inFlight ? 'hsl(var(--sc-accent-color) / 0.95)' : 'var(--sc-accent-foreground, #fff)',\n border: inFlight ? '1px solid hsl(var(--sc-accent-color) / 0.55)' : 'none',\n cursor: 'pointer',\n flexShrink: '0',\n transition: 'background 150ms ease, color 150ms ease, border-color 150ms ease',\n };\n}\n\n/**\n * Compact single-line chat header. Prior version stacked the name over a\n * two-line status block inside a flex-column wrapper with a 28px avatar and\n * `padding: 4px 4px 12px`, which wasted a lot of vertical space (user\n * feedback: \"bad use of vertical space\"). This is now ONE inline row \u2014\n * small avatar dot \u00B7 name \u00B7 live-status dot \u00B7 short status \u2014 at roughly half\n * the height. All surfaces share this chrome.\n */\nfunction renderHeader(assistantName: string, persona: ChatPersona | null) {\n const headerStyles: Record<string, string> = {\n // Container-owned visibility: velvet's collapsed strip (V3 tight +\n // deck focus) sets --sc-chat-bar-header-display: none so the strip is\n // JUST the text entry (the header painted over the absolute-pinned\n // input when the region was clamped to the strip height). Expanded\n // chat keeps the header \u2014 the AI-disclosure microline must stay\n // wherever a conversation is actually visible.\n display: 'var(--sc-chat-bar-header-display, flex)',\n alignItems: 'center',\n gap: '7px',\n // Halved vertical footprint: 6px total block padding vs the old 16px\n // (4px top + 12px bottom). Horizontal padding unchanged.\n padding: '3px 4px',\n borderBottom: '1px solid var(--sc-content-divider-color, rgba(0, 0, 0, 0.08))',\n flex: '0 0 auto',\n };\n // Small brand dot \u2014 shrunk from a 28px avatar to a 16px chip so the\n // whole header collapses to a single text line. Still tracks the\n // workspace brand primary so it matches the rest of the canvas chrome.\n const avatar: Record<string, string> = {\n display: 'inline-flex',\n alignItems: 'center',\n justifyContent: 'center',\n width: '16px',\n height: '16px',\n borderRadius: '9999px',\n background:\n 'var(--sc-tile-icon-background, linear-gradient(135deg, var(--sc-color-primary, #5faf7d) 0%, var(--sc-color-primary-hover, #3d8a5e) 100%))',\n color: '#fff',\n flexShrink: '0',\n fontSize: '9px',\n lineHeight: '1',\n };\n const name: Record<string, string> = {\n fontSize: '0.8rem',\n fontWeight: '600',\n color: 'var(--sc-content-text-color, inherit)',\n lineHeight: '1.2',\n flexShrink: '0',\n };\n // No status line or presence dot (\"Online \u00B7 understands your session\") \u2014\n // user feedback: that's chrome a visitor doesn't need. Avatar + name only.\n // Persona mode (FEAT-1784917430): the brand dot becomes the persona's face,\n // the name becomes the persona's name, and an explicit \"AI concierge\"\n // microline rides alongside \u2014 a photoreal face + human name must never\n // imply a human agent (Moffatt/R-001 lineage, CA B.O.T./EU AI Act).\n const personaImg: Record<string, string> = {\n width: '22px',\n height: '22px',\n borderRadius: '9999px',\n objectFit: 'cover',\n flexShrink: '0',\n };\n const disclosure: Record<string, string> = {\n fontSize: '0.62rem',\n fontWeight: '500',\n color: 'var(--sc-content-text-color, inherit)',\n opacity: '0.55',\n lineHeight: '1.2',\n flexShrink: '0',\n };\n return html`\n <div style=${styleMap(headerStyles)} data-chat-header>\n ${\n persona?.avatar?.md || persona?.avatar?.sm\n ? html`<img\n style=${styleMap(personaImg)}\n src=${persona.avatar.md ?? persona.avatar.sm ?? ''}\n alt=\"\"\n data-persona-avatar\n />`\n : html`<span style=${styleMap(avatar)} aria-hidden=\"true\">\u2726</span>`\n }\n <span style=${styleMap(name)} data-persona-name>${persona?.name ?? assistantName}</span>\n ${persona ? html`<span style=${styleMap(disclosure)} data-persona-disclosure>\u00B7 AI concierge</span>` : nothing}\n </div>\n `;\n}\n\nif (!customElements.get('adaptive-chat-bar')) {\n customElements.define('adaptive-chat-bar', AdaptiveChatBar);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'adaptive-chat-bar': AdaptiveChatBar;\n }\n}\n", "/**\n * Chat personas \u2014 the \"human face\" of the concierge (FEAT-1784917430).\n *\n * A workspace ships a persona ROSTER in the chat-bar's props (plain config,\n * like assistantName). Assignment is 50:50 sticky-random per visitor: first\n * visit picks uniformly, the pick persists in localStorage, and every later\n * session reuses it \u2014 consistency reads as a real team member, not a slot\n * machine. An invalid/vanished persisted id re-picks (roster changes are\n * config changes; visitors must never be stranded on a ghost).\n *\n * The persona never claims to be human: the header carries an explicit\n * \"AI concierge\" microline (see renderHeader) and the server threads the\n * no-human-claim rule into the prompt (Moffatt/R-001 lineage).\n */\n\nexport interface PersonaAvatar {\n sm?: string;\n md?: string;\n lg?: string;\n}\n\nexport interface ChatPersona {\n id: string;\n name: string;\n role_title?: string;\n pronouns?: string;\n avatar?: PersonaAvatar;\n intro_line?: string;\n}\n\nconst STORAGE_KEY = 'syntro:persona';\n\n/** Pick (or recall) the visitor's sticky persona from the roster. */\nexport function pickPersona(roster: ChatPersona[] | undefined | null): ChatPersona | null {\n if (!Array.isArray(roster) || roster.length === 0) return null;\n let storedId: string | null = null;\n try {\n storedId = localStorage.getItem(STORAGE_KEY);\n } catch {\n // Storage unavailable (private mode) \u2014 session-scoped random pick.\n }\n const stored = storedId ? roster.find((p) => p.id === storedId) : undefined;\n if (stored) return stored;\n const pick = roster[Math.floor(Math.random() * roster.length)]!;\n try {\n localStorage.setItem(STORAGE_KEY, pick.id);\n } catch {\n /* non-persistent pick is fine */\n }\n return pick;\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * AdaptiveChatBar \u2014 the canvas lid composition for the chat-canvas\n * experience. Renders the bubble-up trail on top and an always-visible\n * input row below.\n *\n * Wiring contract (events out):\n * - `chat-message-sent` ({ text, via, turn }): the user submitted text\n * via Enter, the send button, an intro suggestion, or a reply pill.\n * The owning code is responsible for pushing this into the\n * conversation history and producing a reply.\n *\n * Wiring contract (props in):\n * - `messages`: the conversation trail, passed straight through to\n * `<adaptive-chat-trail>`.\n * - `placeholder`: input placeholder copy. Defaults to a generic\n * \"Ask, find, or navigate\u2026\" \u2014 the host (typically the slot's lid\n * widget mount) can override to a context-specific string.\n *\n * Light DOM. Glassmorphism is applied at the input-row level so the\n * trail above floats over the host page without chrome. See PRD \u00A74.4.\n */\n\nimport { createDictation, type DictationController, isDictationSupported } from '@syntrologie/chat';\nimport type { ChatSendVia } from '@syntrologie/sdk-contracts';\nimport { html, LitElement, nothing, svg } from 'lit';\nimport { styleMap } from 'lit/directives/style-map.js';\nimport { type ChatPersona, pickPersona } from './personas.js';\n\nimport './AdaptiveChatTrail.js';\nimport type {\n InlineTrailWidget,\n InlineWidgetMounter,\n TileReceipt,\n TrailMessage,\n} from './AdaptiveChatTrail.js';\nimport { getElementInstanceStore } from './elements/store-singleton.js';\n\nconst DEFAULT_PLACEHOLDER = 'Ask, find, or navigate\u2026';\nconst DEFAULT_ASSISTANT_NAME = 'Assistant';\n\nexport class AdaptiveChatBar extends LitElement {\n static override properties = {\n personas: { attribute: false },\n _persona: { state: true },\n messages: { attribute: false },\n placeholder: { type: String },\n assistantName: { type: String },\n greeting: { type: String },\n inFlight: { type: Boolean, reflect: true },\n turn: { type: Number },\n thinkingText: { attribute: false },\n introSuggestion: { attribute: false },\n forceExpanded: { type: Boolean, reflect: true },\n maximized: { type: Boolean },\n tileReceipts: { attribute: false },\n inlineWidgets: { attribute: false },\n mountInlineWidget: { attribute: false },\n voiceInput: { type: Boolean },\n _input: { state: true },\n _composerFocused: { state: true },\n _micListening: { state: true },\n _micDisabled: { state: true },\n };\n\n messages: TrailMessage[] = [];\n /**\n * Mirrored from the chat session by the mountable. The bar keeps no\n * counter of its own \u2014 this is the number of sends already committed;\n * a send this bar makes opens turn `this.turn + 1`.\n */\n turn = 0;\n maximized = false;\n tileReceipts: TileReceipt[] = [];\n /** Widgets rendered inline in the trail (reco carousel), sourced from\n * the store's `chat-inline` slot mounts. Forwarded to the trail. */\n inlineWidgets: InlineTrailWidget[] = [];\n /** Registry-backed mounter for inline widgets whose id is not their\n * custom-element tag (product card, comparison, FAQ). Set by the\n * mountable from `runtime.widgets`; forwarded to the trail. */\n mountInlineWidget: InlineWidgetMounter | undefined = undefined;\n\n /**\n * Live reasoning narration from the model \u2014 streamed in via\n * AG-UI THINKING_TEXT_MESSAGE_CONTENT events and forwarded by\n * the ChatSession subscription on the host (mountable).\n * Forwarded verbatim to the trail, which renders it in place of\n * the silent loading dots between turns. Empty string when no\n * thinking turn is active.\n */\n thinkingText = '';\n /**\n * Pre-conversation suggestion button \u2014 forwarded to the trail's\n * empty-state pane. When the visitor clicks it the trail emits\n * `trail-intro-suggestion`; we catch that here and submit the\n * configured prompt through the normal send path.\n */\n introSuggestion: { label: string; prompt: string } | undefined = undefined;\n\n /** Pass-through to {@link AdaptiveChatTrail}'s ``forceExpanded`` prop.\n * Hosts that mount the chat-bar in a full-screen surface (mobile\n * panel, agent app, etc.) set this to skip the collapse affordance\n * \u2014 there is no smaller state to fall back to. */\n forceExpanded = false;\n placeholder = DEFAULT_PLACEHOLDER;\n /**\n * Name shown in the (forceExpanded) header. Defaults to a generic\n * \"Assistant\". Hosts can override per-workspace (e.g. Vela sets\n * \"Vela Concierge\") without inventing new SDK surface \u2014 it threads\n * through the same mount config the placeholder does.\n */\n assistantName = DEFAULT_ASSISTANT_NAME;\n /** Persona roster from config (FEAT-1784917430); one is sticky-assigned. */\n personas: ChatPersona[] | null = null;\n /** @internal \u2014 the visitor's sticky persona, resolved from the roster. */\n _persona: ChatPersona | null = null;\n\n /**\n * Initial assistant message shown before any real conversation\n * starts. Renders inside the trail as a phantom assistant chip\n * when messages.length === 0; auto-disappears as soon as the\n * first user message lands. Mirrors ChatAssistantLit's greeting.\n */\n greeting: string | undefined = undefined;\n /**\n * Whether a chat round-trip is in flight (request sent, awaiting\n * reply). The single send/stop button morphs accordingly: \u2191 when\n * ready, \u23F9 when in-flight. Parent owns this state \u2014 set true after\n * `chat-message-sent` fires, false when the reply lands or the\n * request is aborted. Enter is inert while in-flight.\n */\n inFlight = false;\n /** Show a dictation mic when the Web Speech API is available. Config-gated, default off. */\n voiceInput = false;\n _input = '';\n _micListening = false;\n _micDisabled = false;\n private _micBase = '';\n private _micNetworkErrors = 0;\n private _dictation: DictationController | null = null;\n /** True while focus is inside the composer row \u2014 drives the accent\n * focus ring so the active text-entry surface is unmistakable on any\n * theme (the idle pill is deliberately subtle). */\n _composerFocused = false;\n\n override createRenderRoot(): HTMLElement {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n // Listen on the host element so that `tile-receipt-activate` events\n // dispatched directly on this element (as well as bubbling up from the\n // trail child) are handled in one place.\n this.addEventListener('tile-receipt-activate', this._onTileReceiptActivate);\n this.addEventListener('alert-receipt-activate', this._onAlertReceiptActivate);\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback();\n this.removeEventListener('tile-receipt-activate', this._onTileReceiptActivate);\n this.removeEventListener('alert-receipt-activate', this._onAlertReceiptActivate);\n }\n\n // Host-level styling (display, width, height, flex, etc.) is owned\n // by the container \u2014 SDK shadow root provides default rules, panel\n // overrides via design tokens (--sc-chat-bar-host-*). This adaptive\n // never sets inline styles on itself. See SmartCanvasElementLit's\n // static styles for the rule.\n\n private _onInput = (e: Event): void => {\n // Typing wins: the user taking over the input ends dictation so a\n // late transcript can't clobber their edit.\n if (this._micListening) this._dictation?.stop();\n this._input = (e.target as HTMLInputElement).value;\n };\n\n private _onMicClick = (): void => {\n if (this._micListening) {\n this._dictation?.stop();\n return;\n }\n this._micBase = this._input;\n this._dictation = createDictation({\n onTranscript: (text) => {\n this._micNetworkErrors = 0;\n this._input = this._micBase + (this._micBase ? ' ' : '') + text;\n },\n onEnd: () => {\n this._micListening = false;\n },\n onError: (code) => {\n this._micListening = false;\n if (code === 'not-allowed') this._micDisabled = true;\n if (code === 'network' && ++this._micNetworkErrors >= 2) this._micDisabled = true;\n },\n });\n this._dictation.start();\n // Mirror the controller: if start() bailed (support vanished, engine\n // threw), don't paint a listening state that can never end.\n this._micListening = this._dictation.listening;\n };\n\n private _onKeyDown = (e: KeyboardEvent): void => {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n // Enter is the send affordance \u2014 while in-flight there's nothing\n // to send, so it's a no-op. Interrupt requires the explicit\n // \u23F9 click to avoid surprise-aborting on a stray keystroke.\n if (this.inFlight) return;\n this._send();\n }\n };\n\n // Single button. Click delegates to send-or-interrupt based on\n // current state. Same DOM node, different behavior \u2014 discoverability\n // wins from the user not having to scan for which control is active.\n private _onSendOrInterrupt = (): void => {\n if (this.inFlight) {\n // Stop means stop: discard any chip tap queued during this run \u2014\n // otherwise the interrupt flips inFlight false and the queue flush\n // would fire a NEW turn the instant the user pressed stop.\n this._queuedSuggestedReply = null;\n this.dispatchEvent(new CustomEvent('chat-interrupt', { bubbles: true, composed: true }));\n return;\n }\n this._send();\n };\n\n private _onTileReceiptActivate = (e: Event): void => {\n const { instanceId } = (e as CustomEvent<{ instanceId: string }>).detail;\n getElementInstanceStore()?.recreate(instanceId);\n this.dispatchEvent(\n new CustomEvent('tile-recall', { detail: { instanceId }, bubbles: true, composed: true })\n );\n };\n\n private _onAlertReceiptActivate = (e: Event): void => {\n const { instanceId } = (e as CustomEvent<{ instanceId: string }>).detail;\n this.dispatchEvent(\n new CustomEvent('alert-recall', { detail: { instanceId }, bubbles: true, composed: true })\n );\n };\n\n private _onComposerFocusIn = (): void => {\n this._composerFocused = true;\n };\n\n private _onComposerFocusOut = (): void => {\n this._composerFocused = false;\n };\n\n private _onMaximizeToggle = (): void => {\n this.dispatchEvent(new CustomEvent('chat-maximize-toggle', { bubbles: true, composed: true }));\n };\n\n /** The one channel every send path uses; `turn` is the turn this send opens. */\n private _dispatchSent(text: string, via: ChatSendVia): void {\n this.dispatchEvent(\n new CustomEvent<{ text: string; via: ChatSendVia; turn: number }>('chat-message-sent', {\n detail: { text, via, turn: this.turn + 1 },\n bubbles: true,\n composed: true,\n })\n );\n }\n\n private _send(): void {\n // Sending supersedes dictation \u2014 stop first so a late final transcript\n // can't resurrect the just-sent text into the emptied input.\n if (this._micListening) this._dictation?.stop();\n const text = this._input.trim();\n if (!text) return;\n // A typed message is the user's NEWEST intent \u2014 it supersedes any chip\n // tap still queued from the run tail (defense: Enter is currently inert\n // while inFlight, but this guard must not depend on that staying true).\n this._queuedSuggestedReply = null;\n this._dispatchSent(text, 'typed');\n this._input = '';\n // Drop the input value too \u2014 the input's bound value is `this._input`\n // via the render, but jsdom's two-way binding via property only takes\n // effect after the next render, so set it imperatively for the test\n // and for browser symmetry (most browsers do the right thing here\n // anyway because Lit re-renders on the state change).\n const input = this.querySelector<HTMLInputElement>('input[data-chat-input]');\n if (input) input.value = '';\n }\n\n /**\n * Visitor tapped the intro suggestion button in the empty-state\n * trail. Submit the configured prompt through the same channel as\n * a typed message so listeners (mountable \u2192 chatSession.send) see\n * a uniform shape \u2014 no need for a parallel \"suggestion-sent\" path.\n * The trail's intro pane auto-disappears the moment the first\n * message lands, so this also closes the empty state in one go.\n */\n private _onIntroSuggestion = (e: Event): void => {\n const detail = (e as CustomEvent<{ prompt: string }>).detail;\n if (!detail?.prompt) return;\n e.stopPropagation();\n if (this.inFlight) return;\n // Latch in-flight optimistically. `inFlight` is owned by the parent\n // mountable and only pushed back down after it handles\n // `chat-message-sent` + a Lit render cycle. Two taps in the same frame\n // both see inFlight===false otherwise, double-dispatching one intended\n // reply (C3). Set it true synchronously here; the parent confirms/owns\n // it from the next render and clears it when the reply lands.\n this.inFlight = true;\n this._dispatchSent(detail.prompt, 'intro_suggestion');\n };\n\n /**\n * Reply-chip tap that arrived during the run TAIL. Chips render the\n * moment the reply text lands, but `inFlight` (which includes\n * `runActive` \u2014 typing events, tool tail, RUN_FINISHED) can lag for\n * seconds. The old `if (this.inFlight) return;` guard silently ATE\n * those taps \u2014 the visitor's chosen reply never appeared as their\n * message (live repro, deployed vela 2026-07-08). Now the tap queues\n * (latest wins) and dispatches when `inFlight` clears.\n */\n private _queuedSuggestedReply: string | null = null;\n\n protected override willUpdate(changed: Map<string, unknown>): void {\n super.willUpdate?.(changed);\n if (changed.has('personas')) {\n this._persona = pickPersona(this.personas);\n }\n if (\n changed.has('inFlight') &&\n changed.get('inFlight') === true &&\n !this.inFlight &&\n this._queuedSuggestedReply\n ) {\n const text = this._queuedSuggestedReply;\n this._queuedSuggestedReply = null;\n // Same latch + channel as a direct tap.\n this.inFlight = true;\n this._dispatchSent(text, 'pill');\n }\n }\n\n /**\n * Visitor tapped an inline `suggest_replies` chip in the trail. Submit\n * the chip text through the SAME `chat-message-sent` channel a typed\n * message uses, so the reply rides the existing send path (mountable \u2192\n * chatSession.send \u2192 transport) \u2014 no fabricated AG-UI events. The\n * trail clears the chips on its own once the reply lands and a newer\n * assistant turn becomes the latest.\n *\n * A tap while the previous run is still closing is QUEUED, never\n * dropped \u2014 see `_queuedSuggestedReply`.\n */\n private _onSuggestedReply = (e: Event): void => {\n const detail = (e as CustomEvent<{ text: string }>).detail;\n if (!detail?.text) return;\n e.stopPropagation();\n if (this.inFlight) {\n this._queuedSuggestedReply = detail.text;\n return;\n }\n // Latch in-flight optimistically \u2014 same race as `_onIntroSuggestion`\n // (C3). The parent owns `inFlight` and only re-asserts it after a render\n // cycle, so a same-frame double tap would otherwise fire two\n // `chat-message-sent` events for one chip.\n this.inFlight = true;\n this._dispatchSent(detail.text, 'pill');\n };\n\n override render() {\n return html`\n <div style=${styleMap(rootStyles(this.forceExpanded))}>\n ${this.forceExpanded ? renderHeader(this.assistantName, this._persona) : nothing}\n <adaptive-chat-trail\n .messages=${this.messages}\n .greeting=${this._persona?.intro_line ?? this.greeting}\n .forceExpanded=${this.forceExpanded}\n .inFlight=${this.inFlight}\n .thinkingText=${this.thinkingText}\n .personaName=${this._persona?.name ?? ''}\n .personaAvatarSm=${this._persona?.avatar?.sm ?? ''}\n .introSuggestion=${this.introSuggestion}\n .tileReceipts=${this.tileReceipts}\n .inlineWidgets=${this.inlineWidgets}\n .mountInlineWidget=${this.mountInlineWidget}\n @trail-intro-suggestion=${this._onIntroSuggestion}\n @trail-suggested-reply=${this._onSuggestedReply}\n ></adaptive-chat-trail>\n <div\n style=${styleMap(rowStyles(this.forceExpanded, this._composerFocused))}\n data-chat-row\n @focusin=${this._onComposerFocusIn}\n @focusout=${this._onComposerFocusOut}\n >\n <button\n type=\"button\"\n data-chat-maximize\n @click=${this._onMaximizeToggle}\n aria-label=${this.maximized ? 'Exit full screen' : 'Maximize chat'}\n title=${this.maximized ? 'Exit full screen' : 'Maximize'}\n aria-pressed=${this.maximized ? 'true' : 'false'}\n style=${styleMap(maximizeStyles(this.maximized))}\n >${this.maximized ? MINIMIZE_ICON : MAXIMIZE_ICON}</button>\n <input\n data-chat-input\n type=\"text\"\n placeholder=${this._micListening ? 'Listening \u2014 tap the mic to stop' : this.placeholder}\n .value=${this._input}\n @input=${this._onInput}\n @keydown=${this._onKeyDown}\n style=${styleMap(inputStyles())}\n aria-label=\"Chat input\"\n />\n ${\n this.voiceInput && isDictationSupported()\n ? html`${MIC_RING_STYLES}<button\n type=\"button\"\n data-chat-mic\n data-listening=${this._micListening ? 'true' : 'false'}\n ?disabled=${this._micDisabled}\n @click=${this._onMicClick}\n aria-pressed=${this._micListening ? 'true' : 'false'}\n aria-label=${this._micListening ? 'Stop dictation' : 'Start dictation'}\n title=${this._micDisabled ? 'Microphone blocked' : this._micListening ? 'Stop dictation' : 'Dictate'}\n style=${styleMap(micStyles(this._micListening, this._micDisabled))}\n >${MIC_ICON}</button>`\n : nothing\n }\n <button\n type=\"button\"\n data-chat-send\n data-state=${this.inFlight ? 'in-flight' : 'ready'}\n @click=${this._onSendOrInterrupt}\n aria-label=${this.inFlight ? 'Stop' : 'Send message'}\n title=${this.inFlight ? 'Interrupt' : 'Send'}\n style=${styleMap(sendStyles(this.inFlight))}\n >${this.inFlight ? '\u25A0' : '\u2191'}</button>\n </div>\n </div>\n `;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Styles \u2014 kept inline (Lit styleMap) so they participate in light-DOM\n// rendering and pick up host CSS variables (e.g. --sc-tile-background).\n// ---------------------------------------------------------------------------\n\nfunction rootStyles(forceExpanded: boolean): Record<string, string> {\n // When the host expects the chat to fill its container (full-screen\n // panel, agent app), we grow vertically AND wrap the whole widget in\n // its own chrome card \u2014 trail + input read as one cohesive surface\n // instead of a naked trail above a chrome'd input. Floating mounts\n // (bottom-right pinned, etc.) stay chrome-less here because their\n // chrome is owned by the host's positioned wrapper.\n if (!forceExpanded) {\n return {\n display: 'flex',\n flexDirection: 'column',\n gap: '6px',\n width: '100%',\n pointerEvents: 'auto',\n };\n }\n // Input is ABSOLUTELY positioned at the bottom of this chrome card\n // (see rowStyles forceExpanded mode). That guarantees the input is\n // always visible \u2014 even when the lid region is tighter than the\n // chat-bar would naturally want. The flex column above holds\n // header + trail; padding-bottom reserves the input's footprint so\n // trail content can never overlap.\n //\n // Priority of \"what disappears when space is tight\":\n // Trail (shrinks to 0 first via flex: 1 1 0)\n // Header (can shrink via default flex: 0 1 auto)\n // Input (NEVER \u2014 it's positioned absolute, always at the bottom)\n return {\n position: 'relative',\n display: 'flex',\n flexDirection: 'column',\n gap: '8px',\n width: '100%',\n height: '100%',\n boxSizing: 'border-box',\n flex: '1 1 auto',\n minHeight: '0',\n overflow: 'hidden',\n pointerEvents: 'auto',\n background: 'var(--sc-tile-background, rgba(255, 255, 255, 0.92))',\n border: 'var(--sc-tile-border, 1px solid rgba(0, 0, 0, 0.06))',\n borderRadius: 'var(--sc-tile-border-radius, 14px)',\n boxShadow: 'var(--sc-tile-shadow, 0 12px 32px -16px rgba(0, 0, 0, 0.18))',\n // Reserves room for the absolute-positioned input pill (36px high\n // + 12px from bottom) plus a 16px visual gap above the input.\n padding: '0.75rem 0.75rem calc(36px + 1.75rem)',\n color: 'var(--sc-tile-title-color, inherit)',\n fontFamily: 'var(--sc-font-family, inherit)',\n // Override the trail's host-layout tokens so the trail (a flex item\n // of this column) fills a definite-height parent AND contributes its\n // content height in an indefinite one. basis AUTO (not 0) is\n // load-bearing: grow:1 still fills a full-screen panel, but basis auto\n // also lets the trail's content height propagate up the fill chain so a\n // SHRINK-WRAP host (velvet's bottom sheet) hugs the content with no void.\n // A basis-0 layer anywhere in the chain reports 0 min-content and\n // collapses the sheet to header+input. shrink:1 + min-height:0 still lets\n // the trail yield first when space is tight (input is absolute-pinned).\n // The SDK-level rule in SmartCanvasElementLit consumes these tokens on\n // adaptive-chat-trail's host element. Containers OWN host styling\n // via tokens; the adaptive never sets inline styles on its host.\n ['--sc-chat-trail-host-display']: 'flex',\n ['--sc-chat-trail-host-flex-direction']: 'column',\n ['--sc-chat-trail-host-flex']: '1 1 auto',\n ['--sc-chat-trail-host-min-height']: '0',\n ['--sc-chat-trail-host-overflow']: 'hidden',\n };\n}\n\n// Focus ring shared by both composer variants: while focus is inside the\n// row, the border switches to the accent so the active text-entry surface\n// is unmistakable regardless of theme.\nconst COMPOSER_FOCUS_BORDER = '1px solid hsl(var(--sc-accent-color) / 0.6)';\n\nfunction rowStyles(forceExpanded: boolean, focused: boolean): Record<string, string> {\n // forceExpanded \u2014 root container already carries the chrome; row\n // becomes a flat input-pill inside that chrome (subtle border + soft\n // background, no shadow/blur).\n if (forceExpanded) {\n // Absolute-pinned to the chrome card's bottom edge so it never\n // disappears under any container size. The chrome reserves\n // padding-bottom equal to this row's footprint so flex content\n // above (header + trail) can't overlap.\n //\n // The pill's tint derives from currentColor (the theme's text color),\n // NOT a hardcoded black alpha: rgba(0,0,0,.04) over a near-black card\n // is invisible \u2014 on dark themes visitors couldn't find the text entry\n // at all. color-mix keeps one declaration correct on every theme, and\n // the --sc-chat-composer-* tokens let a workspace override the tint.\n return {\n position: 'absolute',\n left: '0.75rem',\n right: '0.75rem',\n bottom: '0.75rem',\n display: 'flex',\n alignItems: 'center',\n gap: '8px',\n padding: '6px 14px',\n borderRadius: '9999px',\n background:\n 'var(--sc-chat-composer-background, color-mix(in srgb, currentColor 8%, transparent))',\n border: focused\n ? COMPOSER_FOCUS_BORDER\n : '1px solid var(--sc-chat-composer-border, color-mix(in srgb, currentColor 22%, transparent))',\n color: 'var(--sc-content-text-color, inherit)',\n fontFamily: 'var(--sc-font-family, inherit)',\n minHeight: '36px',\n };\n }\n // Floating mount \u2014 keeps the original tile-chrome look.\n return {\n display: 'flex',\n alignItems: 'center',\n gap: '8px',\n padding: '10px 12px',\n borderRadius: 'var(--sc-tile-border-radius, 14px)',\n background: 'var(--sc-tile-background, rgba(15, 19, 24, 0.6))',\n backdropFilter: 'var(--sc-chat-bar-blur, blur(24px) saturate(140%))',\n WebkitBackdropFilter: 'var(--sc-chat-bar-blur, blur(24px) saturate(140%))',\n border: focused\n ? COMPOSER_FOCUS_BORDER\n : 'var(--sc-tile-border, 1px solid rgba(255, 255, 255, 0.08))',\n boxShadow: 'var(--sc-tile-shadow, 0 2px 12px rgba(0, 0, 0, 0.3))',\n color: 'var(--sc-tile-title-color, #fafafa)',\n fontFamily: 'var(--sc-font-family, inherit)',\n };\n}\n\n// Maximize / restore toggle in the chat bar. Two hard-won legibility rules\n// live here:\n// 1. The glyph is an inline SVG, never a Unicode arrow (`\u2922`/`\u2921`): glyph\n// metrics vary per platform font, so the character rendered small and\n// visibly off-center inside the circular chip.\n// 2. The chip mirrors the send button's filled-accent language but at\n// 32px, so the expand/collapse control reads as a control \u2014 the ACTIVE\n// (maximized) state is a filled chip, idle is a ringed chip.\n// Expand: arrows pointing out to opposite corners (Lucide maximize-2).\nconst MIC_ICON = svg`<svg viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" fill=\"none\" aria-hidden=\"true\"><rect x=\"6\" y=\"1.5\" width=\"4\" height=\"8\" rx=\"2\" stroke=\"currentColor\" stroke-width=\"1.6\"></rect><path d=\"M3.5 7.5a4.5 4.5 0 0 0 9 0M8 12v2.5\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"></path></svg>`;\n\nfunction micStyles(listening: boolean, disabled: boolean): Record<string, string> {\n return {\n display: 'inline-flex',\n 'align-items': 'center',\n 'justify-content': 'center',\n width: '32px',\n height: '32px',\n border: 'none',\n 'border-radius': '50%',\n // Listening is a MODE: the mic flips to a filled primary chip with a\n // ripple ring (MIC_RING_STYLES) so recording is unmissable, and the\n // input placeholder says how to end it.\n background: listening ? 'var(--sc-color-primary, #6a59ce)' : 'transparent',\n color: listening ? 'var(--sc-color-primary-text, #ffffff)' : 'currentColor',\n opacity: disabled ? '0.3' : listening ? '1' : '0.65',\n cursor: disabled ? 'not-allowed' : 'pointer',\n transition: 'color 150ms ease, background 150ms ease, opacity 150ms ease',\n 'flex-shrink': '0',\n };\n}\n\n// Ripple keyframes can't ride styleMap (light DOM, no stylesheet of our\n// own) \u2014 rendered once next to the mic button. Scoped to [data-chat-mic]\n// inside the canvas shadow root, so nothing leaks to the host page.\nconst MIC_RING_STYLES = html`<style>\n [data-chat-mic][data-listening='true'] {\n animation: sc-chatbar-mic-ring 1.5s ease-out infinite;\n }\n @keyframes sc-chatbar-mic-ring {\n 0% {\n box-shadow: 0 0 0 0 color-mix(in srgb, var(--sc-color-primary, #6a59ce) 45%, transparent);\n transform: scale(1);\n }\n 50% { transform: scale(1.08); }\n 70% { box-shadow: 0 0 0 12px transparent; }\n 100% {\n box-shadow: 0 0 0 0 transparent;\n transform: scale(1);\n }\n }\n @media (prefers-reduced-motion: reduce) {\n [data-chat-mic][data-listening='true'] {\n animation: none;\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--sc-color-primary, #6a59ce) 35%, transparent);\n }\n }\n</style>`;\n\nconst MAXIMIZE_ICON = svg`<svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.25\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><polyline points=\"15 3 21 3 21 9\"></polyline><polyline points=\"9 21 3 21 3 15\"></polyline><line x1=\"21\" y1=\"3\" x2=\"14\" y2=\"10\"></line><line x1=\"3\" y1=\"21\" x2=\"10\" y2=\"14\"></line></svg>`;\n\n// Collapse: arrows pointing in toward the center (Lucide minimize-2).\nconst MINIMIZE_ICON = svg`<svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.25\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><polyline points=\"4 14 10 14 10 20\"></polyline><polyline points=\"20 10 14 10 14 4\"></polyline><line x1=\"14\" y1=\"10\" x2=\"21\" y2=\"3\"></line><line x1=\"3\" y1=\"21\" x2=\"10\" y2=\"14\"></line></svg>`;\n\nfunction maximizeStyles(maximized: boolean): Record<string, string> {\n return {\n display: 'inline-flex',\n alignItems: 'center',\n justifyContent: 'center',\n width: '32px',\n height: '32px',\n borderRadius: '9999px',\n // Active (maximized): filled accent chip. Idle: subtle accent chip with a\n // visible ring. Colour is full-opacity in both states (no 0.95 ghosting).\n background: maximized\n ? 'hsl(var(--sc-accent-color) / 0.85)'\n : 'hsl(var(--sc-accent-color) / 0.15)',\n border: maximized ? 'none' : '1px solid hsl(var(--sc-accent-color) / 0.45)',\n color: maximized ? 'var(--sc-accent-foreground, #fff)' : 'hsl(var(--sc-accent-color))',\n flexShrink: '0',\n lineHeight: '1',\n padding: '0',\n cursor: 'pointer',\n transition: 'background 150ms ease, color 150ms ease, border-color 150ms ease',\n };\n}\n\nfunction inputStyles(): Record<string, string> {\n return {\n flex: '1',\n minWidth: '0',\n background: 'transparent',\n border: 'none',\n outline: 'none',\n color: 'inherit',\n // MUST be >= 16px. iOS Safari auto-zooms the page when a focused input has a\n // font-size below 16px, then does NOT zoom back out on blur \u2014 so the composer\n // zoomed in, the input ended up behind the keyboard (the zoom breaks the\n // visualViewport keyboard-lift math), and tapping \"Done\" left the whole site\n // stuck zoomed in. 16px is the standard, accessibility-safe fix (no\n // user-scalable=no / maximum-scale hacks that would kill pinch-zoom).\n fontSize: '16px',\n // WCAG 2.2 AA, 2.5.8 \u2014 every pointer target is at least 24x24 CSS px, and\n // an input's own box is what gets measured, not the bar it sits inside\n // (BUG-1787284957: the manual keyboard walk measured 278x20 while the bar\n // around it looked perfectly comfortable). A bare 16px input's line box is\n // ~20px, so the floor has to be declared.\n //\n // Painted height is UNCHANGED in both composer variants: the pinned pill\n // is already `min-height: 36px` with 6px block padding (24 + 12 = 36,\n // exactly the floor it was already holding), and the floating row's\n // tallest child is the 32px mic.\n //\n // The fallback is what carries the guarantee, and only for the UNSET case:\n // a workspace that never publishes --sc-chat-input-min-height gets 24px and\n // meets AA. A workspace that DOES publish it can set anything, including a\n // value below 24 \u2014 CSS has no floor to enforce here. Catching that belongs\n // at authoring time, in `validate-config.mjs` alongside the contrast\n // warnings, per the repo's validators-at-authoring-time law; tracked\n // follow-up, not something this declaration can promise.\n minHeight: 'var(--sc-chat-input-min-height, 24px)',\n };\n}\n\nfunction sendStyles(inFlight: boolean): Record<string, string> {\n return {\n width: '28px',\n height: '28px',\n display: 'inline-flex',\n alignItems: 'center',\n justifyContent: 'center',\n borderRadius: '9999px',\n // Ready: filled accent (send is the affordance). In-flight: hollow\n // ring + accent ring (stop is a one-of-a-kind disruptive action,\n // styled differently so the user reads \"this isn't just send\").\n background: inFlight ? 'transparent' : 'hsl(var(--sc-accent-color) / 0.85)',\n color: inFlight ? 'hsl(var(--sc-accent-color) / 0.95)' : 'var(--sc-accent-foreground, #fff)',\n border: inFlight ? '1px solid hsl(var(--sc-accent-color) / 0.55)' : 'none',\n cursor: 'pointer',\n flexShrink: '0',\n transition: 'background 150ms ease, color 150ms ease, border-color 150ms ease',\n };\n}\n\n/**\n * Compact single-line chat header. Prior version stacked the name over a\n * two-line status block inside a flex-column wrapper with a 28px avatar and\n * `padding: 4px 4px 12px`, which wasted a lot of vertical space (user\n * feedback: \"bad use of vertical space\"). This is now ONE inline row \u2014\n * small avatar dot \u00B7 name \u00B7 live-status dot \u00B7 short status \u2014 at roughly half\n * the height. All surfaces share this chrome.\n */\nfunction renderHeader(assistantName: string, persona: ChatPersona | null) {\n const headerStyles: Record<string, string> = {\n // Container-owned visibility: velvet's collapsed strip (V3 tight +\n // deck focus) sets --sc-chat-bar-header-display: none so the strip is\n // JUST the text entry (the header painted over the absolute-pinned\n // input when the region was clamped to the strip height). Expanded\n // chat keeps the header \u2014 the AI-disclosure microline must stay\n // wherever a conversation is actually visible.\n display: 'var(--sc-chat-bar-header-display, flex)',\n alignItems: 'center',\n gap: '7px',\n // Halved vertical footprint: 6px total block padding vs the old 16px\n // (4px top + 12px bottom). Horizontal padding unchanged.\n padding: '3px 4px',\n borderBottom: '1px solid var(--sc-content-divider-color, rgba(0, 0, 0, 0.08))',\n flex: '0 0 auto',\n };\n // Small brand dot \u2014 shrunk from a 28px avatar to a 16px chip so the\n // whole header collapses to a single text line. Still tracks the\n // workspace brand primary so it matches the rest of the canvas chrome.\n const avatar: Record<string, string> = {\n display: 'inline-flex',\n alignItems: 'center',\n justifyContent: 'center',\n width: '16px',\n height: '16px',\n borderRadius: '9999px',\n background:\n 'var(--sc-tile-icon-background, linear-gradient(135deg, var(--sc-color-primary, #5faf7d) 0%, var(--sc-color-primary-hover, #3d8a5e) 100%))',\n color: '#fff',\n flexShrink: '0',\n fontSize: '9px',\n lineHeight: '1',\n };\n const name: Record<string, string> = {\n fontSize: '0.8rem',\n fontWeight: '600',\n color: 'var(--sc-content-text-color, inherit)',\n lineHeight: '1.2',\n flexShrink: '0',\n };\n // No status line or presence dot (\"Online \u00B7 understands your session\") \u2014\n // user feedback: that's chrome a visitor doesn't need. Avatar + name only.\n // Persona mode (FEAT-1784917430): the brand dot becomes the persona's face,\n // the name becomes the persona's name, and an explicit \"AI concierge\"\n // microline rides alongside \u2014 a photoreal face + human name must never\n // imply a human agent (Moffatt/R-001 lineage, CA B.O.T./EU AI Act).\n const personaImg: Record<string, string> = {\n width: '22px',\n height: '22px',\n borderRadius: '9999px',\n objectFit: 'cover',\n flexShrink: '0',\n };\n const disclosure: Record<string, string> = {\n fontSize: '0.62rem',\n fontWeight: '500',\n color: 'var(--sc-content-text-color, inherit)',\n opacity: '0.55',\n lineHeight: '1.2',\n flexShrink: '0',\n };\n return html`\n <div style=${styleMap(headerStyles)} data-chat-header>\n ${\n persona?.avatar?.md || persona?.avatar?.sm\n ? html`<img\n style=${styleMap(personaImg)}\n src=${persona.avatar.md ?? persona.avatar.sm ?? ''}\n alt=\"\"\n data-persona-avatar\n />`\n : html`<span style=${styleMap(avatar)} aria-hidden=\"true\">\u2726</span>`\n }\n <span style=${styleMap(name)} data-persona-name>${persona?.name ?? assistantName}</span>\n ${persona ? html`<span style=${styleMap(disclosure)} data-persona-disclosure>\u00B7 AI concierge</span>` : nothing}\n </div>\n `;\n}\n\nif (!customElements.get('adaptive-chat-bar')) {\n customElements.define('adaptive-chat-bar', AdaptiveChatBar);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'adaptive-chat-bar': AdaptiveChatBar;\n }\n}\n", "/**\n * Chat personas \u2014 the \"human face\" of the concierge (FEAT-1784917430).\n *\n * A workspace ships a persona ROSTER in the chat-bar's props (plain config,\n * like assistantName). Assignment is 50:50 sticky-random per visitor: first\n * visit picks uniformly, the pick persists in localStorage, and every later\n * session reuses it \u2014 consistency reads as a real team member, not a slot\n * machine. An invalid/vanished persisted id re-picks (roster changes are\n * config changes; visitors must never be stranded on a ghost).\n *\n * The persona never claims to be human: the header carries an explicit\n * \"AI concierge\" microline (see renderHeader) and the server threads the\n * no-human-claim rule into the prompt (Moffatt/R-001 lineage).\n */\n\nexport interface PersonaAvatar {\n sm?: string;\n md?: string;\n lg?: string;\n}\n\nexport interface ChatPersona {\n id: string;\n name: string;\n role_title?: string;\n pronouns?: string;\n avatar?: PersonaAvatar;\n intro_line?: string;\n}\n\nconst STORAGE_KEY = 'syntro:persona';\n\n/** Pick (or recall) the visitor's sticky persona from the roster. */\nexport function pickPersona(roster: ChatPersona[] | undefined | null): ChatPersona | null {\n if (!Array.isArray(roster) || roster.length === 0) return null;\n let storedId: string | null = null;\n try {\n storedId = localStorage.getItem(STORAGE_KEY);\n } catch {\n // Storage unavailable (private mode) \u2014 session-scoped random pick.\n }\n const stored = storedId ? roster.find((p) => p.id === storedId) : undefined;\n if (stored) return stored;\n const pick = roster[Math.floor(Math.random() * roster.length)]!;\n try {\n localStorage.setItem(STORAGE_KEY, pick.id);\n } catch {\n /* non-persistent pick is fine */\n }\n return pick;\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/dist/runtime.js CHANGED
@@ -9,24 +9,25 @@ import {
9
9
  decodeMutationEnvelope,
10
10
  fetchMountedElements,
11
11
  renderFallbackHtml
12
- } from "./chunk-IALBNWHO.js";
12
+ } from "./chunk-57MPLQOL.js";
13
13
  import {
14
14
  pickPersona
15
- } from "./chunk-74CVNFXS.js";
15
+ } from "./chunk-VFWA5W56.js";
16
16
  import {
17
17
  getElementInstanceStore,
18
18
  setElementInstanceStore
19
19
  } from "./chunk-EWPPVPJ4.js";
20
- import "./chunk-M7FVDW3M.js";
20
+ import "./chunk-LJEPZI6Y.js";
21
21
  import "./chunk-TZA7572J.js";
22
- import "./chunk-A36JVPEF.js";
22
+ import "./chunk-ETDHHQS3.js";
23
23
  import {
24
24
  DIVE_DEEPER_EVENT,
25
+ TELEMETRY_DOM_EVENT,
25
26
  TILE_MOUNTED_EVENT,
26
27
  TILE_UNMOUNTED_EVENT,
27
28
  isCanonicalBusEvent,
28
29
  stripMountPlumbing
29
- } from "./chunk-2OS5KLNX.js";
30
+ } from "./chunk-NGV35N4T.js";
30
31
  import "./chunk-UVKRO5ER.js";
31
32
 
32
33
  // src/ChatSession.ts
@@ -41,6 +42,7 @@ var ChatSession = class {
41
42
  this._runActive = false;
42
43
  this._thinkingText = "";
43
44
  this._nextId = 1;
45
+ this._turn = 0;
44
46
  this.subscribers = /* @__PURE__ */ new Set();
45
47
  this.sendListeners = /* @__PURE__ */ new Set();
46
48
  this.interruptListeners = /* @__PURE__ */ new Set();
@@ -51,9 +53,14 @@ var ChatSession = class {
51
53
  return {
52
54
  messages: [...this._messages],
53
55
  inFlight: this._inFlight || this._runActive,
54
- thinkingText: this._thinkingText
56
+ thinkingText: this._thinkingText,
57
+ turn: this._turn
55
58
  };
56
59
  }
60
+ /** User sends so far in this session; the single turn counter. */
61
+ get turn() {
62
+ return this._turn;
63
+ }
57
64
  /**
58
65
  * Append a delta to the active thinking turn's narration buffer.
59
66
  * Forwarded from the AG-UI THINKING_TEXT_MESSAGE_CONTENT event so
@@ -116,8 +123,9 @@ var ChatSession = class {
116
123
  const id = generateUserMessageId();
117
124
  this._messages.push({ id, role: "user", text: trimmed });
118
125
  this._inFlight = true;
126
+ this._turn += 1;
119
127
  this.notify();
120
- const event = { text: trimmed, id };
128
+ const event = { text: trimmed, id, turn: this._turn, via: opts.via };
121
129
  if (opts?.activeLidSlot) event.activeLidSlot = opts.activeLidSlot;
122
130
  if (opts?.forwardedProps) event.forwardedProps = opts.forwardedProps;
123
131
  for (const listener of this.sendListeners) listener(event);
@@ -217,6 +225,7 @@ var ChatSession = class {
217
225
  this._runActive = false;
218
226
  this._thinkingText = "";
219
227
  this._nextId = 1;
228
+ this._turn = 0;
220
229
  this.notify();
221
230
  }
222
231
  /** Register a transport's send listener. Returns unsubscribe. */
@@ -456,7 +465,7 @@ function mergeForwardedProps(prev, next) {
456
465
  return { ...next, forwardedProps: prev.forwardedProps };
457
466
  }
458
467
  var ChatTransport = class {
459
- constructor() {
468
+ constructor(opts = {}) {
460
469
  this._config = null;
461
470
  this._status = "idle";
462
471
  this._agui = null;
@@ -525,6 +534,26 @@ var ChatTransport = class {
525
534
  */
526
535
  this._transportId = `tx_${Math.random().toString(36).slice(2, 8)}`;
527
536
  this._configuredAt = 0;
537
+ /** Per-run telemetry state (FEAT-1789150184). The chat session owns the turn; this mirrors it per run. */
538
+ this._runTurn = 0;
539
+ this._runStartedAt = 0;
540
+ this._firstDeltaAt = null;
541
+ this._runMounts = 0;
542
+ this._runCards = 0;
543
+ /**
544
+ * Count of RUN_FINISHED events the server still owes us that must be
545
+ * swallowed, not reported — one per interrupt. NOT a boolean: an
546
+ * interrupt latches this debt against whichever run's RUN_FINISHED
547
+ * arrives next, and a visitor can interrupt then immediately send again
548
+ * before the first run's late RUN_FINISHED shows up. Clearing this in
549
+ * `onSend` (as a boolean would need to, to arm for the NEXT run) would
550
+ * let that late RUN_FINISHED slip through and get misattributed to the
551
+ * new run — a phantom `reply_received` at turn N+1 with near-zero
552
+ * ttft/total. The counter survives across sends and is only decremented
553
+ * by an actual RUN_FINISHED, so debt from run N is always paid by run
554
+ * N's own (possibly late) finish, never by run N+1's.
555
+ */
556
+ this._pendingSuppressedFinishes = 0;
528
557
  // -------------------------------------------------------------------------
529
558
  // Session stream — server-initiated events (proactive nudges)
530
559
  // -------------------------------------------------------------------------
@@ -533,6 +562,18 @@ var ChatTransport = class {
533
562
  // (read-deadline constant lives at module scope: SESSION_READ_DEADLINE_MS)
534
563
  /** Capped reconnect backoff for the session stream. */
535
564
  this._sessionStreamBackoffMs = 2e3;
565
+ this._now = opts.now ?? (() => Date.now());
566
+ }
567
+ /** The transport is not an element: atoms go straight to document, where TelemetryCore listens. */
568
+ _reportAtom(name, props) {
569
+ if (typeof document === "undefined") return;
570
+ document.dispatchEvent(
571
+ new CustomEvent(TELEMETRY_DOM_EVENT, {
572
+ detail: { name, props },
573
+ bubbles: true,
574
+ composed: true
575
+ })
576
+ );
536
577
  }
537
578
  /**
538
579
  * Configure the transport. Idempotent — calling again with the same
@@ -677,6 +718,12 @@ var ChatTransport = class {
677
718
  this._fallbackListeners.clear();
678
719
  this._dispatchedNudgeIntentIds.clear();
679
720
  this.stopSessionStream();
721
+ this._runTurn = 0;
722
+ this._runStartedAt = 0;
723
+ this._firstDeltaAt = null;
724
+ this._runMounts = 0;
725
+ this._runCards = 0;
726
+ this._pendingSuppressedFinishes = 0;
680
727
  }
681
728
  /**
682
729
  * Open the persistent `/api/adaptive/session` SSE and feed its frames
@@ -931,13 +978,28 @@ var ChatTransport = class {
931
978
  return { ...base ?? {}, ...pending };
932
979
  }
933
980
  _wireSession() {
934
- this._sessionUnsubSend = chatSession.onSend(({ text, id, activeLidSlot, forwardedProps }) => {
935
- this._pendingLidSlot = typeof activeLidSlot === "string" && activeLidSlot.length > 0 ? activeLidSlot : null;
936
- this._pendingForwardedProps = forwardedProps ?? null;
937
- void this._forwardUserMessage(text, id);
938
- });
981
+ this._sessionUnsubSend = chatSession.onSend(
982
+ ({ text, id, turn, via, activeLidSlot, forwardedProps }) => {
983
+ this._pendingLidSlot = typeof activeLidSlot === "string" && activeLidSlot.length > 0 ? activeLidSlot : null;
984
+ this._pendingForwardedProps = forwardedProps ?? null;
985
+ this._runTurn = turn;
986
+ this._runStartedAt = this._now();
987
+ this._firstDeltaAt = null;
988
+ this._runMounts = 0;
989
+ this._runCards = 0;
990
+ this._reportAtom("syntro_chat_message_sent", { turn, chars: text.length, via });
991
+ void this._forwardUserMessage(text, id);
992
+ }
993
+ );
939
994
  this._sessionUnsubInterrupt = chatSession.onInterrupt(() => {
940
995
  this._agui?.send({ type: "stop-generation" });
996
+ if (this._runStartedAt) {
997
+ this._pendingSuppressedFinishes += 1;
998
+ this._reportAtom("syntro_chat_interrupted", {
999
+ turn: this._runTurn,
1000
+ after_ms: this._now() - this._runStartedAt
1001
+ });
1002
+ }
941
1003
  });
942
1004
  this._sessionUnsubToolResult = chatSession.onToolResult(({ toolCallId, result, approved }) => {
943
1005
  this._agui?.send({ type: "tool-result", toolCallId, result, approved });
@@ -1181,6 +1243,7 @@ var ChatTransport = class {
1181
1243
  return;
1182
1244
  }
1183
1245
  case "message-delta": {
1246
+ if (this._firstDeltaAt === null) this._firstDeltaAt = this._now();
1184
1247
  const id = event.messageId ?? this._currentAssistantMessageId;
1185
1248
  if (!id || !event.delta) return;
1186
1249
  chatSession.receiveDelta(id, event.delta);
@@ -1243,6 +1306,10 @@ var ChatTransport = class {
1243
1306
  }
1244
1307
  case "tool-call": {
1245
1308
  const existing = this._findToolCallStatus(event.toolCall.id);
1309
+ if (!existing && event.toolCall.name.startsWith("mount_")) {
1310
+ this._runMounts += 1;
1311
+ if (/card/i.test(event.toolCall.name)) this._runCards += 1;
1312
+ }
1246
1313
  if (!existing) {
1247
1314
  this._startThinkingCycle(toolNarrationVariants(event.toolCall.name));
1248
1315
  }
@@ -1309,21 +1376,42 @@ var ChatTransport = class {
1309
1376
  return;
1310
1377
  }
1311
1378
  // ── Deliberate no-ops, made EXPLICIT so the exhaustiveness check below
1312
- // can do its job. All four were already falling through to nothing; the
1379
+ // can do its job. All three were already falling through to nothing; the
1313
1380
  // `never` guard is what surfaced that they were never written down.
1314
1381
  //
1315
- // The three tool-call events belong to a deferred-tool approval flow
1382
+ // These tool-call events belong to a deferred-tool approval flow
1316
1383
  // that exists on <syntro-chat> (the editor / assistant surface), not
1317
1384
  // here: the ambient visitor trail renders no approve/deny affordance, so
1318
1385
  // there is nothing for an interrupt, a cross-run result, or a
1319
- // cancellation to update. `run-finished` (BUG-1787292410) settles
1320
- // messages the run left streaming in <syntro-chat>; this surface tracks
1321
- // run liveness through `typing`, which the transport emits alongside it.
1386
+ // cancellation to update.
1322
1387
  case "tool-call-deferred":
1323
1388
  case "tool-call-result":
1324
1389
  case "tool-call-cancelled":
1325
- case "run-finished":
1326
1390
  return;
1391
+ // `run-finished` (BUG-1787292410) settles messages the run left
1392
+ // streaming in <syntro-chat>; this surface tracks run liveness through
1393
+ // `typing`, which the transport emits alongside it. It's also the one
1394
+ // telemetry-relevant signal: the reply_received atom fires here,
1395
+ // unless this run was interrupted — the server still emits
1396
+ // RUN_FINISHED after stop-generation, and that must not double-report.
1397
+ case "run-finished": {
1398
+ if (this._pendingSuppressedFinishes > 0) {
1399
+ this._pendingSuppressedFinishes -= 1;
1400
+ return;
1401
+ }
1402
+ if (this._runStartedAt) {
1403
+ const end = this._now();
1404
+ this._reportAtom("syntro_chat_reply_received", {
1405
+ turn: this._runTurn,
1406
+ ttft_ms: (this._firstDeltaAt ?? end) - this._runStartedAt,
1407
+ total_ms: end - this._runStartedAt,
1408
+ mounts: this._runMounts,
1409
+ cards: this._runCards
1410
+ });
1411
+ }
1412
+ this._runStartedAt = 0;
1413
+ return;
1414
+ }
1327
1415
  default: {
1328
1416
  const _exhaustive = event;
1329
1417
  void _exhaustive;
@@ -1332,7 +1420,7 @@ var ChatTransport = class {
1332
1420
  }
1333
1421
  }
1334
1422
  };
1335
- var chatTransport = new ChatTransport();
1423
+ var chatTransport = new ChatTransport({ now: () => Date.now() });
1336
1424
 
1337
1425
  // src/findSmartCanvas.ts
1338
1426
  function findSmartCanvas() {
@@ -2791,6 +2879,7 @@ var LeaveAMessageLit = class extends LitElement {
2791
2879
  .messages=${this._trailMessages()}
2792
2880
  .forceExpanded=${true}
2793
2881
  .hideTopBorder=${true}
2882
+ .followBottom=${true}
2794
2883
  style=${styleMap(trailHostStyles)}
2795
2884
  ></adaptive-chat-trail>
2796
2885
  ${copy?.replyLabel ? html`<label data-lam-reply-label for=${replyId} style=${styleMap(srOnlyStyles)}
@@ -3154,7 +3243,7 @@ function wireDiveDeeperListener() {
3154
3243
  const origin = { kind: "dive-deeper" };
3155
3244
  if (detail.title) origin.title = detail.title;
3156
3245
  if (detail.context) origin.context = detail.context;
3157
- chatSession.send(prompt, { forwardedProps: { origin } });
3246
+ chatSession.send(prompt, { forwardedProps: { origin }, via: "dive_deeper" });
3158
3247
  if (!chatSession.hasTransport()) {
3159
3248
  chatSession.error("Chat backend not configured \u2014 set backendUrl in the canvas config.");
3160
3249
  }
@@ -3206,8 +3295,8 @@ function startObserverIfPossible(cfg2) {
3206
3295
  }
3207
3296
  function wireListeners(bar, state) {
3208
3297
  const onMessageSent = (e) => {
3209
- const text = e.detail.text;
3210
- chatSession.send(text, { activeLidSlot: state.cfg._syntroSlotName });
3298
+ const { text, via } = e.detail;
3299
+ chatSession.send(text, { activeLidSlot: state.cfg._syntroSlotName, via });
3211
3300
  if (!chatSession.hasTransport()) {
3212
3301
  chatSession.error("Chat backend not configured \u2014 set backendUrl in the canvas config.");
3213
3302
  }
@@ -3265,6 +3354,7 @@ var AdaptiveChatBarMountable = {
3265
3354
  const unsubSession = chatSession.subscribe((s) => {
3266
3355
  bar.messages = [...s.messages];
3267
3356
  bar.inFlight = s.inFlight;
3357
+ bar.turn = s.turn;
3268
3358
  bar.thinkingText = s.thinkingText;
3269
3359
  });
3270
3360
  const runtimeEvents = cfg2.runtime ? cfg2.runtime.events : void 0;