@vertesia/ui 1.5.0-dev.20260725.083715Z → 1.5.0-dev.20260804.124748Z

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 (176) hide show
  1. package/lib/core/hooks/useScrollableSearch.d.ts +1 -1
  2. package/lib/core/hooks/useScrollableSearch.d.ts.map +1 -1
  3. package/lib/core/hooks/useScrollableSearch.js +15 -2
  4. package/lib/core/hooks/useScrollableSearch.js.map +1 -1
  5. package/lib/env/index.d.ts +4 -0
  6. package/lib/env/index.d.ts.map +1 -1
  7. package/lib/env/index.js.map +1 -1
  8. package/lib/features/agent/chat/AgentRequestInputOverlay.d.ts +1 -1
  9. package/lib/features/agent/chat/AgentRequestInputOverlay.d.ts.map +1 -1
  10. package/lib/features/agent/chat/AgentRequestInputOverlay.js +3 -8
  11. package/lib/features/agent/chat/AgentRequestInputOverlay.js.map +1 -1
  12. package/lib/features/agent/chat/ModernAgentConversation.d.ts +8 -1
  13. package/lib/features/agent/chat/ModernAgentConversation.d.ts.map +1 -1
  14. package/lib/features/agent/chat/ModernAgentConversation.js +53 -17
  15. package/lib/features/agent/chat/ModernAgentConversation.js.map +1 -1
  16. package/lib/features/agent/chat/ModernAgentOutput/ActiveWorkstreamsSummary.d.ts +2 -1
  17. package/lib/features/agent/chat/ModernAgentOutput/ActiveWorkstreamsSummary.d.ts.map +1 -1
  18. package/lib/features/agent/chat/ModernAgentOutput/ActiveWorkstreamsSummary.js +25 -10
  19. package/lib/features/agent/chat/ModernAgentOutput/ActiveWorkstreamsSummary.js.map +1 -1
  20. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.d.ts.map +1 -1
  21. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js +24 -9
  22. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js.map +1 -1
  23. package/lib/features/agent/chat/ModernAgentOutput/AttachmentPreview.d.ts.map +1 -1
  24. package/lib/features/agent/chat/ModernAgentOutput/AttachmentPreview.js +15 -3
  25. package/lib/features/agent/chat/ModernAgentOutput/AttachmentPreview.js.map +1 -1
  26. package/lib/features/agent/chat/ModernAgentOutput/Header.js +2 -2
  27. package/lib/features/agent/chat/ModernAgentOutput/Header.js.map +1 -1
  28. package/lib/features/agent/chat/ModernAgentOutput/MessageInput.d.ts +2 -1
  29. package/lib/features/agent/chat/ModernAgentOutput/MessageInput.d.ts.map +1 -1
  30. package/lib/features/agent/chat/ModernAgentOutput/MessageInput.js +2 -2
  31. package/lib/features/agent/chat/ModernAgentOutput/MessageInput.js.map +1 -1
  32. package/lib/features/agent/chat/ModernAgentOutput/MessageItem.d.ts.map +1 -1
  33. package/lib/features/agent/chat/ModernAgentOutput/MessageItem.js +2 -2
  34. package/lib/features/agent/chat/ModernAgentOutput/MessageItem.js.map +1 -1
  35. package/lib/features/agent/chat/ModernAgentOutput/requestInputMessages.d.ts +9 -1
  36. package/lib/features/agent/chat/ModernAgentOutput/requestInputMessages.d.ts.map +1 -1
  37. package/lib/features/agent/chat/ModernAgentOutput/requestInputMessages.js +97 -2
  38. package/lib/features/agent/chat/ModernAgentOutput/requestInputMessages.js.map +1 -1
  39. package/lib/features/agent/chat/hooks/useAgentStream.d.ts.map +1 -1
  40. package/lib/features/agent/chat/hooks/useAgentStream.js +12 -3
  41. package/lib/features/agent/chat/hooks/useAgentStream.js.map +1 -1
  42. package/lib/features/agent/chat/index.d.ts +1 -1
  43. package/lib/features/agent/chat/index.d.ts.map +1 -1
  44. package/lib/features/agent/chat/index.js.map +1 -1
  45. package/lib/features/facets/PromptsFacetsNav.d.ts +8 -6
  46. package/lib/features/facets/PromptsFacetsNav.d.ts.map +1 -1
  47. package/lib/features/facets/PromptsFacetsNav.js +8 -3
  48. package/lib/features/facets/PromptsFacetsNav.js.map +1 -1
  49. package/lib/features/store/objects/components/ContentOverview.js +4 -1
  50. package/lib/features/store/objects/components/ContentOverview.js.map +1 -1
  51. package/lib/features/store/objects/components/useContentPanelHooks.d.ts +31 -3
  52. package/lib/features/store/objects/components/useContentPanelHooks.d.ts.map +1 -1
  53. package/lib/features/store/objects/search/DocumentSearchContext.d.ts +128 -6
  54. package/lib/features/store/objects/search/DocumentSearchContext.d.ts.map +1 -1
  55. package/lib/features/store/objects/selection/ObjectsActionContext.d.ts +2 -2
  56. package/lib/features/store/objects/selection/ObjectsActionContext.d.ts.map +1 -1
  57. package/lib/features/store/objects/selection/ObjectsActionSpec.d.ts +2 -2
  58. package/lib/features/store/objects/selection/ObjectsActionSpec.d.ts.map +1 -1
  59. package/lib/features/store/objects/selection/SelectionActions.d.ts +2 -2
  60. package/lib/features/store/objects/selection/SelectionActions.d.ts.map +1 -1
  61. package/lib/features/store/objects/upload/DocumentUploadModal.js.map +1 -1
  62. package/lib/features/store/types/IntakePolicyEditor.d.ts +2 -2
  63. package/lib/features/store/types/IntakePolicyEditor.d.ts.map +1 -1
  64. package/lib/features/store/types/IntakePolicyEditor.js +5 -1
  65. package/lib/features/store/types/IntakePolicyEditor.js.map +1 -1
  66. package/lib/features/store/types/IntakePolicyForm.d.ts.map +1 -1
  67. package/lib/features/store/types/IntakePolicyForm.js +2 -2
  68. package/lib/features/store/types/IntakePolicyForm.js.map +1 -1
  69. package/lib/features/store/types/ObjectSchemaEditor.d.ts +2 -2
  70. package/lib/features/store/types/ObjectSchemaEditor.d.ts.map +1 -1
  71. package/lib/features/store/types/SelectContentType.d.ts +2 -2
  72. package/lib/features/store/types/SelectContentType.d.ts.map +1 -1
  73. package/lib/features/store/types/SelectContentType.js.map +1 -1
  74. package/lib/features/store/types/SelectContentTypeModal.d.ts +2 -2
  75. package/lib/features/store/types/SelectContentTypeModal.d.ts.map +1 -1
  76. package/lib/features/store/types/SelectContentTypeModal.js.map +1 -1
  77. package/lib/features/store/types/TableLayoutEditor.d.ts +2 -2
  78. package/lib/features/store/types/TableLayoutEditor.d.ts.map +1 -1
  79. package/lib/features/store/types/TypeRegistry.d.ts +1267 -6
  80. package/lib/features/store/types/TypeRegistry.d.ts.map +1 -1
  81. package/lib/features/store/types/TypeRegistry.js +5 -0
  82. package/lib/features/store/types/TypeRegistry.js.map +1 -1
  83. package/lib/features/store/types/intake-policy-editor.logic.d.ts +9 -1
  84. package/lib/features/store/types/intake-policy-editor.logic.d.ts.map +1 -1
  85. package/lib/features/store/types/intake-policy-editor.logic.js +9 -1
  86. package/lib/features/store/types/intake-policy-editor.logic.js.map +1 -1
  87. package/lib/features/store/types/search/ObjectTypeSearchContext.d.ts +1251 -1
  88. package/lib/features/store/types/search/ObjectTypeSearchContext.d.ts.map +1 -1
  89. package/lib/features/store/types/typeCatalogCache.d.ts +2 -2
  90. package/lib/features/store/types/typeCatalogCache.d.ts.map +1 -1
  91. package/lib/features/store/types/typeCatalogCache.js +4 -0
  92. package/lib/features/store/types/typeCatalogCache.js.map +1 -1
  93. package/lib/features/user/UserInfo.d.ts +118 -7
  94. package/lib/features/user/UserInfo.d.ts.map +1 -1
  95. package/lib/i18n/locales/ar.json +2 -1
  96. package/lib/i18n/locales/de.json +2 -1
  97. package/lib/i18n/locales/en.json +5 -1
  98. package/lib/i18n/locales/es.json +2 -1
  99. package/lib/i18n/locales/fr.json +2 -1
  100. package/lib/i18n/locales/it.json +2 -1
  101. package/lib/i18n/locales/ja.json +2 -1
  102. package/lib/i18n/locales/ko.json +2 -1
  103. package/lib/i18n/locales/pt.json +2 -1
  104. package/lib/i18n/locales/ru.json +2 -1
  105. package/lib/i18n/locales/tr.json +2 -1
  106. package/lib/i18n/locales/zh-TW.json +2 -1
  107. package/lib/i18n/locales/zh.json +2 -1
  108. package/lib/session/UserSession.d.ts +18 -4
  109. package/lib/session/UserSession.d.ts.map +1 -1
  110. package/lib/shell/apps/AppInstallationProvider.d.ts +222 -2
  111. package/lib/shell/apps/AppInstallationProvider.d.ts.map +1 -1
  112. package/lib/shell/login/InviteAcceptModal.js.map +1 -1
  113. package/lib/vertesia-ui-core.js +1 -1
  114. package/lib/vertesia-ui-core.js.map +1 -1
  115. package/lib/vertesia-ui-features.js +34 -34
  116. package/lib/vertesia-ui-features.js.map +1 -1
  117. package/lib/vertesia-ui-i18n.js +13 -13
  118. package/lib/vertesia-ui-widgets.js +18 -18
  119. package/lib/vertesia-ui-widgets.js.map +1 -1
  120. package/lib/widgets/monacoEditor/MonacoEditor.d.ts +7 -1
  121. package/lib/widgets/monacoEditor/MonacoEditor.d.ts.map +1 -1
  122. package/lib/widgets/monacoEditor/MonacoEditor.js +62 -6
  123. package/lib/widgets/monacoEditor/MonacoEditor.js.map +1 -1
  124. package/package.json +31 -31
  125. package/src/core/hooks/useScrollableSearch.test.tsx +139 -0
  126. package/src/core/hooks/useScrollableSearch.tsx +17 -3
  127. package/src/env/index.ts +2 -0
  128. package/src/features/agent/chat/AgentChatPlaybackControls.test.tsx +2 -0
  129. package/src/features/agent/chat/AgentRequestInputOverlay.test.tsx +51 -6
  130. package/src/features/agent/chat/AgentRequestInputOverlay.tsx +5 -7
  131. package/src/features/agent/chat/ModernAgentConversation.test.tsx +195 -3
  132. package/src/features/agent/chat/ModernAgentConversation.tsx +106 -22
  133. package/src/features/agent/chat/ModernAgentOutput/ActiveWorkstreamsSummary.tsx +75 -20
  134. package/src/features/agent/chat/ModernAgentOutput/AllMessagesMixed.test.tsx +112 -0
  135. package/src/features/agent/chat/ModernAgentOutput/AllMessagesMixed.tsx +62 -10
  136. package/src/features/agent/chat/ModernAgentOutput/AttachmentPreview.tsx +36 -4
  137. package/src/features/agent/chat/ModernAgentOutput/Header.tsx +2 -2
  138. package/src/features/agent/chat/ModernAgentOutput/MessageInput.test.tsx +71 -0
  139. package/src/features/agent/chat/ModernAgentOutput/MessageInput.tsx +3 -1
  140. package/src/features/agent/chat/ModernAgentOutput/MessageItem.tsx +18 -4
  141. package/src/features/agent/chat/ModernAgentOutput/requestInputMessages.test.ts +95 -0
  142. package/src/features/agent/chat/ModernAgentOutput/requestInputMessages.ts +111 -1
  143. package/src/features/agent/chat/hooks/useAgentStream.test.tsx +57 -0
  144. package/src/features/agent/chat/hooks/useAgentStream.ts +12 -3
  145. package/src/features/agent/chat/index.ts +1 -0
  146. package/src/features/facets/PromptsFacetsNav.tsx +17 -9
  147. package/src/features/store/objects/components/ContentOverview.tsx +4 -1
  148. package/src/features/store/objects/selection/ObjectsActionContext.tsx +2 -2
  149. package/src/features/store/objects/selection/ObjectsActionSpec.ts +2 -2
  150. package/src/features/store/objects/selection/SelectionActions.tsx +2 -2
  151. package/src/features/store/objects/upload/DocumentUploadModal.tsx +2 -2
  152. package/src/features/store/types/IntakePolicyEditor.tsx +12 -4
  153. package/src/features/store/types/IntakePolicyForm.tsx +25 -0
  154. package/src/features/store/types/ObjectSchemaEditor.tsx +2 -2
  155. package/src/features/store/types/SelectContentType.tsx +9 -9
  156. package/src/features/store/types/SelectContentTypeModal.tsx +5 -5
  157. package/src/features/store/types/TableLayoutEditor.tsx +2 -2
  158. package/src/features/store/types/TypeRegistry.ts +8 -3
  159. package/src/features/store/types/intake-policy-editor.logic.ts +10 -2
  160. package/src/features/store/types/typeCatalogCache.ts +10 -3
  161. package/src/i18n/locales/ar.json +2 -1
  162. package/src/i18n/locales/de.json +2 -1
  163. package/src/i18n/locales/en.json +5 -1
  164. package/src/i18n/locales/es.json +2 -1
  165. package/src/i18n/locales/fr.json +2 -1
  166. package/src/i18n/locales/it.json +2 -1
  167. package/src/i18n/locales/ja.json +2 -1
  168. package/src/i18n/locales/ko.json +2 -1
  169. package/src/i18n/locales/pt.json +2 -1
  170. package/src/i18n/locales/ru.json +2 -1
  171. package/src/i18n/locales/tr.json +2 -1
  172. package/src/i18n/locales/zh-TW.json +2 -1
  173. package/src/i18n/locales/zh.json +2 -1
  174. package/src/shell/login/InviteAcceptModal.tsx +4 -4
  175. package/src/widgets/markdown/ArtifactEditingSurface.test.tsx +13 -9
  176. package/src/widgets/monacoEditor/MonacoEditor.tsx +83 -7
@@ -1,9 +1,16 @@
1
1
  import { type AgentMessage, AgentMessageType } from '@vertesia/common';
2
2
  import { describe, expect, it } from 'vitest';
3
3
  import {
4
+ clearRequestInputIdAnsweredForSession,
5
+ getAnsweredRequestInputKeys,
4
6
  getAnsweredToolApprovalRequestInputKeys,
7
+ getPendingRequestInputMessage,
5
8
  getRequestInputMessageKey,
9
+ getRequestInputResponseIdFromMetadata,
10
+ getRequestInputResponseMetadata,
6
11
  getToolApprovalResponseMetadata,
12
+ isRequestInputAnsweredForSession,
13
+ markRequestInputIdAnsweredForSession,
7
14
  } from './requestInputMessages';
8
15
 
9
16
  const APPROVAL_KEY = 'create_interaction:name:JDE AR Payment Processing Agent';
@@ -78,3 +85,91 @@ describe('getAnsweredToolApprovalRequestInputKeys', () => {
78
85
  expect(answered.size).toBe(0);
79
86
  });
80
87
  });
88
+
89
+ describe('request input correlation', () => {
90
+ it('does not apply session acknowledgement to non-request-input messages with the same id', () => {
91
+ const agentRunId = 'agent-run-1';
92
+ const requestId = 'request-1';
93
+ const request = {
94
+ ...makeApprovalRequest(),
95
+ details: { request_id: requestId, ux: { options: [{ id: 'allow_once' }] } },
96
+ };
97
+ const unrelatedMessage = {
98
+ ...makeUserAnswer('Still working'),
99
+ details: { request_id: requestId },
100
+ };
101
+
102
+ markRequestInputIdAnsweredForSession(agentRunId, requestId);
103
+
104
+ expect(isRequestInputAnsweredForSession(agentRunId, request)).toBe(true);
105
+ expect(isRequestInputAnsweredForSession(agentRunId, unrelatedMessage)).toBe(false);
106
+
107
+ clearRequestInputIdAnsweredForSession(agentRunId, requestId);
108
+ });
109
+
110
+ it('correlates a persisted response to the matching request id', () => {
111
+ const firstRequest = {
112
+ ...makeApprovalRequest(),
113
+ details: { request_id: 'request-1', ux: { options: [{ id: 'red', label: 'Red' }] } },
114
+ };
115
+ const secondRequest = {
116
+ ...makeApprovalRequest(),
117
+ timestamp: 2,
118
+ details: { request_id: 'request-2', ux: { options: [{ id: 'blue', label: 'Blue' }] } },
119
+ };
120
+ const answer = {
121
+ ...makeUserAnswer('red'),
122
+ timestamp: 3,
123
+ details: { request_input_response: { request_id: 'request-1' } },
124
+ };
125
+
126
+ const answered = getAnsweredRequestInputKeys([firstRequest, secondRequest, answer]);
127
+
128
+ expect(answered.has(getRequestInputMessageKey(firstRequest))).toBe(true);
129
+ expect(answered.has(getRequestInputMessageKey(secondRequest))).toBe(false);
130
+ expect(getPendingRequestInputMessage([firstRequest, secondRequest, answer])).toBe(secondRequest);
131
+ });
132
+
133
+ it('adds the request id without discarding existing response metadata', () => {
134
+ const request = {
135
+ ...makeApprovalRequest(),
136
+ details: { request_id: 'request-1', ux: { options: [{ id: 'red', label: 'Red' }] } },
137
+ };
138
+
139
+ expect(getRequestInputResponseMetadata(request, { source: 'free-response' })).toEqual({
140
+ source: 'free-response',
141
+ request_input_response: { request_id: 'request-1' },
142
+ });
143
+ });
144
+
145
+ it('does not let an earlier response resolve a later prompt that reused its request id', () => {
146
+ const firstRequest = {
147
+ ...makeApprovalRequest(),
148
+ details: { request_id: 'reused-request', ux: { options: [{ id: 'allow_once' }] } },
149
+ };
150
+ const firstAnswer = {
151
+ ...makeUserAnswer('allow_once'),
152
+ details: { request_input_response: { request_id: 'reused-request' } },
153
+ };
154
+ const secondRequest = {
155
+ ...makeApprovalRequest(),
156
+ timestamp: 3,
157
+ details: { request_id: 'reused-request', ux: { options: [{ id: 'allow_once' }] } },
158
+ };
159
+
160
+ const answered = getAnsweredRequestInputKeys([firstRequest, firstAnswer, secondRequest]);
161
+
162
+ expect(answered.has(getRequestInputMessageKey(firstRequest))).toBe(true);
163
+ expect(answered.has(getRequestInputMessageKey(secondRequest))).toBe(false);
164
+ expect(getPendingRequestInputMessage([firstRequest, firstAnswer, secondRequest])).toBe(secondRequest);
165
+ });
166
+
167
+ it('reads a request id from response metadata', () => {
168
+ expect(
169
+ getRequestInputResponseIdFromMetadata({
170
+ request_input_response: { request_id: 'request-1' },
171
+ }),
172
+ ).toBe('request-1');
173
+ expect(getRequestInputResponseIdFromMetadata({ request_input_response: {} })).toBeUndefined();
174
+ });
175
+ });
@@ -96,6 +96,65 @@ export function getRequestInputMessageKey(message: AgentMessage): string {
96
96
  .join('|');
97
97
  }
98
98
 
99
+ export function getRequestInputId(message: AgentMessage): string | undefined {
100
+ const details = message.details as Record<string, unknown> | undefined;
101
+ const requestId = details?.request_id ?? details?.tool_use_id;
102
+ return typeof requestId === 'string' && requestId ? requestId : undefined;
103
+ }
104
+
105
+ export function getRequestInputResolutionKey(message: AgentMessage): string {
106
+ return getRequestInputId(message) ?? getRequestInputMessageKey(message);
107
+ }
108
+
109
+ const ANSWERED_REQUEST_INPUT_STORAGE_PREFIX = 'vertesia:answered-request-input:';
110
+
111
+ function getAnsweredRequestInputStorageKey(agentRunId: string, requestId: string): string {
112
+ return `${ANSWERED_REQUEST_INPUT_STORAGE_PREFIX}${agentRunId}:${requestId}`;
113
+ }
114
+
115
+ export function markRequestInputIdAnsweredForSession(agentRunId: string, requestId: string): void {
116
+ if (typeof window === 'undefined') return;
117
+
118
+ try {
119
+ window.sessionStorage.setItem(getAnsweredRequestInputStorageKey(agentRunId, requestId), '1');
120
+ } catch {
121
+ // Session storage can be unavailable in privacy-restricted browser contexts.
122
+ }
123
+ }
124
+
125
+ export function clearRequestInputIdAnsweredForSession(agentRunId: string, requestId: string): void {
126
+ if (typeof window === 'undefined') return;
127
+
128
+ try {
129
+ window.sessionStorage.removeItem(getAnsweredRequestInputStorageKey(agentRunId, requestId));
130
+ } catch {
131
+ // Session storage can be unavailable in privacy-restricted browser contexts.
132
+ }
133
+ }
134
+
135
+ export function isRequestInputAnsweredForSession(agentRunId: string, message: AgentMessage): boolean {
136
+ if (message.type !== AgentMessageType.REQUEST_INPUT) return false;
137
+
138
+ const requestId = getRequestInputId(message);
139
+ if (!requestId || typeof window === 'undefined') return false;
140
+
141
+ try {
142
+ return window.sessionStorage.getItem(getAnsweredRequestInputStorageKey(agentRunId, requestId)) === '1';
143
+ } catch {
144
+ return false;
145
+ }
146
+ }
147
+
148
+ function getRequestInputResponseId(message: AgentMessage): string | undefined {
149
+ if (message.type !== AgentMessageType.QUESTION) return undefined;
150
+
151
+ const details = getRecord(message.details);
152
+ const metadata = getRecord(details?.metadata);
153
+ const response = getRecord(details?.request_input_response) ?? getRecord(metadata?.request_input_response);
154
+ const requestId = response?.request_id;
155
+ return typeof requestId === 'string' && requestId ? requestId : undefined;
156
+ }
157
+
99
158
  export function getRequestInputAnswerMessageKey(message: AgentMessage): string {
100
159
  const details = message.details as Record<string, unknown> | undefined;
101
160
  const keyParts = [
@@ -114,10 +173,22 @@ export function getRequestInputAnswerMessageKey(message: AgentMessage): string {
114
173
 
115
174
  export function getAnsweredRequestInputKeys(messages: AgentMessage[]): Set<string> {
116
175
  const answered = new Set<string>();
176
+ const latestResponseIndexByRequestId = new Map<string, number>();
177
+ messages.forEach((message, index) => {
178
+ const requestId = getRequestInputResponseId(message);
179
+ if (requestId) latestResponseIndexByRequestId.set(requestId, index);
180
+ });
117
181
 
118
182
  messages.forEach((message, index) => {
119
183
  if (message.type !== AgentMessageType.REQUEST_INPUT) return;
120
184
 
185
+ const requestId = getRequestInputId(message);
186
+ const correlatedResponseIndex = requestId ? latestResponseIndexByRequestId.get(requestId) : undefined;
187
+ if (correlatedResponseIndex !== undefined && correlatedResponseIndex > index) {
188
+ answered.add(getRequestInputMessageKey(message));
189
+ return;
190
+ }
191
+
121
192
  const workstreamId = getWorkstreamId(message);
122
193
  for (let nextIndex = index + 1; nextIndex < messages.length; nextIndex += 1) {
123
194
  const nextMessage = messages[nextIndex];
@@ -125,7 +196,9 @@ export function getAnsweredRequestInputKeys(messages: AgentMessage[]): Set<strin
125
196
 
126
197
  if (nextMessage.type === AgentMessageType.REQUEST_INPUT) break;
127
198
  if (nextMessage.type === AgentMessageType.QUESTION) {
128
- answered.add(getRequestInputMessageKey(message));
199
+ if (!getRequestInputResponseId(nextMessage)) {
200
+ answered.add(getRequestInputMessageKey(message));
201
+ }
129
202
  break;
130
203
  }
131
204
  }
@@ -134,6 +207,41 @@ export function getAnsweredRequestInputKeys(messages: AgentMessage[]): Set<strin
134
207
  return answered;
135
208
  }
136
209
 
210
+ export function getRequestInputResponseMetadata(
211
+ message: AgentMessage,
212
+ metadata?: Record<string, unknown>,
213
+ ): Record<string, unknown> | undefined {
214
+ const requestId = getRequestInputId(message);
215
+ if (!requestId) return metadata;
216
+
217
+ return {
218
+ ...metadata,
219
+ request_input_response: { request_id: requestId },
220
+ };
221
+ }
222
+
223
+ export function sendRequestInputResponse(
224
+ onSendMessage: ((message: string, metadata?: Record<string, unknown>) => void) | undefined,
225
+ requestMessage: AgentMessage,
226
+ response: string,
227
+ metadata?: Record<string, unknown>,
228
+ ): void {
229
+ if (!onSendMessage) return;
230
+
231
+ const responseMetadata = getRequestInputResponseMetadata(requestMessage, metadata);
232
+ if (responseMetadata) {
233
+ onSendMessage(response, responseMetadata);
234
+ } else {
235
+ onSendMessage(response);
236
+ }
237
+ }
238
+
239
+ export function getRequestInputResponseIdFromMetadata(metadata?: Record<string, unknown>): string | undefined {
240
+ const response = getRecord(metadata?.request_input_response);
241
+ const requestId = response?.request_id;
242
+ return typeof requestId === 'string' && requestId ? requestId : undefined;
243
+ }
244
+
137
245
  export function getResolvedToolApprovalKeys(messages: AgentMessage[]): Set<string> {
138
246
  const resolved = new Set<string>();
139
247
 
@@ -300,6 +408,7 @@ export function isRequestInputAnswered(message: AgentMessage, answeredRequestInp
300
408
  export function getPendingRequestInputMessage(
301
409
  messages: AgentMessage[],
302
410
  workstreamId = 'main',
411
+ locallyAnsweredRequestInputKeys?: ReadonlySet<string>,
303
412
  ): RequestInputMessageWithUx | undefined {
304
413
  const answeredRequestInputKeys = getAnsweredRequestInputKeys(messages);
305
414
  const resolvedToolApprovalKeys = getResolvedToolApprovalKeys(messages);
@@ -308,6 +417,7 @@ export function getPendingRequestInputMessage(
308
417
  const message = messages[index];
309
418
  if (!hasRequestInputUx(message)) continue;
310
419
  if (getWorkstreamId(message) !== workstreamId) continue;
420
+ if (locallyAnsweredRequestInputKeys?.has(getRequestInputResolutionKey(message))) continue;
311
421
  if (isRequestInputAnswered(message, answeredRequestInputKeys)) continue;
312
422
  if (isRequestInputResolvedByToolApprovalEvent(message, resolvedToolApprovalKeys)) continue;
313
423
  return message;
@@ -312,6 +312,63 @@ describe('useAgentStream', () => {
312
312
  expect(onMessage).toHaveBeenCalledTimes(1);
313
313
  });
314
314
 
315
+ it('does not apply replayed file_processing snapshots to server file updates', async () => {
316
+ let deliver: ((message: AgentMessage) => void) | undefined;
317
+ const fileSnapshot = (timestamp: number, id: string): AgentMessage => ({
318
+ timestamp,
319
+ workflow_run_id: 'run-1',
320
+ type: AgentMessageType.SYSTEM,
321
+ message: '',
322
+ workstream_id: 'main',
323
+ details: {
324
+ system_type: 'file_processing',
325
+ batch_id: 'batch-1',
326
+ files: [
327
+ {
328
+ id,
329
+ name: `${id}.pdf`,
330
+ content_type: 'application/pdf',
331
+ size: 1,
332
+ status: FileProcessingStatus.READY,
333
+ artifact_path: `files/${id}.pdf`,
334
+ reference: `artifact:files/${id}.pdf`,
335
+ started_at: timestamp,
336
+ },
337
+ ],
338
+ pending_count: 0,
339
+ ready_count: 1,
340
+ error_count: 0,
341
+ },
342
+ });
343
+ const historical = fileSnapshot(1_000, 'old-upload');
344
+ const streamMessages = vi.fn<
345
+ (
346
+ id: string,
347
+ onMessage?: (message: AgentMessage, exitFn?: (payload: unknown) => void) => void,
348
+ since?: number,
349
+ signal?: AbortSignal,
350
+ options?: AgentRunStreamMessagesOptions,
351
+ ) => Promise<unknown>
352
+ >(async (_id, onMessage, _since, _signal, options) => {
353
+ deliver = onMessage;
354
+ options?.onHistoryLoaded?.([historical]);
355
+ // Completed runs replay archived file_processing snapshots through the
356
+ // live callback; applying them would rehydrate every historical upload
357
+ // as a staged composer chip on reopen.
358
+ onMessage?.(historical);
359
+ return null;
360
+ });
361
+ const client = createClient(streamMessages);
362
+ const { result } = renderHook(() => useAgentStream(client, 'agent-run-1', vi.fn()));
363
+
364
+ await waitFor(() => expect(deliver).toBeDefined());
365
+ expect(result.current.serverFileUpdates.size).toBe(0);
366
+
367
+ act(() => deliver?.(fileSnapshot(1_100, 'new-upload')));
368
+ expect(result.current.serverFileUpdates.size).toBe(1);
369
+ expect(result.current.serverFileUpdates.has('new-upload')).toBe(true);
370
+ });
371
+
315
372
  it('marks initial history as errored when the history fetch fails', async () => {
316
373
  const streamMessages = vi.fn<
317
374
  (
@@ -357,9 +357,12 @@ export function useAgentStream(
357
357
  (message) => {
358
358
  if (abortController.signal.aborted) return;
359
359
  // Completed and idle runs replay their archived history through the live
360
- // stream. Only forward genuinely new deliveries so onMessage consumers
360
+ // stream. Track replay status once, before the cursor advances below —
361
+ // every replay-sensitive consumer in this callback must use it.
362
+ const isReplay = Boolean(message.timestamp && message.timestamp <= lastDeliveredTsRef.current);
363
+ // Only forward genuinely new deliveries so onMessage consumers
361
364
  // never treat replayed events as fresh ones.
362
- if (!message.timestamp || message.timestamp > lastDeliveredTsRef.current) {
365
+ if (!isReplay) {
363
366
  onMessageRef.current?.(message);
364
367
  }
365
368
 
@@ -426,7 +429,13 @@ export function useAgentStream(
426
429
  if (message.type === AgentMessageType.SYSTEM) {
427
430
  const details = message.details as FileProcessingDetails | undefined;
428
431
  if (details?.system_type === 'file_processing' && details.files) {
429
- setServerFileUpdates(new Map(details.files.map((file) => [file.id, file])));
432
+ // Replayed snapshots list every file ever uploaded to the run as
433
+ // READY; applying them on reconnect would rehydrate historical
434
+ // uploads as staged composer chips. Only genuinely new updates
435
+ // may touch the file state.
436
+ if (!isReplay) {
437
+ setServerFileUpdates(new Map(details.files.map((file) => [file.id, file])));
438
+ }
430
439
  return;
431
440
  }
432
441
  // Other SYSTEM messages fall through to normal handling
@@ -39,6 +39,7 @@ export {
39
39
  } from './AskUserWidget';
40
40
  export * from './JumpingDots';
41
41
  export {
42
+ type AgentMessageFilter,
42
43
  ModernAgentConversation,
43
44
  type ModernAgentConversationProps,
44
45
  type SendAgentMessageFn,
@@ -1,4 +1,4 @@
1
- import type { FacetBucket } from '@vertesia/common';
1
+ import type { ComputedFacetResponse, FacetBucket } from '@vertesia/common';
2
2
  import {
3
3
  type Filter as BaseFilter,
4
4
  FilterBar,
@@ -11,11 +11,13 @@ import { useState } from 'react';
11
11
  import { filterValueToQueryValue, type SearchInterface, setSearchQueryValue } from './utils/SearchInterface';
12
12
 
13
13
  interface PromptsFacetsNavProps {
14
- facets: {
15
- role?: FacetBucket[];
16
- status?: FacetBucket[];
17
- tags?: FacetBucket[];
18
- };
14
+ /**
15
+ * The facet response as the endpoint publishes it: `total` plus one entry per facet the caller
16
+ * asked for. The names are chosen per request, so the buckets are narrowed below rather than
17
+ * declared here — the previous `{ role, status, tags }` shape named facets this component never
18
+ * requests and could not describe a caller that asks for a different one.
19
+ */
20
+ facets: ComputedFacetResponse;
19
21
  search: SearchInterface;
20
22
  /**
21
23
  * Optional controlled filter state. When provided, the parent owns the filter list (and is
@@ -28,8 +30,14 @@ interface PromptsFacetsNavProps {
28
30
  filterGroups?: FilterGroup[];
29
31
  }
30
32
 
33
+ function facetBuckets(facets: ComputedFacetResponse, name: string): FacetBucket[] {
34
+ const value = facets[name];
35
+ return Array.isArray(value) ? (value as FacetBucket[]) : [];
36
+ }
37
+
31
38
  // Hook to create filter groups for prompts
32
39
  export function usePromptsFilterGroups(facets: PromptsFacetsNavProps['facets']): FilterGroup[] {
40
+ const roleBuckets = facetBuckets(facets, 'role');
33
41
  const customFilterGroups: FilterGroup[] = [];
34
42
 
35
43
  // Add name filter as text type
@@ -42,12 +50,12 @@ export function usePromptsFilterGroups(facets: PromptsFacetsNavProps['facets']):
42
50
  customFilterGroups.push(nameFilterGroup);
43
51
 
44
52
  // Add role filter as select type if role facets are available
45
- if (facets.role && facets.role.length > 0) {
53
+ if (roleBuckets.length > 0) {
46
54
  const rolesFilterGroup = {
47
55
  name: 'role',
48
56
  placeholder: 'Role',
49
57
  type: 'select' as const,
50
- options: facets.role.map((facet) => ({
58
+ options: roleBuckets.map((facet) => ({
51
59
  label: facet._id,
52
60
  value: facet._id,
53
61
  count: facet.count,
@@ -62,7 +70,7 @@ export function usePromptsFilterGroups(facets: PromptsFacetsNavProps['facets']):
62
70
  placeholder: 'Tags',
63
71
  type: 'stringList' as const,
64
72
  multiple: true,
65
- options: (facets.tags ?? []).map((facet) => ({
73
+ options: facetBuckets(facets, 'tags').map((facet) => ({
66
74
  label: facet._id,
67
75
  value: facet._id,
68
76
  })),
@@ -843,7 +843,10 @@ function TextActions({
843
843
  () => (project ? client.projects.retrieve(project.id) : Promise.resolve(undefined)),
844
844
  [project?.id],
845
845
  );
846
- const pdfTemplateObjectId = fullProject?.configuration?.pdf_template_object_id;
846
+ // `null` is how the setting is cleared, and it means the same thing here as an absent value: no
847
+ // custom template, so the export falls back to the built-in one. Collapsing the two spellings at
848
+ // the read keeps `templateObjectId` below a plain optional string.
849
+ const pdfTemplateObjectId = fullProject?.configuration?.pdf_template_object_id ?? undefined;
847
850
 
848
851
  const isMarkdown = isMarkdownContentType(content?.type);
849
852
  const isDocumentEditingContent = isDocumentEditingContentType(content?.type);
@@ -1,4 +1,4 @@
1
- import type { ContentObjectTypeItem } from '@vertesia/common';
1
+ import type { ContentObjectTypeCatalogEntry } from '@vertesia/common';
2
2
  import { useToast } from '@vertesia/ui/core';
3
3
  import { useUserSession } from '@vertesia/ui/session';
4
4
  import { type ReactNode, useMemo } from 'react';
@@ -25,7 +25,7 @@ const DEFAULT_ACTIONS: ObjectsActionSpec[] = [
25
25
 
26
26
  interface ObjectsActionContextProps {
27
27
  children: ReactNode;
28
- type?: ContentObjectTypeItem;
28
+ type?: ContentObjectTypeCatalogEntry;
29
29
  }
30
30
  export function ObjectsActionContextProvider({ children, type }: ObjectsActionContextProps) {
31
31
  const selection = useDocumentSelection();
@@ -1,5 +1,5 @@
1
1
  import type { VertesiaClient } from '@vertesia/client';
2
- import type { ColumnLayout, ContentObjectTypeItem } from '@vertesia/common';
2
+ import type { ColumnLayout, ContentObjectTypeCatalogEntry } from '@vertesia/common';
3
3
  import type { ToastFn } from '@vertesia/ui/core';
4
4
  import type { DocumentSelection } from '../DocumentSelectionProvider';
5
5
  import type { DocumentSearch } from '../search/DocumentSearchContext';
@@ -22,7 +22,7 @@ export interface ObjectsActionParams {
22
22
  toast: ToastFn;
23
23
  search: DocumentSearch;
24
24
  table_layout?: ColumnLayout[];
25
- type?: ContentObjectTypeItem;
25
+ type?: ContentObjectTypeCatalogEntry;
26
26
  }
27
27
 
28
28
  export interface ActionComponentTypeProps {
@@ -1,4 +1,4 @@
1
- import type { ContentObjectTypeItem } from '@vertesia/common';
1
+ import type { ContentObjectTypeCatalogEntry } from '@vertesia/common';
2
2
  import { Button, Popover, PopoverContent, PopoverTrigger, SelectList } from '@vertesia/ui/core';
3
3
  import { useUITranslation } from '@vertesia/ui/i18n';
4
4
  import clsx from 'clsx';
@@ -13,7 +13,7 @@ import { useObjectsActionContext } from './ObjectsActionHooks';
13
13
  import type { ObjectsActionSpec } from './ObjectsActionSpec';
14
14
 
15
15
  interface SelectionActionsProps {
16
- type?: ContentObjectTypeItem;
16
+ type?: ContentObjectTypeCatalogEntry;
17
17
  allowMutations?: boolean;
18
18
  allowDelete?: boolean;
19
19
  /**
@@ -1,4 +1,4 @@
1
- import type { Collection, ContentObjectTypeItem, DynamicCollection } from '@vertesia/common';
1
+ import type { Collection, ContentObjectTypeCatalogEntry, DynamicCollection } from '@vertesia/common';
2
2
  import {
3
3
  Button,
4
4
  errorMessage,
@@ -90,7 +90,7 @@ export function DocumentUploadModal({
90
90
  const [files, setFiles] = useState<File[]>([]);
91
91
  const [processedFiles, setProcessedFiles] = useState<FileWithMetadata[]>([]);
92
92
  const [processingDone, setProcessingDone] = useState(false);
93
- const [selectedType, setSelectedType] = useState<ContentObjectTypeItem | null>(null);
93
+ const [selectedType, setSelectedType] = useState<ContentObjectTypeCatalogEntry | null>(null);
94
94
  const [fileStatuses, setFileStatuses] = useState<FileUploadStatus[]>([]);
95
95
  const [isUploading, setIsUploading] = useState(false);
96
96
  const [uploadComplete, setUploadComplete] = useState(false);
@@ -1,4 +1,8 @@
1
- import { type ContentObjectType, type ContentTypeIntakePolicy, ContentTypeIntakePolicySchema } from '@vertesia/common';
1
+ import {
2
+ type ContentObjectTypeCatalogEntry,
3
+ type ContentTypeIntakePolicy,
4
+ ContentTypeIntakePolicySchema,
5
+ } from '@vertesia/common';
2
6
  import {
3
7
  Badge,
4
8
  Button,
@@ -25,7 +29,7 @@ import { IntakePolicyForm, type IntakePolicyFormSection } from './IntakePolicyFo
25
29
 
26
30
  interface IntakePolicyEditorProps {
27
31
  /** Content type whose intake policy is edited; omit when editing a standalone policy. */
28
- objectType?: ContentObjectType;
32
+ objectType?: ContentObjectTypeCatalogEntry;
29
33
  /** Initial policy value when no objectType is given (e.g. the project default policy). */
30
34
  value?: ContentTypeIntakePolicy;
31
35
  /** Custom persistence (e.g. save to project configuration). Returns the saved policy.
@@ -644,13 +648,17 @@ function stringifyPolicy(policy: ContentTypeIntakePolicy | undefined) {
644
648
  return JSON.stringify(policy ?? EMPTY_POLICY, null, 2);
645
649
  }
646
650
 
647
- function createPolicyValidator() {
651
+ function createPolicyValidator(): ValidateFunction<ContentTypeIntakePolicy> {
648
652
  const ajv = new Ajv({
649
653
  strict: false,
650
654
  allErrors: true,
651
655
  });
652
656
  addFormats(ajv);
653
- return ajv.compile(ContentTypeIntakePolicySchema);
657
+ // The schema is plain JSON — generated from the canonical Zod schema, and deliberately typed as
658
+ // opaque data so the package root can export it without pulling Zod into the browser bundle. AJV
659
+ // therefore infers `unknown`; the type argument states what compiling THIS schema validates, and
660
+ // `store.contract.test.ts` is what keeps the claim true.
661
+ return ajv.compile<ContentTypeIntakePolicy>(ContentTypeIntakePolicySchema);
654
662
  }
655
663
 
656
664
  function parseAndValidatePolicy(
@@ -152,6 +152,13 @@ export function IntakePolicyForm({ policy, section, onChange, readonly = false }
152
152
  onChange={(value) => setValue(['text_conversion', 'scope'], value)}
153
153
  readonly={readonly}
154
154
  />
155
+ <NumberField
156
+ label={t('intakePolicy.field.renderDpi')}
157
+ value={policy.text_conversion?.render_dpi}
158
+ min={72}
159
+ onChange={(value) => setValue(['text_conversion', 'render_dpi'], value)}
160
+ readonly={readonly}
161
+ />
155
162
  </div>
156
163
  <TextAreaField
157
164
  label={t('intakePolicy.field.instructions')}
@@ -161,6 +168,15 @@ export function IntakePolicyForm({ policy, section, onChange, readonly = false }
161
168
  />
162
169
  </FormSection>
163
170
 
171
+ <FormSection title={t('intakePolicy.section.conversionModel')}>
172
+ <ModelConfigFields
173
+ title={t('intakePolicy.field.model')}
174
+ config={policy.text_conversion?.config}
175
+ onChange={(value) => setValue(['text_conversion', 'config'], value)}
176
+ readonly={readonly}
177
+ />
178
+ </FormSection>
179
+
164
180
  {policy.text_conversion?.method === 'custom' && (
165
181
  <FormSection title={t('intakePolicy.section.customConversion')}>
166
182
  <div className="grid gap-4 md:grid-cols-2">
@@ -243,6 +259,15 @@ export function IntakePolicyForm({ policy, section, onChange, readonly = false }
243
259
  />
244
260
  </FormSection>
245
261
 
262
+ <FormSection title={t('intakePolicy.section.extractionModel')}>
263
+ <ModelConfigFields
264
+ title={t('intakePolicy.field.model')}
265
+ config={policy.extraction?.config}
266
+ onChange={(value) => setValue(['extraction', 'config'], value)}
267
+ readonly={readonly}
268
+ />
269
+ </FormSection>
270
+
246
271
  <FormSection title={t('intakePolicy.section.visionBudget')}>
247
272
  <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
248
273
  <SelectField
@@ -1,4 +1,4 @@
1
- import type { ContentObjectType } from '@vertesia/common';
1
+ import type { ContentObjectTypeCatalogEntry } from '@vertesia/common';
2
2
  import { Button, errorMessage, Panel, useTheme, useToast } from '@vertesia/ui/core';
3
3
  import { useUITranslation } from '@vertesia/ui/i18n';
4
4
  import { useUserSession } from '@vertesia/ui/session';
@@ -7,7 +7,7 @@ import { Ajv } from 'ajv';
7
7
  import { useMemo, useRef, useState } from 'react';
8
8
 
9
9
  interface ObjectSchemaEditorProps {
10
- objectType: ContentObjectType;
10
+ objectType: ContentObjectTypeCatalogEntry;
11
11
  onSchemaUpdate: (jsonSchema: unknown) => void;
12
12
  readonly?: boolean;
13
13
  }