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

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 (143) hide show
  1. package/lib/boot/index.d.ts +12 -1
  2. package/lib/boot/index.d.ts.map +1 -1
  3. package/lib/boot/index.js +65 -10
  4. package/lib/boot/index.js.map +1 -1
  5. package/lib/boot/text.d.ts +13 -0
  6. package/lib/boot/text.d.ts.map +1 -0
  7. package/lib/boot/text.js +68 -0
  8. package/lib/boot/text.js.map +1 -0
  9. package/lib/features/agent/PayloadBuilder.d.ts +6 -0
  10. package/lib/features/agent/PayloadBuilder.d.ts.map +1 -1
  11. package/lib/features/agent/chat/ModernAgentConversation.js +6 -4
  12. package/lib/features/agent/chat/ModernAgentConversation.js.map +1 -1
  13. package/lib/features/agent/chat/ModernAgentOutput/SummaryConversation.d.ts.map +1 -1
  14. package/lib/features/agent/chat/ModernAgentOutput/SummaryConversation.js +58 -0
  15. package/lib/features/agent/chat/ModernAgentOutput/SummaryConversation.js.map +1 -1
  16. package/lib/features/agent/chat/SkillWidgetProvider.d.ts.map +1 -1
  17. package/lib/features/agent/chat/SkillWidgetProvider.js +3 -2
  18. package/lib/features/agent/chat/SkillWidgetProvider.js.map +1 -1
  19. package/lib/i18n/LanguageProvider.d.ts +9 -1
  20. package/lib/i18n/LanguageProvider.d.ts.map +1 -1
  21. package/lib/i18n/LanguageProvider.js +7 -4
  22. package/lib/i18n/LanguageProvider.js.map +1 -1
  23. package/lib/i18n/locales/ar.json +5 -1
  24. package/lib/i18n/locales/de.json +5 -1
  25. package/lib/i18n/locales/en.json +5 -1
  26. package/lib/i18n/locales/es.json +5 -1
  27. package/lib/i18n/locales/fr.json +5 -1
  28. package/lib/i18n/locales/it.json +5 -1
  29. package/lib/i18n/locales/ja.json +5 -1
  30. package/lib/i18n/locales/ko.json +5 -1
  31. package/lib/i18n/locales/pt.json +5 -1
  32. package/lib/i18n/locales/ru.json +5 -1
  33. package/lib/i18n/locales/tr.json +5 -1
  34. package/lib/i18n/locales/zh-TW.json +5 -1
  35. package/lib/i18n/locales/zh.json +5 -1
  36. package/lib/i18n/rtl.d.ts +4 -2
  37. package/lib/i18n/rtl.d.ts.map +1 -1
  38. package/lib/i18n/rtl.js +4 -2
  39. package/lib/i18n/rtl.js.map +1 -1
  40. package/lib/session/UserSessionProvider.d.ts.map +1 -1
  41. package/lib/session/UserSessionProvider.js +10 -4
  42. package/lib/session/UserSessionProvider.js.map +1 -1
  43. package/lib/session/auth/oauth.d.ts.map +1 -1
  44. package/lib/session/auth/oauth.js +15 -11
  45. package/lib/session/auth/oauth.js.map +1 -1
  46. package/lib/shell/VertesiaShell.d.ts +7 -2
  47. package/lib/shell/VertesiaShell.d.ts.map +1 -1
  48. package/lib/shell/VertesiaShell.js +2 -2
  49. package/lib/shell/VertesiaShell.js.map +1 -1
  50. package/lib/shell/login/InviteAcceptModal.d.ts.map +1 -1
  51. package/lib/shell/login/InviteAcceptModal.js +83 -42
  52. package/lib/shell/login/InviteAcceptModal.js.map +1 -1
  53. package/lib/vertesia-ui-boot.js +52 -15
  54. package/lib/vertesia-ui-boot.js.map +1 -1
  55. package/lib/vertesia-ui-core.js +43 -43
  56. package/lib/vertesia-ui-core.js.map +1 -1
  57. package/lib/vertesia-ui-features.js +334 -334
  58. package/lib/vertesia-ui-features.js.map +1 -1
  59. package/lib/vertesia-ui-i18n.B1Nd8-dE.chunk.js +5 -0
  60. package/lib/vertesia-ui-i18n.B1Nd8-dE.chunk.js.map +1 -0
  61. package/lib/vertesia-ui-i18n.B2Z7mx1x.chunk.js +5 -0
  62. package/lib/vertesia-ui-i18n.B2Z7mx1x.chunk.js.map +1 -0
  63. package/lib/vertesia-ui-i18n.B756-YtS.chunk.js +5 -0
  64. package/lib/vertesia-ui-i18n.B756-YtS.chunk.js.map +1 -0
  65. package/lib/vertesia-ui-i18n.BICdgAEn.chunk.js +5 -0
  66. package/lib/vertesia-ui-i18n.BICdgAEn.chunk.js.map +1 -0
  67. package/lib/vertesia-ui-i18n.B_yx0c2A.chunk.js +5 -0
  68. package/lib/vertesia-ui-i18n.B_yx0c2A.chunk.js.map +1 -0
  69. package/lib/vertesia-ui-i18n.BwPrGiQn.chunk.js +5 -0
  70. package/lib/vertesia-ui-i18n.BwPrGiQn.chunk.js.map +1 -0
  71. package/lib/vertesia-ui-i18n.CfQbOHm3.chunk.js +5 -0
  72. package/lib/vertesia-ui-i18n.CfQbOHm3.chunk.js.map +1 -0
  73. package/lib/vertesia-ui-i18n.DMISlF16.chunk.js +5 -0
  74. package/lib/vertesia-ui-i18n.DMISlF16.chunk.js.map +1 -0
  75. package/lib/vertesia-ui-i18n.D_WA36Yq.chunk.js +5 -0
  76. package/lib/vertesia-ui-i18n.D_WA36Yq.chunk.js.map +1 -0
  77. package/lib/vertesia-ui-i18n.Dp3KuMs-.chunk.js +5 -0
  78. package/lib/vertesia-ui-i18n.Dp3KuMs-.chunk.js.map +1 -0
  79. package/lib/vertesia-ui-i18n.DsioKoQ3.chunk.js +5 -0
  80. package/lib/vertesia-ui-i18n.DsioKoQ3.chunk.js.map +1 -0
  81. package/lib/vertesia-ui-i18n.QEIqEPlv.chunk.js +5 -0
  82. package/lib/vertesia-ui-i18n.QEIqEPlv.chunk.js.map +1 -0
  83. package/lib/vertesia-ui-i18n.js +2 -2
  84. package/lib/vertesia-ui-i18n.js.map +1 -1
  85. package/lib/vertesia-ui-layout.js +15 -15
  86. package/lib/vertesia-ui-layout.js.map +1 -1
  87. package/lib/vertesia-ui-rich-text.js +44 -44
  88. package/lib/vertesia-ui-rich-text.js.map +1 -1
  89. package/lib/vertesia-ui-session.js +1 -1
  90. package/lib/vertesia-ui-session.js.map +1 -1
  91. package/lib/vertesia-ui-shell.js +51 -51
  92. package/lib/vertesia-ui-shell.js.map +1 -1
  93. package/lib/vertesia-ui-widgets.js +51 -51
  94. package/lib/vertesia-ui-widgets.js.map +1 -1
  95. package/package.json +20 -20
  96. package/src/boot/index.ts +74 -11
  97. package/src/boot/text.ts +80 -0
  98. package/src/features/agent/chat/ModernAgentConversation.tsx +7 -4
  99. package/src/features/agent/chat/ModernAgentOutput/SummaryConversation.ts +60 -0
  100. package/src/features/agent/chat/SkillWidgetProvider.tsx +3 -2
  101. package/src/i18n/LanguageProvider.tsx +16 -4
  102. package/src/i18n/locales/ar.json +5 -1
  103. package/src/i18n/locales/de.json +5 -1
  104. package/src/i18n/locales/en.json +5 -1
  105. package/src/i18n/locales/es.json +5 -1
  106. package/src/i18n/locales/fr.json +5 -1
  107. package/src/i18n/locales/it.json +5 -1
  108. package/src/i18n/locales/ja.json +5 -1
  109. package/src/i18n/locales/ko.json +5 -1
  110. package/src/i18n/locales/pt.json +5 -1
  111. package/src/i18n/locales/ru.json +5 -1
  112. package/src/i18n/locales/tr.json +5 -1
  113. package/src/i18n/locales/zh-TW.json +5 -1
  114. package/src/i18n/locales/zh.json +5 -1
  115. package/src/i18n/rtl.ts +5 -2
  116. package/src/session/UserSessionProvider.tsx +11 -4
  117. package/src/session/auth/oauth.ts +26 -12
  118. package/src/shell/VertesiaShell.tsx +8 -2
  119. package/src/shell/login/InviteAcceptModal.tsx +112 -65
  120. package/lib/vertesia-ui-i18n.1NWiB6fw.chunk.js +0 -5
  121. package/lib/vertesia-ui-i18n.1NWiB6fw.chunk.js.map +0 -1
  122. package/lib/vertesia-ui-i18n.6Ozlf6Ks.chunk.js +0 -5
  123. package/lib/vertesia-ui-i18n.6Ozlf6Ks.chunk.js.map +0 -1
  124. package/lib/vertesia-ui-i18n.AttJpg8U.chunk.js +0 -5
  125. package/lib/vertesia-ui-i18n.AttJpg8U.chunk.js.map +0 -1
  126. package/lib/vertesia-ui-i18n.BI_xdp49.chunk.js +0 -5
  127. package/lib/vertesia-ui-i18n.BI_xdp49.chunk.js.map +0 -1
  128. package/lib/vertesia-ui-i18n.C-2EEX6w.chunk.js +0 -5
  129. package/lib/vertesia-ui-i18n.C-2EEX6w.chunk.js.map +0 -1
  130. package/lib/vertesia-ui-i18n.CAmo54FO.chunk.js +0 -5
  131. package/lib/vertesia-ui-i18n.CAmo54FO.chunk.js.map +0 -1
  132. package/lib/vertesia-ui-i18n.CbBzmHNP.chunk.js +0 -5
  133. package/lib/vertesia-ui-i18n.CbBzmHNP.chunk.js.map +0 -1
  134. package/lib/vertesia-ui-i18n.DZgTzkIg.chunk.js +0 -5
  135. package/lib/vertesia-ui-i18n.DZgTzkIg.chunk.js.map +0 -1
  136. package/lib/vertesia-ui-i18n.DZsITxq5.chunk.js +0 -5
  137. package/lib/vertesia-ui-i18n.DZsITxq5.chunk.js.map +0 -1
  138. package/lib/vertesia-ui-i18n.Dbf2GHth.chunk.js +0 -5
  139. package/lib/vertesia-ui-i18n.Dbf2GHth.chunk.js.map +0 -1
  140. package/lib/vertesia-ui-i18n.lleWnH5n.chunk.js +0 -5
  141. package/lib/vertesia-ui-i18n.lleWnH5n.chunk.js.map +0 -1
  142. package/lib/vertesia-ui-i18n.wG_XSkbr.chunk.js +0 -5
  143. package/lib/vertesia-ui-i18n.wG_XSkbr.chunk.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vertesia/ui",
3
- "version": "1.6.0-dev.20261009.095813Z",
3
+ "version": "1.6.0-dev.20261014.015055Z",
4
4
  "description": "Vertesia UI components and and hooks",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -47,11 +47,11 @@
47
47
  "@radix-ui/react-switch": "^1.3.7",
48
48
  "@radix-ui/react-tabs": "^1.1.21",
49
49
  "@radix-ui/react-tooltip": "^1.2.16",
50
- "@vertesia/client": "1.6.0-dev.20261009.095813Z",
51
- "@vertesia/common": "1.6.0-dev.20261009.095813Z",
52
- "@vertesia/fusion-ux": "1.6.0-dev.20261009.095813Z",
53
- "@vertesia/json": "1.6.0-dev.20261009.095813Z",
54
- "@vertesia/rich-text": "1.6.0-dev.20261009.095813Z",
50
+ "@vertesia/client": "1.6.0-dev.20261014.015055Z",
51
+ "@vertesia/common": "1.6.0-dev.20261014.015055Z",
52
+ "@vertesia/fusion-ux": "1.6.0-dev.20261014.015055Z",
53
+ "@vertesia/json": "1.6.0-dev.20261014.015055Z",
54
+ "@vertesia/rich-text": "1.6.0-dev.20261014.015055Z",
55
55
  "ajv": "^8.20.0",
56
56
  "ajv-formats": "^3.0.1",
57
57
  "aria-hidden": "^1.2.6",
@@ -63,15 +63,15 @@
63
63
  "dompurify": "^3.4.16",
64
64
  "fast-xml-parser": "^5.11.2",
65
65
  "firebase": "^12.19.0",
66
- "framer-motion": "^13.4.4",
66
+ "framer-motion": "^13.5.1",
67
67
  "i18next": "^26.4.2",
68
68
  "jwt-decode": "^4.0.0",
69
- "katex": "^0.18.9",
69
+ "katex": "^0.19.0",
70
70
  "lodash-es": "^4.18.1",
71
- "lucide-react": "^1.48.0",
72
- "mermaid": "^12.0.0",
71
+ "lucide-react": "^1.52.0",
72
+ "mermaid": "^12.1.0",
73
73
  "monaco-editor": "^0.57.0",
74
- "motion": "^13.4.4",
74
+ "motion": "^13.5.1",
75
75
  "papaparse": "^5.7.0",
76
76
  "react": "19.3.0",
77
77
  "react-calendar": "^6.0.1",
@@ -83,11 +83,11 @@
83
83
  "react-pdf": "^10.5.0",
84
84
  "react-remove-scroll": "^2.7.2",
85
85
  "react-remove-scroll-bar": "^2.3.8",
86
- "react-resizable-panels": "^4.14.1",
86
+ "react-resizable-panels": "^4.14.2",
87
87
  "react-style-singleton": "^2.2.3",
88
88
  "react-vega": "^8.0.0",
89
89
  "rehype-katex": "^7.0.1",
90
- "remark-definition-list": "^2.0.0",
90
+ "remark-definition-list": "^2.0.1",
91
91
  "remark-directive": "^4.0.0",
92
92
  "remark-gfm": "^4.0.1",
93
93
  "remark-github-blockquote-alert": "^2.1.0",
@@ -105,19 +105,19 @@
105
105
  "@testing-library/user-event": "^14.6.7",
106
106
  "@types/hast": "^3.0.5",
107
107
  "@types/lodash-es": "^4.17.12",
108
- "@types/node": "^24.19.0",
108
+ "@types/node": "^24.19.1",
109
109
  "@types/papaparse": "^5.5.2",
110
110
  "@types/react": "19.3.0",
111
111
  "@types/react-dom": "19.3.0",
112
112
  "@vertesia/tsconfig": "0.1.0",
113
- "axe-core": "^4.13.0",
113
+ "axe-core": "^4.14.0",
114
114
  "culori": "^4.0.2",
115
- "i18next-cli": "^1.74.2",
116
- "jsdom": "^30.1.1",
117
- "rolldown": "1.2.11",
115
+ "i18next-cli": "^1.74.8",
116
+ "jsdom": "^30.1.2",
117
+ "rolldown": "1.2.12",
118
118
  "tailwindcss": "^4.3.3",
119
119
  "typescript": "7.0.2",
120
- "vitest": "5.0.2",
120
+ "vitest": "5.0.3",
121
121
  "vitest-axe": "^0.1.0",
122
122
  "wcag-contrast": "^3.0.0"
123
123
  },
@@ -193,7 +193,7 @@
193
193
  "url": "https://github.com/vertesia/composableai.git",
194
194
  "directory": "packages/ui"
195
195
  },
196
- "gitHead": "e45bc09ecf2de4c8fe8a8d39fb52c1521980e7b9",
196
+ "gitHead": "468240ecf939f968e1324116be26386cdf5ada49",
197
197
  "scripts": {
198
198
  "clean:lib": "rimraf ./lib ./tsconfig.tsbuildinfo",
199
199
  "clean": "rimraf ./node_modules ./lib ./tsconfig.tsbuildinfo",
package/src/boot/index.ts CHANGED
@@ -1,5 +1,7 @@
1
+ import { isRTL, LANGUAGE_STORAGE_KEY, SUPPORTED_LANGUAGES, type SupportedLanguage } from '../i18n/rtl.js';
1
2
  import { vertesiaBranding } from './branding.js';
2
3
  import { LOADING_ICON_SIZE, LOADING_INDICATOR_STYLES } from './loading.js';
4
+ import { BOOT_TEXT, type BootText } from './text.js';
3
5
 
4
6
  export interface BootScreenOptions {
5
7
  storageKey?: string;
@@ -8,6 +10,12 @@ export interface BootScreenOptions {
8
10
  slowLoadingLabel?: string;
9
11
  reloadLabel?: string;
10
12
  loadingLabel?: string;
13
+ /**
14
+ * Language of the boot copy. `'auto'` (the default) resolves it the way `LanguageProvider` does:
15
+ * the saved choice, then the browser language, then English. Pin a language when the app's
16
+ * React tree pins one, so the boot screen and the app agree.
17
+ */
18
+ language?: SupportedLanguage | 'auto';
11
19
  autoStart?: boolean;
12
20
  /** Trusted, app-authored HTML inside the loading container. Never pass user content. */
13
21
  html?: string;
@@ -22,7 +30,6 @@ export interface BootScreenHtmlPlugin {
22
30
 
23
31
  const DEFAULT_STORAGE_KEY = 'vite-ui-theme';
24
32
  const DEFAULT_ICON_SRC = vertesiaBranding.loadingIcon.light;
25
- const DEFAULT_LOADING_LABEL = 'Loading';
26
33
 
27
34
  export const BOOT_SCREEN_STYLES = `
28
35
  ${LOADING_INDICATOR_STYLES}
@@ -103,44 +110,95 @@ ${LOADING_INDICATOR_STYLES}
103
110
  }
104
111
  `;
105
112
 
106
- function inlineJson(value: string): string {
113
+ function inlineJson(value: unknown): string {
107
114
  return JSON.stringify(value).replaceAll('<', '\\u003c');
108
115
  }
109
116
 
110
117
  function normalizedOptions(options: BootScreenOptions) {
111
118
  return {
119
+ ...options,
112
120
  storageKey: options.storageKey ?? DEFAULT_STORAGE_KEY,
113
121
  iconSrc: options.iconSrc ?? DEFAULT_ICON_SRC,
114
- loadingLabel: options.loadingLabel ?? DEFAULT_LOADING_LABEL,
122
+ language: options.language ?? 'auto',
115
123
  autoStart: options.autoStart ?? true,
116
- html: options.html,
117
- styles: options.styles,
118
124
  };
119
125
  }
120
126
 
121
- /** Default markup shared by Studio and configurable applications. */
127
+ /** An app-supplied label wins in every language. */
128
+ function bootText(options: BootScreenOptions, language: SupportedLanguage): BootText {
129
+ return {
130
+ loading: options.loadingLabel ?? BOOT_TEXT[language].loading,
131
+ slowLoading: options.slowLoadingLabel ?? BOOT_TEXT[language].slowLoading,
132
+ reload: options.reloadLabel ?? BOOT_TEXT[language].reload,
133
+ };
134
+ }
135
+
136
+ /** The copy of every language the runtime may pick: only the pinned one, or all of them. */
137
+ function bootTexts(options: BootScreenOptions): Partial<Record<SupportedLanguage, BootText>> {
138
+ const languages = options.language && options.language !== 'auto' ? [options.language] : SUPPORTED_LANGUAGES;
139
+ return Object.fromEntries(languages.map((language) => [language, bootText(options, language)]));
140
+ }
141
+
142
+ /**
143
+ * Default markup shared by Studio and configurable applications. It carries the copy of the pinned
144
+ * language (English under `'auto'`); `data-boot-text` and `data-boot-alt` let the runtime swap in
145
+ * the language it resolves at page load. Custom `html` may use the same attributes.
146
+ */
122
147
  export function renderDefaultBootContent(options: BootScreenOptions = {}): string {
123
148
  const escapeMarkup = (value: string) =>
124
149
  value.replace(
125
150
  /[&<>"']/g,
126
151
  (char) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[char] ?? char,
127
152
  );
128
- const { iconSrc, loadingLabel } = normalizedOptions(options);
153
+ const { iconSrc, language } = normalizedOptions(options);
154
+ const text = bootText(options, language === 'auto' ? 'en' : language);
129
155
  const icon = (src: string, className = '') =>
130
- `<img class="vboot-spinner vertesia-loading-icon vertesia-loading-motion ${className}" width="${LOADING_ICON_SIZE}" height="${LOADING_ICON_SIZE}" src="${escapeMarkup(src)}" alt="${escapeMarkup(loadingLabel)}" />`;
156
+ `<img class="vboot-spinner vertesia-loading-icon vertesia-loading-motion ${className}" width="${LOADING_ICON_SIZE}" height="${LOADING_ICON_SIZE}" src="${escapeMarkup(src)}" alt="${escapeMarkup(text.loading)}" data-boot-alt="loading" />`;
131
157
  return `<div class="vboot-overlay" role="status" aria-live="polite">${options.darkIconSrc ? icon(iconSrc, 'vboot-icon-light') + icon(options.darkIconSrc, 'vboot-icon-dark') : icon(iconSrc)}
132
158
  <div id="loading-slow-notice" class="vboot-slow" style="display: none;">
133
- <p>${escapeMarkup(options.slowLoadingLabel ?? 'Still loading — this is taking longer than usual.')}</p>
134
- <div id="loading-slow-reload" style="display: none;"><button type="button" class="vboot-btn" data-boot-reload>${escapeMarkup(options.reloadLabel ?? 'Reload page')}</button></div></div></div>`;
159
+ <p data-boot-text="slowLoading">${escapeMarkup(text.slowLoading)}</p>
160
+ <div id="loading-slow-reload" style="display: none;"><button type="button" class="vboot-btn" data-boot-reload data-boot-text="reload">${escapeMarkup(text.reload)}</button></div></div></div>`;
135
161
  }
136
162
 
137
163
  export function renderBootScreenRuntime(options: BootScreenOptions = {}): string {
138
- const { storageKey, html } = normalizedOptions(options);
164
+ const { storageKey, html, language } = normalizedOptions(options);
139
165
  const defaultHtml = renderDefaultBootContent(options);
140
166
  return `(() => {
141
167
  const storageKey = ${inlineJson(storageKey)};
142
168
 
143
169
  const customHtml = ${html === undefined ? 'undefined' : inlineJson(html)};
170
+ const bootText = ${inlineJson(bootTexts(options))};
171
+ // Mirrors resolveLanguage and LanguageProvider's initial read (i18n/rtl.ts, i18n/LanguageProvider.tsx).
172
+ const hasText = (code) => Object.prototype.hasOwnProperty.call(bootText, code);
173
+ const resolveLanguage = (raw) => {
174
+ if (!raw) return 'en';
175
+ if (hasText(raw)) return raw;
176
+ const base = String(raw).split('-')[0];
177
+ return hasText(base) ? base : 'en';
178
+ };
179
+ const language = (() => {
180
+ const pinned = ${inlineJson(language)};
181
+ if (pinned !== 'auto') return pinned;
182
+ try {
183
+ const stored = window.localStorage.getItem(${inlineJson(LANGUAGE_STORAGE_KEY)});
184
+ if (stored) return resolveLanguage(stored);
185
+ } catch (_error) {
186
+ // Storage can be unavailable in private or restricted browsing contexts.
187
+ }
188
+ return resolveLanguage(window.navigator && window.navigator.language);
189
+ })();
190
+ const direction = ${inlineJson(SUPPORTED_LANGUAGES.filter((code) => isRTL(code)))}.includes(language) ? 'rtl' : 'ltr';
191
+ const localize = (container) => {
192
+ const text = bootText[language];
193
+ container.querySelectorAll('[data-boot-text]').forEach((node) => {
194
+ const value = text[node.getAttribute('data-boot-text')];
195
+ if (value) node.textContent = value;
196
+ });
197
+ container.querySelectorAll('[data-boot-alt]').forEach((node) => {
198
+ const value = text[node.getAttribute('data-boot-alt')];
199
+ if (value) node.setAttribute('alt', value);
200
+ });
201
+ };
144
202
  let slowNoticeTimer;
145
203
  let slowReloadTimer;
146
204
  const previewTheme = (() => {
@@ -196,7 +254,10 @@ export function renderBootScreenRuntime(options: BootScreenOptions = {}): string
196
254
  const loading = document.createElement('div');
197
255
  loading.id = 'loading-indicator';
198
256
  loading.className = bootThemeClass();
257
+ loading.lang = language;
258
+ loading.dir = direction;
199
259
  loading.innerHTML = customHtml ?? ${inlineJson(defaultHtml)};
260
+ localize(loading);
200
261
  loading.querySelectorAll('[data-boot-title]').forEach((heading) => {
201
262
  if (document.title) heading.textContent = document.title;
202
263
  });
@@ -251,6 +312,8 @@ export function renderBootScreenRuntime(options: BootScreenOptions = {}): string
251
312
  }, 30000);
252
313
  };
253
314
  window.__vertesiaBoot = {
315
+ direction,
316
+ language,
254
317
  scheduleSlowLoadingNotice,
255
318
  applyTheme,
256
319
  bootThemeClass,
@@ -0,0 +1,80 @@
1
+ import type { SupportedLanguage } from '../i18n/rtl.js';
2
+
3
+ /**
4
+ * Copy for the pre-React boot overlay. It renders before any bundle loads, so it cannot use the
5
+ * `vertesia.ui` locale files: each string is inlined into the page by the boot runtime.
6
+ */
7
+ export interface BootText {
8
+ /** Accessible name of the loading spinner. */
9
+ loading: string;
10
+ slowLoading: string;
11
+ reload: string;
12
+ }
13
+
14
+ export const BOOT_TEXT: Record<SupportedLanguage, BootText> = {
15
+ ar: {
16
+ loading: 'جارٍ التحميل',
17
+ slowLoading: 'لا يزال التحميل جاريًا — يستغرق الأمر وقتًا أطول من المعتاد.',
18
+ reload: 'إعادة تحميل الصفحة',
19
+ },
20
+ de: {
21
+ loading: 'Wird geladen',
22
+ slowLoading: 'Wird noch geladen – das dauert länger als üblich.',
23
+ reload: 'Seite neu laden',
24
+ },
25
+ en: {
26
+ loading: 'Loading',
27
+ slowLoading: 'Still loading — this is taking longer than usual.',
28
+ reload: 'Reload page',
29
+ },
30
+ es: {
31
+ loading: 'Cargando',
32
+ slowLoading: 'Todavía cargando: está tardando más de lo habitual.',
33
+ reload: 'Recargar la página',
34
+ },
35
+ fr: {
36
+ loading: 'Chargement',
37
+ slowLoading: 'Chargement en cours — cela prend plus de temps que d’habitude.',
38
+ reload: 'Recharger la page',
39
+ },
40
+ it: {
41
+ loading: 'Caricamento',
42
+ slowLoading: 'Caricamento ancora in corso: ci vuole più tempo del solito.',
43
+ reload: 'Ricarica la pagina',
44
+ },
45
+ ja: {
46
+ loading: '読み込み中',
47
+ slowLoading: '読み込み中です。通常より時間がかかっています。',
48
+ reload: 'ページを再読み込み',
49
+ },
50
+ ko: {
51
+ loading: '로드 중',
52
+ slowLoading: '아직 로드 중입니다. 평소보다 시간이 더 걸리고 있습니다.',
53
+ reload: '페이지 새로고침',
54
+ },
55
+ pt: {
56
+ loading: 'Carregando',
57
+ slowLoading: 'Ainda carregando — está demorando mais do que o normal.',
58
+ reload: 'Recarregar a página',
59
+ },
60
+ ru: {
61
+ loading: 'Загрузка',
62
+ slowLoading: 'Загрузка продолжается — это занимает больше времени, чем обычно.',
63
+ reload: 'Перезагрузить страницу',
64
+ },
65
+ tr: {
66
+ loading: 'Yükleniyor',
67
+ slowLoading: 'Hâlâ yükleniyor — bu, normalden uzun sürüyor.',
68
+ reload: 'Sayfayı yeniden yükle',
69
+ },
70
+ zh: {
71
+ loading: '正在加载',
72
+ slowLoading: '仍在加载,所需时间比平时更长。',
73
+ reload: '重新加载页面',
74
+ },
75
+ 'zh-TW': {
76
+ loading: '載入中',
77
+ slowLoading: '仍在載入,所需時間比平常更長。',
78
+ reload: '重新載入頁面',
79
+ },
80
+ };
@@ -1397,10 +1397,13 @@ function StartWorkflowView({
1397
1397
  rows={2}
1398
1398
  className={cn(
1399
1399
  'min-h-[72px] resize-none border-0 bg-transparent px-0 py-0 text-sm leading-6 shadow-none focus-visible:ring-0',
1400
- // The expanded composer is a fixed-height flex box (adjustTextareaHeight
1401
- // pins it to 100%), so it has to scroll its own overflow. The auto-growing
1402
- // variant hides overflow because it resizes to fit instead.
1403
- isExpandedStartComposer ? 'min-h-0 flex-1 overflow-y-auto' : 'overflow-hidden',
1400
+ // Both variants scroll their own overflow: the expanded composer is a
1401
+ // fixed-height flex box (adjustTextareaHeight pins it to 100%), and the
1402
+ // auto-growing one stops growing at 200px. Hiding overflow there left long
1403
+ // text unscrollable, so the wheel scrolled the surrounding page instead.
1404
+ // overscroll-contain keeps that from happening at the ends of the text too.
1405
+ 'overflow-y-auto overscroll-contain',
1406
+ isExpandedStartComposer && 'min-h-0 flex-1',
1404
1407
  inputClassName,
1405
1408
  )}
1406
1409
  style={
@@ -281,6 +281,54 @@ function isSummaryWorkMessage(message: AgentMessage): boolean {
281
281
  return message.type === AgentMessageType.THOUGHT && !message.details?.streamed;
282
282
  }
283
283
 
284
+ function isEmptyAnswerMessage(message: AgentMessage): boolean {
285
+ return message.type === AgentMessageType.ANSWER && !getMessageText(message);
286
+ }
287
+
288
+ function isToolPreambleProse(message: AgentMessage): boolean {
289
+ const text = getMessageText(message);
290
+ return message.details?.display_role === 'tool_preamble' && Boolean(text) && !isLowSignalSummaryText(text);
291
+ }
292
+
293
+ // Tools a model tends to call last, with its answer written as that call's preamble.
294
+ const ANSWER_CARRYING_TOOLS = new Set(['update_plan']);
295
+
296
+ function getFinalToolPreambleProseIndex(messages: AgentMessage[]): number {
297
+ // Check the latest preamble before filtering its text, so an empty or low-signal
298
+ // final preamble cannot make us fall back to prose from an earlier call.
299
+ const index = messages.findLastIndex(isToolPreambleMessage);
300
+ if (index < 0 || !isToolPreambleProse(messages[index])) return -1;
301
+
302
+ const details = messages[index].details;
303
+ const tools = details?.tools?.length ? details.tools : details?.tool ? [details.tool] : [];
304
+ if (tools.length === 0 || !tools.every((tool) => ANSWER_CARRYING_TOOLS.has(tool))) return -1;
305
+
306
+ let iteration = details?.tool_iteration;
307
+ let hasMatchingTool = false;
308
+ const toolRuns = new Map<string, Set<string>>();
309
+ for (const message of messages.slice(index + 1)) {
310
+ const tool = message.details?.tool;
311
+ if (!tool) continue;
312
+ // A later call without a preamble must not reuse an earlier call's prose.
313
+ if (!tools.includes(tool)) return -1;
314
+ const runId = message.details?.tool_run_id ?? message.details?.tool_use_id;
315
+ if (runId) {
316
+ const runs = toolRuns.get(tool) ?? new Set<string>();
317
+ runs.add(runId);
318
+ toolRuns.set(tool, runs);
319
+ if (runs.size > tools.filter((name) => name === tool).length) return -1;
320
+ }
321
+ const toolIteration = message.details?.tool_iteration;
322
+ if (toolIteration !== undefined) {
323
+ if (iteration !== undefined && iteration !== toolIteration) return -1;
324
+ iteration = toolIteration;
325
+ }
326
+ hasMatchingTool = true;
327
+ }
328
+
329
+ return hasMatchingTool ? index : -1;
330
+ }
331
+
284
332
  export function buildSummaryConversationItems(
285
333
  messages: AgentMessage[],
286
334
  isCompleted: boolean,
@@ -332,6 +380,18 @@ export function buildSummaryConversationItems(
332
380
  }
333
381
  }
334
382
 
383
+ if (isEmptyAnswerMessage(message)) {
384
+ // A model that writes its answer alongside a last tool call leaves that prose as a
385
+ // tool preamble and ends the turn on an empty answer; show the prose as the answer.
386
+ const proseIndex = getFinalToolPreambleProseIndex(pendingWork);
387
+ if (proseIndex >= 0) {
388
+ const [prose] = pendingWork.splice(proseIndex, 1);
389
+ flushWork(false, message);
390
+ items.push({ type: 'message', message: { ...message, message: prose.message } });
391
+ }
392
+ continue;
393
+ }
394
+
335
395
  if (message.type === AgentMessageType.COMPLETE || message.type === AgentMessageType.IDLE) {
336
396
  if (isUserStoppedMessage(message)) {
337
397
  const pendingWorkStartTimestamp = pendingWork[0]?.timestamp;
@@ -142,8 +142,9 @@ async function fetchSkillWidgets(
142
142
  }
143
143
  return widgets;
144
144
  })
145
- .catch(() => {
146
- console.error('Failed to fetch skill widgets from ', url);
145
+ .catch((err: unknown) => {
146
+ // The chat still renders; only that app's custom widgets are missing.
147
+ console.warn('Failed to fetch skill widgets from', url, err);
147
148
  return null;
148
149
  });
149
150
  }),
@@ -1,21 +1,30 @@
1
1
  import { DirectionProvider } from '@radix-ui/react-direction';
2
2
  import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
3
- import { isRTL, resolveLanguage, type SupportedLanguage } from './rtl.js';
3
+ import { isRTL, LANGUAGE_STORAGE_KEY, resolveLanguage, type SupportedLanguage } from './rtl.js';
4
4
 
5
5
  type LanguageProviderProps = {
6
6
  children: React.ReactNode;
7
7
  defaultLanguage?: SupportedLanguage;
8
+ /**
9
+ * Pin the language this app renders in, whatever the saved choice or browser language. The
10
+ * user's own language is still tracked, and exposed as `preferredLanguage`.
11
+ */
12
+ language?: SupportedLanguage;
8
13
  storageKey?: string;
9
14
  };
10
15
 
11
16
  type LanguageProviderState = {
17
+ /** The language the app renders in. */
12
18
  language: SupportedLanguage;
19
+ /** The user's own language (saved choice, then browser); equals `language` unless pinned. */
20
+ preferredLanguage: SupportedLanguage;
13
21
  setLanguage: (language: SupportedLanguage, options?: { persist?: boolean }) => void;
14
22
  isRTL: boolean;
15
23
  };
16
24
 
17
25
  const initialState: LanguageProviderState = {
18
26
  language: 'en',
27
+ preferredLanguage: 'en',
19
28
  setLanguage: () => null,
20
29
  isRTL: false,
21
30
  };
@@ -36,12 +45,14 @@ function readInitialLanguage(storageKey: string, defaultLanguage: SupportedLangu
36
45
  export function LanguageProvider({
37
46
  children,
38
47
  defaultLanguage = 'en',
39
- storageKey = 'vertesia-ui-language',
48
+ language: pinnedLanguage,
49
+ storageKey = LANGUAGE_STORAGE_KEY,
40
50
  ...props
41
51
  }: LanguageProviderProps) {
42
- const [language, setLanguageState] = useState<SupportedLanguage>(() =>
52
+ const [preferredLanguage, setLanguageState] = useState<SupportedLanguage>(() =>
43
53
  readInitialLanguage(storageKey, defaultLanguage),
44
54
  );
55
+ const language = pinnedLanguage ?? preferredLanguage;
45
56
 
46
57
  const rtl = isRTL(language);
47
58
 
@@ -62,10 +73,11 @@ export function LanguageProvider({
62
73
  const value = useMemo<LanguageProviderState>(
63
74
  () => ({
64
75
  language,
76
+ preferredLanguage,
65
77
  isRTL: rtl,
66
78
  setLanguage,
67
79
  }),
68
- [language, rtl, setLanguage],
80
+ [language, preferredLanguage, rtl, setLanguage],
69
81
  );
70
82
 
71
83
  return (
@@ -263,7 +263,7 @@
263
263
  "agent.toggleRightSidebar": "تبديل الشريط الجانبي الأيمن",
264
264
  "agent.typeDifferentResponse": "أو اكتب ردًا مختلفًا...",
265
265
  "agent.typeYourMessage": "اكتب رسالتك...",
266
- "agent.typeYourResponse": "اكتب ردك...",
266
+ "agent.typeYourResponse": "أو اكتب ردًا مختلفًا...",
267
267
  "agent.unableToOpenPrint": "تعذر فتح معاينة الطباعة",
268
268
  "agent.unknownError": "خطأ غير معروف",
269
269
  "agent.unknownTime": "وقت غير معروف",
@@ -391,6 +391,10 @@
391
391
  "layout.searchPlaceholder": "بحث...",
392
392
  "layout.toggleNavigationMenu": "تبديل قائمة التنقل",
393
393
  "login.accept": "قبول",
394
+ "login.inviteActionFailed": "تعذّر تحديث هذه الدعوة. تم تحديث القائمة.",
395
+ "login.inviteRole": "الدور: {{role}}",
396
+ "login.invitedBy": "دعاك {{name}}",
397
+ "login.invitesIntro": "تمت دعوتك للانضمام إلى الحسابات التالية. اقبل كل دعوة أو ارفضها.",
394
398
  "login.reject": "رفض",
395
399
  "login.reviewInvites": "مراجعة الدعوات",
396
400
  "login.terminal.account": "الحساب",
@@ -251,7 +251,7 @@
251
251
  "agent.toggleRightSidebar": "Rechte Seitenleiste umschalten",
252
252
  "agent.typeDifferentResponse": "Oder geben Sie eine andere Antwort ein...",
253
253
  "agent.typeYourMessage": "Geben Sie Ihre Nachricht ein...",
254
- "agent.typeYourResponse": "Geben Sie Ihre Antwort ein...",
254
+ "agent.typeYourResponse": "Oder geben Sie eine andere Antwort ein...",
255
255
  "agent.unableToOpenPrint": "Druckvorschau konnte nicht geöffnet werden",
256
256
  "agent.unknownError": "Unbekannter Fehler",
257
257
  "agent.unknownTime": "Unbekannte Zeit",
@@ -379,6 +379,10 @@
379
379
  "layout.searchPlaceholder": "Suchen...",
380
380
  "layout.toggleNavigationMenu": "Navigationsmenü umschalten",
381
381
  "login.accept": "Akzeptieren",
382
+ "login.inviteActionFailed": "Diese Einladung konnte nicht aktualisiert werden. Die Liste wurde aktualisiert.",
383
+ "login.inviteRole": "Rolle: {{role}}",
384
+ "login.invitedBy": "Eingeladen von {{name}}",
385
+ "login.invitesIntro": "Sie wurden eingeladen, den folgenden Konten beizutreten. Nehmen Sie jede Einladung an oder lehnen Sie sie ab.",
382
386
  "login.reject": "Ablehnen",
383
387
  "login.reviewInvites": "Einladungen überprüfen",
384
388
  "login.terminal.account": "Konto",
@@ -324,7 +324,7 @@
324
324
  "agent.toggleRightSidebar": "Toggle right sidebar",
325
325
  "agent.typeDifferentResponse": "Or type a different response...",
326
326
  "agent.typeYourMessage": "Type your message...",
327
- "agent.typeYourResponse": "Type your response...",
327
+ "agent.typeYourResponse": "Or type a different response...",
328
328
  "agent.unableToOpenPrint": "Unable to open print preview",
329
329
  "agent.unknownError": "Unknown error",
330
330
  "agent.unknownTime": "Unknown time",
@@ -689,6 +689,10 @@
689
689
  "layout.searchPlaceholder": "Search...",
690
690
  "layout.toggleNavigationMenu": "Toggle navigation menu",
691
691
  "login.accept": "Accept",
692
+ "login.inviteActionFailed": "This invite could not be updated. The list has been refreshed.",
693
+ "login.inviteRole": "Role: {{role}}",
694
+ "login.invitedBy": "Invited by {{name}}",
695
+ "login.invitesIntro": "You have been invited to join the following accounts. Accept or reject each invite.",
692
696
  "login.reject": "Reject",
693
697
  "login.reviewInvites": "Review Invites",
694
698
  "login.terminal.account": "Account",
@@ -253,7 +253,7 @@
253
253
  "agent.toggleRightSidebar": "Alternar panel lateral",
254
254
  "agent.typeDifferentResponse": "O escribe una respuesta diferente...",
255
255
  "agent.typeYourMessage": "Escribe tu mensaje...",
256
- "agent.typeYourResponse": "Escribe tu respuesta...",
256
+ "agent.typeYourResponse": "O escribe una respuesta diferente...",
257
257
  "agent.unableToOpenPrint": "No se pudo abrir la vista previa de impresión",
258
258
  "agent.unknownError": "Error desconocido",
259
259
  "agent.unknownTime": "Hora desconocida",
@@ -381,6 +381,10 @@
381
381
  "layout.searchPlaceholder": "Buscar...",
382
382
  "layout.toggleNavigationMenu": "Alternar menú de navegación",
383
383
  "login.accept": "Aceptar",
384
+ "login.inviteActionFailed": "No se pudo actualizar esta invitación. La lista se ha actualizado.",
385
+ "login.inviteRole": "Rol: {{role}}",
386
+ "login.invitedBy": "Invitado por {{name}}",
387
+ "login.invitesIntro": "Ha sido invitado a unirse a las siguientes cuentas. Acepte o rechace cada invitación.",
384
388
  "login.reject": "Rechazar",
385
389
  "login.reviewInvites": "Revisar invitaciones",
386
390
  "login.terminal.account": "Cuenta",
@@ -253,7 +253,7 @@
253
253
  "agent.toggleRightSidebar": "Basculer le panneau latéral",
254
254
  "agent.typeDifferentResponse": "Ou saisissez une autre réponse...",
255
255
  "agent.typeYourMessage": "Saisissez votre message...",
256
- "agent.typeYourResponse": "Saisissez votre réponse...",
256
+ "agent.typeYourResponse": "Ou saisissez une autre réponse...",
257
257
  "agent.unableToOpenPrint": "Impossible d'ouvrir l'aperçu d'impression",
258
258
  "agent.unknownError": "Erreur inconnue",
259
259
  "agent.unknownTime": "Heure inconnue",
@@ -381,6 +381,10 @@
381
381
  "layout.searchPlaceholder": "Rechercher...",
382
382
  "layout.toggleNavigationMenu": "Afficher ou masquer le menu de navigation",
383
383
  "login.accept": "Accepter",
384
+ "login.inviteActionFailed": "Cette invitation n'a pas pu être mise à jour. La liste a été actualisée.",
385
+ "login.inviteRole": "Rôle : {{role}}",
386
+ "login.invitedBy": "Invité par {{name}}",
387
+ "login.invitesIntro": "Vous avez été invité à rejoindre les comptes suivants. Acceptez ou refusez chaque invitation.",
384
388
  "login.reject": "Refuser",
385
389
  "login.reviewInvites": "Examiner les invitations",
386
390
  "login.terminal.account": "Compte",
@@ -253,7 +253,7 @@
253
253
  "agent.toggleRightSidebar": "Alterna pannello laterale",
254
254
  "agent.typeDifferentResponse": "Oppure scrivi una risposta diversa...",
255
255
  "agent.typeYourMessage": "Scrivi il tuo messaggio...",
256
- "agent.typeYourResponse": "Scrivi la tua risposta...",
256
+ "agent.typeYourResponse": "Oppure scrivi una risposta diversa...",
257
257
  "agent.unableToOpenPrint": "Impossibile aprire l'anteprima di stampa",
258
258
  "agent.unknownError": "Errore sconosciuto",
259
259
  "agent.unknownTime": "Ora sconosciuta",
@@ -381,6 +381,10 @@
381
381
  "layout.searchPlaceholder": "Cerca...",
382
382
  "layout.toggleNavigationMenu": "Attiva o disattiva il menu di navigazione",
383
383
  "login.accept": "Accetta",
384
+ "login.inviteActionFailed": "Impossibile aggiornare questo invito. L'elenco è stato aggiornato.",
385
+ "login.inviteRole": "Ruolo: {{role}}",
386
+ "login.invitedBy": "Invitato da {{name}}",
387
+ "login.invitesIntro": "Sei stato invitato a unirti ai seguenti account. Accetta o rifiuta ogni invito.",
384
388
  "login.reject": "Rifiuta",
385
389
  "login.reviewInvites": "Rivedi inviti",
386
390
  "login.terminal.account": "Account",
@@ -251,7 +251,7 @@
251
251
  "agent.toggleRightSidebar": "右サイドバーの切り替え",
252
252
  "agent.typeDifferentResponse": "または別の回答を入力...",
253
253
  "agent.typeYourMessage": "メッセージを入力...",
254
- "agent.typeYourResponse": "回答を入力...",
254
+ "agent.typeYourResponse": "または別の回答を入力...",
255
255
  "agent.unableToOpenPrint": "印刷プレビューを開けません",
256
256
  "agent.unknownError": "不明なエラー",
257
257
  "agent.unknownTime": "不明な時刻",
@@ -379,6 +379,10 @@
379
379
  "layout.searchPlaceholder": "検索...",
380
380
  "layout.toggleNavigationMenu": "ナビゲーションメニューを切り替え",
381
381
  "login.accept": "承認",
382
+ "login.inviteActionFailed": "この招待を更新できませんでした。リストを更新しました。",
383
+ "login.inviteRole": "ロール: {{role}}",
384
+ "login.invitedBy": "招待者: {{name}}",
385
+ "login.invitesIntro": "次のアカウントへの参加に招待されています。各招待を承認または拒否してください。",
382
386
  "login.reject": "拒否",
383
387
  "login.reviewInvites": "招待を確認",
384
388
  "login.terminal.account": "アカウント",