@vertesia/ui 1.6.0-dev.20260912.110338Z → 1.6.0-dev.20260918.063824Z

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 (89) hide show
  1. package/lib/env/index.d.ts +10 -4
  2. package/lib/env/index.d.ts.map +1 -1
  3. package/lib/env/index.js +8 -0
  4. package/lib/env/index.js.map +1 -1
  5. package/lib/features/agent/PayloadBuilder.d.ts.map +1 -1
  6. package/lib/features/agent/PayloadBuilder.js +6 -1
  7. package/lib/features/agent/PayloadBuilder.js.map +1 -1
  8. package/lib/features/agent/chat/AgentRunFeedback.d.ts +7 -0
  9. package/lib/features/agent/chat/AgentRunFeedback.d.ts.map +1 -1
  10. package/lib/features/agent/chat/AgentRunFeedback.js +13 -1
  11. package/lib/features/agent/chat/AgentRunFeedback.js.map +1 -1
  12. package/lib/features/agent/chat/ModernAgentConversation.js +4 -3
  13. package/lib/features/agent/chat/ModernAgentConversation.js.map +1 -1
  14. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js +2 -2
  15. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js.map +1 -1
  16. package/lib/features/agent/chat/ModernAgentOutput/MessageItem.js +2 -2
  17. package/lib/features/agent/chat/ModernAgentOutput/MessageItem.js.map +1 -1
  18. package/lib/features/agent/chat/hooks/useAgentPlans.d.ts +1 -1
  19. package/lib/features/agent/chat/hooks/useAgentPlans.d.ts.map +1 -1
  20. package/lib/features/agent/chat/hooks/useAgentPlans.js +5 -5
  21. package/lib/features/agent/chat/hooks/useAgentPlans.js.map +1 -1
  22. package/lib/features/store/objects/search/DocumentSearchContext.d.ts.map +1 -1
  23. package/lib/features/store/objects/search/DocumentSearchContext.js +6 -1
  24. package/lib/features/store/objects/search/DocumentSearchContext.js.map +1 -1
  25. package/lib/features/user/UserInfo.d.ts +2 -0
  26. package/lib/features/user/UserInfo.d.ts.map +1 -1
  27. package/lib/session/UserSessionProvider.js +3 -3
  28. package/lib/session/UserSessionProvider.js.map +1 -1
  29. package/lib/session/auth/domainRouting.d.ts +3 -4
  30. package/lib/session/auth/domainRouting.d.ts.map +1 -1
  31. package/lib/session/auth/domainRouting.js +3 -4
  32. package/lib/session/auth/domainRouting.js.map +1 -1
  33. package/lib/session/auth/firebase.d.ts.map +1 -1
  34. package/lib/session/auth/firebase.js +10 -0
  35. package/lib/session/auth/firebase.js.map +1 -1
  36. package/lib/shell/login/SignInAuthPending.js +1 -1
  37. package/lib/shell/login/SignInAuthPending.js.map +1 -1
  38. package/lib/shell/login/SignInEmailStep.d.ts.map +1 -1
  39. package/lib/shell/login/SignInEmailStep.js +2 -2
  40. package/lib/shell/login/SignInEmailStep.js.map +1 -1
  41. package/lib/shell/login/SignInIcons.d.ts +1 -1
  42. package/lib/shell/login/SignInIcons.d.ts.map +1 -1
  43. package/lib/shell/login/signInUtils.d.ts +1 -1
  44. package/lib/shell/login/signInUtils.d.ts.map +1 -1
  45. package/lib/shell/login/signInUtils.js +5 -6
  46. package/lib/shell/login/signInUtils.js.map +1 -1
  47. package/lib/vertesia-ui-core.js +83 -41
  48. package/lib/vertesia-ui-core.js.map +1 -1
  49. package/lib/vertesia-ui-env.js +1 -1
  50. package/lib/vertesia-ui-env.js.map +1 -1
  51. package/lib/vertesia-ui-features.js +336 -317
  52. package/lib/vertesia-ui-features.js.map +1 -1
  53. package/lib/vertesia-ui-layout.js +43 -19
  54. package/lib/vertesia-ui-layout.js.map +1 -1
  55. package/lib/vertesia-ui-rich-text.js +123 -74
  56. package/lib/vertesia-ui-rich-text.js.map +1 -1
  57. package/lib/vertesia-ui-router.js.map +1 -1
  58. package/lib/vertesia-ui-session.js +1 -1
  59. package/lib/vertesia-ui-session.js.map +1 -1
  60. package/lib/vertesia-ui-shell.js +57 -28
  61. package/lib/vertesia-ui-shell.js.map +1 -1
  62. package/lib/vertesia-ui-widgets.js +153 -97
  63. package/lib/vertesia-ui-widgets.js.map +1 -1
  64. package/lib/widgets/xml-viewer/hooks/useCollapsible.d.ts +1 -1
  65. package/package.json +20 -20
  66. package/src/env/index.test.ts +48 -0
  67. package/src/env/index.ts +15 -2
  68. package/src/features/agent/PayloadBuilder.tsx +6 -1
  69. package/src/features/agent/chat/AgentRunFeedback.test.tsx +18 -1
  70. package/src/features/agent/chat/AgentRunFeedback.tsx +13 -0
  71. package/src/features/agent/chat/ModernAgentConversation.test.tsx +1 -1
  72. package/src/features/agent/chat/ModernAgentConversation.tsx +6 -5
  73. package/src/features/agent/chat/ModernAgentOutput/AllMessagesMixed.tsx +2 -2
  74. package/src/features/agent/chat/ModernAgentOutput/MessageItem.tsx +2 -2
  75. package/src/features/agent/chat/hooks/useAgentPlans.test.ts +52 -0
  76. package/src/features/agent/chat/hooks/useAgentPlans.ts +5 -5
  77. package/src/features/store/objects/search/DocumentSearchContext.ts +8 -3
  78. package/src/router/path.test.ts +5 -5
  79. package/src/session/UserSessionProvider.tsx +3 -3
  80. package/src/session/auth/auth-flow.md +21 -9
  81. package/src/session/auth/domainRouting.test.ts +1 -1
  82. package/src/session/auth/domainRouting.ts +3 -4
  83. package/src/session/auth/firebase.test.ts +25 -0
  84. package/src/session/auth/firebase.ts +10 -0
  85. package/src/shell/login/SignInAuthPending.tsx +3 -3
  86. package/src/shell/login/SignInEmailStep.test.tsx +32 -0
  87. package/src/shell/login/SignInEmailStep.tsx +13 -9
  88. package/src/shell/login/signInUtils.ts +12 -4
  89. package/src/widgets/markdown/CollaborativeMarkdownRenderer.test.tsx +11 -3
@@ -1,8 +1,8 @@
1
1
  export declare function useCollapsible(level: number): {
2
2
  collapsed: boolean;
3
3
  buttonProps: {
4
- role?: undefined;
5
4
  onClick?: undefined;
5
+ role?: undefined;
6
6
  style?: undefined;
7
7
  } | {
8
8
  onClick: () => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vertesia/ui",
3
- "version": "1.6.0-dev.20260912.110338Z",
3
+ "version": "1.6.0-dev.20260918.063824Z",
4
4
  "description": "Vertesia UI components and and hooks",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -45,11 +45,11 @@
45
45
  "@radix-ui/react-switch": "^1.3.7",
46
46
  "@radix-ui/react-tabs": "^1.1.21",
47
47
  "@radix-ui/react-tooltip": "^1.2.16",
48
- "@vertesia/client": "1.6.0-dev.20260912.110338Z",
49
- "@vertesia/common": "1.6.0-dev.20260912.110338Z",
50
- "@vertesia/fusion-ux": "1.6.0-dev.20260912.110338Z",
51
- "@vertesia/json": "1.6.0-dev.20260912.110338Z",
52
- "@vertesia/rich-text": "1.6.0-dev.20260912.110338Z",
48
+ "@vertesia/client": "1.6.0-dev.20260918.063824Z",
49
+ "@vertesia/common": "1.6.0-dev.20260918.063824Z",
50
+ "@vertesia/fusion-ux": "1.6.0-dev.20260918.063824Z",
51
+ "@vertesia/json": "1.6.0-dev.20260918.063824Z",
52
+ "@vertesia/rich-text": "1.6.0-dev.20260918.063824Z",
53
53
  "ajv": "^8.20.0",
54
54
  "ajv-formats": "^3.0.1",
55
55
  "aria-hidden": "^1.2.6",
@@ -58,30 +58,30 @@
58
58
  "cmdk": "^1.1.1",
59
59
  "dayjs": "^1.11.23",
60
60
  "debounce": "^3.0.0",
61
- "dompurify": "^3.4.14",
61
+ "dompurify": "^3.4.15",
62
62
  "fast-xml-parser": "^5.11.1",
63
63
  "firebase": "^12.18.0",
64
- "framer-motion": "^13.1.1",
65
- "i18next": "^26.4.1",
64
+ "framer-motion": "^13.2.0",
65
+ "i18next": "^26.4.2",
66
66
  "jwt-decode": "^4.0.0",
67
- "katex": "^0.18.5",
67
+ "katex": "^0.18.7",
68
68
  "lodash-es": "^4.18.1",
69
- "lucide-react": "^1.38.0",
69
+ "lucide-react": "^1.42.0",
70
70
  "mermaid": "^11.17.2",
71
71
  "monaco-editor": "^0.56.0",
72
- "motion": "^13.1.1",
72
+ "motion": "^13.2.0",
73
73
  "papaparse": "^5.7.0",
74
74
  "react": "19.2.8",
75
75
  "react-calendar": "^6.0.1",
76
76
  "react-date-picker": "^12.1.0",
77
77
  "react-dom": "19.2.8",
78
- "react-error-boundary": "^6.1.4",
79
- "react-i18next": "^17.0.12",
78
+ "react-error-boundary": "^6.1.5",
79
+ "react-i18next": "^17.0.13",
80
80
  "react-markdown": "^10.1.0",
81
81
  "react-pdf": "^10.5.0",
82
82
  "react-remove-scroll": "^2.7.2",
83
83
  "react-remove-scroll-bar": "^2.3.8",
84
- "react-resizable-panels": "^4.12.3",
84
+ "react-resizable-panels": "^4.12.4",
85
85
  "react-style-singleton": "^2.2.3",
86
86
  "react-vega": "^8.0.0",
87
87
  "rehype-katex": "^7.0.1",
@@ -106,16 +106,16 @@
106
106
  "@types/node": "^24.13.3",
107
107
  "@types/papaparse": "^5.5.2",
108
108
  "@types/react": "19.2.18",
109
- "@types/react-dom": "19.2.5",
109
+ "@types/react-dom": "19.2.7",
110
110
  "@vertesia/tsconfig": "0.1.0",
111
111
  "axe-core": "^4.13.0",
112
112
  "culori": "^4.0.2",
113
- "i18next-cli": "^1.72.2",
113
+ "i18next-cli": "^1.73.1",
114
114
  "jsdom": "^30.0.1",
115
- "rolldown": "1.2.6",
115
+ "rolldown": "1.2.8",
116
116
  "tailwindcss": "^4.3.3",
117
117
  "typescript": "7.0.2",
118
- "vitest": "^4.1.11",
118
+ "vitest": "5.0.0",
119
119
  "vitest-axe": "^0.1.0",
120
120
  "wcag-contrast": "^3.0.0"
121
121
  },
@@ -188,7 +188,7 @@
188
188
  "url": "https://github.com/vertesia/composableai.git",
189
189
  "directory": "packages/ui"
190
190
  },
191
- "gitHead": "504e8df031c3950ec324c865385caf460b1c1cd7",
191
+ "gitHead": "5e9878a6ab82875248b0696621bb957732d42377",
192
192
  "scripts": {
193
193
  "clean:lib": "rimraf ./lib ./tsconfig.tsbuildinfo",
194
194
  "clean": "rimraf ./node_modules ./lib ./tsconfig.tsbuildinfo",
@@ -81,6 +81,27 @@ describe('VertesiaEnvironment runtime configuration', () => {
81
81
  expect(window.AUTH_MODE).toBeUndefined();
82
82
  });
83
83
 
84
+ it('uses the gateway broker ahead of an app build endpoint', () => {
85
+ vi.stubGlobal('window', {
86
+ __VERTESIA_RUNTIME_CONFIG__: { authMode: 'central', authUrl: 'https://auth.dev1.vertesia.io/' },
87
+ });
88
+ const env = new VertesiaEnvironment().init({
89
+ ...baseProps,
90
+ endpoints: { ...baseProps.endpoints, auth: 'https://internal-auth.vertesia.app/' },
91
+ });
92
+ expect(env.endpoints.auth).toBe('https://auth.dev1.vertesia.io/');
93
+ expect(window.AUTH_MODE).toBe('central');
94
+ });
95
+
96
+ it('preserves the app broker with an older central runtime contract', () => {
97
+ vi.stubGlobal('window', { __VERTESIA_RUNTIME_CONFIG__: { authMode: 'central' } });
98
+ const env = new VertesiaEnvironment().init({
99
+ ...baseProps,
100
+ endpoints: { ...baseProps.endpoints, auth: 'https://auth.dev1.vertesia.io/' },
101
+ });
102
+ expect(env.endpoints.auth).toBe('https://auth.dev1.vertesia.io/');
103
+ });
104
+
84
105
  it('accepts injected Central Auth mode without Firebase configuration', () => {
85
106
  vi.stubGlobal('window', {
86
107
  __VERTESIA_RUNTIME_CONFIG__: { authMode: 'central' },
@@ -192,3 +213,30 @@ describe('build-time default workspace', () => {
192
213
  expect(env.defaultAuthSelection).toEqual({ accountId: 'explicit' });
193
214
  });
194
215
  });
216
+
217
+ describe('fixed Firebase tenant', () => {
218
+ afterEach(() => vi.unstubAllGlobals());
219
+
220
+ it('combines an app tenant with gateway credentials', () => {
221
+ vi.stubGlobal('window', {
222
+ __VERTESIA_RUNTIME_CONFIG__: {
223
+ authMode: 'firebase',
224
+ firebase: {
225
+ apiKey: 'gateway',
226
+ authDomain: 'app.example.com',
227
+ projectId: 'project',
228
+ appId: 'app',
229
+ },
230
+ },
231
+ });
232
+ const env = new VertesiaEnvironment().init(baseProps, { VITE_FIREBASE_TENANT_ID: ' example-tenant ' });
233
+ expect(env.firebase?.tenantId).toBe('example-tenant');
234
+ expect(env.firebase?.apiKey).toBe('gateway');
235
+ });
236
+
237
+ it('does not enable Firebase just because a tenant is supplied', () => {
238
+ vi.stubGlobal('window', {});
239
+ const env = new VertesiaEnvironment().init(baseProps, { VITE_FIREBASE_TENANT_ID: 'tenant' });
240
+ expect(env.firebase).toBeUndefined();
241
+ });
242
+ });
package/src/env/index.ts CHANGED
@@ -18,8 +18,8 @@ export interface EnvProps {
18
18
  /**
19
19
  * Central Auth broker that issues the sign-in redirect and hosts `/logout`.
20
20
  *
21
- * Optional: when unset the session falls back to the long-standing broker, so an app that
22
- * does not set it keeps its current behaviour exactly. Set it to move one environment at a
21
+ * Optional: when unset the session falls back to the first-party broker, so an app that
22
+ * does not set it uses auth.vertesia.io. Set it to move one environment at a
23
23
  * time onto a different broker.
24
24
  */
25
25
  auth?: string;
@@ -34,6 +34,8 @@ export interface EnvProps {
34
34
  projectId: string;
35
35
  appId?: string;
36
36
  providerType?: string;
37
+ /** Fixed Identity Platform tenant, preserved throughout sign-in. */
38
+ tenantId?: string;
37
39
  };
38
40
  /** Default workspace selection when the URL does not explicitly select an account or project. */
39
41
  defaultAuthSelection?: { accountId?: string; projectId?: string };
@@ -77,6 +79,8 @@ export type VertesiaRuntimeConfig =
77
79
  }
78
80
  | {
79
81
  authMode: 'central';
82
+ /** Broker selected by the serving gateway. Overrides build-time configuration. */
83
+ authUrl?: string;
80
84
  };
81
85
 
82
86
  declare global {
@@ -139,6 +143,15 @@ export class VertesiaEnvironment implements Readonly<EnvProps> {
139
143
  const runtimeConfig = injectedRuntimeConfig() ?? buildRuntimeConfig(buildEnv);
140
144
  const runtimeFirebase = runtimeConfig?.authMode === 'firebase' ? runtimeConfig.firebase : undefined;
141
145
  this._props = props && runtimeFirebase && !props.firebase ? { ...props, firebase: runtimeFirebase } : props;
146
+ const authUrl = runtimeConfig?.authMode === 'central' ? runtimeConfig.authUrl?.trim() : undefined;
147
+ if (this._props && authUrl) {
148
+ this._props = { ...this._props, endpoints: { ...this._props.endpoints, auth: authUrl } };
149
+ }
150
+ const tenantId =
151
+ typeof buildEnv?.VITE_FIREBASE_TENANT_ID === 'string' ? buildEnv.VITE_FIREBASE_TENANT_ID.trim() : '';
152
+ if (this._props?.firebase && tenantId && !this._props.firebase.tenantId) {
153
+ this._props = { ...this._props, firebase: { ...this._props.firebase, tenantId } };
154
+ }
142
155
  if (this._props && this._props.defaultAuthSelection === undefined && buildEnv) {
143
156
  const accountId =
144
157
  typeof buildEnv.VITE_VERTESIA_ACCOUNT_ID === 'string'
@@ -332,7 +332,12 @@ export class PayloadBuilder {
332
332
  this._model_options = interaction.model_options as ModelOptions | undefined;
333
333
  if (interaction.runtime?.environment) {
334
334
  const envId = interaction.runtime.environment;
335
- this.vertesia.environments.retrieve(envId).then((environment) => (this.environment = environment));
335
+ this.vertesia.environments
336
+ .retrieve(envId)
337
+ .then((environment) => (this.environment = environment))
338
+ .catch(() => {
339
+ this.environment = undefined;
340
+ });
336
341
  }
337
342
  }
338
343
  this.onStateChanged();
@@ -1,8 +1,14 @@
1
1
  import { fireEvent, screen, waitFor } from '@testing-library/react';
2
2
  import type { AgentRunFeedbackEntry, AgentRunFeedbackPayload, AgentRunFeedbackStatus } from '@vertesia/common';
3
+ import { AgentMessageType } from '@vertesia/common';
3
4
  import { beforeEach, describe, expect, it, vi } from 'vitest';
4
5
  import { renderWithProviders } from '../../../__tests__/test-utils.js';
5
- import { AgentRunFeedback, AgentRunFeedbackProvider, agentRunFeedbackReasonCodes } from './AgentRunFeedback';
6
+ import {
7
+ AgentRunFeedback,
8
+ AgentRunFeedbackProvider,
9
+ agentRunFeedbackReasonCodes,
10
+ isAgentMessageRatable,
11
+ } from './AgentRunFeedback';
6
12
 
7
13
  const mocks = vi.hoisted(() => {
8
14
  const recordFeedback = vi.fn();
@@ -38,6 +44,17 @@ function payloadOf(call: number): AgentRunFeedbackPayload {
38
44
  return mocks.recordFeedback.mock.calls[call][1] as AgentRunFeedbackPayload;
39
45
  }
40
46
 
47
+ describe('isAgentMessageRatable', () => {
48
+ it('offers a rating on a final answer only, never on a streaming placeholder', () => {
49
+ expect(isAgentMessageRatable({ type: AgentMessageType.ANSWER, details: { streamed: true } })).toBe(true);
50
+ expect(isAgentMessageRatable({ type: AgentMessageType.ANSWER, details: undefined })).toBe(true);
51
+ expect(isAgentMessageRatable({ type: AgentMessageType.ANSWER, details: { source: 'streaming_summary' } })).toBe(
52
+ false,
53
+ );
54
+ expect(isAgentMessageRatable({ type: AgentMessageType.THOUGHT, details: undefined })).toBe(false);
55
+ });
56
+ });
57
+
41
58
  describe('AgentRunFeedback', () => {
42
59
  beforeEach(() => {
43
60
  mocks.recordFeedback.mockReset();
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  AGENT_RUN_FEEDBACK_COMMENT_MAX_LENGTH,
3
3
  type AgentMessage,
4
+ AgentMessageType,
4
5
  type AgentRunFeedbackEntry,
5
6
  type AgentRunFeedbackPayload,
6
7
  type AgentRunFeedbackRating,
@@ -61,6 +62,18 @@ export function agentMessageFeedbackId(message: Pick<AgentMessage, 'workstream_i
61
62
  return `${message.workstream_id || 'main'}:${message.timestamp}`;
62
63
  }
63
64
 
65
+ /**
66
+ * Whether a transcript message can carry a rating. Only a final answer can: while an answer is
67
+ * still streaming, the transcript shows a placeholder stamped with the browser clock, and a
68
+ * rating keyed to that timestamp would never match the answer the worker eventually posts, so
69
+ * the same answer would end up rated twice.
70
+ */
71
+ export function isAgentMessageRatable(message: Pick<AgentMessage, 'type' | 'details'>): boolean {
72
+ if (message.type !== AgentMessageType.ANSWER) return false;
73
+ const details = message.details as { source?: unknown } | undefined;
74
+ return details?.source !== 'streaming_summary';
75
+ }
76
+
64
77
  /** Idempotency key for one submission; the server counts a retried id once. */
65
78
  function newFeedbackId(): string {
66
79
  if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) return crypto.randomUUID();
@@ -1278,7 +1278,7 @@ describe('ModernAgentConversation send handling', () => {
1278
1278
  expect.objectContaining({ workstream_id: 'beta', status: 'running' }),
1279
1279
  expect.objectContaining({ workstream_id: 'gamma', status: 'completed' }),
1280
1280
  ]);
1281
- expect(latestRightPanelProps.activeTab).toBe('plan');
1281
+ expect(latestRightPanelProps.activeTab).toBe('workstreams');
1282
1282
  expect(latestMessageInputProps.activeTaskCount).toBe(2);
1283
1283
  expect(latestMessageInputProps.activeWorkstreams).toEqual([
1284
1284
  expect.objectContaining({ workstream_id: 'alpha', status: 'running' }),
@@ -1554,7 +1554,7 @@ function ModernAgentConversationInner({
1554
1554
  showInput,
1555
1555
  showSlidingPanel,
1556
1556
  setShowSlidingPanel,
1557
- } = useAgentPlans(messages, interactive, isModal);
1557
+ } = useAgentPlans(messages, interactive);
1558
1558
 
1559
1559
  const {
1560
1560
  openDocuments,
@@ -1938,9 +1938,10 @@ function ModernAgentConversationInner({
1938
1938
  // Unified right panel state
1939
1939
  // ────────────────────────────────────────────
1940
1940
  type RightPanelTab = 'plan' | 'workstreams' | 'documents' | 'uploads' | 'artifacts' | 'payload' | 'conversation';
1941
- const [rightPanelTab, setRightPanelTab] = useState<RightPanelTab>(
1942
- conversationContent || conversationTab ? 'conversation' : 'plan',
1941
+ const [rightPanelTab, setRightPanelTab] = useState<RightPanelTab | undefined>(
1942
+ conversationContent || conversationTab ? 'conversation' : undefined,
1943
1943
  );
1944
+ const defaultRightPanelTab = plans.length > 0 || panelWorkstreams.length === 0 ? 'plan' : 'workstreams';
1944
1945
  const [selectedArtifactPath, setSelectedArtifactPath] = useState<string | null>(null);
1945
1946
  const [rightPanelWidth, setRightPanelWidth] = useState(400);
1946
1947
  const [isRightPanelResizing, setIsRightPanelResizing] = useState(false);
@@ -3085,8 +3086,8 @@ function ModernAgentConversationInner({
3085
3086
  conversationContent={conversationTab ? conversationAreaJsx : conversationContent}
3086
3087
  // Panel control
3087
3088
  onClose={handleCloseRightPanel}
3088
- defaultTab={rightPanelTab}
3089
- activeTab={rightPanelTab}
3089
+ defaultTab={rightPanelTab ?? defaultRightPanelTab}
3090
+ activeTab={rightPanelTab ?? defaultRightPanelTab}
3090
3091
  onTabChange={setRightPanelTab}
3091
3092
  />
3092
3093
  </div>
@@ -29,7 +29,7 @@ import {
29
29
  Wrench,
30
30
  } from 'lucide-react';
31
31
  import React, { Component, type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';
32
- import { AgentRunFeedback, agentMessageFeedbackId } from '../AgentRunFeedback';
32
+ import { AgentRunFeedback, agentMessageFeedbackId, isAgentMessageRatable } from '../AgentRunFeedback';
33
33
  import { AnimatedThinkingDots, PulsatingCircle } from '../AnimatedThinkingDots';
34
34
  import { AskUserWidget, isAskUserOptions } from '../AskUserWidget';
35
35
  import { DocumentEditingActionCard, parseMarkdownEditingAction } from '../DocumentEditingActionCard.js';
@@ -823,7 +823,7 @@ function SummaryMessage({
823
823
  </MarkdownRenderer>
824
824
  </div>
825
825
  )}
826
- {feedbackAgentRunId && message.type === AgentMessageType.ANSWER && (
826
+ {feedbackAgentRunId && isAgentMessageRatable(message) && (
827
827
  <AgentRunFeedback
828
828
  agentRunId={feedbackAgentRunId}
829
829
  messageId={agentMessageFeedbackId(message)}
@@ -27,7 +27,7 @@ import type React from 'react';
27
27
  import { memo, useEffect, useMemo, useRef, useState } from 'react';
28
28
  import dayjs from '../../../../core/utils/dayjs.js';
29
29
  import { useDownloadFile } from '../../../store/objects/components/useDownloadFile.js';
30
- import { AgentRunFeedback, agentMessageFeedbackId } from '../AgentRunFeedback';
30
+ import { AgentRunFeedback, agentMessageFeedbackId, isAgentMessageRatable } from '../AgentRunFeedback';
31
31
  import { PulsatingCircle } from '../AnimatedThinkingDots';
32
32
  import { AskUserWidget, isAskUserOptions } from '../AskUserWidget';
33
33
  import { DocumentEditingActionCard, parseMarkdownEditingAction } from '../DocumentEditingActionCard.js';
@@ -683,7 +683,7 @@ function MessageItemComponent({
683
683
  )
684
684
  )}
685
685
 
686
- {feedbackAgentRunId && message.type === AgentMessageType.ANSWER && (
686
+ {feedbackAgentRunId && isAgentMessageRatable(message) && (
687
687
  <AgentRunFeedback
688
688
  agentRunId={feedbackAgentRunId}
689
689
  messageId={agentMessageFeedbackId(message)}
@@ -0,0 +1,52 @@
1
+ import { act, cleanup, renderHook } from '@testing-library/react';
2
+ import { type AgentMessage, AgentMessageType } from '@vertesia/common';
3
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
4
+ import { useAgentPlans } from './useAgentPlans.js';
5
+
6
+ const question: AgentMessage = {
7
+ timestamp: 1,
8
+ workflow_run_id: 'run-1',
9
+ workstream_id: 'main',
10
+ type: AgentMessageType.QUESTION,
11
+ message: 'Help with a task',
12
+ };
13
+ const plan: AgentMessage = {
14
+ ...question,
15
+ timestamp: 2,
16
+ type: AgentMessageType.PLAN,
17
+ details: { plan: [{ id: 1, goal: 'Review the task', status: 'pending' }] },
18
+ };
19
+
20
+ describe('useAgentPlans sidebar visibility', () => {
21
+ beforeEach(() => sessionStorage.clear());
22
+ afterEach(cleanup);
23
+
24
+ it('stays closed while history loads and when history has no plan', () => {
25
+ const { result, rerender } = renderHook(({ messages }) => useAgentPlans(messages, true), {
26
+ initialProps: { messages: [] as AgentMessage[] },
27
+ });
28
+ expect(result.current.showSlidingPanel).toBe(false);
29
+ rerender({ messages: [question] });
30
+ expect(result.current.showSlidingPanel).toBe(false);
31
+ });
32
+
33
+ it('opens when a plan arrives and resets for a new empty run', () => {
34
+ const { result, rerender } = renderHook(({ messages }) => useAgentPlans(messages, true), {
35
+ initialProps: { messages: [question] },
36
+ });
37
+ rerender({ messages: [question, plan] });
38
+ expect(result.current.showSlidingPanel).toBe(true);
39
+ expect(result.current.plans).toHaveLength(1);
40
+ rerender({ messages: [] });
41
+ expect(result.current.showSlidingPanel).toBe(false);
42
+ });
43
+
44
+ it('preserves manual opening when subsequent messages have no plan', () => {
45
+ const { result, rerender } = renderHook(({ messages }) => useAgentPlans(messages, true), {
46
+ initialProps: { messages: [question] },
47
+ });
48
+ act(() => result.current.setShowSlidingPanel(true));
49
+ rerender({ messages: [question, { ...question, timestamp: 3, type: AgentMessageType.ANSWER }] });
50
+ expect(result.current.showSlidingPanel).toBe(true);
51
+ });
52
+ });
@@ -18,14 +18,14 @@ export interface UseAgentPlansResult {
18
18
  * Key improvement: incremental processing. Instead of scanning ALL messages
19
19
  * on every change, tracks `lastProcessedIndex` and only scans new messages.
20
20
  */
21
- export function useAgentPlans(messages: AgentMessage[], interactive: boolean, isModal = false): UseAgentPlansResult {
21
+ export function useAgentPlans(messages: AgentMessage[], interactive: boolean): UseAgentPlansResult {
22
22
  const [plans, setPlans] = useState<Array<{ plan: Plan; timestamp: number }>>([]);
23
23
  const [activePlanIndex, setActivePlanIndex] = useState<number>(0);
24
24
  const [workstreamStatusMap, setWorkstreamStatusMap] = useState<
25
25
  Map<number, Map<string, 'pending' | 'in_progress' | 'completed' | 'skipped'>>
26
26
  >(new Map());
27
27
  const [showInput, setShowInput] = useState(interactive);
28
- const [showSlidingPanel, setShowSlidingPanel] = useState<boolean>(!isModal);
28
+ const [showSlidingPanel, setShowSlidingPanel] = useState<boolean>(false);
29
29
 
30
30
  // Incremental processing: track how far we've scanned
31
31
  const lastProcessedIndex = useRef<number>(-1);
@@ -38,12 +38,12 @@ export function useAgentPlans(messages: AgentMessage[], interactive: boolean, is
38
38
  setPlans([]);
39
39
  setActivePlanIndex(0);
40
40
  setWorkstreamStatusMap(new Map());
41
- // Keep right panel open by default on desktop sessions.
42
- setShowSlidingPanel(!isModal);
41
+ // Open automatically only once a plan is available.
42
+ setShowSlidingPanel(false);
43
43
  lastProcessedIndex.current = -1;
44
44
  knownPlanTimestamps.current.clear();
45
45
  }
46
- }, [messages.length, isModal]);
46
+ }, [messages.length]);
47
47
 
48
48
  // Helper to determine showInput from the latest message
49
49
  const updateShowInput = useCallback(
@@ -176,9 +176,14 @@ export class DocumentSearch implements SearchInterface {
176
176
  }
177
177
 
178
178
  computeFacets(_query: ObjectSearchQuery) {
179
- this._facetsRequest().then((facets) => {
180
- this.facets.value = facets;
181
- });
179
+ this._facetsRequest()
180
+ .then((facets) => {
181
+ this.facets.value = facets;
182
+ })
183
+ .catch((err: unknown) => {
184
+ const error = err instanceof Error ? err : new Error(String(err));
185
+ this.result.value = { ...this.result.value, error };
186
+ });
182
187
  }
183
188
 
184
189
  async _search(loadMore = false, noFacets = false): Promise<boolean> {
@@ -1,20 +1,20 @@
1
- import { afterEach, describe, expect, it } from 'vitest';
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
2
  import { getMountBasename, joinPath, stripMountBasename, withMountBasename } from './path';
3
3
 
4
4
  // Simulate a served `<base href>` (or its absence) by stubbing document.querySelector + document.URL.
5
5
  function setBaseHref(href: string | null, docUrl = 'https://gw.example.com/x') {
6
- (globalThis as { document?: unknown }).document = {
6
+ vi.stubGlobal('document', {
7
7
  URL: docUrl,
8
8
  querySelector: (sel: string) =>
9
9
  sel === 'base[href]' && href ? ({ href: new URL(href, docUrl).href } as unknown) : null,
10
- };
10
+ });
11
11
  }
12
12
 
13
13
  const MOUNT = '/tenants/05948c_5ed5f4/apps/furniture-catalog/versions/20260620T064257113Z/app';
14
14
 
15
15
  describe('mount basename helpers', () => {
16
16
  afterEach(() => {
17
- delete (globalThis as { document?: unknown }).document;
17
+ vi.unstubAllGlobals();
18
18
  });
19
19
 
20
20
  describe('getMountBasename', () => {
@@ -31,7 +31,7 @@ describe('mount basename helpers', () => {
31
31
  expect(getMountBasename()).toBe('');
32
32
  });
33
33
  it('returns "" when there is no document (SSR)', () => {
34
- delete (globalThis as { document?: unknown }).document;
34
+ vi.stubGlobal('document', undefined);
35
35
  expect(getMountBasename()).toBe('');
36
36
  });
37
37
  });
@@ -134,7 +134,7 @@ export function UserSessionProvider({ children, loadOnboardingStatus = true }: U
134
134
  session.setSession = setSession;
135
135
  getComposableToken(selectedAccount, selectedProject, Env.devAuthToken)
136
136
  .then((res) => {
137
- session.login(res.rawToken).then(() => setSession(session.clone()));
137
+ return session.login(res.rawToken).then(() => setSession(session.clone()));
138
138
  })
139
139
  .catch((err) => {
140
140
  if (surfaceAuthError(err)) return;
@@ -169,7 +169,7 @@ export function UserSessionProvider({ children, loadOnboardingStatus = true }: U
169
169
  }
170
170
  getComposableToken(selectedAccount, selectedProject, token, false, shouldRedirectToCentralAuth())
171
171
  .then((res) => {
172
- session.login(res.rawToken, { loadOnboardingStatus }).then(() => {
172
+ return session.login(res.rawToken, { loadOnboardingStatus }).then(() => {
173
173
  setSession(session.clone());
174
174
  clearAuthHash();
175
175
  });
@@ -238,7 +238,7 @@ export function UserSessionProvider({ children, loadOnboardingStatus = true }: U
238
238
  shouldRedirectToCentralAuth(),
239
239
  )
240
240
  .then((res) => {
241
- session
241
+ return session
242
242
  .login(res.rawToken, { loadOnboardingStatus })
243
243
  .then(() => setSession(session.clone()));
244
244
  })
@@ -6,10 +6,22 @@ This document describes the authentication flow for the Vertesia user-facing app
6
6
 
7
7
  The authentication system uses:
8
8
  - **Firebase** for identity management and OAuth provider integration
9
- - **Central Auth** (internal-auth.vertesia.app) for centralized authentication flow
9
+ - **Central Auth** (auth.vertesia.io) for centralized authentication flow
10
10
  - **STS (Security Token Service)** (sts.vertesia.io) to validate Firebase tokens and generate Vertesia JWT tokens
11
11
  - **Session state management** to validate redirects and prevent CSRF attacks
12
12
 
13
+ ### Central Auth endpoint selection
14
+
15
+ `https://auth.vertesia.io/` is the default broker. Regional scaffolding can set
16
+ `VITE_AUTH_ENDPOINT`; applications can also configure the `Env` initialization property `endpoints.auth` explicitly.
17
+ For compatible generated app shells, a gateway-provided central runtime config
18
+ (`window.__VERTESIA_RUNTIME_CONFIG__` with `authMode: 'central'` and `authUrl`)
19
+ takes precedence over the build-time endpoint. A runtime config without `authUrl`
20
+ retains the configured endpoint or default. Explicit Firebase mode uses direct
21
+ Firebase authentication instead of the central broker.
22
+
23
+ The diagrams below use the default hostname; redirects use the selected broker.
24
+
13
25
  ### STS Endpoints
14
26
  - Production/Preview: `https://sts.vertesia.io`
15
27
  - Staging: `https://sts.dev1.vertesia.io`
@@ -179,7 +191,7 @@ The authentication system uses:
179
191
  ┌──────────────────────┐ │ - Microsoft │ ┌──────────────────────┐
180
192
  │ Build redirect URL: │ └──────────┬───────────┘ │ Redirect to SSO │
181
193
  │ │ │ │ provider (Okta, │
182
- │ internal-auth │ ▼ │ Azure AD, etc.) │
194
+ │ auth.vertesia.io │ ▼ │ Azure AD, etc.) │
183
195
  │ .vertesia.app │ ┌──────────────────────┐ └──────────┬───────────┘
184
196
  │ ?sts=... │ │ Firebase OAuth │ │
185
197
  │ &redirect_uri=... │ │ redirect & return │ ▼
@@ -329,7 +341,7 @@ The authentication system uses:
329
341
 
330
342
  ### Path A: Central Auth (Option 1) - Returns with JWT
331
343
  1. User clicks "Continue with Central Auth"
332
- 2. Redirects to internal-auth.vertesia.app
344
+ 2. Redirects to auth.vertesia.io
333
345
  3. Central auth authenticates user and gets Firebase token
334
346
  4. **Central auth calls STS** (`POST sts.vertesia.io/token/issue` with Firebase token)
335
347
  5. **STS validates Firebase token and generates Vertesia JWT**
@@ -472,7 +484,7 @@ The authentication system uses:
472
484
  │ Redirect to Central │ │ getFirebaseAuth() │
473
485
  │ Auth for logout: │ │ .signOut() │
474
486
  │ │ │ │
475
- │ internal-auth │ │ (Triggers │
487
+ │ auth.vertesia.io │ │ (Triggers │
476
488
  │ .vertesia.app │ │ onAuthStateChanged │
477
489
  │ /logout │ │ with anonymous user)│
478
490
  │ │ └──────────┬───────────┘
@@ -712,7 +724,7 @@ flowchart TB
712
724
  subgraph Option1[OPTION 1: CENTRAL AUTH]
713
725
  C1[Generate state<br/>Save to sessionStorage]
714
726
  C2[Get account/project<br/>from URL or localStorage]
715
- C3[Build redirect URL:<br/>internal-auth.vertesia.app<br/>?sts=...&redirect_uri=...&state=...]
727
+ C3[Build redirect URL:<br/>auth.vertesia.io<br/>?sts=...&redirect_uri=...&state=...]
716
728
  C4[window.location.replace<br/>to central auth]
717
729
  C5[Central Auth Page<br/>User authenticates<br/>Gets Firebase token]
718
730
  C6[Central auth calls STS:<br/>POST sts.vertesia.io/token/issue<br/>Authorization: Bearer Firebase<br/>Body: type, account_id, project_id]
@@ -774,7 +786,7 @@ flowchart TB
774
786
 
775
787
  Check -->|YES| ShouldRedirect{shouldRedirectTo<br/>CentralAuth?}
776
788
 
777
- ShouldRedirect -->|YES| RedirectLogout[Redirect to Central Auth<br/>for logout<br/><br/>internal-auth.vertesia.app<br/>/logout<br/><br/>Central auth handles<br/>Firebase logout]
789
+ ShouldRedirect -->|YES| RedirectLogout[Redirect to Central Auth<br/>for logout<br/><br/>auth.vertesia.io<br/>/logout<br/><br/>Central auth handles<br/>Firebase logout]
778
790
 
779
791
  ShouldRedirect -->|NO| FirebaseSignout[getFirebaseAuth.signOut<br/><br/>Triggers onAuthStateChanged<br/>with anonymous user]
780
792
 
@@ -837,7 +849,7 @@ flowchart TB
837
849
  sequenceDiagram
838
850
  actor User
839
851
  participant App as Composable UI
840
- participant CentralAuth as Central Auth<br/>(internal-auth.vertesia.app)
852
+ participant CentralAuth as Central Auth<br/>(auth.vertesia.io)
841
853
  participant Firebase as Firebase Auth
842
854
  participant STS as STS<br/>(sts.vertesia.io)
843
855
 
@@ -1030,7 +1042,7 @@ flowchart TB
1030
1042
 
1031
1043
  Check -->|YES| ShouldRedirect{shouldRedirectTo<br/>CentralAuth?}
1032
1044
 
1033
- ShouldRedirect -->|YES| RedirectLogout[Redirect to Central Auth<br/>for logout<br/><br/>internal-auth.vertesia.app<br/>/logout<br/><br/>Central auth handles<br/>Firebase logout]
1045
+ ShouldRedirect -->|YES| RedirectLogout[Redirect to Central Auth<br/>for logout<br/><br/>auth.vertesia.io<br/>/logout<br/><br/>Central auth handles<br/>Firebase logout]
1034
1046
 
1035
1047
  ShouldRedirect -->|NO| FirebaseSignout[getFirebaseAuth.signOut<br/><br/>Triggers onAuthStateChanged<br/>with anonymous user]
1036
1048
 
@@ -1078,7 +1090,7 @@ flowchart TB
1078
1090
  LO2[session.logout or<br/>session.signOut]
1079
1091
  LO3{authToken<br/>exists?}
1080
1092
  LO4{shouldRedirectTo<br/>CentralAuth?}
1081
- LO5[Redirect to Central Auth<br/>for logout<br/><br/>internal-auth.vertesia.app<br/>/logout<br/><br/>Central auth handles<br/>Firebase logout]
1093
+ LO5[Redirect to Central Auth<br/>for logout<br/><br/>auth.vertesia.io<br/>/logout<br/><br/>Central auth handles<br/>Firebase logout]
1082
1094
  LO6[getFirebaseAuth.signOut<br/><br/>Triggers onAuthStateChanged<br/>with anonymous user]
1083
1095
  LO7[Clear session data:<br/>- authError = undefined<br/>- isLoading = false<br/>- authToken = undefined<br/>- typeRegistry = undefined]
1084
1096
  LO8[client.withAuthCallback<br/>undefined<br/><br/>Clear client auth]
@@ -77,7 +77,7 @@ describe('centralAuthUrl', () => {
77
77
  it('falls back to the default broker when no auth endpoint is configured', () => {
78
78
  initEnv(undefined);
79
79
  expect(centralAuthUrl()).toBe(DEFAULT_CENTRAL_AUTH_URL);
80
- expect(DEFAULT_CENTRAL_AUTH_URL).toBe('https://internal-auth.vertesia.app/');
80
+ expect(DEFAULT_CENTRAL_AUTH_URL).toBe('https://auth.vertesia.io/');
81
81
  });
82
82
 
83
83
  it('uses the configured auth endpoint when one is set', () => {