@qualified-salesforce/web 0.0.1-oidc-bootstrap → 0.1.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 (97) hide show
  1. package/CAPABILITY-REFERENCE.md +44 -0
  2. package/CHANGELOG.md +75 -0
  3. package/constants.generated.js +30 -2
  4. package/css/widget/multimodal_v2/index.split-css.panda.css +265 -47
  5. package/package.json +17 -3
  6. package/packs/widget/api/docking/viewport_lock.js +93 -2
  7. package/packs/widget/esm/index.d.ts +134 -0
  8. package/packs/widget/esm/index.js +18 -2
  9. package/packs/widget/esm/qualified.css +265 -47
  10. package/src/graphql-types.js +74 -1
  11. package/src/url.js +4 -1
  12. package/vendor/actioncable/connection.js +40 -3
  13. package/vendor/actioncable/connection_monitor.js +8 -0
  14. package/vendor/actioncable/consumer.js +7 -1
  15. package/vendor/actioncable/index.js +5 -2
  16. package/vendor/actioncable/sse_socket.js +262 -0
  17. package/widget/analytics.js +8 -0
  18. package/widget/api_handler/base_api_handler.js +24 -0
  19. package/widget/booker_outcome_tracker.js +58 -20
  20. package/widget/capabilities.js +190 -0
  21. package/widget/channel.js +38 -6
  22. package/widget/constants.generated.js +2 -1
  23. package/widget/multimodal_v2/components/AppLayout.js +1 -1
  24. package/widget/multimodal_v2/components/app/Foreground.js +10 -9
  25. package/widget/multimodal_v2/components/app/Input.js +8 -3
  26. package/widget/multimodal_v2/components/app/LightboxExperience.js +17 -3
  27. package/widget/multimodal_v2/components/app/Main.js +87 -31
  28. package/widget/multimodal_v2/components/app/MessengerButton.js +18 -1
  29. package/widget/multimodal_v2/components/chat/ChatInputForm.js +8 -6
  30. package/widget/multimodal_v2/components/chat/timeline/CalendarEventPickRenderer.js +2 -2
  31. package/widget/multimodal_v2/components/chat/timeline/PresentDeckRenderer.js +346 -170
  32. package/widget/multimodal_v2/components/chat/timeline/SystemRenderer.js +4 -7
  33. package/widget/multimodal_v2/components/decks/index.js +506 -52
  34. package/widget/multimodal_v2/components/helpers/content_click_payloads.js +67 -1
  35. package/widget/multimodal_v2/components/lightbox_gather/index.js +4 -4
  36. package/widget/multimodal_v2/components/meetings/MeetingBookerEntry.js +8 -1
  37. package/widget/multimodal_v2/components/meetings/MeetingOfferEmailStep.js +1 -1
  38. package/widget/multimodal_v2/components/meetings/MeetingSummaryRail.js +1 -1
  39. package/widget/multimodal_v2/components/meetings/MeetingTimeSelector.js +1 -1
  40. package/widget/multimodal_v2/components/multimodal/Controls.js +11 -3
  41. package/widget/multimodal_v2/components/multimodal/useMultimodal.js +19 -7
  42. package/widget/multimodal_v2/components/presentations/index.js +61 -0
  43. package/widget/multimodal_v2/constants.js +1 -1
  44. package/widget/multimodal_v2/images/icons.js +17 -1
  45. package/widget/multimodal_v2/index.panda.js +2 -2
  46. package/widget/multimodal_v2/locales/cs.js +8 -1
  47. package/widget/multimodal_v2/locales/da.js +8 -1
  48. package/widget/multimodal_v2/locales/de.js +8 -1
  49. package/widget/multimodal_v2/locales/en.js +8 -1
  50. package/widget/multimodal_v2/locales/en_AU.js +8 -1
  51. package/widget/multimodal_v2/locales/en_GB.js +8 -1
  52. package/widget/multimodal_v2/locales/es.js +8 -1
  53. package/widget/multimodal_v2/locales/es_LA.js +8 -1
  54. package/widget/multimodal_v2/locales/fi.js +8 -1
  55. package/widget/multimodal_v2/locales/fr.js +8 -1
  56. package/widget/multimodal_v2/locales/fr_CA.js +8 -1
  57. package/widget/multimodal_v2/locales/id.js +8 -1
  58. package/widget/multimodal_v2/locales/it.js +8 -1
  59. package/widget/multimodal_v2/locales/ja.js +8 -1
  60. package/widget/multimodal_v2/locales/ko.js +8 -1
  61. package/widget/multimodal_v2/locales/nb.js +8 -1
  62. package/widget/multimodal_v2/locales/nl.js +8 -1
  63. package/widget/multimodal_v2/locales/pl.js +8 -1
  64. package/widget/multimodal_v2/locales/pt_BR.js +8 -1
  65. package/widget/multimodal_v2/locales/ru.js +8 -1
  66. package/widget/multimodal_v2/locales/sv.js +8 -1
  67. package/widget/multimodal_v2/locales/th.js +8 -1
  68. package/widget/multimodal_v2/locales/tr.js +8 -1
  69. package/widget/multimodal_v2/locales/vi.js +8 -1
  70. package/widget/multimodal_v2/locales/zh_CN.js +8 -1
  71. package/widget/multimodal_v2/locales/zh_TW.js +8 -1
  72. package/widget/multimodal_v2/multimodal.js +5 -1
  73. package/widget/multimodal_v2/multimodal_channel.js +13 -8
  74. package/widget/multimodal_v2/signals/messages_signal.js +4 -0
  75. package/widget/multimodal_v2/stores/ai_conversation_store.js +6 -2
  76. package/widget/multimodal_v2/stores/artifact_store.js +352 -27
  77. package/widget/multimodal_v2/stores/call_store.js +40 -1
  78. package/widget/multimodal_v2/stores/call_ui_adapter.js +1 -1
  79. package/widget/multimodal_v2/stores/decks_store.js +405 -39
  80. package/widget/multimodal_v2/stores/meeting_store.js +15 -10
  81. package/widget/multimodal_v2/stores/presentation_ga_adapter.js +64 -0
  82. package/widget/multimodal_v2/stores/presentation_telemetry_store.js +556 -0
  83. package/widget/multimodal_v2/stores/presentations_store.js +1 -0
  84. package/widget/multimodal_v2/stores/root_store.js +157 -268
  85. package/widget/multimodal_v2/stores/screen_share_store.js +41 -21
  86. package/widget/multimodal_v2/stores/translation_store.js +9 -0
  87. package/widget/multimodal_v2/stores/ui_store.js +7 -7
  88. package/widget/multimodal_v2/stores/website_navigation_store.js +3 -1
  89. package/widget/multimodal_v2/text_turn_response_tracker.js +419 -0
  90. package/widget/multimodal_v2/types.js +21 -1
  91. package/widget/multimodal_v2/utils/locale.js +2 -1
  92. package/widget/multimodal_v2/utils/signals.js +35 -4
  93. package/widget/pardot_visitor.js +7 -1
  94. package/widget/server-logger.js +97 -2
  95. package/widget/tracker.js +69 -21
  96. package/widget/widget/page_mirror_factory.js +9 -1
  97. package/widget/widget/widget.js +29 -4
@@ -1,5 +1,6 @@
1
1
  import { __webpack_require__ } from "../../../js/runtime.js";
2
2
  import { signals_core_module_E, signals_core_module_w } from "../../../js/multimodal_v2.js";
3
+ import { isCapabilityAllowed } from "../../capabilities.js";
3
4
  import { getSentryClient } from "../../widget/sentry.js";
4
5
  import "../../utils/native_globals.js";
5
6
 
@@ -7,6 +8,7 @@ import "../../utils/native_globals.js";
7
8
  /* provided dependency */ var screen_share_store_clearInterval = __webpack_require__(68034).wm;
8
9
 
9
10
 
11
+
10
12
  // Cap the longest side so the encoded JPEG stays small enough to travel through
11
13
  // ActionCable -> Redis -> the OpenAI websocket comfortably, while remaining
12
14
  // readable (text + faces) for the agent.
@@ -42,41 +44,51 @@ class ScreenShareStore {
42
44
  // window and rejects page frames while it owns the slot, so hand the slot back on the way out.
43
45
  this.clearPerception();
44
46
  }
45
- // Reactively push the current slide (on change) and clear perception when no slide is shown or
46
- // the call ends.
47
+ // Reactively push the current slide (on change) and clear perception when no slide is shown,
48
+ // while a replacement is loading, or when the call ends.
47
49
  sync() {
48
50
  // Server-side perception is disabled without the flag, so don't capture or ship frames.
49
51
  if (!this.rootStore.featureFlags.piperPerceptionSlides) return;
52
+ // QAI-5864: don't capture or egress slide frames to the perception loop unless the consumer
53
+ // allowed the "slidePerception" capability. Defense-in-depth alongside the flag check — the
54
+ // runtime-config filter strips `piperPerceptionSlides` when denied, and this guard holds even
55
+ // if the flag reached us another way. Allow-all for the hosted widget (no boundary installed).
56
+ if (!isCapabilityAllowed("slidePerception")) return;
50
57
  const slideUrl = this.currentSlideImageUrl.value;
58
+ const frameKey = this.currentSlideFrameKey.value;
51
59
  const inCall = this.rootStore.callInProgress.value;
52
60
  if (!inCall) {
53
61
  this.clearPerception();
54
62
  return;
55
63
  }
56
- if (slideUrl) {
57
- if (slideUrl !== this.lastPushedKey && slideUrl !== this.pendingKey) {
58
- void this.pushSlide(slideUrl);
64
+ if (slideUrl && frameKey) {
65
+ if (frameKey !== this.lastPushedKey && frameKey !== this.pendingKey) {
66
+ // The previous frame no longer matches what's visible. Retract it before awaiting the
67
+ // replacement image so pointing cannot resolve against the old slide during the load.
68
+ // clearPerception() is a no-op for the first slide and after the first transition clear.
69
+ this.clearPerception();
70
+ void this.pushSlide(slideUrl, frameKey);
59
71
  }
60
72
  return;
61
73
  }
62
74
  // No slide on screen: clear whatever slide we last pushed.
63
75
  this.clearPerception();
64
76
  }
65
- async pushSlide(url) {
66
- this.pendingKey = url;
77
+ async pushSlide(url, frameKey) {
78
+ this.pendingKey = frameKey;
67
79
  try {
68
80
  const image = await this.loadImage(url);
69
- if (this.currentSlideImageUrl.peek() !== url) return; // superseded while loading
81
+ if (this.currentSlideFrameKey.peek() !== frameKey) return; // superseded while loading
70
82
  // The call may have ended while the image was loading; don't push after call end
71
83
  // (the !inCall branch of sync() has already cleared perception).
72
84
  if (!this.rootStore.callInProgress.peek()) return;
73
85
  const imageData = this.encodeImage(image);
74
86
  if (!imageData) {
75
- this.retractOnFailure(url);
87
+ this.retractOnFailure(frameKey);
76
88
  return;
77
89
  }
78
90
  this.push(imageData);
79
- this.lastPushedKey = url;
91
+ this.lastPushedKey = frameKey;
80
92
  this.lastPushedImageData = imageData;
81
93
  this.startKeepalive();
82
94
  } catch (error) {
@@ -84,17 +96,15 @@ class ScreenShareStore {
84
96
  (_getSentryClient = getSentryClient({
85
97
  project: "multimodal"
86
98
  })) == null ? void 0 : _getSentryClient.captureException(error);
87
- this.retractOnFailure(url);
99
+ this.retractOnFailure(frameKey);
88
100
  } finally{
89
- if (this.pendingKey === url) this.pendingKey = null;
101
+ if (this.pendingKey === frameKey) this.pendingKey = null;
90
102
  }
91
103
  }
92
- // A slide that failed to load or encode leaves the *previous* slide pushed, with the keepalive
93
- // actively refreshing it — so the agent keeps describing content the visitor has already moved
94
- // on from, and coffee's staleness expiry never fires because the frame keeps arriving. Retract
95
- // it: showing the agent nothing beats showing it something confidently wrong.
96
- retractOnFailure(url) {
97
- if (this.currentSlideImageUrl.peek() !== url) return; // already superseded; leave it alone
104
+ // Ensure a slide that failed to load or encode cannot leave any stale frame behind. Replacement
105
+ // slides are already retracted before loading, so clearPerception() keeps this idempotent.
106
+ retractOnFailure(frameKey) {
107
+ if (this.currentSlideFrameKey.peek() !== frameKey) return; // already superseded; leave it alone
98
108
  this.clearPerception();
99
109
  }
100
110
  loadImage(url) {
@@ -183,11 +193,21 @@ class ScreenShareStore {
183
193
  if (slide) return (_slide_url = slide.url) != null ? _slide_url : null;
184
194
  return null;
185
195
  });
196
+ // A new presentation message is a fresh share even when it displays the same URL. The backend
197
+ // clears perception before broadcasting that message, so include its id to ensure the widget
198
+ // replaces the cleared frame instead of treating the share as an already-pushed slide.
199
+ this.currentSlideFrameKey = signals_core_module_w(()=>{
200
+ const url = this.currentSlideImageUrl.value;
201
+ const messageId = this.rootStore.artifact.messageId.value;
202
+ return url && messageId ? messageId + ":" + url : null;
203
+ });
186
204
  this.disposeEffect = null;
187
- // The slide URL we last pushed, so we only send on change and know when to clear.
205
+ // The presentation-message/slide pair we last pushed, so we only send on change and know when
206
+ // to clear. Message identity matters because re-sharing the visible slide clears it server-side.
188
207
  this.lastPushedKey = null;
189
- // The slide URL currently loading/encoding for a push. sync() can re-run while pushSlide is
190
- // still awaiting the image load; without this an in-flight URL would be pushed twice.
208
+ // The presentation-message/slide pair currently loading/encoding for a push. sync() can re-run
209
+ // while pushSlide is still awaiting the image load; without this an in-flight frame would be
210
+ // pushed twice.
191
211
  this.pendingKey = null;
192
212
  // The encoded payload we last pushed, so the keepalive can re-send it without re-encoding.
193
213
  this.lastPushedImageData = null;
@@ -1,10 +1,12 @@
1
1
  import { signals_core_module_d, signals_core_module_w } from "../../../js/multimodal_v2.js";
2
2
  import { importLocale, isSupported } from "../locales/index.js";
3
+ import { toIntlLocale } from "../utils/locale.js";
3
4
  import { useRootStore } from "./root_store.js";
4
5
 
5
6
 
6
7
 
7
8
 
9
+
8
10
  const DEFAULT_LOCALE = "en";
9
11
  /**
10
12
  * Flatten a nested translations object into a Map with dot-separated keys.
@@ -156,6 +158,13 @@ const DEFAULT_LOCALE = "en";
156
158
  return DEFAULT_LOCALE;
157
159
  });
158
160
  /**
161
+ * Browser/Intl-compatible form of `effectiveLocale`. This is the shared
162
+ * normalization boundary: code that hands a locale to `Intl.*` or a
163
+ * `toLocale*String` call should read this instead of normalizing `effectiveLocale`
164
+ * itself at the call site (see INC-136 / QAI-5726 -- an unnormalized locale reaching
165
+ * Intl throws and crashes the widget render).
166
+ */ this.effectiveIntlLocale = signals_core_module_w(()=>toIntlLocale(this.effectiveLocale.value));
167
+ /**
159
168
  * Translate a key with optional field substitution.
160
169
  *
161
170
  * @param key - Dot-separated key path (e.g., "chat.enterMessage")
@@ -274,9 +274,9 @@ class UIStore extends BaseStore {
274
274
  break;
275
275
  }
276
276
  }, /**
277
- * Dismisses the widget to CLOSED state and marks that the user has manually dismissed.
278
- * Once dismissed, subsequent close actions will always return to CLOSED instead of
279
- * the configured starting position.
277
+ * Dismisses the widget to CLOSED state and records the manual dismissal for
278
+ * persistence and redirect recovery. Reopening starts a new open → compact →
279
+ * launcher cycle for experiences with a compact starting position.
280
280
  */ this.dismissWidget = ()=>{
281
281
  signals_core_module_r(()=>{
282
282
  this.rootStore.clearElementConversationState();
@@ -289,15 +289,15 @@ class UIStore extends BaseStore {
289
289
  this.saveUIState.flush();
290
290
  this.persistRedirectDismissedUIState();
291
291
  }, /**
292
- * Closes the widget, respecting the user's previous dismiss preference.
293
- * If the user has previously dismissed the widget (userHasDismissed), goes to CLOSED.
294
- * Otherwise, goes to the configured starting position.
292
+ * Closes the opened widget to its configured starting position. Experiences
293
+ * configured to start opened have no compact position, so they close directly
294
+ * to the launcher.
295
295
  */ this.closeWidget = ()=>{
296
296
  signals_core_module_r(()=>{
297
297
  this.rootStore.clearElementConversationState();
298
298
  this.rootStore.closeLightbox();
299
299
  this.rootStore.clearArtifact();
300
- if (this.uiState.userHasDismissed.value || this.initialDisplayMode.value === types_InitialDisplayMode.OPENED) {
300
+ if (this.initialDisplayMode.value === types_InitialDisplayMode.OPENED) {
301
301
  this.setDisplayMode(types_DisplayMode.CLOSED);
302
302
  } else {
303
303
  this.resetToStartingPosition();
@@ -85,7 +85,9 @@ class WebsiteNavigationStore {
85
85
  }
86
86
  // Minimize video if maximized so user can see the navigated content,
87
87
  // this will also clear the slide if it is maximized with an artifact
88
- this.rootStore.clearMaximizedState();
88
+ this.rootStore.endPresentationAndClearMaximizedState({
89
+ reason: "replaced"
90
+ });
89
91
  const promises = [];
90
92
  // Perform soft navigation if URL is provided
91
93
  if (message.url) {
@@ -0,0 +1,419 @@
1
+ import { __webpack_require__ } from "../../js/runtime.js";
2
+ import { serverLogger } from "../server-logger.js";
3
+ import "../telemetry/index.js";
4
+ import "./constants.js";
5
+ import "../utils/native_globals.js";
6
+ import { metrics } from "../telemetry/metrics.js";
7
+
8
+ /* provided dependency */ var text_turn_response_tracker_setTimeout = __webpack_require__(68034).pS;
9
+ /* provided dependency */ var text_turn_response_tracker_clearTimeout = __webpack_require__(68034).Kj;
10
+ function _extends() {
11
+ _extends = Object.assign || function(target) {
12
+ for(var i = 1; i < arguments.length; i++){
13
+ var source = arguments[i];
14
+ for(var key in source){
15
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
16
+ target[key] = source[key];
17
+ }
18
+ }
19
+ }
20
+ return target;
21
+ };
22
+ return _extends.apply(this, arguments);
23
+ }
24
+
25
+
26
+
27
+ const defaultDependencies = {
28
+ now: ()=>performance.now(),
29
+ setTimer: (callback, delayMs)=>text_turn_response_tracker_setTimeout(callback, delayMs),
30
+ clearTimer: (timerId)=>text_turn_response_tracker_clearTimeout(timerId),
31
+ isCurrentlyHidden: ()=>typeof document !== "undefined" && document.hidden,
32
+ emitIncrement: (metricName, tags)=>metrics.emitIncrement(metricName, tags),
33
+ emitTiming: (metricName, deltaMs, tags)=>metrics.emitTiming(metricName, deltaMs, tags),
34
+ serverLog: (params)=>serverLogger.serverLog(params)
35
+ };
36
+ const TEXT_RESPONSE_ENDED_REASONS = [
37
+ "suppressed",
38
+ "blank_output"
39
+ ];
40
+ const isTextResponseEndedReason = (reason)=>TEXT_RESPONSE_ENDED_REASONS.includes(reason);
41
+ // Kept in sync with AiConversations::TurnCause::CAUSES.
42
+ const TURN_CAUSES = [
43
+ "queue_wait",
44
+ "setup",
45
+ "llm_slow",
46
+ "agent_loop",
47
+ "tool_slow",
48
+ "side_llm_wait",
49
+ "generation_failed",
50
+ "output_moderation_retry",
51
+ "unknown"
52
+ ];
53
+ const isRecognizedTurnCause = (cause)=>TURN_CAUSES.includes(cause);
54
+ const CAUSE_REPORT_SOURCES = [
55
+ "deadline_watchdog",
56
+ "slow_turn_completion",
57
+ "output_moderation_retry"
58
+ ];
59
+ const causeReportSourceForLog = (source)=>CAUSE_REPORT_SOURCES.includes(source != null ? source : "") ? source : "unavailable";
60
+ /**
61
+ * Owns the non-reactive lifecycle and telemetry for one outstanding text turn.
62
+ * RootStore remains responsible for deciding which messages start and resolve a turn.
63
+ */ class TextTurnResponseTracker {
64
+ begin(param) {
65
+ let { clientId, callActive, initiallyHidden } = param;
66
+ // First-message-wins while a turn is still outstanding.
67
+ if (this.sentAt !== null && !this.counterEmitted) return null;
68
+ // A held metric belongs to the prior turn and reads its cause when flushed.
69
+ this.flushCauseHoldFor("turn_superseded");
70
+ this.flushHeldTimeout();
71
+ if (this.timedOut && !this.acceptedOutcomeLogged && this.clientId && this.sentAt !== null) {
72
+ var _this_timeoutFlushReason;
73
+ this.previousTimedOutTurn = {
74
+ clientId: this.clientId,
75
+ deadlineAt: this.sentAt + (/* inlined export .RESPONSE_BUDGET_MS */25000),
76
+ flushReason: (_this_timeoutFlushReason = this.timeoutFlushReason) != null ? _this_timeoutFlushReason : "turn_superseded"
77
+ };
78
+ }
79
+ this.sentAt = this.dependencies.now();
80
+ this.clientId = clientId;
81
+ this.counterEmitted = false;
82
+ this.replyHandled = false;
83
+ this.obscured = initiallyHidden;
84
+ this.callActive = callActive;
85
+ this.cause = null;
86
+ this.hasToolCall = false;
87
+ this.timedOut = false;
88
+ this.causeHoldActive = false;
89
+ this.timeoutFlushReason = null;
90
+ this.acceptedCauseReceivedAt = null;
91
+ this.acceptedCauseReportSource = "unavailable";
92
+ this.acceptedOutcomeLogged = false;
93
+ this.unrecognizedCause = null;
94
+ this.unrecognizedCauseReportSource = "unavailable";
95
+ this.unrecognizedCauseReceivedAt = null;
96
+ const handle = ++this.turnToken;
97
+ this.clearResponseTimer();
98
+ this.responseTimerId = this.dependencies.setTimer(()=>{
99
+ var _this_clientId;
100
+ this.responseTimerId = null;
101
+ if (handle !== this.turnToken || this.counterEmitted) return;
102
+ const obscured = this.obscured || this.dependencies.isCurrentlyHidden();
103
+ this.counterEmitted = true;
104
+ if (obscured) {
105
+ this.dependencies.emitIncrement("multimodal_text_response", {
106
+ status: "backgrounded",
107
+ call_active: this.callActiveTag
108
+ });
109
+ return;
110
+ }
111
+ this.clientTimedOut();
112
+ this.holdTimeoutForCause();
113
+ this.dependencies.serverLog({
114
+ type: "log",
115
+ message: "Multimodal text reply did not arrive within response budget",
116
+ extra: {
117
+ text_turn_client_id: (_this_clientId = this.clientId) != null ? _this_clientId : ""
118
+ }
119
+ });
120
+ }, (/* inlined export .RESPONSE_BUDGET_MS */25000));
121
+ return handle;
122
+ }
123
+ handleSendResult(handle, result) {
124
+ if (!(result == null ? void 0 : result.error)) return;
125
+ if (handle !== this.turnToken || this.counterEmitted) return;
126
+ this.clearResponseTimer();
127
+ this.dependencies.emitIncrement("multimodal_text_response", {
128
+ status: "send_failed",
129
+ call_active: this.callActiveTag
130
+ });
131
+ this.counterEmitted = true;
132
+ this.replyHandled = true;
133
+ this.turnToken++;
134
+ }
135
+ handleAssistantReply(param) {
136
+ let { currentlyHidden } = param;
137
+ if (this.sentAt === null || this.replyHandled) return;
138
+ this.clearResponseTimer();
139
+ const deltaMs = this.dependencies.now() - this.sentAt;
140
+ const withinBudget = deltaMs <= (/* inlined export .RESPONSE_BUDGET_MS */25000);
141
+ const obscured = this.obscured || currentlyHidden;
142
+ if (withinBudget || !obscured) {
143
+ this.dependencies.emitTiming("multimodal_text_response_ms", deltaMs, {
144
+ call_active: this.callActiveTag,
145
+ has_tool_call: this.hasToolCall ? "true" : "false"
146
+ });
147
+ }
148
+ if (!this.counterEmitted) {
149
+ var _this_cause;
150
+ const status = withinBudget ? "success" : obscured ? "backgrounded" : "timeout";
151
+ this.dependencies.emitIncrement("multimodal_text_response", _extends({
152
+ status,
153
+ call_active: this.callActiveTag
154
+ }, status === "timeout" ? {
155
+ cause: (_this_cause = this.cause) != null ? _this_cause : "unknown"
156
+ } : {}));
157
+ this.counterEmitted = true;
158
+ } else {
159
+ this.dependencies.serverLog({
160
+ type: "log",
161
+ message: "Multimodal text reply arrived after response budget (late)",
162
+ extra: {
163
+ context: "elapsed_ms:" + Math.round(deltaMs)
164
+ }
165
+ });
166
+ }
167
+ this.replyHandled = true;
168
+ this.turnToken++;
169
+ }
170
+ handleResponseEnded(payload) {
171
+ var _ref;
172
+ if (this.sentAt === null || this.counterEmitted) return;
173
+ if ((payload == null ? void 0 : payload.clientId) !== this.clientId) return;
174
+ const reason = (_ref = payload == null ? void 0 : payload.reason) != null ? _ref : "";
175
+ const status = isTextResponseEndedReason(reason) ? reason : "suppressed";
176
+ this.clearResponseTimer();
177
+ this.dependencies.emitIncrement("multimodal_text_response", _extends({
178
+ status,
179
+ call_active: this.callActiveTag
180
+ }, this.cause ? {
181
+ cause: this.cause
182
+ } : {}));
183
+ this.counterEmitted = true;
184
+ this.replyHandled = true;
185
+ this.turnToken++;
186
+ }
187
+ handleDiagnostic(payload) {
188
+ var _ref;
189
+ const cause = (_ref = payload == null ? void 0 : payload.cause) != null ? _ref : "";
190
+ const receivedAt = this.dependencies.now();
191
+ const causeReportSource = causeReportSourceForLog(payload == null ? void 0 : payload.causeReportSource);
192
+ if (this.handlePreviousTurnDiagnostic(payload, cause, causeReportSource, receivedAt)) return;
193
+ if (this.handleMismatchedDiagnostic(payload, cause, causeReportSource, receivedAt)) return;
194
+ if ((payload == null ? void 0 : payload.hasToolCall) === true) this.hasToolCall = true;
195
+ if (!cause) return;
196
+ if (!isRecognizedTurnCause(cause)) {
197
+ this.handleUnrecognizedCause(cause, causeReportSource, receivedAt);
198
+ return;
199
+ }
200
+ this.acceptDiagnosticCause(cause, causeReportSource, receivedAt);
201
+ }
202
+ onHidden() {
203
+ if (this.sentAt !== null && !this.replyHandled) this.obscured = true;
204
+ }
205
+ dispose() {
206
+ this.clearResponseTimer();
207
+ this.flushCauseHoldFor("store_disposed");
208
+ this.flushHeldTimeout();
209
+ this.turnToken++;
210
+ }
211
+ get callActiveTag() {
212
+ return this.callActive ? "true" : "false";
213
+ }
214
+ holdTimeoutForCause() {
215
+ this.flushHeldTimeout();
216
+ const callActive = this.callActiveTag;
217
+ this.heldTimeoutFlush = ()=>{
218
+ var _this_cause;
219
+ this.clearCauseHold();
220
+ this.dependencies.emitIncrement("multimodal_text_response", {
221
+ status: "timeout",
222
+ call_active: callActive,
223
+ cause: (_this_cause = this.cause) != null ? _this_cause : "unknown"
224
+ });
225
+ };
226
+ this.causeHoldTimerId = this.dependencies.setTimer(()=>{
227
+ this.flushCauseHoldFor("grace_expired");
228
+ this.heldTimeoutFlush == null ? void 0 : this.heldTimeoutFlush.call(this);
229
+ }, (/* inlined export .CAUSE_REPORT_GRACE_MS */1500));
230
+ }
231
+ handlePreviousTurnDiagnostic(payload, cause, causeReportSource, receivedAt) {
232
+ const previous = this.previousTimedOutTurn;
233
+ if (!previous || (payload == null ? void 0 : payload.clientId) !== previous.clientId || !isRecognizedTurnCause(cause)) {
234
+ return false;
235
+ }
236
+ this.logDiagnostic("accepted_after_timeout_flush", previous, {
237
+ cause,
238
+ causeReportSource,
239
+ receivedAt,
240
+ timeoutFlushReason: previous.flushReason
241
+ });
242
+ this.previousTimedOutTurn = null;
243
+ return true;
244
+ }
245
+ handleMismatchedDiagnostic(payload, cause, causeReportSource, receivedAt) {
246
+ if ((payload == null ? void 0 : payload.clientId) === this.clientId) return false;
247
+ // Cause-less tool progress is valid and deliberately excluded from mismatch noise.
248
+ if (this.timedOut && cause) {
249
+ const turn = this.currentDiagnosticTurn;
250
+ if (turn) {
251
+ this.logDiagnostic("client_id_mismatch", turn, {
252
+ cause,
253
+ causeReportSource,
254
+ receivedAt,
255
+ receivedTextTurnClientId: payload == null ? void 0 : payload.clientId
256
+ });
257
+ }
258
+ }
259
+ return true;
260
+ }
261
+ handleUnrecognizedCause(cause, causeReportSource, receivedAt) {
262
+ if (this.timedOut) {
263
+ const turn = this.currentDiagnosticTurn;
264
+ if (turn) {
265
+ this.logDiagnostic("unrecognized_cause", turn, {
266
+ cause,
267
+ causeReportSource,
268
+ receivedAt
269
+ });
270
+ }
271
+ return;
272
+ }
273
+ this.unrecognizedCause = cause;
274
+ this.unrecognizedCauseReportSource = causeReportSource;
275
+ this.unrecognizedCauseReceivedAt = receivedAt;
276
+ }
277
+ acceptDiagnosticCause(cause, causeReportSource, receivedAt) {
278
+ var _this_acceptedCauseReceivedAt;
279
+ if (this.cause === null || this.cause === "unknown") {
280
+ this.cause = cause;
281
+ this.acceptedCauseReportSource = causeReportSource;
282
+ }
283
+ (_this_acceptedCauseReceivedAt = this.acceptedCauseReceivedAt) != null ? _this_acceptedCauseReceivedAt : this.acceptedCauseReceivedAt = receivedAt;
284
+ if (this.timedOut && !this.acceptedOutcomeLogged) {
285
+ const status = this.causeHoldActive ? "accepted_within_grace" : this.timeoutFlushReason === "grace_expired" ? "accepted_after_grace" : "accepted_after_timeout_flush";
286
+ const turn = this.currentDiagnosticTurn;
287
+ if (turn) {
288
+ this.logDiagnostic(status, turn, _extends({
289
+ cause,
290
+ causeReportSource,
291
+ receivedAt
292
+ }, status === "accepted_after_timeout_flush" && this.timeoutFlushReason ? {
293
+ timeoutFlushReason: this.timeoutFlushReason
294
+ } : {}));
295
+ }
296
+ this.acceptedOutcomeLogged = true;
297
+ }
298
+ if (this.causeHoldActive) {
299
+ this.causeHoldActive = false;
300
+ this.flushHeldTimeout();
301
+ }
302
+ }
303
+ clientTimedOut() {
304
+ this.timedOut = true;
305
+ this.causeHoldActive = true;
306
+ const turn = this.currentDiagnosticTurn;
307
+ if (!turn) return;
308
+ if (this.acceptedCauseReceivedAt !== null && !this.acceptedOutcomeLogged) {
309
+ var _this_cause;
310
+ this.logDiagnostic("accepted_within_grace", turn, {
311
+ cause: (_this_cause = this.cause) != null ? _this_cause : undefined,
312
+ causeReportSource: this.acceptedCauseReportSource,
313
+ receivedAt: this.acceptedCauseReceivedAt
314
+ });
315
+ this.acceptedOutcomeLogged = true;
316
+ }
317
+ if (this.unrecognizedCause !== null) {
318
+ this.logDiagnostic("unrecognized_cause", turn, {
319
+ cause: this.unrecognizedCause,
320
+ causeReportSource: this.unrecognizedCauseReportSource,
321
+ receivedAt: this.unrecognizedCauseReceivedAt
322
+ });
323
+ this.unrecognizedCause = null;
324
+ this.unrecognizedCauseReportSource = "unavailable";
325
+ this.unrecognizedCauseReceivedAt = null;
326
+ }
327
+ }
328
+ flushCauseHoldFor(reason) {
329
+ if (!this.causeHoldActive) return;
330
+ this.causeHoldActive = false;
331
+ this.timeoutFlushReason = reason;
332
+ if (this.cause === null) {
333
+ const turn = this.currentDiagnosticTurn;
334
+ if (turn) this.logDiagnostic("no_valid_diagnostic", turn, {
335
+ timeoutFlushReason: reason
336
+ });
337
+ }
338
+ }
339
+ get currentDiagnosticTurn() {
340
+ if (!this.clientId || this.sentAt === null) return null;
341
+ return {
342
+ clientId: this.clientId,
343
+ deadlineAt: this.sentAt + (/* inlined export .RESPONSE_BUDGET_MS */25000)
344
+ };
345
+ }
346
+ logDiagnostic(diagnosticStatus, turn, param) {
347
+ let { cause, causeReportSource, receivedAt, receivedTextTurnClientId, timeoutFlushReason } = param === void 0 ? {} : param;
348
+ try {
349
+ this.dependencies.serverLog({
350
+ type: "log",
351
+ message: "Multimodal text turn diagnostic",
352
+ extra: _extends({
353
+ diagnostic_source: "client_timeout_attribution",
354
+ diagnostic_status: diagnosticStatus,
355
+ text_turn_client_id: turn.clientId
356
+ }, receivedTextTurnClientId ? {
357
+ received_text_turn_client_id: receivedTextTurnClientId
358
+ } : {}, cause ? {
359
+ cause
360
+ } : {}, causeReportSource ? {
361
+ cause_report_source: causeReportSource
362
+ } : {}, timeoutFlushReason ? {
363
+ timeout_flush_reason: timeoutFlushReason
364
+ } : {}, receivedAt !== null && receivedAt !== undefined ? {
365
+ delivery_lag_ms: Math.round(receivedAt - turn.deadlineAt)
366
+ } : {})
367
+ });
368
+ } catch (unused) {
369
+ // Diagnostics are best-effort and must never alter the response SLI lifecycle.
370
+ }
371
+ }
372
+ flushHeldTimeout() {
373
+ this.heldTimeoutFlush == null ? void 0 : this.heldTimeoutFlush.call(this);
374
+ }
375
+ clearCauseHold() {
376
+ if (this.causeHoldTimerId !== null) {
377
+ this.dependencies.clearTimer(this.causeHoldTimerId);
378
+ this.causeHoldTimerId = null;
379
+ }
380
+ this.heldTimeoutFlush = null;
381
+ }
382
+ clearResponseTimer() {
383
+ if (this.responseTimerId !== null) {
384
+ this.dependencies.clearTimer(this.responseTimerId);
385
+ this.responseTimerId = null;
386
+ }
387
+ }
388
+ constructor(dependencies = {}){
389
+ this.sentAt = null;
390
+ this.counterEmitted = false;
391
+ this.replyHandled = false;
392
+ this.responseTimerId = null;
393
+ this.causeHoldTimerId = null;
394
+ this.heldTimeoutFlush = null;
395
+ this.turnToken = 0;
396
+ this.clientId = null;
397
+ this.cause = null;
398
+ this.hasToolCall = false;
399
+ this.obscured = false;
400
+ this.callActive = false;
401
+ this.timedOut = false;
402
+ // True only during the 1.5-second window when the timeout metric is held
403
+ // so a matching backend cause can still replace `unknown`.
404
+ this.causeHoldActive = false;
405
+ // Why the client stopped waiting for a cause. Retained so a later diagnostic
406
+ // can distinguish normal grace expiry from a turn/lifecycle interruption.
407
+ this.timeoutFlushReason = null;
408
+ this.acceptedCauseReceivedAt = null;
409
+ this.acceptedCauseReportSource = "unavailable";
410
+ this.acceptedOutcomeLogged = false;
411
+ this.unrecognizedCause = null;
412
+ this.unrecognizedCauseReportSource = "unavailable";
413
+ this.unrecognizedCauseReceivedAt = null;
414
+ this.previousTimedOutTurn = null;
415
+ this.dependencies = _extends({}, defaultDependencies, dependencies);
416
+ }
417
+ }
418
+
419
+ export { TextTurnResponseTracker };
@@ -80,6 +80,26 @@ var types_ButtonShape = /*#__PURE__*/ function(ButtonShape) {
80
80
  InitialDisplayMode["CLOSED"] = "closed";
81
81
  return InitialDisplayMode;
82
82
  }({});
83
+ /**
84
+ * Which of the two shapes the deck artifact takes. Every difference in the deck
85
+ * component — navigation, caption panel, zoom-follow, dot palette, surface and
86
+ * sizing — turns on this one discriminant, because all of them are consequences
87
+ * of the same fact: whose hands the deck is in.
88
+ *
89
+ * - `Guided`: Piper narrates, slides advance on their own (the decks store's
90
+ * timer), and she can zoom into the slide she is pointing at. Also covers a
91
+ * revisit, where the store hands over a single static slide with
92
+ * `sequenceLength: 1` — same chrome, minus the dots.
93
+ * - `SelfGuided`: the visitor opened the deck from its timeline card and steps
94
+ * through it themselves, reading the narration beneath each slide.
95
+ *
96
+ * Local to the widget, unlike the enums at the top of this file: the values
97
+ * cross no wire, they only name the mode and key the deck's style variants.
98
+ */ var types_DeckMode = /*#__PURE__*/ function(DeckMode) {
99
+ DeckMode["Guided"] = "guided";
100
+ DeckMode["SelfGuided"] = "self_guided";
101
+ return DeckMode;
102
+ }({});
83
103
  /**
84
104
  * Identifies what triggered a call start so the agent can open the call with the
85
105
  * right intent (e.g. begin a presentation) instead of a generic greeting.
@@ -102,4 +122,4 @@ var types_ButtonShape = /*#__PURE__*/ function(ButtonShape) {
102
122
  }({});
103
123
 
104
124
  var multimodal_v2_types_CallMode = /* unused export */ undefined;
105
- export { multimodal_v2_types_CallMode as CallMode, types_AvatarProvider as AvatarProvider, types_ButtonShape as ButtonShape, types_ButtonStyle as ButtonStyle, types_ConversationMode as ConversationMode, types_DisplayMode as DisplayMode, types_InitialDisplayMode as InitialDisplayMode, types_InputHiddenReason as InputHiddenReason, types_InputState as InputState, types_LoadingState as LoadingState, types_MaximizedState as MaximizedState, types_OpenedDisplayMode as OpenedDisplayMode, types_StartCallTrigger as StartCallTrigger };
125
+ export { multimodal_v2_types_CallMode as CallMode, types_AvatarProvider as AvatarProvider, types_ButtonShape as ButtonShape, types_ButtonStyle as ButtonStyle, types_ConversationMode as ConversationMode, types_DeckMode as DeckMode, types_DisplayMode as DisplayMode, types_InitialDisplayMode as InitialDisplayMode, types_InputHiddenReason as InputHiddenReason, types_InputState as InputState, types_LoadingState as LoadingState, types_MaximizedState as MaximizedState, types_OpenedDisplayMode as OpenedDisplayMode, types_StartCallTrigger as StartCallTrigger };
@@ -7,7 +7,8 @@ import "../stores/translation_store.js";
7
7
  *
8
8
  * Our locale identifiers use underscores (they mirror the locale filenames), which the
9
9
  * Intl constructors reject with `RangeError: Invalid language tag`. Anything handed to
10
- * `Intl.*` or a `toLocale*String` call must go through this first.
10
+ * `Intl.*` or a `toLocale*String` call must go through this first -- prefer reading
11
+ * `TranslationStore.effectiveIntlLocale` over calling this directly on a store locale.
11
12
  */ function toIntlLocale(locale) {
12
13
  if (!locale) return (/* inlined export .DEFAULT_LOCALE */"en");
13
14
  return locale.replace(/_/g, "-");