@vertesia/ui 1.6.0-dev.20261008.064737Z → 1.6.0-dev.20261009.095813Z

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 (147) hide show
  1. package/README.md +24 -0
  2. package/lib/core/components/shadcn/button.d.ts +1 -0
  3. package/lib/core/components/shadcn/button.d.ts.map +1 -1
  4. package/lib/core/components/shadcn/button.js +2 -2
  5. package/lib/core/components/shadcn/button.js.map +1 -1
  6. package/lib/core/components/shadcn/selectBox.d.ts.map +1 -1
  7. package/lib/core/components/shadcn/selectBox.js +2 -2
  8. package/lib/core/components/shadcn/selectBox.js.map +1 -1
  9. package/lib/env/index.d.ts +1 -1
  10. package/lib/env/index.d.ts.map +1 -1
  11. package/lib/env/index.js +1 -1
  12. package/lib/env/index.js.map +1 -1
  13. package/lib/features/agent/chat/AskUserWidget.d.ts.map +1 -1
  14. package/lib/features/agent/chat/AskUserWidget.js +2 -1
  15. package/lib/features/agent/chat/AskUserWidget.js.map +1 -1
  16. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.d.ts.map +1 -1
  17. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js +2 -253
  18. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js.map +1 -1
  19. package/lib/features/agent/chat/ModernAgentOutput/CollapsibleAgentMarkdown.d.ts +1 -3
  20. package/lib/features/agent/chat/ModernAgentOutput/CollapsibleAgentMarkdown.d.ts.map +1 -1
  21. package/lib/features/agent/chat/ModernAgentOutput/CollapsibleAgentMarkdown.js +2 -8
  22. package/lib/features/agent/chat/ModernAgentOutput/CollapsibleAgentMarkdown.js.map +1 -1
  23. package/lib/features/agent/chat/ModernAgentOutput/agentMarkdownStyles.d.ts +4 -0
  24. package/lib/features/agent/chat/ModernAgentOutput/agentMarkdownStyles.d.ts.map +1 -0
  25. package/lib/features/agent/chat/ModernAgentOutput/agentMarkdownStyles.js +248 -0
  26. package/lib/features/agent/chat/ModernAgentOutput/agentMarkdownStyles.js.map +1 -0
  27. package/lib/i18n/locales/ar.json +1 -0
  28. package/lib/i18n/locales/de.json +1 -0
  29. package/lib/i18n/locales/en.json +1 -0
  30. package/lib/i18n/locales/es.json +1 -0
  31. package/lib/i18n/locales/fr.json +1 -0
  32. package/lib/i18n/locales/it.json +1 -0
  33. package/lib/i18n/locales/ja.json +1 -0
  34. package/lib/i18n/locales/ko.json +1 -0
  35. package/lib/i18n/locales/pt.json +1 -0
  36. package/lib/i18n/locales/ru.json +1 -0
  37. package/lib/i18n/locales/tr.json +1 -0
  38. package/lib/i18n/locales/zh-TW.json +1 -0
  39. package/lib/i18n/locales/zh.json +1 -0
  40. package/lib/session/auth/composable.d.ts +1 -5
  41. package/lib/session/auth/composable.d.ts.map +1 -1
  42. package/lib/session/auth/composable.js +1 -10
  43. package/lib/session/auth/composable.js.map +1 -1
  44. package/lib/session/auth/firebase.d.ts.map +1 -1
  45. package/lib/session/auth/firebase.js +8 -5
  46. package/lib/session/auth/firebase.js.map +1 -1
  47. package/lib/session/auth/oauth.d.ts.map +1 -1
  48. package/lib/session/auth/oauth.js +38 -14
  49. package/lib/session/auth/oauth.js.map +1 -1
  50. package/lib/session/auth/vertesiaHosts.d.ts +0 -40
  51. package/lib/session/auth/vertesiaHosts.d.ts.map +1 -1
  52. package/lib/session/auth/vertesiaHosts.js +2 -45
  53. package/lib/session/auth/vertesiaHosts.js.map +1 -1
  54. package/lib/session/index.d.ts +1 -1
  55. package/lib/session/index.d.ts.map +1 -1
  56. package/lib/session/index.js +1 -1
  57. package/lib/session/index.js.map +1 -1
  58. package/lib/session/scopeSelection.d.ts +4 -0
  59. package/lib/session/scopeSelection.d.ts.map +1 -1
  60. package/lib/session/scopeSelection.js +11 -0
  61. package/lib/session/scopeSelection.js.map +1 -1
  62. package/lib/vertesia-ui-core.js +2 -2
  63. package/lib/vertesia-ui-core.js.map +1 -1
  64. package/lib/vertesia-ui-env.js +1 -1
  65. package/lib/vertesia-ui-env.js.map +1 -1
  66. package/lib/vertesia-ui-features.js +242 -257
  67. package/lib/vertesia-ui-features.js.map +1 -1
  68. package/lib/vertesia-ui-i18n.1NWiB6fw.chunk.js +5 -0
  69. package/lib/vertesia-ui-i18n.1NWiB6fw.chunk.js.map +1 -0
  70. package/lib/vertesia-ui-i18n.6Ozlf6Ks.chunk.js +5 -0
  71. package/lib/vertesia-ui-i18n.6Ozlf6Ks.chunk.js.map +1 -0
  72. package/lib/vertesia-ui-i18n.AttJpg8U.chunk.js +5 -0
  73. package/lib/vertesia-ui-i18n.AttJpg8U.chunk.js.map +1 -0
  74. package/lib/vertesia-ui-i18n.BI_xdp49.chunk.js +5 -0
  75. package/lib/vertesia-ui-i18n.BI_xdp49.chunk.js.map +1 -0
  76. package/lib/vertesia-ui-i18n.C-2EEX6w.chunk.js +5 -0
  77. package/lib/vertesia-ui-i18n.C-2EEX6w.chunk.js.map +1 -0
  78. package/lib/vertesia-ui-i18n.CAmo54FO.chunk.js +5 -0
  79. package/lib/vertesia-ui-i18n.CAmo54FO.chunk.js.map +1 -0
  80. package/lib/vertesia-ui-i18n.CbBzmHNP.chunk.js +5 -0
  81. package/lib/vertesia-ui-i18n.CbBzmHNP.chunk.js.map +1 -0
  82. package/lib/vertesia-ui-i18n.DZgTzkIg.chunk.js +5 -0
  83. package/lib/vertesia-ui-i18n.DZgTzkIg.chunk.js.map +1 -0
  84. package/lib/vertesia-ui-i18n.DZsITxq5.chunk.js +5 -0
  85. package/lib/vertesia-ui-i18n.DZsITxq5.chunk.js.map +1 -0
  86. package/lib/vertesia-ui-i18n.Dbf2GHth.chunk.js +5 -0
  87. package/lib/vertesia-ui-i18n.Dbf2GHth.chunk.js.map +1 -0
  88. package/lib/vertesia-ui-i18n.js +2 -2
  89. package/lib/vertesia-ui-i18n.lleWnH5n.chunk.js +5 -0
  90. package/lib/vertesia-ui-i18n.lleWnH5n.chunk.js.map +1 -0
  91. package/lib/vertesia-ui-i18n.wG_XSkbr.chunk.js +5 -0
  92. package/lib/vertesia-ui-i18n.wG_XSkbr.chunk.js.map +1 -0
  93. package/lib/vertesia-ui-session.js +1 -1
  94. package/lib/vertesia-ui-session.js.map +1 -1
  95. package/package.json +7 -7
  96. package/src/core/components/shadcn/button.tsx +3 -2
  97. package/src/core/components/shadcn/selectBox.tsx +12 -4
  98. package/src/css/index.css +1 -1
  99. package/src/css/typography.css +111 -88
  100. package/src/env/index.ts +0 -2
  101. package/src/features/agent/chat/AskUserWidget.tsx +3 -1
  102. package/src/features/agent/chat/ModernAgentOutput/AllMessagesMixed.tsx +2 -253
  103. package/src/features/agent/chat/ModernAgentOutput/CollapsibleAgentMarkdown.tsx +2 -8
  104. package/src/features/agent/chat/ModernAgentOutput/agentMarkdownStyles.ts +248 -0
  105. package/src/i18n/locales/ar.json +1 -0
  106. package/src/i18n/locales/de.json +1 -0
  107. package/src/i18n/locales/en.json +1 -0
  108. package/src/i18n/locales/es.json +1 -0
  109. package/src/i18n/locales/fr.json +1 -0
  110. package/src/i18n/locales/it.json +1 -0
  111. package/src/i18n/locales/ja.json +1 -0
  112. package/src/i18n/locales/ko.json +1 -0
  113. package/src/i18n/locales/pt.json +1 -0
  114. package/src/i18n/locales/ru.json +1 -0
  115. package/src/i18n/locales/tr.json +1 -0
  116. package/src/i18n/locales/zh-TW.json +1 -0
  117. package/src/i18n/locales/zh.json +1 -0
  118. package/src/session/auth/composable.ts +1 -11
  119. package/src/session/auth/firebase.ts +9 -6
  120. package/src/session/auth/oauth.ts +39 -13
  121. package/src/session/auth/vertesiaHosts.ts +2 -47
  122. package/src/session/index.ts +1 -1
  123. package/src/session/scopeSelection.ts +13 -0
  124. package/lib/vertesia-ui-i18n.B0orbJb6.chunk.js +0 -5
  125. package/lib/vertesia-ui-i18n.B0orbJb6.chunk.js.map +0 -1
  126. package/lib/vertesia-ui-i18n.BChwIQVA.chunk.js +0 -5
  127. package/lib/vertesia-ui-i18n.BChwIQVA.chunk.js.map +0 -1
  128. package/lib/vertesia-ui-i18n.BTEcAVQm.chunk.js +0 -5
  129. package/lib/vertesia-ui-i18n.BTEcAVQm.chunk.js.map +0 -1
  130. package/lib/vertesia-ui-i18n.BeJQKbv7.chunk.js +0 -5
  131. package/lib/vertesia-ui-i18n.BeJQKbv7.chunk.js.map +0 -1
  132. package/lib/vertesia-ui-i18n.BpGr0-5g.chunk.js +0 -5
  133. package/lib/vertesia-ui-i18n.BpGr0-5g.chunk.js.map +0 -1
  134. package/lib/vertesia-ui-i18n.Bs8x-1wI.chunk.js +0 -5
  135. package/lib/vertesia-ui-i18n.Bs8x-1wI.chunk.js.map +0 -1
  136. package/lib/vertesia-ui-i18n.BymQoOm0.chunk.js +0 -5
  137. package/lib/vertesia-ui-i18n.BymQoOm0.chunk.js.map +0 -1
  138. package/lib/vertesia-ui-i18n.CNT_wabV.chunk.js +0 -5
  139. package/lib/vertesia-ui-i18n.CNT_wabV.chunk.js.map +0 -1
  140. package/lib/vertesia-ui-i18n.Cxh1iVk0.chunk.js +0 -5
  141. package/lib/vertesia-ui-i18n.Cxh1iVk0.chunk.js.map +0 -1
  142. package/lib/vertesia-ui-i18n.DLYCM9fV.chunk.js +0 -5
  143. package/lib/vertesia-ui-i18n.DLYCM9fV.chunk.js.map +0 -1
  144. package/lib/vertesia-ui-i18n.QvAVguDs.chunk.js +0 -5
  145. package/lib/vertesia-ui-i18n.QvAVguDs.chunk.js.map +0 -1
  146. package/lib/vertesia-ui-i18n.smozeSmI.chunk.js +0 -5
  147. package/lib/vertesia-ui-i18n.smozeSmI.chunk.js.map +0 -1
@@ -0,0 +1,248 @@
1
+ /** Prose styling used for agent/model message markdown, shared across the conversation UI. */
2
+ export const AGENT_PROSE_CLASS = [
3
+ 'agent-markdown vprose prose max-w-none break-words text-sm leading-6 text-foreground/80',
4
+ 'prose-p:my-2 prose-p:leading-6 prose-li:my-0.5 prose-pre:my-3 prose-headings:tracking-normal',
5
+ 'prose-headings:text-foreground prose-strong:text-foreground prose-code:text-foreground',
6
+ 'prose-a:text-foreground prose-a:underline prose-a:decoration-muted prose-a:underline-offset-4',
7
+ 'prose-p:text-foreground/80 prose-li:text-foreground/80 prose-li:marker:text-muted',
8
+ ].join(' ');
9
+
10
+ // Shared with rendered regression checks so they exercise the styles the chat actually injects.
11
+ export const agentMarkdownStyles = `
12
+ /* Better vertical rhythm for markdown */
13
+ .vprose > * + *:not(:where(.not-prose, .not-prose *)) {
14
+ margin-top: 0.625rem;
15
+ }
16
+ .vprose > h1 + *:not(:where(.not-prose, .not-prose *)),
17
+ .vprose > h2 + *:not(:where(.not-prose, .not-prose *)),
18
+ .vprose > h3 + *:not(:where(.not-prose, .not-prose *)) {
19
+ margin-top: 0.375rem;
20
+ }
21
+ /* Tables should read like part of the conversation, not a separate app grid. */
22
+ .vprose table:not(:where(.not-prose, .not-prose *)) {
23
+ margin-top: 0.875rem;
24
+ margin-bottom: 0.875rem;
25
+ border-collapse: collapse;
26
+ width: 100%;
27
+ background: transparent;
28
+ }
29
+ .vprose th:not(:where(.not-prose, .not-prose *)),
30
+ .vprose td:not(:where(.not-prose, .not-prose *)) {
31
+ padding: 0.5rem 0.625rem;
32
+ border: 0;
33
+ border-bottom: 1px solid color-mix(in oklch, var(--border) 70%, transparent);
34
+ background: transparent;
35
+ text-align: left;
36
+ }
37
+ .vprose thead th:not(:where(.not-prose, .not-prose *)) {
38
+ background: transparent;
39
+ font-weight: 600;
40
+ color: var(--muted);
41
+ font-size: 0.75rem;
42
+ text-transform: none;
43
+ letter-spacing: 0;
44
+ }
45
+ .vprose tbody tr:hover:not(:where(.not-prose, .not-prose *)) {
46
+ background: transparent;
47
+ }
48
+ /* Horizontal rules as section dividers */
49
+ .vprose hr:not(:where(.not-prose, .not-prose *)) {
50
+ margin-top: 1rem;
51
+ margin-bottom: 1rem;
52
+ border-color: var(--border);
53
+ }
54
+ /* Better blockquote styling */
55
+ .vprose blockquote:not(:where(.not-prose, .not-prose *)) {
56
+ margin-top: 0.875rem;
57
+ margin-bottom: 0.875rem;
58
+ padding-left: 1rem;
59
+ border-left-width: 3px;
60
+ border-left-color: var(--border);
61
+ color: var(--muted);
62
+ }
63
+ /* Code blocks */
64
+ .vprose pre:not(:where(.not-prose, .not-prose *)) {
65
+ margin-top: 0.75rem;
66
+ margin-bottom: 0.75rem;
67
+ padding: 0.75rem;
68
+ border-radius: 0.5rem;
69
+ overflow-x: auto;
70
+ background-color: var(--muted-background);
71
+ color: var(--foreground);
72
+ }
73
+ .vprose pre code:not(:where(.not-prose, .not-prose *)) {
74
+ color: inherit;
75
+ }
76
+
77
+ /* Summary chat markdown: keep structure, but match the app's quieter conversation surface. */
78
+ .agent-markdown:not(:where(.not-prose, .not-prose *)) {
79
+ color: color-mix(in oklch, var(--foreground) 78%, transparent);
80
+ font-size: 0.875rem;
81
+ line-height: 1.625;
82
+ overflow-x: auto;
83
+ }
84
+ .agent-markdown > * + *:not(:where(.not-prose, .not-prose *)) {
85
+ margin-top: 0.65rem;
86
+ }
87
+ .agent-markdown p:not(:where(.not-prose, .not-prose *)) {
88
+ margin-top: 0.5rem;
89
+ margin-bottom: 0.5rem;
90
+ }
91
+ .agent-markdown h1:not(:where(.not-prose, .not-prose *)),
92
+ .agent-markdown h2:not(:where(.not-prose, .not-prose *)),
93
+ .agent-markdown h3:not(:where(.not-prose, .not-prose *)),
94
+ .agent-markdown h4:not(:where(.not-prose, .not-prose *)) {
95
+ margin-top: 1.2rem;
96
+ margin-bottom: 0.45rem;
97
+ color: var(--foreground);
98
+ font-weight: 650;
99
+ letter-spacing: 0;
100
+ line-height: 1.3;
101
+ }
102
+ .agent-markdown h1:not(:where(.not-prose, .not-prose *)) {
103
+ font-size: 1.2rem;
104
+ }
105
+ .agent-markdown h2:not(:where(.not-prose, .not-prose *)) {
106
+ font-size: 1.05rem;
107
+ }
108
+ .agent-markdown h3:not(:where(.not-prose, .not-prose *)),
109
+ .agent-markdown h4:not(:where(.not-prose, .not-prose *)) {
110
+ font-size: 0.95rem;
111
+ }
112
+ .agent-markdown h1:first-child:not(:where(.not-prose, .not-prose *)),
113
+ .agent-markdown h2:first-child:not(:where(.not-prose, .not-prose *)),
114
+ .agent-markdown h3:first-child:not(:where(.not-prose, .not-prose *)),
115
+ .agent-markdown h4:first-child:not(:where(.not-prose, .not-prose *)) {
116
+ margin-top: 0;
117
+ }
118
+ .agent-markdown hr:not(:where(.not-prose, .not-prose *)) {
119
+ margin: 1.15rem 0;
120
+ border: 0;
121
+ border-top: 1px solid var(--border);
122
+ opacity: 0.65;
123
+ }
124
+ .agent-markdown ul:not(:where(.not-prose, .not-prose *)),
125
+ .agent-markdown ol:not(:where(.not-prose, .not-prose *)) {
126
+ margin-top: 0.55rem;
127
+ margin-bottom: 0.55rem;
128
+ padding-left: 1.25rem;
129
+ }
130
+ .agent-markdown li:not(:where(.not-prose, .not-prose *)) {
131
+ margin-top: 0.2rem;
132
+ margin-bottom: 0.2rem;
133
+ padding-left: 0.1rem;
134
+ color: inherit;
135
+ }
136
+ .agent-markdown li:not(:where(.not-prose, .not-prose *))::marker {
137
+ color: var(--muted);
138
+ }
139
+ .agent-markdown strong:not(:where(.not-prose, .not-prose *)) {
140
+ color: var(--foreground);
141
+ font-weight: 600;
142
+ }
143
+ .agent-markdown em:not(:where(.not-prose, .not-prose *)) {
144
+ color: color-mix(in oklch, var(--foreground) 72%, transparent);
145
+ }
146
+ .agent-markdown a:not(:where(.not-prose, .not-prose *)) {
147
+ color: var(--foreground);
148
+ text-decoration-color: var(--muted);
149
+ text-underline-offset: 3px;
150
+ }
151
+ .agent-markdown blockquote:not(:where(.not-prose, .not-prose *)) {
152
+ margin-top: 0.85rem;
153
+ margin-bottom: 0.85rem;
154
+ padding-left: 0.875rem;
155
+ border-left: 2px solid var(--border);
156
+ color: var(--muted);
157
+ font-style: normal;
158
+ }
159
+ .agent-markdown :not(pre) > code:not(:where(.not-prose, .not-prose *)) {
160
+ border: 1px solid var(--border);
161
+ border-radius: 0.375rem;
162
+ background: var(--muted-background);
163
+ color: var(--foreground);
164
+ padding: 0.1rem 0.35rem;
165
+ font-size: 0.8125em;
166
+ font-weight: 500;
167
+ }
168
+ .agent-markdown pre:not(:where(.not-prose, .not-prose *)) {
169
+ margin-top: 0.85rem;
170
+ margin-bottom: 0.85rem;
171
+ max-height: 32rem;
172
+ overflow: auto;
173
+ border: 1px solid var(--border);
174
+ border-radius: 0.75rem;
175
+ background: var(--muted-background);
176
+ color: var(--foreground);
177
+ padding: 0.875rem;
178
+ font-size: 0.8125rem;
179
+ line-height: 1.55;
180
+ }
181
+ .agent-markdown pre code:not(:where(.not-prose, .not-prose *)) {
182
+ border: 0;
183
+ background: transparent;
184
+ color: inherit;
185
+ padding: 0;
186
+ font-size: inherit;
187
+ }
188
+ .agent-markdown table:not(:where(.not-prose, .not-prose *)) {
189
+ display: table;
190
+ width: 100%;
191
+ max-width: 100%;
192
+ margin: 0.9rem 0 1rem;
193
+ border: 0;
194
+ border-collapse: collapse;
195
+ border-spacing: 0;
196
+ font-size: 0.8125rem;
197
+ table-layout: fixed;
198
+ background: transparent;
199
+ }
200
+ .agent-markdown thead:not(:where(.not-prose, .not-prose *)) {
201
+ background: transparent;
202
+ }
203
+ .agent-markdown th:not(:where(.not-prose, .not-prose *)),
204
+ .agent-markdown td:not(:where(.not-prose, .not-prose *)) {
205
+ border: 0;
206
+ border-bottom: 1px solid color-mix(in oklch, var(--border) 70%, transparent);
207
+ background: transparent;
208
+ padding: 0.55rem 0.75rem;
209
+ text-align: left;
210
+ vertical-align: top;
211
+ overflow-wrap: anywhere;
212
+ word-break: normal;
213
+ white-space: normal;
214
+ }
215
+ .agent-markdown th:not(:where(.not-prose, .not-prose *)) {
216
+ color: var(--muted);
217
+ font-size: 0.75rem;
218
+ font-weight: 600;
219
+ letter-spacing: 0;
220
+ text-transform: none;
221
+ white-space: nowrap;
222
+ }
223
+ .agent-markdown td:not(:where(.not-prose, .not-prose *)) {
224
+ color: inherit;
225
+ }
226
+ .agent-markdown th + th:not(:where(.not-prose, .not-prose *)),
227
+ .agent-markdown td + td:not(:where(.not-prose, .not-prose *)) {
228
+ padding-inline-start: 1.75rem;
229
+ }
230
+ .agent-markdown th:first-child:not(:where(.not-prose, .not-prose *)),
231
+ .agent-markdown td:first-child:not(:where(.not-prose, .not-prose *)) {
232
+ min-width: 0;
233
+ width: auto;
234
+ white-space: normal;
235
+ }
236
+ .agent-markdown col:not(:where(.not-prose, .not-prose *)) {
237
+ width: var(--agent-markdown-table-column-width);
238
+ }
239
+ .agent-markdown tr:last-child td:not(:where(.not-prose, .not-prose *)) {
240
+ border-bottom: 0;
241
+ }
242
+ .agent-markdown tbody tr:nth-child(even):not(:where(.not-prose, .not-prose *)) {
243
+ background: transparent;
244
+ }
245
+ .agent-markdown tbody tr:hover:not(:where(.not-prose, .not-prose *)) {
246
+ background: transparent;
247
+ }
248
+ `;
@@ -261,6 +261,7 @@
261
261
  "agent.thinking": "جارٍ التفكير...",
262
262
  "agent.timeout": "انتهت المهلة",
263
263
  "agent.toggleRightSidebar": "تبديل الشريط الجانبي الأيمن",
264
+ "agent.typeDifferentResponse": "أو اكتب ردًا مختلفًا...",
264
265
  "agent.typeYourMessage": "اكتب رسالتك...",
265
266
  "agent.typeYourResponse": "اكتب ردك...",
266
267
  "agent.unableToOpenPrint": "تعذر فتح معاينة الطباعة",
@@ -249,6 +249,7 @@
249
249
  "agent.thinking": "Denke nach...",
250
250
  "agent.timeout": "Zeitüberschreitung",
251
251
  "agent.toggleRightSidebar": "Rechte Seitenleiste umschalten",
252
+ "agent.typeDifferentResponse": "Oder geben Sie eine andere Antwort ein...",
252
253
  "agent.typeYourMessage": "Geben Sie Ihre Nachricht ein...",
253
254
  "agent.typeYourResponse": "Geben Sie Ihre Antwort ein...",
254
255
  "agent.unableToOpenPrint": "Druckvorschau konnte nicht geöffnet werden",
@@ -322,6 +322,7 @@
322
322
  "agent.thinking": "Thinking...",
323
323
  "agent.timeout": "Timed out",
324
324
  "agent.toggleRightSidebar": "Toggle right sidebar",
325
+ "agent.typeDifferentResponse": "Or type a different response...",
325
326
  "agent.typeYourMessage": "Type your message...",
326
327
  "agent.typeYourResponse": "Type your response...",
327
328
  "agent.unableToOpenPrint": "Unable to open print preview",
@@ -251,6 +251,7 @@
251
251
  "agent.thinking": "Pensando...",
252
252
  "agent.timeout": "Tiempo agotado",
253
253
  "agent.toggleRightSidebar": "Alternar panel lateral",
254
+ "agent.typeDifferentResponse": "O escribe una respuesta diferente...",
254
255
  "agent.typeYourMessage": "Escribe tu mensaje...",
255
256
  "agent.typeYourResponse": "Escribe tu respuesta...",
256
257
  "agent.unableToOpenPrint": "No se pudo abrir la vista previa de impresión",
@@ -251,6 +251,7 @@
251
251
  "agent.thinking": "Réflexion...",
252
252
  "agent.timeout": "Délai dépassé",
253
253
  "agent.toggleRightSidebar": "Basculer le panneau latéral",
254
+ "agent.typeDifferentResponse": "Ou saisissez une autre réponse...",
254
255
  "agent.typeYourMessage": "Saisissez votre message...",
255
256
  "agent.typeYourResponse": "Saisissez votre réponse...",
256
257
  "agent.unableToOpenPrint": "Impossible d'ouvrir l'aperçu d'impression",
@@ -251,6 +251,7 @@
251
251
  "agent.thinking": "Sto pensando...",
252
252
  "agent.timeout": "Tempo scaduto",
253
253
  "agent.toggleRightSidebar": "Alterna pannello laterale",
254
+ "agent.typeDifferentResponse": "Oppure scrivi una risposta diversa...",
254
255
  "agent.typeYourMessage": "Scrivi il tuo messaggio...",
255
256
  "agent.typeYourResponse": "Scrivi la tua risposta...",
256
257
  "agent.unableToOpenPrint": "Impossibile aprire l'anteprima di stampa",
@@ -249,6 +249,7 @@
249
249
  "agent.thinking": "考え中...",
250
250
  "agent.timeout": "タイムアウト",
251
251
  "agent.toggleRightSidebar": "右サイドバーの切り替え",
252
+ "agent.typeDifferentResponse": "または別の回答を入力...",
252
253
  "agent.typeYourMessage": "メッセージを入力...",
253
254
  "agent.typeYourResponse": "回答を入力...",
254
255
  "agent.unableToOpenPrint": "印刷プレビューを開けません",
@@ -249,6 +249,7 @@
249
249
  "agent.thinking": "생각 중...",
250
250
  "agent.timeout": "시간 초과",
251
251
  "agent.toggleRightSidebar": "오른쪽 사이드바 전환",
252
+ "agent.typeDifferentResponse": "또는 다른 응답을 입력하세요...",
252
253
  "agent.typeYourMessage": "메시지를 입력하세요...",
253
254
  "agent.typeYourResponse": "응답을 입력하세요...",
254
255
  "agent.unableToOpenPrint": "인쇄 미리보기를 열 수 없습니다",
@@ -251,6 +251,7 @@
251
251
  "agent.thinking": "Pensando...",
252
252
  "agent.timeout": "Tempo esgotado",
253
253
  "agent.toggleRightSidebar": "Alternar painel lateral",
254
+ "agent.typeDifferentResponse": "Ou digite uma resposta diferente...",
254
255
  "agent.typeYourMessage": "Digite sua mensagem...",
255
256
  "agent.typeYourResponse": "Digite sua resposta...",
256
257
  "agent.unableToOpenPrint": "Não foi possível abrir a visualização de impressão",
@@ -255,6 +255,7 @@
255
255
  "agent.thinking": "Размышляю...",
256
256
  "agent.timeout": "Время ожидания истекло",
257
257
  "agent.toggleRightSidebar": "Переключить правую панель",
258
+ "agent.typeDifferentResponse": "Или введите другой ответ...",
258
259
  "agent.typeYourMessage": "Введите сообщение...",
259
260
  "agent.typeYourResponse": "Введите ваш ответ...",
260
261
  "agent.unableToOpenPrint": "Не удалось открыть предпросмотр печати",
@@ -249,6 +249,7 @@
249
249
  "agent.thinking": "Düşünüyor...",
250
250
  "agent.timeout": "Zaman aşımına uğradı",
251
251
  "agent.toggleRightSidebar": "Sağ kenar çubuğunu aç/kapat",
252
+ "agent.typeDifferentResponse": "Ya da farklı bir yanıt yazın...",
252
253
  "agent.typeYourMessage": "Mesajınızı yazın...",
253
254
  "agent.typeYourResponse": "Yanıtınızı yazın...",
254
255
  "agent.unableToOpenPrint": "Yazdırma önizlemesi açılamadı",
@@ -249,6 +249,7 @@
249
249
  "agent.thinking": "思考中...",
250
250
  "agent.timeout": "已逾時",
251
251
  "agent.toggleRightSidebar": "切換右側邊欄",
252
+ "agent.typeDifferentResponse": "或輸入其他回覆...",
252
253
  "agent.typeYourMessage": "輸入訊息...",
253
254
  "agent.typeYourResponse": "輸入您的回覆...",
254
255
  "agent.unableToOpenPrint": "無法開啟列印預覽",
@@ -249,6 +249,7 @@
249
249
  "agent.thinking": "思考中...",
250
250
  "agent.timeout": "已超时",
251
251
  "agent.toggleRightSidebar": "切换右侧边栏",
252
+ "agent.typeDifferentResponse": "或输入其他回复...",
252
253
  "agent.typeYourMessage": "输入消息...",
253
254
  "agent.typeYourResponse": "输入您的回复...",
254
255
  "agent.unableToOpenPrint": "无法打开打印预览",
@@ -116,17 +116,7 @@ function identityFromAcceptedToken(token: string): AuthenticatedIdentity | undef
116
116
  }
117
117
  }
118
118
 
119
- export function resolveAuthSelection(currentUrl: URL): { accountId?: string; projectId?: string } {
120
- // A URL selection owns the whole pair: never attach an unrelated configured account/project.
121
- const hasUrlScope = currentUrl.searchParams.has('a') || currentUrl.searchParams.has('p');
122
- const defaults = hasUrlScope ? undefined : Env.defaultAuthSelection;
123
- const urlAccount = currentUrl.searchParams.get('a') ?? defaults?.accountId;
124
- const urlProject = currentUrl.searchParams.get('p') ?? defaults?.projectId;
125
- // A project alone identifies its account, so the stored selection only fills in a missing project.
126
- const stored = urlProject === undefined ? readScopeSelection(urlAccount) : undefined;
127
-
128
- return { accountId: urlAccount ?? stored?.accountId, projectId: urlProject ?? stored?.projectId };
129
- }
119
+ export { resolveAuthSelection } from '../scopeSelection';
130
120
 
131
121
  function decodeToken(token: string): AuthTokenPayload {
132
122
  return jwtDecode(token) as AuthTokenPayload;
@@ -92,10 +92,9 @@ export async function setFirebaseTenant(tenantEmail?: string) {
92
92
  // Handle HTTP error responses
93
93
  if (!response.ok) {
94
94
  // If the error is 404 Not Found, no need to retry
95
+ // How older servers answered "this address has no SSO tenant": an ordinary outcome.
95
96
  if (response.status === 404) {
96
- Env.logger.warn('Firebase tenant was not found; continuing with the default tenant', {
97
- vertesia: { status: response.status },
98
- });
97
+ Env.logger.debug('No Firebase tenant for this address; using the default tenant');
99
98
  return;
100
99
  }
101
100
 
@@ -103,10 +102,14 @@ export async function setFirebaseTenant(tenantEmail?: string) {
103
102
  throw new Error(errorData?.error ?? `HTTP error ${response.status}`);
104
103
  }
105
104
 
106
- // Successfully got a response, parse it
107
- const data = (await response.json()) as UIResolvedTenant;
105
+ // Successfully got a response, parse it. `null` means the address has no SSO tenant.
106
+ const data = (await response.json()) as UIResolvedTenant | null;
107
+ if (data === null) {
108
+ Env.logger.debug('No Firebase tenant for this address; using the default tenant');
109
+ return;
110
+ }
108
111
 
109
- if (data?.firebaseTenantId) {
112
+ if (data.firebaseTenantId) {
110
113
  const auth = getFirebaseAuth();
111
114
  auth.tenantId = data.firebaseTenantId;
112
115
  Env.firebase.providerType = data.provider ?? 'oidc';
@@ -1,6 +1,7 @@
1
- import type { AuthTokenPayload } from '@vertesia/common';
1
+ import { type AuthTokenPayload, REQUESTED_SCOPE_UNAVAILABLE_ERROR_CODE } from '@vertesia/common';
2
2
  import { Env } from '@vertesia/ui/env';
3
3
  import { jwtDecode } from 'jwt-decode';
4
+ import { forgetRejectedScopeSelection, resolveAuthSelection, type ScopeSelection } from '../scopeSelection';
4
5
  import { markCentralAuthRoundTripStarted } from './authRoundTrip';
5
6
  import { verifyAuthState } from './authState';
6
7
  import { usesGatewaySession } from './gateway';
@@ -61,6 +62,8 @@ interface Transaction {
61
62
  redirectUri: string;
62
63
  issuer: string;
63
64
  target: string;
65
+ rememberedScope?: ScopeSelection;
66
+ retryScope?: ScopeSelection;
64
67
  }
65
68
 
66
69
  export function clearAppOAuth(): void {
@@ -87,7 +90,8 @@ async function acquireToken(forceRefresh = false): Promise<string> {
87
90
  const redirectUri = redirect.toString();
88
91
  if (new URL(redirectUri).origin !== window.location.origin) throw new Error('OAuth callback must be same-origin');
89
92
  const issuer = (config.issuer ? httpsUrl(config.issuer).toString() : Env.endpoints.sts).replace(/\/+$/, '');
90
- const current = new URL(window.location.href);
93
+ let current = new URL(window.location.href);
94
+ let { accountId, projectId } = resolveAuthSelection(current);
91
95
  const callback = current.origin + current.pathname === new URL(redirectUri).origin + new URL(redirectUri).pathname;
92
96
  const state = callback ? current.searchParams.get('state') : null;
93
97
  const code = callback ? current.searchParams.get('code') : null;
@@ -109,7 +113,19 @@ async function acquireToken(forceRefresh = false): Promise<string> {
109
113
  ) {
110
114
  throw new Error('Invalid or expired OAuth login state');
111
115
  }
112
- if (error || !code) throw new Error(`Application sign-in was not completed (${error || 'missing code'})`);
116
+ if (error === REQUESTED_SCOPE_UNAVAILABLE_ERROR_CODE && transaction.rememberedScope?.accountId) {
117
+ const target = new URL(transaction.target);
118
+ if (target.origin !== window.location.origin) throw new Error('Invalid OAuth return target');
119
+ forgetRejectedScopeSelection(transaction.rememberedScope.accountId, transaction.rememberedScope.projectId);
120
+ clearAppOAuth();
121
+ current = target;
122
+ window.history.replaceState(window.history.state, '', current);
123
+ accountId = transaction.retryScope?.accountId;
124
+ projectId = undefined;
125
+ transaction = undefined;
126
+ } else if (error || !code) {
127
+ throw new Error(`Application sign-in was not completed (${error || 'missing code'})`);
128
+ }
113
129
  } else {
114
130
  const raw = sessionStorage.getItem(TOKEN_KEY);
115
131
  if (raw) {
@@ -124,8 +140,8 @@ async function acquireToken(forceRefresh = false): Promise<string> {
124
140
  config.offlineAccess &&
125
141
  cached.clientId === clientId &&
126
142
  cached.issuer === issuer &&
127
- (!current.searchParams.get('p') || claims.project?.id === current.searchParams.get('p')) &&
128
- (!current.searchParams.get('a') || claims.account?.id === current.searchParams.get('a'))
143
+ (!projectId || claims.project?.id === projectId) &&
144
+ (!accountId || claims.account?.id === accountId)
129
145
  )
130
146
  refreshCredential =
131
147
  refreshSession?.clientId === clientId && refreshSession.issuer === issuer
@@ -138,8 +154,8 @@ async function acquireToken(forceRefresh = false): Promise<string> {
138
154
  cached.issuer === issuer &&
139
155
  !forceRefresh &&
140
156
  claims.exp > Date.now() / 1000 + (config.offlineAccess ? 300 : 30) &&
141
- (!current.searchParams.get('p') || claims.project?.id === current.searchParams.get('p')) &&
142
- (!current.searchParams.get('a') || claims.account?.id === current.searchParams.get('a'))
157
+ (!projectId || claims.project?.id === projectId) &&
158
+ (!accountId || claims.account?.id === accountId)
143
159
  ) {
144
160
  return cached.token;
145
161
  }
@@ -189,8 +205,8 @@ async function acquireToken(forceRefresh = false): Promise<string> {
189
205
  claims.iss.replace(/\/+$/, '') !== issuer ||
190
206
  claims.client_id !== clientId ||
191
207
  claims.exp <= Date.now() / 1000 ||
192
- (!!current.searchParams.get('a') && claims.account?.id !== current.searchParams.get('a')) ||
193
- (!!current.searchParams.get('p') && claims.project?.id !== current.searchParams.get('p'))
208
+ (!!accountId && claims.account?.id !== accountId) ||
209
+ (!!projectId && claims.project?.id !== projectId)
194
210
  )
195
211
  throw new OAuthLoginError('Invalid OAuth refresh identity');
196
212
  sessionStorage.setItem(
@@ -277,6 +293,13 @@ async function acquireToken(forceRefresh = false): Promise<string> {
277
293
  throw new Error('Application CIMD does not allow this callback');
278
294
  if (!config.scopes && document.scope) scopes = document.scope.split(/\s+/);
279
295
  }
296
+ const explicitScope =
297
+ current.searchParams.has('a') || current.searchParams.has('p')
298
+ ? {
299
+ accountId: current.searchParams.get('a') ?? undefined,
300
+ projectId: current.searchParams.get('p') ?? undefined,
301
+ }
302
+ : Env.defaultAuthSelection;
280
303
  const verifier = randomValue();
281
304
  const next: Transaction = {
282
305
  state: randomValue(),
@@ -285,6 +308,11 @@ async function acquireToken(forceRefresh = false): Promise<string> {
285
308
  redirectUri,
286
309
  issuer,
287
310
  target: current.toString(),
311
+ rememberedScope:
312
+ accountId && explicitScope?.projectId === undefined && (!explicitScope?.accountId || projectId)
313
+ ? { accountId, projectId }
314
+ : undefined,
315
+ retryScope: explicitScope?.accountId ? { accountId: explicitScope.accountId } : undefined,
288
316
  created: Date.now(),
289
317
  };
290
318
  const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
@@ -302,10 +330,8 @@ async function acquireToken(forceRefresh = false): Promise<string> {
302
330
  : scopes.filter((s) => s !== 'offline_access')
303
331
  ).join(' '),
304
332
  );
305
- const account = current.searchParams.get('a');
306
- if (account) authorize.searchParams.set('account_id', account);
307
- const project = current.searchParams.get('p');
308
- if (project) authorize.searchParams.set('project_id', project);
333
+ if (accountId) authorize.searchParams.set('account_id', accountId);
334
+ if (projectId) authorize.searchParams.set('project_id', projectId);
309
335
  sessionStorage.setItem(TRANSACTION_KEY, JSON.stringify(next));
310
336
  markCentralAuthRoundTripStarted();
311
337
  window.location.replace(authorize.toString());
@@ -1,54 +1,9 @@
1
1
  const LOOPBACK_IPV4 = /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
2
2
  /** A single DNS label: alphanumeric, inner hyphens allowed. */
3
- export const LABEL = '[a-z0-9](?:[a-z0-9-]*[a-z0-9])?';
3
+ const LABEL = '[a-z0-9](?:[a-z0-9-]*[a-z0-9])?';
4
4
 
5
5
  /** The region label is optional throughout, because each regional host has a global US alias. */
6
- export const REGION = `(?:${LABEL}\\.)?`;
7
-
8
- /**
9
- * Hostnames served by the UI backend — Vertesia's own code, not merely a Vertesia-owned name:
10
- * `cloud.vertesia.io`, `cloud.us1.vertesia.io` — the Cloud UI
11
- * `preprod.cloud.vertesia.io`, `preview.cloud.us1.vertesia.io` — its preview/preprod tiers
12
- * `acme.cloud.us1.vertesia.io` — per-IdP-tenant branded hosts
13
- * `dev-feat-x.ui.dev1.vertesia.io` — dynamic per-branch dev envs
14
- *
15
- * Mirrors the load-balancer host rules (region-lb/main.tf: `cloud.${var.domain}`,
16
- * `*.cloud.${var.domain}`, `*.ui.${var.domain}`) rather than the DNS zone, so a name we merely own
17
- * is not trusted for what serves it.
18
- */
19
- export const FIRST_PARTY_HOST_PATTERNS: readonly RegExp[] = [
20
- new RegExp(`^cloud\\.${REGION}vertesia\\.io$`),
21
- new RegExp(`^${LABEL}\\.cloud\\.${REGION}vertesia\\.io$`),
22
- new RegExp(`^${LABEL}\\.ui\\.${LABEL}\\.vertesia\\.io$`),
23
- ];
24
-
25
- /**
26
- * Public app-gateway origins. Stable tiers use the `apps*` browser-facing host, while development
27
- * environments reach the gateway through its `*.api.*` Cloud Run route:
28
- * `apps.us1.vertesia.io`, `apps-preview.us1.vertesia.io`
29
- * `app-gateway-dev1.api.dev1.vertesia.io`, `app-gateway-us3.api.us3.vertesia.io`
30
- *
31
- * The second label is deliberately unconstrained. Branch environments do NOT each get a gateway —
32
- * they share their region's (`detectGatewayUrl` in packages/server-common/src/env/ServerEnv.ts:444
33
- * and `getAppgenServerBaseUrl` in packages/workflows/.../app_workspace/workspace-utils.ts), so the
34
- * host published to browsers is region-scoped, while the per-branch `app-gateway-dev-<branch>`
35
- * shape in the infrastructure README also exists. Matching `app-gateway-<anything>` covers both,
36
- * and every Cloud Run service named that way *is* a gateway (region-compute/main.tf:373).
37
- *
38
- * These serve every tenant's published application from one shared origin, and published assets
39
- * need no authentication — so this is generated, low-trust code sitting on a Vertesia-owned
40
- * hostname. Owning the domain is not the test; which backend serves the origin is.
41
- *
42
- * Two load-balancer variables can map further hostnames onto the same backend —
43
- * `appgen_host_aliases` and `appgen_preview_host_aliases` in
44
- * infrastructure/terraform/modules/region-lb/variables.tf. Both are empty in every region today; if
45
- * one is ever set, add the alias here too, or it silently falls through to the permissive branch of
46
- * Policy L and becomes an origin that may receive a token at any path.
47
- */
48
- export const GATEWAY_HOST_PATTERNS: readonly RegExp[] = [
49
- new RegExp(`^apps(?:-preview|-preprod)?\\.${REGION}vertesia\\.io$`),
50
- new RegExp(`^app-gateway-${LABEL}\\.api\\.${LABEL}\\.vertesia\\.io$`),
51
- ];
6
+ const REGION = `(?:${LABEL}\\.)?`;
52
7
 
53
8
  export function isLoopbackHostname(hostname: string): boolean {
54
9
  if (hostname === 'localhost' || hostname.endsWith('.localhost')) {
@@ -8,7 +8,7 @@ export {
8
8
  redirectToCentralAuth,
9
9
  } from './auth/domainRouting';
10
10
  export * from './auth/firebase';
11
- export { isAppOAuthLoginPending, startAppOAuthLogin } from './auth/oauth';
11
+ export { isAppOAuthLoginPending, revokeAppOAuthSession, startAppOAuthLogin } from './auth/oauth';
12
12
  export * from './auth/useAuthState';
13
13
  export * from './auth/useCurrentTenant';
14
14
  export * from './DevSessionProvider';
@@ -6,6 +6,7 @@
6
6
  * another tab's next token refresh. `localStorage` holds the last selection made in any tab, and
7
7
  * only seeds a tab that has no scope of its own yet (a new tab, or the app after a restart).
8
8
  */
9
+ import { Env } from '@vertesia/ui/env';
9
10
  import { LastSelectedAccountId_KEY, LastSelectedProjectId_KEY } from './constants';
10
11
 
11
12
  const TabAccountId_KEY = 'composableai.tabAccountId';
@@ -52,6 +53,18 @@ export function readScopeSelection(accountId?: string): ScopeSelection {
52
53
  return { accountId: account, projectId: projectId || undefined };
53
54
  }
54
55
 
56
+ export function resolveAuthSelection(currentUrl: URL): { accountId?: string; projectId?: string } {
57
+ // A URL selection owns the whole pair: never attach an unrelated configured account/project.
58
+ const hasUrlScope = currentUrl.searchParams.has('a') || currentUrl.searchParams.has('p');
59
+ const defaults = hasUrlScope ? undefined : Env.defaultAuthSelection;
60
+ const urlAccount = currentUrl.searchParams.get('a') ?? defaults?.accountId;
61
+ const urlProject = currentUrl.searchParams.get('p') ?? defaults?.projectId;
62
+ // A project alone identifies its account, so the stored selection only fills in a missing project.
63
+ const stored = urlProject === undefined ? readScopeSelection(urlAccount) : undefined;
64
+
65
+ return { accountId: urlAccount ?? stored?.accountId, projectId: urlProject ?? stored?.projectId };
66
+ }
67
+
55
68
  /** Make this the tab's scope and the default for new tabs. */
56
69
  export function rememberScopeSelection(accountId: string, projectId?: string): void {
57
70
  sessionSet(TabAccountId_KEY, accountId);