@wix/himalaya-cli 0.960.0 → 0.962.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 (90) hide show
  1. package/dist/cli.mjs +1 -0
  2. package/package.json +2 -2
  3. package/templates/beauty-salon-owner/config.ts +1 -1
  4. package/templates/beauty-salon-owner/dev/index.ts +3075 -0
  5. package/templates/beauty-salon-owner/tier5-src/_shared/aria-chat.ts +466 -9
  6. package/templates/beauty-salon-owner/tier5-src/_shared/aria-generic-widget.ts +251 -0
  7. package/templates/beauty-salon-owner/tier5-src/_shared/aria-widget-kinds.ts +332 -0
  8. package/templates/beauty-salon-owner/tier5-src/_shared/aria-widget-model.ts +331 -0
  9. package/templates/beauty-salon-owner/tier5-src/_shared/aria-widget-view.ts +722 -0
  10. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
  11. package/templates/community-club-owner/config.ts +1 -1
  12. package/templates/community-club-owner/dev/index.ts +3075 -0
  13. package/templates/community-club-owner/tier5-src/_shared/aria-chat.ts +466 -9
  14. package/templates/community-club-owner/tier5-src/_shared/aria-generic-widget.ts +251 -0
  15. package/templates/community-club-owner/tier5-src/_shared/aria-widget-kinds.ts +332 -0
  16. package/templates/community-club-owner/tier5-src/_shared/aria-widget-model.ts +331 -0
  17. package/templates/community-club-owner/tier5-src/_shared/aria-widget-view.ts +722 -0
  18. package/templates/community-club-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
  19. package/templates/events-meetups-owner/config.ts +1 -1
  20. package/templates/events-meetups-owner/dev/index.ts +3075 -0
  21. package/templates/events-meetups-owner/tier5-src/_shared/aria-chat.ts +466 -9
  22. package/templates/events-meetups-owner/tier5-src/_shared/aria-generic-widget.ts +251 -0
  23. package/templates/events-meetups-owner/tier5-src/_shared/aria-widget-kinds.ts +332 -0
  24. package/templates/events-meetups-owner/tier5-src/_shared/aria-widget-model.ts +331 -0
  25. package/templates/events-meetups-owner/tier5-src/_shared/aria-widget-view.ts +722 -0
  26. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
  27. package/templates/gym-fitness-owner/config.ts +1 -1
  28. package/templates/gym-fitness-owner/dev/index.ts +3075 -0
  29. package/templates/gym-fitness-owner/tier5-src/_shared/aria-chat.ts +466 -9
  30. package/templates/gym-fitness-owner/tier5-src/_shared/aria-generic-widget.ts +251 -0
  31. package/templates/gym-fitness-owner/tier5-src/_shared/aria-widget-kinds.ts +332 -0
  32. package/templates/gym-fitness-owner/tier5-src/_shared/aria-widget-model.ts +331 -0
  33. package/templates/gym-fitness-owner/tier5-src/_shared/aria-widget-view.ts +722 -0
  34. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
  35. package/templates/nonprofit-cause-owner/config.ts +1 -1
  36. package/templates/nonprofit-cause-owner/dev/index.ts +3075 -0
  37. package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-chat.ts +466 -9
  38. package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-generic-widget.ts +251 -0
  39. package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-widget-kinds.ts +332 -0
  40. package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-widget-model.ts +331 -0
  41. package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-widget-view.ts +722 -0
  42. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
  43. package/templates/owner-lite/config.ts +1 -1
  44. package/templates/owner-lite/dev/index.ts +3075 -0
  45. package/templates/owner-lite/tier5-src/_shared/aria-chat.ts +466 -9
  46. package/templates/owner-lite/tier5-src/_shared/aria-generic-widget.ts +251 -0
  47. package/templates/owner-lite/tier5-src/_shared/aria-widget-kinds.ts +332 -0
  48. package/templates/owner-lite/tier5-src/_shared/aria-widget-model.ts +331 -0
  49. package/templates/owner-lite/tier5-src/_shared/aria-widget-view.ts +722 -0
  50. package/templates/owner-lite/tier5-src/himi-owner-lite-aria/index.ts +29 -0
  51. package/templates/podcast-creator-owner/config.ts +1 -1
  52. package/templates/podcast-creator-owner/dev/index.ts +3075 -0
  53. package/templates/podcast-creator-owner/tier5-src/_shared/aria-chat.ts +466 -9
  54. package/templates/podcast-creator-owner/tier5-src/_shared/aria-generic-widget.ts +251 -0
  55. package/templates/podcast-creator-owner/tier5-src/_shared/aria-widget-kinds.ts +332 -0
  56. package/templates/podcast-creator-owner/tier5-src/_shared/aria-widget-model.ts +331 -0
  57. package/templates/podcast-creator-owner/tier5-src/_shared/aria-widget-view.ts +722 -0
  58. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
  59. package/templates/portfolio-creative-owner/config.ts +1 -1
  60. package/templates/portfolio-creative-owner/dev/index.ts +3075 -0
  61. package/templates/portfolio-creative-owner/tier5-src/_shared/aria-chat.ts +466 -9
  62. package/templates/portfolio-creative-owner/tier5-src/_shared/aria-generic-widget.ts +251 -0
  63. package/templates/portfolio-creative-owner/tier5-src/_shared/aria-widget-kinds.ts +332 -0
  64. package/templates/portfolio-creative-owner/tier5-src/_shared/aria-widget-model.ts +331 -0
  65. package/templates/portfolio-creative-owner/tier5-src/_shared/aria-widget-view.ts +722 -0
  66. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
  67. package/templates/restaurant-owner/config.ts +1 -1
  68. package/templates/restaurant-owner/dev/index.ts +3075 -0
  69. package/templates/restaurant-owner/tier5-src/_shared/aria-chat.ts +466 -9
  70. package/templates/restaurant-owner/tier5-src/_shared/aria-generic-widget.ts +251 -0
  71. package/templates/restaurant-owner/tier5-src/_shared/aria-widget-kinds.ts +332 -0
  72. package/templates/restaurant-owner/tier5-src/_shared/aria-widget-model.ts +331 -0
  73. package/templates/restaurant-owner/tier5-src/_shared/aria-widget-view.ts +722 -0
  74. package/templates/restaurant-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
  75. package/templates/retail-store-owner/config.ts +1 -1
  76. package/templates/retail-store-owner/dev/index.ts +3075 -0
  77. package/templates/retail-store-owner/tier5-src/_shared/aria-chat.ts +466 -9
  78. package/templates/retail-store-owner/tier5-src/_shared/aria-generic-widget.ts +251 -0
  79. package/templates/retail-store-owner/tier5-src/_shared/aria-widget-kinds.ts +332 -0
  80. package/templates/retail-store-owner/tier5-src/_shared/aria-widget-model.ts +331 -0
  81. package/templates/retail-store-owner/tier5-src/_shared/aria-widget-view.ts +722 -0
  82. package/templates/retail-store-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
  83. package/templates/yoga-studio-owner/config.ts +1 -1
  84. package/templates/yoga-studio-owner/dev/index.ts +3075 -0
  85. package/templates/yoga-studio-owner/tier5-src/_shared/aria-chat.ts +466 -9
  86. package/templates/yoga-studio-owner/tier5-src/_shared/aria-generic-widget.ts +251 -0
  87. package/templates/yoga-studio-owner/tier5-src/_shared/aria-widget-kinds.ts +332 -0
  88. package/templates/yoga-studio-owner/tier5-src/_shared/aria-widget-model.ts +331 -0
  89. package/templates/yoga-studio-owner/tier5-src/_shared/aria-widget-view.ts +722 -0
  90. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
@@ -0,0 +1,722 @@
1
+ // The active response widget, flattened into something a descriptor can bind
2
+ // to.
3
+ //
4
+ // `aria-widget-kinds.ts` turns a scanned envelope into a
5
+ // `NormalizedWidget`. That model is shaped for CODE — nested objects, a typed
6
+ // `extra` bag, `null` for an absent button. A descriptor cannot read any of
7
+ // that: `$service:` resolves a path to a SCALAR, and `$item.` substitutes whole
8
+ // prop values only, so a row whose field is missing renders as nothing at all.
9
+ //
10
+ // This module is the adapter between the two. Everything it produces is either
11
+ // a scalar or an array of all-scalar rows, every field is always present, and
12
+ // every collapsible element carries its own `*Hidden` boolean — the same shape
13
+ // the transcript bubbles already use for their three link slots.
14
+
15
+ import type { NormalizedWidget, WidgetKind } from "./aria-widget-model.js";
16
+ import { composeGenericCard } from "./aria-generic-widget.js";
17
+ import { normalizeWidget } from "./aria-widget-kinds.js";
18
+ import { scanEnvelopes, widgetsOfParts } from "./aria-widgets.js";
19
+
20
+ /** Just enough of a transcript row to find the active widget in. Declared here
21
+ * rather than imported from `aria-chat.ts` so this module has no dependency on
22
+ * it at all — see `Disclose` below for why that matters. */
23
+ export interface WidgetSourceMessage {
24
+ role?: string;
25
+ content?: unknown;
26
+ }
27
+
28
+ /** How a button's label is rendered when it carries an off-Wix destination.
29
+ *
30
+ * Passed IN rather than imported, and the reason is mechanical: `aria-chat.ts`
31
+ * imports this module to build its state, so importing `disclosedLabel` back
32
+ * out of it would close a runtime cycle through the bundler. Injection keeps
33
+ * the single implementation — the caller hands over the very same
34
+ * `disclosedLabel` the transcript's link rows use — while leaving this module
35
+ * standalone and testable without booting the chat core. */
36
+ export type Disclose = (link: { label: string; url: string }) => string;
37
+
38
+ /** What the region keys on. The widget kinds, plus the one state that is not a
39
+ * widget: nothing to show, region collapsed. */
40
+ export type WidgetCase = WidgetKind | "NONE";
41
+
42
+ /** Every case the descriptor must carry, in a fixed order.
43
+ *
44
+ * Exported so the `FragmentSlot` and the test that pins it read the SAME list
45
+ * rather than two hand-kept copies — a case the union has and the descriptor
46
+ * does not is a widget that renders nothing, silently. */
47
+ export const WIDGET_CASES: readonly WidgetCase[] = [
48
+ "SINGLE_SELECT", "MULTI_SELECT", "SINGLE_SELECT_BUTTON", "RADIO_BUTTON", "PLANS",
49
+ "PREMIUM_UPSELL", "VISUAL_DISPLAY", "AUTOCOMPLETE", "ITEMS_SELECTION", "QUESTIONNAIRE",
50
+ "CHART", "TODO_PLAN", "ASYNC_TOOL", "OAUTH_REQUIRED", "UNKNOWN", "NONE",
51
+ ];
52
+
53
+ /** One body row of a display widget. All-scalar, every field present: this is
54
+ * `$item.`-templated, and a row missing a field substitutes as nothing. */
55
+ export interface WidgetRow {
56
+ id: string;
57
+ title: string;
58
+ subtitle: string;
59
+ /** `true` when `subtitle` is empty, so the row collapses its second line
60
+ * instead of leaving a gap. `$item.` cannot express "hide when empty". */
61
+ subtitleHidden: boolean;
62
+ }
63
+
64
+ /** One selectable option, flattened for binding.
65
+ *
66
+ * Same rule as `WidgetRow` and for the same reason: `$item.` substitutes whole
67
+ * prop values, so every field is present and scalar or the row renders as
68
+ * nothing at all. `selected` is a boolean the worker owns — a `$service:`
69
+ * binding is a read-only snapshot, so a checkbox reads it and writes through a
70
+ * `widgetToggle` dispatch rather than mutating anything itself. */
71
+ export interface WidgetOptionRow {
72
+ id: string;
73
+ value: string;
74
+ title: string;
75
+ subtitle: string;
76
+ subtitleHidden: boolean;
77
+ text: string;
78
+ textHidden: boolean;
79
+ pictureUrl: string;
80
+ pictureHidden: boolean;
81
+ /** The per-option button's label, already host-disclosed. */
82
+ buttonLabel: string;
83
+ buttonHidden: boolean;
84
+ /** PLANS' benefit list, joined — an array cannot ride on a templated row. */
85
+ listText: string;
86
+ listHidden: boolean;
87
+ selected: boolean;
88
+ /** This option's own button `respondText`, before substitution. Carried on
89
+ * the row because the row is what a tap identifies. */
90
+ respondText: string;
91
+ }
92
+
93
+ /** One slice of a CHART. `value` is a RAW magnitude, never a percentage: the
94
+ * renderer computes each share, because one handed percentages that do not sum
95
+ * to 100 cannot draw them honestly. */
96
+ export interface WidgetSlice {
97
+ label: string;
98
+ value: number;
99
+ }
100
+
101
+ /** One step of a TODO_PLAN, shaped for `Timeline`. */
102
+ export interface WidgetStep {
103
+ id: string;
104
+ title: string;
105
+ subtitle: string;
106
+ /** `Timeline` draws a completed entry differently. */
107
+ done: boolean;
108
+ }
109
+
110
+ /** The active widget, as the screen reads it. */
111
+ export interface WidgetView {
112
+ kind: WidgetCase;
113
+ /** `true` when there is nothing to show. The whole region is hidden on this
114
+ * rather than on `kind === "NONE"`, because a descriptor cannot compare —
115
+ * `hiddenWhen` binds a boolean and nothing else. It is what keeps the region
116
+ * from leaving a gap in the transcript when the assistant is just talking. */
117
+ hidden: boolean;
118
+ /** Prose. For PREMIUM_UPSELL this is the body under `title`. */
119
+ text: string;
120
+ textHidden: boolean;
121
+ title: string;
122
+ titleHidden: boolean;
123
+ bottomText: string;
124
+ bottomTextHidden: boolean;
125
+ badge: string;
126
+ badgeHidden: boolean;
127
+ imageUrl: string;
128
+ imageHidden: boolean;
129
+ /** Footer buttons, already host-disclosed (see `discloseButton`). */
130
+ primaryLabel: string;
131
+ primaryHidden: boolean;
132
+ primaryUrl: string;
133
+ /** The footer buttons' `respondText`, before substitution. */
134
+ primaryRespondText: string;
135
+ secondaryRespondText: string;
136
+ secondaryLabel: string;
137
+ secondaryHidden: boolean;
138
+ secondaryUrl: string;
139
+ rows: WidgetRow[];
140
+ /** The selectable options. Separate from `rows`, which is VISUAL_DISPLAY's
141
+ * body sections — the catalogue sends BOTH at once, so conflating them
142
+ * silently drops one. */
143
+ options: WidgetOptionRow[];
144
+ /** How many options may be chosen at once, `0` for no limit. Enforced in the
145
+ * worker: no component carries a selection-count prop, so this cannot be
146
+ * declared in the descriptor. */
147
+ maxSelected: number;
148
+ /** `true` once the limit is reached, so further options can be disabled. */
149
+ limitReached: boolean;
150
+ /** The line explaining why, empty when there is nothing to explain. */
151
+ limitText: string;
152
+ limitTextHidden: boolean;
153
+ /** The payload said the primary button needs a selection
154
+ * (`disableIfNothingSelected`). Kept as its own field because
155
+ * `primaryDisabled` is derived and has to be recomputed on every toggle —
156
+ * a derived value cannot also be its own input. */
157
+ requiresSelection: boolean;
158
+ /** `true` when the primary button must not be tappable — either
159
+ * `requiresSelection` with nothing chosen, or a send in flight. */
160
+ primaryDisabled: boolean;
161
+ /** Its exact inverse, carried rather than computed, because a descriptor
162
+ * cannot negate: `hiddenPath` binds a boolean and there is no `not`. The
163
+ * live button hides on `primaryDisabled` and the greyed twin hides on this,
164
+ * so exactly one of the two is ever on screen. */
165
+ primaryEnabled: boolean;
166
+ /** A send started from a widget button is in flight. */
167
+ submitting: boolean;
168
+
169
+ // --- CHART ---------------------------------------------------------------
170
+ /** `$service:`-bound, and it has to be: `DonutChart.slices_path` and
171
+ * `ChartSeries.values_path` are SERVICE-ONLY, so a chart cannot live in a
172
+ * templated row. That constraint is why the widget region exists at all.
173
+ * <!-- platform-lint-ok: a property of the two native decoders, not of every surface --> */
174
+ slices: WidgetSlice[];
175
+ /** The raw magnitudes, for the non-pie chart kinds. */
176
+ values: number[];
177
+ /** `"pie"` draws a `DonutChart` at `thickness: 0`; anything else is a
178
+ * `Chart`. Carried as a boolean as well, because a descriptor cannot
179
+ * compare strings. */
180
+ chartIsPie: boolean;
181
+ /** Hide the pie. A dedicated flag because `chartIsSeries` is NOT its
182
+ * negation: an EMPTY chart is neither, so hiding each component on the
183
+ * other's flag left both on screen as blank boxes. */
184
+ chartPieHidden: boolean;
185
+ chartSeriesHidden: boolean;
186
+ chartEmptyHidden: boolean;
187
+ /** Hide the async spinner. `asyncUntracked` is not the right flag: a TRACKED
188
+ * job that reaches `finished` or `failed` still has an id, so hiding on
189
+ * untracked left a terminal job spinning forever. */
190
+ asyncSpinnerHidden: boolean;
191
+ /** Hide the provider spinner. Separate from `itemsErrorHidden`, which is
192
+ * TRUE while loading and FALSE after a failure — using it hid the spinner
193
+ * exactly when it was needed and showed it beside the error. */
194
+ itemsLoadingHidden: boolean;
195
+ chartIsSeries: boolean;
196
+ chartKind: string;
197
+ chartEmpty: boolean;
198
+ description: string;
199
+ descriptionHidden: boolean;
200
+
201
+ // --- TODO_PLAN -----------------------------------------------------------
202
+ steps: WidgetStep[];
203
+ stepsEmpty: boolean;
204
+ /** 0–1. `Timeline` shows WHICH steps are done; this says how far along the
205
+ * whole plan is, which a list of ticks does not answer at a glance. */
206
+ progress: number;
207
+ progressText: string;
208
+
209
+ // --- ASYNC_TOOL ----------------------------------------------------------
210
+ asyncJobId: string;
211
+ /** The job's last known state. Kept so a tick can tell "no change" from
212
+ * "not asked yet" without a second field. */
213
+ asyncState: string;
214
+ /** The message for the job's CURRENT state, already chosen from
215
+ * `statusMessages` — a descriptor cannot pick between four strings. */
216
+ asyncMessage: string;
217
+ /** `true` while the job is still running, so the spinner shows. */
218
+ asyncRunning: boolean;
219
+ /** A job with no id: the status messages are still worth showing, but there
220
+ * is nothing to poll for, so it never spins. */
221
+ asyncUntracked: boolean;
222
+ /** The four status lines, kept so a later state change can pick a different
223
+ * one without re-parsing the payload. */
224
+ extraStatusMessages: Record<string, string>;
225
+
226
+ // --- AUTOCOMPLETE --------------------------------------------------------
227
+ /** What the owner has typed. There is no autocomplete component anywhere in
228
+ * the proto — `TextFieldProps.suggestions` is the OS keyboard strip, not a
229
+ * dropdown — so this is a composition: `SearchField` → worker filter → a
230
+ * `$service:`-bound list that collapses when empty. */
231
+ query: string;
232
+ /** The options that survive `query`. A separate array from `options`, so
233
+ * filtering never destroys what the service sent. */
234
+ suggestions: WidgetOptionRow[];
235
+ suggestionsEmpty: boolean;
236
+ /** `true` when a query is typed and matches nothing — which is a different
237
+ * state from "nothing typed yet", and reads differently to the owner. */
238
+ noMatches: boolean;
239
+ /** `single` or `multi`. */
240
+ selectionType: string;
241
+
242
+ // --- QUESTIONNAIRE -------------------------------------------------------
243
+ questions: WidgetQuestion[];
244
+ questionsEmpty: boolean;
245
+ /** `true` while any `required` question is unanswered. The submit is gated
246
+ * on it — there is no cross-field validation in `Validation`, so "every
247
+ * required question has an answer" has to be worked out here. */
248
+ answersIncomplete: boolean;
249
+ answersText: string;
250
+ answersTextHidden: boolean;
251
+
252
+ // --- ITEMS_SELECTION -----------------------------------------------------
253
+ providerKey: string;
254
+ /** The provider query is in flight. */
255
+ itemsLoading: boolean;
256
+ /** It failed. The chat stays usable; only the widget says so. */
257
+ itemsError: string;
258
+ itemsErrorHidden: boolean;
259
+ itemsEmpty: boolean;
260
+
261
+ // --- UNKNOWN --------------------------------------------------------------
262
+ /** A generic card composed by the worker for a widget kind this build does
263
+ * not recognise, as a JSON string for a `WorkerSubtree` slot.
264
+ *
265
+ * Empty for every KNOWN kind — those have a real `FragmentSlot` case — and
266
+ * empty too when the composer could not build one it is sure of. Empty is
267
+ * not a special case the descriptor has to test for: an empty string fails
268
+ * validation, and a failed validation renders the declared fallback, which
269
+ * is the plain prose card. The fail-closed path and the nothing-to-show
270
+ * path are the same path. */
271
+ treeJson: string;
272
+ }
273
+
274
+ /** One question of a QUESTIONNAIRE, flattened. `$item.`-templated, so every
275
+ * field is present and scalar. */
276
+ export interface WidgetQuestion {
277
+ id: string;
278
+ title: string;
279
+ subtitle: string;
280
+ subtitleHidden: boolean;
281
+ /** `single-select`, `multi-select`, `text`, … as the service spells it. */
282
+ type: string;
283
+ required: boolean;
284
+ /** `true` when `required` and nothing chosen or typed. */
285
+ missing: boolean;
286
+ /** The chosen option's title, or the typed text. Empty when unanswered. */
287
+ answer: string;
288
+ answerHidden: boolean;
289
+ /** The free-text escape hatch, per question. */
290
+ freePlaceholder: string;
291
+ freeHidden: boolean;
292
+ }
293
+
294
+ /** Nothing to show. The region binds this and collapses. */
295
+ export const NO_WIDGET: WidgetView = {
296
+ kind: "NONE",
297
+ hidden: true,
298
+ text: "", textHidden: true,
299
+ title: "", titleHidden: true,
300
+ bottomText: "", bottomTextHidden: true,
301
+ badge: "", badgeHidden: true,
302
+ imageUrl: "", imageHidden: true,
303
+ primaryLabel: "", primaryHidden: true, primaryUrl: "", primaryRespondText: "",
304
+ secondaryLabel: "", secondaryHidden: true, secondaryUrl: "", secondaryRespondText: "",
305
+ rows: [],
306
+ options: [], maxSelected: 0, limitReached: false, requiresSelection: false,
307
+ limitText: "", limitTextHidden: true,
308
+ primaryDisabled: false, primaryEnabled: true, submitting: false,
309
+ slices: [], values: [], chartIsPie: false, chartIsSeries: false, chartKind: "",
310
+ chartPieHidden: true, chartSeriesHidden: true, chartEmptyHidden: true,
311
+ asyncSpinnerHidden: true, itemsLoadingHidden: true,
312
+ chartEmpty: true, description: "", descriptionHidden: true,
313
+ steps: [], stepsEmpty: true, progress: 0, progressText: "",
314
+ asyncJobId: "", asyncState: "", asyncMessage: "", asyncRunning: false, asyncUntracked: false,
315
+ extraStatusMessages: {},
316
+ query: "", suggestions: [], suggestionsEmpty: true, noMatches: false, selectionType: "",
317
+ questions: [], questionsEmpty: true, answersIncomplete: false,
318
+ answersText: "", answersTextHidden: true,
319
+ providerKey: "", itemsLoading: false, itemsError: "", itemsErrorHidden: true, itemsEmpty: true,
320
+ treeJson: "",
321
+ };
322
+
323
+ /** A button's label as the owner should see it.
324
+ *
325
+ * A widget-borne url is model-influenced content and the label beside it is
326
+ * too — nothing makes them agree, which is the shape of a phishing link. This
327
+ * reuses `disclosedLabel` from the transcript's link rows rather than adding a
328
+ * second URL check: that one already survives the two defeats it was
329
+ * hardened against (a backslash ending the authority, and `endsWith` being a
330
+ * string
331
+ * test) and renders an unreadable authority as "unknown site" instead of
332
+ * silently. A second implementation is a second thing to get wrong.
333
+ *
334
+ * A button with no url is left alone: there is no destination to disclose. */
335
+ function discloseButton(disclose: Disclose, label: string, url: string): string {
336
+ return url ? disclose({ label, url }) : label;
337
+ }
338
+
339
+ /** One option, flattened. `selected` is passed in because it is worker state
340
+ * rather than anything the payload said — except on the first build, where the
341
+ * service's `isPreselected` seeds it. */
342
+ function optionRow(
343
+ o: {
344
+ value: string; title: string; subtitle: string; text: string;
345
+ pictureUrl: string; buttonLabel: string; actionUrl: string; listText: string;
346
+ respondText: string;
347
+ },
348
+ i: number,
349
+ selected: boolean,
350
+ disclose: Disclose,
351
+ /** May this option's picture be loaded? A different question from the link
352
+ * disclosure beside it — see `isWixMediaHost`. */
353
+ allowPicture: (url: string) => boolean,
354
+ /** UNKNOWN kinds synthesize a missing `respondText` from the row's own
355
+ * label — see `viewOf`. Known kinds must NOT: an absent template there is
356
+ * a payload bug for a shape that has a real case, and inventing one would
357
+ * hide it. */
358
+ genericFallback = false,
359
+ ): WidgetOptionRow {
360
+ // An option's PICTURE is a url off the wire like any other, and this is the
361
+ // one the epic's acceptance list calls out. It is not a label, so there is
362
+ // nothing to disclose INTO — an off-Wix picture is dropped rather than
363
+ // annotated, because a decorative image cannot carry its own provenance and
364
+ // a silently-loaded remote asset is a tracking pixel with extra steps.
365
+ // Judged by `allowPicture` — the MEDIA list — rather than by the link
366
+ // discloser. `static.wixstatic.com` is where every Wix-hosted image lives,
367
+ // so asking the link question about it blanked the pictures the service
368
+ // really sends.
369
+ const pictureUrl = o.pictureUrl && allowPicture(o.pictureUrl) ? o.pictureUrl : "";
370
+ // Via `discloseButton`, NOT `disclose` directly: an option button usually has
371
+ // no url at all (it sends `respondText`), and disclosing an empty url appends
372
+ // "(unknown site)" to a button that goes nowhere. That reads as a phishing
373
+ // warning on the ordinary path and devalues the annotation on the path that
374
+ // needs it.
375
+ const buttonLabel = discloseButton(disclose, o.buttonLabel, o.actionUrl);
376
+
377
+ // An option's two display lines.
378
+ //
379
+ // The service uses `subtitle` and `text` as ALTERNATES, never together:
380
+ // AUTOCOMPLETE and PLANS send `subtitle`, while SINGLE_SELECT, MULTI_SELECT
381
+ // and RADIO_BUTTON send `text`. A row that binds only `subtitle` therefore
382
+ // drops the descriptive line for three of the fourteen kinds — and a
383
+ // RADIO_BUTTON option, which may carry `text` and no `title` at all, renders
384
+ // as an empty row. So fold here rather than in each case's composition: the
385
+ // raw `text` is still exported below for anything that wants both.
386
+ const hasTitle = !!o.title;
387
+ const rowTitle = hasTitle ? o.title : (o.text || o.subtitle);
388
+ const rowSubtitle = hasTitle
389
+ ? (o.subtitle || o.text)
390
+ : (o.text && o.subtitle ? o.subtitle : "");
391
+ return {
392
+ id: `o${i}`,
393
+ value: o.value,
394
+ title: rowTitle,
395
+ subtitle: rowSubtitle, subtitleHidden: !rowSubtitle,
396
+ text: o.text, textHidden: !o.text,
397
+ pictureUrl, pictureHidden: !pictureUrl,
398
+ buttonLabel, buttonHidden: !buttonLabel,
399
+ listText: o.listText, listHidden: !o.listText,
400
+ selected,
401
+ respondText: o.respondText
402
+ || (genericFallback ? (o.title || o.text || o.subtitle || o.value) : ""),
403
+ };
404
+ }
405
+
406
+ /** Does this url belong to a host the discloser leaves alone?
407
+ *
408
+ * `disclosedLabel` answers "what should the owner read", which for an on-Wix
409
+ * destination is the label unchanged. Feeding it a sentinel and checking
410
+ * whether it came back untouched is how a caller asks the same question
411
+ * without a second host check — the thing the epic's security section forbids
412
+ * writing twice. */
413
+ function isDisclosedSame(disclose: Disclose, url: string): boolean {
414
+ const probe = "\u0000wix-probe";
415
+ return disclose({ label: probe, url }) === probe;
416
+ }
417
+
418
+ /** Re-derive an ASYNC_TOOL widget for a new job state.
419
+ *
420
+ * Separate from `viewOf` because it runs on a LIVE widget rather than a fresh
421
+ * payload — the status messages were parsed once and only the state moves. */
422
+ export function asyncAdvanced(w: WidgetView, state: string): WidgetView {
423
+ const msgs = (w.extraStatusMessages ?? {}) as Record<string, string>;
424
+ const running = state === "initialized" || state === "processing";
425
+ return {
426
+ ...w,
427
+ asyncState: state,
428
+ asyncMessage: msgs[state] || w.asyncMessage,
429
+ asyncRunning: running,
430
+ // Recomputed here too: a terminal job keeps its id, so a spinner hidden
431
+ // on trackedness alone would never stop.
432
+ asyncSpinnerHidden: !running,
433
+ };
434
+ }
435
+
436
+ /** Turn a normalized widget into the flat view the descriptor binds. */
437
+ /** Where the generic UNKNOWN card is mounted, when the app mounts one.
438
+ *
439
+ * Optional because the second app that mounts Aria has not adopted the
440
+ * subtree — `config.workerSubtreeScreens` is per screen, and a descriptor
441
+ * that is not on the allowlist rejects the node outright. Absent means "the
442
+ * prose card, as before". */
443
+ export interface SubtreeSlot { slotId: string; service: string }
444
+
445
+ export function viewOf(
446
+ w: NormalizedWidget,
447
+ disclose: Disclose,
448
+ subtree?: SubtreeSlot,
449
+ /** Defaults to the link discloser, which is what every caller did before the
450
+ * media list existed — wrong for pictures, but never MORE permissive. */
451
+ allowPicture: (url: string) => boolean = (url) => isDisclosedSame(disclose, url),
452
+ ): WidgetView {
453
+ const extra = w.extra as Record<string, unknown>;
454
+ const title = typeof extra.title === "string" ? extra.title : "";
455
+ // Through the SAME media check as an option's picture. Counting `imageUrl`
456
+ // towards renderability made an image-only VISUAL_DISPLAY reachable, and the
457
+ // descriptor binds this path straight into an `Image` — so an off-Wix url
458
+ // here is a remote asset fetched on sight, which is a tracking pixel with
459
+ // extra steps. The option rows have been gated since #2605; this path had
460
+ // never been reachable on its own before, which is exactly why it was easy
461
+ // to widen without noticing.
462
+ const rawImageUrl = typeof extra.imageUrl === "string" ? extra.imageUrl : "";
463
+ const imageUrl = rawImageUrl && allowPicture(rawImageUrl) ? rawImageUrl : "";
464
+
465
+ const sections = Array.isArray(extra.sections) ? (extra.sections as Record<string, unknown>[]) : [];
466
+ const rows: WidgetRow[] = sections.map((sec, i) => {
467
+ const subtitle = typeof sec.subtitle === "string" ? sec.subtitle : "";
468
+ return {
469
+ // A stable id per row. Index-based on purpose: a section has no id of its
470
+ // own, and the list is rebuilt wholesale on every tick, so there is
471
+ // nothing for a content-derived key to preserve.
472
+ id: `s${i}`,
473
+ title: typeof sec.title === "string" ? sec.title : "",
474
+ subtitle,
475
+ subtitleHidden: !subtitle,
476
+ };
477
+ });
478
+
479
+ const maxSelected = typeof extra.maxSelected === "number" ? extra.maxSelected : 0;
480
+ // `isPreselected` seeds the selection, so the row's `selected` starts true
481
+ // for exactly the options the service marked.
482
+ const options: WidgetOptionRow[] = w.options.map((o, i) => optionRow(o, i, o.preselected, disclose, allowPicture, w.kind === "UNKNOWN"));
483
+
484
+ // CHART. `pieSlices` is the only shape the catalogue sends, but `chartType`
485
+ // decides the component: a pie is a `DonutChart` at zero thickness, anything
486
+ // else a `Chart` over the same magnitudes.
487
+ const rawSlices = Array.isArray(extra.slices) ? (extra.slices as { label?: unknown; value?: unknown }[]) : [];
488
+ const slices: WidgetSlice[] = rawSlices.map((sl) => ({
489
+ label: typeof sl.label === "string" ? sl.label : "",
490
+ value: typeof sl.value === "number" && Number.isFinite(sl.value) ? sl.value : 0,
491
+ }));
492
+ const chartKind = typeof extra.chartType === "string" ? extra.chartType : "";
493
+ // "doughnut" is the spelling the reference catalogue uses; "donut" is the
494
+ // component's. Accepting only the component's spelling classified the real
495
+ // payload as a SERIES and drew it as bars.
496
+ const isPie = !chartKind || chartKind === "pie" || chartKind === "donut" || chartKind === "doughnut";
497
+ const description = typeof extra.description === "string" ? extra.description : "";
498
+
499
+ // TODO_PLAN. `status` is the service's word; `done` is what `Timeline` draws.
500
+ const rawItems = Array.isArray(extra.items) ? (extra.items as { label?: unknown; status?: unknown }[]) : [];
501
+ const steps: WidgetStep[] = rawItems.map((it, i) => {
502
+ const status = typeof it.status === "string" ? it.status : "";
503
+ return {
504
+ id: `p${i}`,
505
+ title: typeof it.label === "string" ? it.label : "",
506
+ subtitle: status,
507
+ done: /^(done|complete|completed|finished|success)$/i.test(status),
508
+ };
509
+ });
510
+ const doneCount = steps.filter((p) => p.done).length;
511
+
512
+ // ASYNC_TOOL. Four messages, one state — the descriptor cannot choose, so the
513
+ // choice is made here.
514
+ const msgs = (extra.statusMessages ?? {}) as Record<string, unknown>;
515
+ const asyncJobId = typeof extra.asyncJobId === "string" ? extra.asyncJobId : "";
516
+ const jobState = typeof extra.asyncState === "string" ? extra.asyncState : "initialized";
517
+ const pick = (k: string): string => (typeof msgs[k] === "string" ? (msgs[k] as string) : "");
518
+ const asyncMessage = pick(jobState) || pick("initialized") || pick("processing");
519
+ const asyncRunning = !!asyncJobId && (jobState === "initialized" || jobState === "processing");
520
+
521
+ // QUESTIONNAIRE. Every label arrived wrapped (`{text: …}`) and the normalizer
522
+ // unwrapped them; this only flattens for binding.
523
+ const rawQs = Array.isArray(extra.questions) ? (extra.questions as Record<string, unknown>[]) : [];
524
+ const questions: WidgetQuestion[] = rawQs.map((q, i) => {
525
+ const sub = typeof q.subtitle === "string" ? q.subtitle : "";
526
+ const free = typeof q.freeFormatPlaceholder === "string" ? q.freeFormatPlaceholder : "";
527
+ const required = q.required === true;
528
+ return {
529
+ id: `q${i}`,
530
+ title: typeof q.title === "string" ? q.title : "",
531
+ subtitle: sub, subtitleHidden: !sub,
532
+ type: typeof q.type === "string" ? q.type : "",
533
+ required,
534
+ // Nothing is answered on arrival, so a required question starts missing.
535
+ missing: required,
536
+ answer: "", answerHidden: true,
537
+ freePlaceholder: free, freeHidden: !free,
538
+ };
539
+ });
540
+
541
+ const providerKey = typeof extra.providerKey === "string" ? extra.providerKey : "";
542
+
543
+ const primaryUrl = w.primary?.externalUrl ?? "";
544
+ const secondaryUrl = w.secondary?.externalUrl ?? "";
545
+ // An UNKNOWN kind is the one case where a MISSING `respondText` must not
546
+ // make a control inert.
547
+ //
548
+ // `widgetRespond` needs a template and returns the state untouched when
549
+ // there is none — correct for a KNOWN kind, where an absent template is a
550
+ // payload bug for a shape that has a real case. But the generic card mints a
551
+ // button for every option precisely because an option the owner cannot
552
+ // choose is decoration, and a future kind has no reason to carry the
553
+ // template field read today. Without this the generic card rendered
554
+ // perfectly and every control did nothing, which looks exactly like it
555
+ // working.
556
+ //
557
+ // The synthesized template is the label itself, which is what `<SELECTED>`
558
+ // substitution produces for the known kinds anyway.
559
+ const generic = w.kind === "UNKNOWN";
560
+ const primaryRespondText = (w.primary?.respondText ?? "")
561
+ || (generic ? (w.primary?.label ?? "") : "");
562
+ const secondaryRespondText = (w.secondary?.respondText ?? "")
563
+ || (generic ? (w.secondary?.label ?? "") : "");
564
+ const primaryLabel = w.primary ? discloseButton(disclose, w.primary.label, primaryUrl) : "";
565
+ const secondaryLabel = w.secondary ? discloseButton(disclose, w.secondary.label, secondaryUrl) : "";
566
+
567
+ // Hoisted out of the object literal below so `renderable` can see it. It
568
+ // was computed inline, so naming it in the guard was a ReferenceError that
569
+ // took the whole `tick` down — the turn never ended and the poll ladder
570
+ // stayed pinned fast. `aria-chat.test.ts` caught it; nothing in the widget
571
+ // suites would have, because they never run a turn.
572
+ const treeJson = subtree && w.kind === "UNKNOWN"
573
+ ? composeGenericCard(w, {
574
+ slotId: subtree.slotId,
575
+ service: subtree.service,
576
+ allowPicture,
577
+ })
578
+ : "";
579
+
580
+ // A payload we could not read normalizes to an EMPTY `UNKNOWN`, and an
581
+ // empty card is not nothing: the `Card` keeps its padding and background,
582
+ // so the region rendered as a blank box with a gap where a widget should
583
+ // be. Nothing renderable means nothing rendered.
584
+ // EVERY field the descriptor can draw. `title` (prose and plan cards),
585
+ // `imageUrl` (VISUAL_DISPLAY) and `description` (CHART) were missing, so a
586
+ // title-only upsell or an image-only display — both valid — collapsed even
587
+ // though they had something to show. Hiding a widget that has content is a
588
+ // worse failure than the blank card this guard exists to prevent, so the
589
+ // list has to match what the descriptor renders, not what I remembered.
590
+ const renderable = !!(w.text || w.bottomText || w.badge || title || imageUrl
591
+ || description || options.length || primaryLabel || secondaryLabel
592
+ || sections.length || slices.length || steps.length || questions.length
593
+ || asyncMessage || treeJson);
594
+
595
+ return {
596
+ kind: w.kind,
597
+ hidden: !renderable,
598
+ options,
599
+ maxSelected,
600
+ limitReached: maxSelected > 0 && options.filter((o) => o.selected).length >= maxSelected,
601
+ limitText: "", limitTextHidden: true,
602
+ requiresSelection: !!w.primary?.disableIfEmpty,
603
+ // A widget arrives with nothing chosen unless the service preselected, so
604
+ // a `disableIfNothingSelected` primary starts disabled.
605
+ primaryDisabled: !!w.primary?.disableIfEmpty && !options.some((o) => o.selected),
606
+ primaryEnabled: !(!!w.primary?.disableIfEmpty && !options.some((o) => o.selected)),
607
+ slices,
608
+ values: slices.map((sl) => sl.value),
609
+ chartIsPie: w.kind === "CHART" && isPie && slices.length > 0,
610
+ chartIsSeries: w.kind === "CHART" && !isPie && slices.length > 0,
611
+ chartPieHidden: !(w.kind === "CHART" && isPie && slices.length > 0),
612
+ chartSeriesHidden: !(w.kind === "CHART" && !isPie && slices.length > 0),
613
+ chartKind,
614
+ chartEmpty: w.kind === "CHART" && slices.length === 0,
615
+ chartEmptyHidden: !(w.kind === "CHART" && slices.length === 0),
616
+ description, descriptionHidden: !description,
617
+ steps,
618
+ stepsEmpty: w.kind === "TODO_PLAN" && steps.length === 0,
619
+ progress: steps.length ? doneCount / steps.length : 0,
620
+ progressText: steps.length ? `${doneCount} of ${steps.length} done` : "",
621
+ asyncJobId,
622
+ asyncState: asyncJobId ? jobState : "",
623
+ asyncMessage,
624
+ asyncRunning,
625
+ // A job the service never named. The status messages still say something
626
+ // useful, so this renders them — it just never spins and never polls.
627
+ asyncUntracked: w.kind === "ASYNC_TOOL" && !asyncJobId,
628
+ // The spinner follows RUNNING, not trackedness.
629
+ asyncSpinnerHidden: !asyncRunning,
630
+ query: "",
631
+ // Nothing typed yet means every option is a suggestion.
632
+ suggestions: options,
633
+ suggestionsEmpty: options.length === 0,
634
+ noMatches: false,
635
+ selectionType: typeof extra.selectionType === "string" ? extra.selectionType : "",
636
+ questions,
637
+ questionsEmpty: w.kind === "QUESTIONNAIRE" && questions.length === 0,
638
+ answersIncomplete: questions.some((q) => q.missing),
639
+ answersText: "", answersTextHidden: true,
640
+ providerKey,
641
+ // An ITEMS_SELECTION arrives with NO rows — they come from the provider —
642
+ // so it starts loading rather than starting empty.
643
+ itemsLoading: w.kind === "ITEMS_SELECTION" && !!providerKey,
644
+ itemsLoadingHidden: !(w.kind === "ITEMS_SELECTION" && !!providerKey),
645
+ itemsError: "", itemsErrorHidden: true,
646
+ itemsEmpty: w.kind === "ITEMS_SELECTION" && options.length === 0,
647
+ // Only for a kind we do not know. A known kind has a real case, and RFC
648
+ // 0046 sends enumerable variants to `FragmentSlot`.
649
+ treeJson,
650
+ extraStatusMessages: Object.fromEntries(
651
+ Object.entries(msgs).filter((kv): kv is [string, string] => typeof kv[1] === "string"),
652
+ ),
653
+ submitting: false,
654
+ text: w.text, textHidden: !w.text,
655
+ title, titleHidden: !title,
656
+ bottomText: w.bottomText, bottomTextHidden: !w.bottomText,
657
+ badge: w.badge, badgeHidden: !w.badge,
658
+ imageUrl, imageHidden: !imageUrl,
659
+ // Shown only when there is a LABEL to read and a DESTINATION to go to.
660
+ //
661
+ // The label half is obvious: the service does send `{mobileAction:{…}}`
662
+ // with nothing written on it, and a control with an empty title is a tap
663
+ // target the owner cannot read.
664
+ //
665
+ // The destination half is a scope decision, and a deliberate visible gap. A
666
+ // button whose only payload is `respondText` needs the send-back loop, which
667
+ // is #2606; until that lands, painting it would put up a control that does
668
+ // nothing when tapped. This repo's own rule for the Aria header is "no dead
669
+ // controls", and a button that looks live and is not is worse than one that
670
+ // is not there yet. #2606 removes the `&& url` and the buttons appear.
671
+ // #2605 hid a button that had nothing to DO. Now that `respondText` can be
672
+ // sent, having something to SAY counts too — which is the `&& url` that
673
+ // issue said this one would remove.
674
+ primaryLabel, primaryHidden: !(primaryLabel && (primaryUrl || primaryRespondText)), primaryUrl,
675
+ primaryRespondText,
676
+ secondaryLabel, secondaryHidden: !(secondaryLabel && (secondaryUrl || secondaryRespondText)), secondaryUrl,
677
+ secondaryRespondText,
678
+ rows,
679
+ };
680
+ }
681
+
682
+ /** The widget the screen should be showing, read off the server transcript.
683
+ *
684
+ * The LAST assistant turn's last envelope, and nothing else. Two decisions
685
+ * behind that, both from the epic:
686
+ *
687
+ * - **One at a time.** The assistant asks one question per turn, and
688
+ * `FragmentSlot` binds one case path. A widget scrolled into history keeps
689
+ * its prose (#2603 already put that in the bubble) and loses its controls,
690
+ * which is correct: a stale questionnaire you can still submit is a bug.
691
+ * - **Assistant turns only.** A user's own text is literal — scanning it would
692
+ * let an owner who typed `<<…>>` inject a widget into their own chat.
693
+ *
694
+ * Answers `NO_WIDGET` for an empty transcript, so a new chat collapses the
695
+ * region without anything having to remember to clear it. */
696
+ export function activeWidgetOf(
697
+ msgs: WidgetSourceMessage[],
698
+ disclose: Disclose,
699
+ subtree?: SubtreeSlot,
700
+ allowPicture?: (url: string) => boolean,
701
+ ): WidgetView {
702
+ // ONLY the newest message is consulted. This used to walk backwards past
703
+ // `USER` rows and empty assistant placeholders looking for a widget, which
704
+ // is wrong in the one state __HIMI_APP_NAME__ is in most often: between persisting
705
+ // the owner's message and the reply arriving, the newest row IS the owner's
706
+ // message — so the scan skipped it, found the PREVIOUS assistant turn, and
707
+ // repainted the controls for the question that had just been superseded.
708
+ // `send` deliberately clears the widget; the very next poll put it back.
709
+ //
710
+ // A widget belongs to the turn that asked it. If the newest turn is the
711
+ // owner's, or is a placeholder with nothing in it yet, then nothing is being
712
+ // asked right now — and that is what NO_WIDGET means.
713
+ const m = msgs[msgs.length - 1];
714
+ if (!m || (m.role || "ASSISTANT") === "USER") return NO_WIDGET;
715
+ const raw = typeof m.content === "string" ? m.content : "";
716
+ if (!raw) return NO_WIDGET;
717
+ const found = widgetsOfParts(scanEnvelopes(raw, 0, true).parts);
718
+ if (!found.length) return NO_WIDGET;
719
+ // A turn with several envelopes shows the LAST, matching "one at a time"
720
+ // and the reading order — the final ask is the live one.
721
+ return viewOf(normalizeWidget(found[found.length - 1]), disclose, subtree, allowPicture);
722
+ }