@wix/himalaya-cli 0.1574.0 → 0.1575.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/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/dev-server/src/app-config.d.ts +16 -5
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/define-app.d.ts +6 -0
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/telemetry/wix-bi/src/index.d.ts +15 -0
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/telemetry/wix-bi/src/oneapp.d.ts +90 -0
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-bi.mjs +165 -5
  6. package/dist/cli.mjs +3479 -2601
  7. package/dist/preview-assets.generated.bin +0 -0
  8. package/guides/designing-himi-screens.md +7 -1
  9. package/guides/hhp.md +2 -2
  10. package/guides/himi-json-output.md +12 -0
  11. package/guides/push-cli-onboarding.md +10 -0
  12. package/package.json +2 -2
  13. package/templates/beauty-salon/dev/index.ts +74 -127
  14. package/templates/beauty-salon/tier5-src/_shared/exclusive-writes.ts +66 -0
  15. package/templates/beauty-salon/tier5-src/himi-branded-lite-cart/index.ts +18 -5
  16. package/templates/beauty-salon-owner/dev/index.ts +5 -0
  17. package/templates/beauty-salon-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  18. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  19. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  20. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
  21. package/templates/branded-lite/dev/index.ts +114 -136
  22. package/templates/branded-lite/l10n/en.json +570 -0
  23. package/templates/branded-lite/l10n/he.json +200 -0
  24. package/templates/branded-lite/tier5-src/_shared/business-chat-copy.ts +40 -0
  25. package/templates/branded-lite/tier5-src/_shared/business-chat-message-model.ts +1 -1
  26. package/templates/branded-lite/tier5-src/_shared/business-chat-model.ts +3 -2
  27. package/templates/branded-lite/tier5-src/_shared/business-chat-presenter.ts +26 -21
  28. package/templates/branded-lite/tier5-src/_shared/exclusive-writes.ts +66 -0
  29. package/templates/branded-lite/tier5-src/business-chat/index.ts +184 -63
  30. package/templates/branded-lite/tier5-src/himi-branded-lite-cart/index.ts +18 -5
  31. package/templates/community-club-owner/dev/index.ts +5 -0
  32. package/templates/community-club-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  33. package/templates/community-club-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  34. package/templates/community-club-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  35. package/templates/community-club-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  36. package/templates/events-meetups-owner/dev/index.ts +3 -0
  37. package/templates/events-meetups-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  38. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  39. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  40. package/templates/gym-fitness/dev/index.ts +74 -127
  41. package/templates/gym-fitness/tier5-src/_shared/exclusive-writes.ts +66 -0
  42. package/templates/gym-fitness/tier5-src/himi-branded-lite-cart/index.ts +18 -5
  43. package/templates/gym-fitness-owner/dev/index.ts +6 -0
  44. package/templates/gym-fitness-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  45. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  46. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  47. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  48. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
  49. package/templates/nonprofit-cause-owner/dev/index.ts +4 -0
  50. package/templates/nonprofit-cause-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  51. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  52. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  53. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
  54. package/templates/owner-lite/dev/index.ts +6 -0
  55. package/templates/owner-lite/tier5-src/_shared/exclusive-writes.ts +66 -0
  56. package/templates/owner-lite/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  57. package/templates/owner-lite/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  58. package/templates/owner-lite/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  59. package/templates/owner-lite/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
  60. package/templates/podcast-creator-owner/dev/index.ts +5 -0
  61. package/templates/podcast-creator-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  62. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  63. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  64. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  65. package/templates/portfolio-creative/dev/index.ts +74 -127
  66. package/templates/portfolio-creative/tier5-src/_shared/exclusive-writes.ts +66 -0
  67. package/templates/portfolio-creative/tier5-src/himi-branded-lite-cart/index.ts +18 -5
  68. package/templates/portfolio-creative-owner/dev/index.ts +5 -0
  69. package/templates/portfolio-creative-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  70. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  71. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  72. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
  73. package/templates/restaurant-owner/dev/index.ts +6 -0
  74. package/templates/restaurant-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  75. package/templates/restaurant-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  76. package/templates/restaurant-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  77. package/templates/restaurant-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  78. package/templates/restaurant-owner/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
  79. package/templates/retail-store/dev/index.ts +74 -127
  80. package/templates/retail-store/tier5-src/_shared/exclusive-writes.ts +66 -0
  81. package/templates/retail-store/tier5-src/himi-branded-lite-cart/index.ts +18 -5
  82. package/templates/retail-store-owner/dev/index.ts +4 -0
  83. package/templates/retail-store-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  84. package/templates/retail-store-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  85. package/templates/retail-store-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  86. package/templates/yoga-studio-owner/dev/index.ts +4 -0
  87. package/templates/yoga-studio-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  88. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  89. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  90. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
@@ -0,0 +1,200 @@
1
+ {
2
+ "$source": "he",
3
+ "messages": {
4
+ "h_c04921f8d946": {
5
+ "source": "No messages yet",
6
+ "key": "businessChat.emptyTitle",
7
+ "translation": "עדיין אין הודעות"
8
+ },
9
+ "h_cf765512cc6d": {
10
+ "source": "Sending…",
11
+ "key": "businessChat.sending",
12
+ "translation": "שולח…"
13
+ },
14
+ "h_b9e09ae263fb": {
15
+ "source": "Not confirmed",
16
+ "key": "businessChat.unconfirmed",
17
+ "translation": "לא אושר"
18
+ },
19
+ "h_9443bda693df": {
20
+ "source": "Your member session changed. Sign in again to continue.",
21
+ "key": "businessChat.sessionChanged",
22
+ "translation": "ההתחברות שלך השתנתה. יש להתחבר שוב כדי להמשיך."
23
+ },
24
+ "h_aa36b83daad9": {
25
+ "source": "This app host cannot reach the inbox yet.",
26
+ "key": "businessChat.hostUnavailable",
27
+ "translation": "לא ניתן לגשת לתיבת ההודעות באפליקציה זו כרגע."
28
+ },
29
+ "h_e9b3cc0ff1a1": {
30
+ "source": "Message delivery could not be confirmed. Check the conversation before trying again.",
31
+ "key": "businessChat.sendFailed",
32
+ "translation": "לא ניתן לאשר שההודעה נמסרה. יש לבדוק את השיחה לפני ניסיון נוסף."
33
+ },
34
+ "h_9a5009643173": {
35
+ "source": "Couldn’t open the conversation. Try again.",
36
+ "key": "businessChat.openFailed",
37
+ "translation": "לא ניתן לפתוח את השיחה. נסו שוב."
38
+ },
39
+ "h_a8e08e79182b": {
40
+ "source": "Couldn’t load the conversation. Try again.",
41
+ "key": "businessChat.loadFailed",
42
+ "translation": "לא ניתן לטעון את השיחה. נסו שוב."
43
+ },
44
+ "h_c21b9303d41d": {
45
+ "source": "Sign in to send a message.",
46
+ "key": "businessChat.signInPrompt",
47
+ "translation": "יש להתחבר כדי לשלוח הודעה."
48
+ },
49
+ "h_c559b26ddd1a": {
50
+ "source": "The message was rejected. Your draft is retained.",
51
+ "key": "businessChat.rejected",
52
+ "translation": "ההודעה נדחתה. הטיוטה נשמרה."
53
+ },
54
+ "h_263adf949cca": {
55
+ "source": "No automatic retry was made. Reopen the conversation to check whether it arrived.",
56
+ "key": "businessChat.checkDelivery",
57
+ "translation": "לא בוצע ניסיון שליחה נוסף. פתחו שוב את השיחה כדי לבדוק אם ההודעה התקבלה."
58
+ },
59
+ "h_6df910132a36": {
60
+ "source": "Suggestion sent.",
61
+ "key": "businessChat.suggestionSent",
62
+ "translation": "ההצעה נשלחה."
63
+ },
64
+ "h_429255ed7d01": {
65
+ "source": "The suggestion was rejected.",
66
+ "key": "businessChat.suggestionRejected",
67
+ "translation": "ההצעה נדחתה."
68
+ },
69
+ "h_093341bec739": {
70
+ "source": "Suggestion delivery is unconfirmed; it will not be sent again automatically.",
71
+ "key": "businessChat.suggestionUnconfirmed",
72
+ "translation": "לא ניתן לאשר שההצעה נמסרה; היא לא תישלח שוב באופן אוטומטי."
73
+ },
74
+ "h_74c459c38758": {
75
+ "source": "Product image",
76
+ "key": "businessChat.productImage",
77
+ "translation": "תמונת מוצר"
78
+ },
79
+ "h_325fcc2da726": {
80
+ "source": "Connecting to live updates",
81
+ "key": "businessChat.connecting",
82
+ "translation": "מתחבר לעדכונים בזמן אמת"
83
+ },
84
+ "h_c23ff9c74e73": {
85
+ "source": "Reconnecting live updates",
86
+ "key": "businessChat.reconnecting",
87
+ "translation": "מתחבר מחדש לעדכונים בזמן אמת"
88
+ },
89
+ "h_f4531c1fb40f": {
90
+ "source": "Live updates unavailable for this session",
91
+ "key": "businessChat.liveSessionUnavailable",
92
+ "translation": "עדכונים בזמן אמת אינם זמינים בחיבור זה"
93
+ },
94
+ "h_42856378cbb8": {
95
+ "source": "Live updates unavailable in this host",
96
+ "key": "businessChat.liveHostUnavailable",
97
+ "translation": "עדכונים בזמן אמת אינם זמינים באפליקציה זו"
98
+ },
99
+ "h_2ef475c9eed8": {
100
+ "source": "Live connection setup failed",
101
+ "key": "businessChat.liveSetupFailed",
102
+ "translation": "הגדרת החיבור בזמן אמת נכשלה"
103
+ },
104
+ "h_40a99786c549": {
105
+ "source": "Live connection authentication failed",
106
+ "key": "businessChat.liveAuthFailed",
107
+ "translation": "אימות החיבור בזמן אמת נכשל"
108
+ },
109
+ "h_e601a70aa6d5": {
110
+ "source": "Live connection interrupted",
111
+ "key": "businessChat.liveInterrupted",
112
+ "translation": "החיבור בזמן אמת נותק"
113
+ },
114
+ "h_4e2c6bddd5ef": {
115
+ "source": "Live update could not be read",
116
+ "key": "businessChat.liveUnreadable",
117
+ "translation": "לא ניתן לקרוא את העדכון בזמן אמת"
118
+ },
119
+ "h_a7ada329317d": {
120
+ "source": "Live updates paused",
121
+ "key": "businessChat.livePaused",
122
+ "translation": "העדכונים בזמן אמת מושהים"
123
+ },
124
+ "h_45d7c3e03a77": {
125
+ "source": "Live updates unavailable",
126
+ "key": "businessChat.liveUnavailable",
127
+ "translation": "עדכונים בזמן אמת אינם זמינים"
128
+ },
129
+ "h_d2de1a2831e7": {
130
+ "source": "Open link",
131
+ "key": "businessChat.openLink",
132
+ "translation": "פתיחת קישור"
133
+ },
134
+ "h_24345a14377f": {
135
+ "source": "Today",
136
+ "key": "businessChat.today",
137
+ "translation": "היום"
138
+ },
139
+ "h_da24830f1f70": {
140
+ "source": "Yesterday",
141
+ "key": "businessChat.yesterday",
142
+ "translation": "אתמול"
143
+ },
144
+ "h_fc7ba35433ce": {
145
+ "source": "AI assistant",
146
+ "key": "businessChat.aiAssistant",
147
+ "translation": "עוזר בינה מלאכותית"
148
+ },
149
+ "h_798ec37b0270": {
150
+ "source": "Staff Response",
151
+ "key": "businessChat.staffResponse",
152
+ "translation": "תשובת הצוות"
153
+ },
154
+ "h_4f3117c17b1a": {
155
+ "source": "Image attachment",
156
+ "key": "businessChat.imageAttachment",
157
+ "translation": "תמונה מצורפת"
158
+ },
159
+ "h_6733d4f14f4d": {
160
+ "source": "Type your message…",
161
+ "key": "chatThread.composerPlaceholder",
162
+ "translation": "כתיבת הודעה…"
163
+ },
164
+ "h_08d738ca2b26": {
165
+ "source": "Type your message",
166
+ "key": "chatThread.composerLabel",
167
+ "translation": "לכתוב הודעה"
168
+ },
169
+ "h_9bc2575c3930": {
170
+ "source": "Send",
171
+ "key": "chatThread.sendCta",
172
+ "translation": "שליחה"
173
+ },
174
+ "h_8b2c12c5d4d5": {
175
+ "source": "Send a message to start the conversation.",
176
+ "key": "chatThread.emptySubtitle",
177
+ "translation": "שליחת הודעה תתחיל את השיחה."
178
+ },
179
+ "h_b227f5879bde": {
180
+ "source": "Coupon code",
181
+ "key": "chatThread.couponLabel",
182
+ "translation": "קוד קופון"
183
+ },
184
+ "h_33ce417454bf": {
185
+ "source": "Loading…",
186
+ "key": "common.detailLoadingText",
187
+ "translation": "בטעינה…"
188
+ },
189
+ "h_042c862e4467": {
190
+ "source": "Try again",
191
+ "key": "common.retryCta",
192
+ "translation": "נסו שוב"
193
+ },
194
+ "h_ada2e9e96fa9": {
195
+ "source": "Sign in",
196
+ "key": "account.signInCta",
197
+ "translation": "התחברות"
198
+ }
199
+ }
200
+ }
@@ -0,0 +1,40 @@
1
+ // Shared defaults keep descriptor copy and worker fallbacks aligned.
2
+ import { copyParam } from "./himi-copy.js";
3
+
4
+ export const BUSINESS_CHAT_COPY = {
5
+ "emptyTitle": "No messages yet",
6
+ "sending": "Sending…",
7
+ "unconfirmed": "Not confirmed",
8
+ "sessionChanged": "Your member session changed. Sign in again to continue.",
9
+ "hostUnavailable": "This app host cannot reach the inbox yet.",
10
+ "sendFailed": "Message delivery could not be confirmed. Check the conversation before trying again.",
11
+ "openFailed": "Couldn’t open the conversation. Try again.",
12
+ "loadFailed": "Couldn’t load the conversation. Try again.",
13
+ "signInPrompt": "Sign in to send a message.",
14
+ "rejected": "The message was rejected. Your draft is retained.",
15
+ "checkDelivery": "No automatic retry was made. Reopen the conversation to check whether it arrived.",
16
+ "suggestionSent": "Suggestion sent.",
17
+ "suggestionRejected": "The suggestion was rejected.",
18
+ "suggestionUnconfirmed": "Suggestion delivery is unconfirmed; it will not be sent again automatically.",
19
+ "productImage": "Product image",
20
+ "connecting": "Connecting to live updates",
21
+ "reconnecting": "Reconnecting live updates",
22
+ "liveSessionUnavailable": "Live updates unavailable for this session",
23
+ "liveHostUnavailable": "Live updates unavailable in this host",
24
+ "liveSetupFailed": "Live connection setup failed",
25
+ "liveAuthFailed": "Live connection authentication failed",
26
+ "liveInterrupted": "Live connection interrupted",
27
+ "liveUnreadable": "Live update could not be read",
28
+ "livePaused": "Live updates paused",
29
+ "liveUnavailable": "Live updates unavailable",
30
+ "openLink": "Open link",
31
+ "today": "Today",
32
+ "yesterday": "Yesterday",
33
+ "aiAssistant": "AI assistant",
34
+ "staffResponse": "Staff Response",
35
+ "imageAttachment": "Image attachment"
36
+ } as const;
37
+
38
+ export function businessChatCopy(key: keyof typeof BUSINESS_CHAT_COPY): string {
39
+ return copyParam(`businessChat.${key}`, BUSINESS_CHAT_COPY[key]);
40
+ }
@@ -57,7 +57,7 @@ function media(id: string, kind: 'image' | 'file' | 'audio' | 'video', input: un
57
57
  const url = safeUrl(candidate); const mime = first(m.mimeType, m.contentType);
58
58
  const resolvedKind = kind === 'file' && /^audio\//i.test(mime) ? 'audio' : kind === 'file' && /^video\//i.test(mime) ? 'video' : kind;
59
59
  const missing = url ? '' : candidate ? 'Attachment URL is unavailable: only safe HTTPS links are supported.' : 'Attachment requires a download URL.';
60
- return { ...base(id, resolvedKind, first(m.text, m.title)), url, fileName: first(m.filename, m.fileName, m.name), caption: first(m.caption, m.alt, missing, resolvedKind === 'image' ? 'Image attachment' : ''), mimeType: mime, mediaId: first(m.mediaFileId, m.id, m._id, m.fileId), mediaProvider: str(m.mediaProvider), thumbnailUrl: safeUrl(m.thumbnailUrl), urlExpiration: str(m.urlExpiration), ...(missing ? { unavailableReason: missing } : {}) };
60
+ return { ...base(id, resolvedKind, first(m.text, m.title)), url, fileName: first(m.filename, m.fileName, m.name), caption: first(m.caption, m.alt, missing), mimeType: mime, mediaId: first(m.mediaFileId, m.id, m._id, m.fileId), mediaProvider: str(m.mediaProvider), thumbnailUrl: safeUrl(m.thumbnailUrl), urlExpiration: str(m.urlExpiration), ...(missing ? { unavailableReason: missing } : {}) };
61
61
  }
62
62
  function buttons(value: unknown, prefix: string): MessageButton[] {
63
63
  if (!Array.isArray(value)) return [];
@@ -5,8 +5,9 @@ import { mergeMessages, normalizeMessages, type MessageRow } from './business-ch
5
5
  * time) or unconfirmed (the request failed without a definite answer). The server names the stored message, so the
6
6
  * new one is recognised by its text, among the messages after `afterId` (the newest stored message on screen when it
7
7
  * was sent), or among all of them when the room was loaded and empty (`fromStart`); `priorCount` is how many earlier
8
- * sends with the same text were still waiting for their stored copy. */
9
- export interface BusinessChatIntent { id:string; text:string; roomId:string; principal:string; createdDate:string; delivery:'sending'|'sent'|'unconfirmed'; senderName?:string; storedId?:string; afterId?:string; fromStart?:boolean; priorCount?:number }
8
+ * sends with the same text were still waiting for their stored copy. `smartChatFromStart` records a successful empty
9
+ * Smart Chat read before sending from Members history; it selects that anchor only for accepted delivery viewed in Smart Chat history. */
10
+ export interface BusinessChatIntent { id:string; text:string; roomId:string; principal:string; createdDate:string; delivery:'sending'|'sent'|'unconfirmed'; senderName?:string; storedId?:string; afterId?:string; fromStart?:boolean; priorCount?:number; smartChatFromStart?:boolean }
10
11
 
11
12
  export function newBusinessChatIntent(text:string,roomId:string,principal:string,random=Math.random):BusinessChatIntent{
12
13
  return {id:'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g,c=>{const r=Math.floor(random()*16);return(c==='x'?r:(r&3)|8).toString(16);}),text,roomId,principal,createdDate:new Date().toISOString(),delivery:'sending'};
@@ -1,3 +1,4 @@
1
+ import { businessChatCopy } from "./business-chat-copy.js";
1
2
  import { formatMonthDay } from "./engine-intl.js";
2
3
  type RecordValue = Record<string, unknown>;
3
4
 
@@ -59,16 +60,16 @@ export type BusinessRoomSummary = {
59
60
  /** Keep host capability failures visible without exposing runtime error details. */
60
61
  export function businessRealtimeStatusLabel(state: string, reason = ""): string {
61
62
  if (state === "connected") return "";
62
- if (state === "connecting") return "Connecting to live updates";
63
- if (state === "reconnecting") return "Reconnecting live updates";
64
- if (reason === "chat_setup_scope_unavailable") return "Live updates unavailable for this session";
65
- if (reason === "runtime_unavailable") return "Live updates unavailable in this host";
66
- if (reason === "invalid_setup_response") return "Live connection setup failed";
67
- if (reason === "firebase_auth_failed") return "Live connection authentication failed";
68
- if (reason === "firebase_connection_failed") return "Live connection interrupted";
69
- if (reason === "invalid_event") return "Live update could not be read";
70
- if (reason === "event_overflow") return "Live updates paused";
71
- return "Live updates unavailable";
63
+ if (state === "connecting") return businessChatCopy('connecting');
64
+ if (state === "reconnecting") return businessChatCopy('reconnecting');
65
+ if (reason === "chat_setup_scope_unavailable") return businessChatCopy('liveSessionUnavailable');
66
+ if (reason === "runtime_unavailable") return businessChatCopy('liveHostUnavailable');
67
+ if (reason === "invalid_setup_response") return businessChatCopy('liveSetupFailed');
68
+ if (reason === "firebase_auth_failed") return businessChatCopy('liveAuthFailed');
69
+ if (reason === "firebase_connection_failed") return businessChatCopy('liveInterrupted');
70
+ if (reason === "invalid_event") return businessChatCopy('liveUnreadable');
71
+ if (reason === "event_overflow") return businessChatCopy('livePaused');
72
+ return businessChatCopy('liveUnavailable');
72
73
  }
73
74
 
74
75
  const object = (value: unknown): RecordValue =>
@@ -76,7 +77,7 @@ const object = (value: unknown): RecordValue =>
76
77
 
77
78
  /** Return a sole existing two-party business room, or a unique business-name match among several;
78
79
  * never create a room or guess among remaining matches. */
79
- export function selectExistingBusinessRoom(rooms: unknown[], businessName?: string): { id: string; participantId: string } | undefined {
80
+ export function selectExistingBusinessRoom(rooms: unknown[], businessName?: string, requireUnambiguous = false): { id: string; participantId: string } | undefined {
80
81
  let candidates = rooms.filter((value) => {
81
82
  const room = object(value);
82
83
  return typeof room.id === "string" && !!room.id &&
@@ -84,7 +85,8 @@ export function selectExistingBusinessRoom(rooms: unknown[], businessName?: stri
84
85
  room.participantCount === 2 && room.clientReferenceType === "contactId" &&
85
86
  Object.keys(object(room.businessContext)).length > 0;
86
87
  });
87
- if (candidates.length > 1 && businessName?.trim()) {
88
+ const candidateCount = candidates.length;
89
+ if (candidateCount > 1 && businessName?.trim()) {
88
90
  // Keep case significant so differently named businesses are not conflated.
89
91
  const name = businessName.trim();
90
92
  candidates = candidates.filter((value) => {
@@ -92,7 +94,10 @@ export function selectExistingBusinessRoom(rooms: unknown[], businessName?: stri
92
94
  return typeof roomName === "string" && roomName.trim() === name;
93
95
  });
94
96
  }
95
- if (candidates.length !== 1) return undefined;
97
+ if (candidates.length !== 1) {
98
+ if (requireUnambiguous && candidateCount > 1) throw new Error("business_room_selection_ambiguous");
99
+ return undefined;
100
+ }
96
101
  const room = object(candidates[0]);
97
102
  return { id: room.id as string, participantId: room.participantId as string };
98
103
  }
@@ -163,7 +168,7 @@ export function businessVisitorOptionSuggestions(options: BusinessVisitorOptions
163
168
  /** Wix Chat realtime currently sends `{type, timestamp, summary, message?}` events. */
164
169
  export function isBusinessMessageEvent(envelope: unknown, roomId: string, smartChat = false): boolean {
165
170
  const event = object(object(envelope).event);
166
- if (!roomId || typeof event.type !== "string" || !/^(message|messagesent)$/i.test(event.type)) return false;
171
+ if ((!roomId && !smartChat) || typeof event.type !== "string" || !/^(message|messagesent)$/i.test(event.type)) return false;
167
172
  const message = object(event.message);
168
173
  const data = object(event.data);
169
174
  const nested = object(data.message);
@@ -180,7 +185,7 @@ export function shouldRefreshBusinessHistory(
180
185
  session: { member: boolean; principal: string; currentPrincipal: string; scope: string; currentScope: string },
181
186
  smartChat = false,
182
187
  ): boolean {
183
- return !!session.member && !!roomId && !!session.principal && session.principal === session.currentPrincipal &&
188
+ return !!session.member && (!!roomId || smartChat) && !!session.principal && session.principal === session.currentPrincipal &&
184
189
  session.scope === session.currentScope && isBusinessMessageEvent(envelope, roomId, smartChat);
185
190
  }
186
191
 
@@ -364,7 +369,7 @@ export function extractBusinessMessageExtras(markdown: string, options: { liftCo
364
369
  // True only when a button was really added: a repeated link, or one past the sixth, stays in the text.
365
370
  const addLink = (label: string, url: string): boolean => {
366
371
  if (links.length >= 6 || links.some((link) => link.url === url)) return false;
367
- links.push({ label: label.replace(/[*_`]/g, "").trim().slice(0, 40) || "Open link", url });
372
+ links.push({ label: label.replace(/[*_`]/g, "").trim().slice(0, 40) || businessChatCopy('openLink'), url });
368
373
  return true;
369
374
  };
370
375
  for (const line of markdown.split("\n")) {
@@ -454,7 +459,7 @@ export function presentBusinessMessages<T extends Pick<BusinessMessage, "id" | "
454
459
  const key = Number.isFinite(timestamp.getTime()) ? dateKey(timestamp) : "";
455
460
  const startsDay = !!key && key !== previousDay;
456
461
  const dayLabel = startsDay
457
- ? key === today ? "Today" : key === yesterdayKey ? "Yesterday" : formatMonthDay(timestamp)
462
+ ? key === today ? businessChatCopy('today') : key === yesterdayKey ? businessChatCopy('yesterday') : formatMonthDay(timestamp)
458
463
  : "";
459
464
  if (key) previousDay = key;
460
465
 
@@ -523,10 +528,10 @@ export function presentBusinessMessages<T extends Pick<BusinessMessage, "id" | "
523
528
  senderAvatar,
524
529
  senderName,
525
530
  senderInitials: typeof display.initials === "string" && display.initials.trim() ? display.initials.trim() : initials(senderName),
526
- senderBadge: participantId && !mine ? isAiGenerated ? "AI assistant" : "Staff Response" : "",
531
+ senderBadge: participantId && !mine ? isAiGenerated ? businessChatCopy('aiAssistant') : businessChatCopy('staffResponse') : "",
527
532
  imageUrl,
528
533
  // A card's own picture is named by the card (a product's name), not as an anonymous attachment.
529
- imageDescription: imagePart?.caption || (imageUrl && !imagePart?.url && first ? first.replace(/\*/g, "") : "Image attachment"),
534
+ imageDescription: imagePart?.caption || (imageUrl && !imagePart?.url && first ? first.replace(/\*/g, "") : businessChatCopy('imageAttachment')),
530
535
  noImage: !imageUrl,
531
536
  bodyText,
532
537
  bodyRuns,
@@ -568,9 +573,9 @@ export async function dispatchBusinessSuggestion<T extends { suggestions?: Busin
568
573
  session: { member: boolean; roomId: string; principal: string; currentPrincipal: string; scope: string; currentScope: string },
569
574
  submit: (suggestion: BusinessSuggestion) => Promise<unknown>,
570
575
  ): Promise<boolean> {
571
- if (!session.member || !session.roomId || !session.principal || session.principal !== session.currentPrincipal || session.scope !== session.currentScope) return false;
576
+ if (!session.member || !session.principal || session.principal !== session.currentPrincipal || session.scope !== session.currentScope) return false;
572
577
  const suggestion = findBusinessSuggestion(rows, suggestionId);
573
- if (!suggestion) return false;
578
+ if (!suggestion || (suggestion.kind === "postback" && !session.roomId)) return false;
574
579
  // A submit that reports `false` (a stale quick reply nobody posted) is not a send.
575
580
  return (await submit(suggestion)) !== false;
576
581
  }
@@ -0,0 +1,66 @@
1
+ import { defineService as register, type ServiceDefinition } from '@himalaya/state';
2
+
3
+ /** Reject repeated taps until the accepted write's result is committed. A promise
4
+ * queue alone releases before the host applies the reducer result, so the next
5
+ * write can still read an old revision/id. The private synchronous reducer below
6
+ * publishes busy and commits completion through the ordinary service handle;
7
+ * awaiting that handle guarantees the host has stored the result before unlock.
8
+ * Skipped actions return undefined, so they cannot roll back the accepted write.
9
+ */
10
+ export function withExclusiveWrites<S extends object = Record<string, unknown>>(
11
+ registerService: typeof register, names: readonly string[],
12
+ options: { prepare?: (action: string, args: readonly unknown[]) => void; coalesce?: readonly string[]; settle?: (before: S, next: S, live: S) => S } = {},
13
+ ): typeof register {
14
+ return ((name: string, definition: ServiceDefinition<Record<string, unknown>>) => {
15
+ let busy = false;
16
+ let activeAction = "";
17
+ let completion = Promise.resolve();
18
+ let complete = () => {};
19
+ let handle: { getState(): Record<string, unknown>; exclusiveWriteState(next: Record<string, unknown>): Promise<unknown> };
20
+ const wrapped: ServiceDefinition<Record<string, unknown>> = {
21
+ ...definition,
22
+ state: { ...definition.state, writeBusy: false },
23
+ notHydrated: [...(definition.notHydrated ?? []), 'writeBusy'],
24
+ actions: {
25
+ ...Object.fromEntries(Object.entries(definition.actions ?? {}).map(([action, reduce]) => [action,
26
+ names.includes(action) ? async (state: Record<string, unknown>, ...args: never[]) => {
27
+ // Intake request metadata even when its state replacement is skipped.
28
+ options.prepare?.(action, args);
29
+ if (busy) {
30
+ // Repeated initial loads must await the accepted load, so callers
31
+ // observe its committed rows rather than the initial skeleton.
32
+ if (activeAction === action && options.coalesce?.includes(action)) await completion;
33
+ return;
34
+ }
35
+ busy = true;
36
+ activeAction = action;
37
+ completion = new Promise<void>(resolve => { complete = resolve; });
38
+ try {
39
+ await handle.exclusiveWriteState({ ...handle.getState(), writeBusy: true });
40
+ const next = await reduce(state, ...args);
41
+ const live = handle.getState();
42
+ // Non-write actions (for example editing a draft field) can still run
43
+ // during I/O. Let the owning service preserve those newer edits.
44
+ const settled = options.settle ? options.settle(state as S, (next ?? live) as S, live as S) : next ?? live;
45
+ await handle.exclusiveWriteState({ ...settled, writeBusy: false });
46
+ } catch (error) {
47
+ await handle.exclusiveWriteState({ ...handle.getState(), writeBusy: false });
48
+ throw error;
49
+ } finally {
50
+ busy = false;
51
+ complete();
52
+ }
53
+ } : reduce,
54
+ ])),
55
+ exclusiveWriteState: (_state, next: Record<string, unknown>) => next,
56
+ },
57
+ };
58
+ const registered = registerService(name, wrapped);
59
+ // Registration preserves this private action, but the dynamic action map
60
+ // cannot express its payload to the generic service handle.
61
+ handle = registered as unknown as typeof handle;
62
+ // Older hosts ignore notHydrated. A fresh worker has no pending write.
63
+ handle.getState().writeBusy = false;
64
+ return registered;
65
+ }) as typeof register;
66
+ }