diff --git a/packages/mock-converse-api/src/server.test.ts b/packages/mock-converse-api/src/server.test.ts index 64bdc6be809..c7610273001 100644 --- a/packages/mock-converse-api/src/server.test.ts +++ b/packages/mock-converse-api/src/server.test.ts @@ -125,6 +125,69 @@ describe('createMockConverseServer', () => { expect(res.headers['content-type']).toBe('text/event-stream'); }); + it('streams the Thermidor schema catalog example for its dedicated prompt', async () => { + await startServer(); + const res = await makeRequest( + server, + { + method: 'POST', + path: '/rest/organizations/myorg/commerce/unstable/agentic/converse', + headers: {'Content-Type': 'application/json'}, + }, + JSON.stringify({message: 'Show the Thermidor catalog'}) + ); + + const events = res.body + .split('\n\n') + .filter(Boolean) + .map( + (frame) => JSON.parse(frame.split('\n')[1].replace('data:', '')) as Record + ); + const activity = events.find((event) => event['type'] === 'ACTIVITY_SNAPSHOT'); + const stateSnapshot = events.find((event) => event['type'] === 'STATE_SNAPSHOT'); + + expect(activity).toMatchObject({ + type: 'ACTIVITY_SNAPSHOT', + messageId: 'commerce-catalog-example', + activityType: 'a2ui-surface', + replace: true, + content: { + a2ui_operations: [ + { + version: 'v0.9', + createSurface: { + surfaceId: 'commerce-catalog-example', + catalogId: 'https://schema.thermidor.coveo.com/a2-ui/catalog.json', + }, + }, + { + version: 'v0.9', + updateComponents: { + components: [ + {id: 'root', component: 'Column', children: ['featured-products', 'cart']}, + {component: 'ProductCarousel'}, + {component: 'Cart'}, + ], + }, + }, + ], + }, + }); + + const snapshot = stateSnapshot?.['snapshot'] as { + controllers: Record; + }; + + expect(snapshot.controllers['featured-products'].products).toEqual( + expect.arrayContaining([expect.objectContaining({permanentid: 'trail-running-shoes-001'})]) + ); + expect(snapshot.controllers['shopping-cart'].items).toEqual( + expect.arrayContaining([ + expect.objectContaining({productId: 'trail-running-shoes-001', quantity: 1}), + ]) + ); + }); + it('returns 400 for invalid JSON payload', async () => { await startServer(); const res = await makeRequest( diff --git a/packages/platform-mock-api/src/converse/generate-response.ts b/packages/platform-mock-api/src/converse/generate-response.ts index 73ac34e9c8f..2a18f1115ac 100644 --- a/packages/platform-mock-api/src/converse/generate-response.ts +++ b/packages/platform-mock-api/src/converse/generate-response.ts @@ -25,6 +25,10 @@ const PROMPT_TEMPLATE_MAP: ReadonlyArray = [ prompt: 'i like cold-water surfing. compare wetsuits for it', templateId: 'response8', }, + { + prompt: 'show the thermidor catalog', + templateId: 'thermidor-schema-catalog', + }, ]; const FALLBACK_TEMPLATE_ID: TemplateId = 'response5'; diff --git a/packages/platform-mock-api/src/converse/templates/response9.ts b/packages/platform-mock-api/src/converse/templates/response9.ts new file mode 100644 index 00000000000..8ed8a22a309 --- /dev/null +++ b/packages/platform-mock-api/src/converse/templates/response9.ts @@ -0,0 +1,121 @@ +import { + ActivitySnapshot, + RunFinished, + RunStarted, + StateSnapshot, + TurnComplete, + TurnStarted, + textMessage, + type ConverseEvent, +} from '../events.js'; + +const thermidorCatalogState = { + controllers: { + 'featured-products': { + products: [ + { + permanentid: 'trail-running-shoes-001', + ec_name: 'Peak Trail Running Shoes', + ec_shortdesc: 'Responsive trail shoes for everyday adventures.', + ec_brand: 'Thermidor Outdoor', + ec_category: ['Footwear', 'Trail Running'], + ec_price: 129.99, + ec_promo_price: 99.99, + ec_images: ['https://images.example.com/products/trail-running-shoes-001.jpg'], + ec_in_stock: true, + ec_rating: 4.7, + clickUri: '/products/trail-running-shoes-001', + additionalFields: {}, + }, + { + permanentid: 'summit-pack-020', + ec_name: 'Summit Day Pack', + ec_shortdesc: 'A compact, weather-ready 20 L day pack.', + ec_brand: 'Thermidor Outdoor', + ec_category: ['Bags', 'Day Packs'], + ec_price: 89.99, + ec_images: ['https://images.example.com/products/summit-pack-020.jpg'], + ec_in_stock: true, + ec_rating: 4.4, + clickUri: '/products/summit-pack-020', + additionalFields: {}, + }, + ], + }, + 'shopping-cart': { + items: [ + { + productId: 'trail-running-shoes-001', + name: 'Peak Trail Running Shoes', + price: 99.99, + quantity: 1, + }, + ], + }, + }, +}; + +const thermidorSchemaCatalogResponseEvents: ConverseEvent[] = [ + TurnStarted(), + RunStarted(), + ...textMessage( + 'thermidor-schema-catalog-message', + 'Here are featured products and the current cart from the Thermidor catalog contract.' + ), + StateSnapshot(thermidorCatalogState), + ActivitySnapshot({ + messageId: 'commerce-catalog-example', + activityType: 'a2ui-surface', + replace: true, + content: { + a2ui_operations: [ + { + version: 'v0.9', + createSurface: { + surfaceId: 'commerce-catalog-example', + catalogId: 'https://schema.thermidor.coveo.com/a2-ui/catalog.json', + }, + }, + { + version: 'v0.9', + updateComponents: { + surfaceId: 'commerce-catalog-example', + components: [ + { + id: 'root', + component: 'Column', + children: ['featured-products', 'cart'], + }, + { + id: 'featured-products', + component: 'ProductCarousel', + controllers: { + productListController: { + controllerId: 'featured-products', + controllerSchema: + 'https://schema.thermidor.coveo.com/controllers/product-list.schema.json', + }, + }, + }, + { + id: 'cart', + component: 'Cart', + controllers: { + cartController: { + controllerId: 'shopping-cart', + controllerSchema: + 'https://schema.thermidor.coveo.com/controllers/cart.schema.json', + }, + }, + }, + ], + }, + }, + ], + }, + }), + RunFinished(), + TurnComplete(), +]; + +export {thermidorSchemaCatalogResponseEvents}; diff --git a/packages/platform-mock-api/src/converse/templates/templates.ts b/packages/platform-mock-api/src/converse/templates/templates.ts index 43b032cf7d7..f047d137720 100644 --- a/packages/platform-mock-api/src/converse/templates/templates.ts +++ b/packages/platform-mock-api/src/converse/templates/templates.ts @@ -7,6 +7,7 @@ import {response5Events} from './response5.js'; import {response6Events} from './response6.js'; import {response7Events} from './response7.js'; import {response8Events} from './response8.js'; +import {thermidorSchemaCatalogResponseEvents} from './response9.js'; type TemplateId = | 'response1' @@ -16,7 +17,8 @@ type TemplateId = | 'response5' | 'response6' | 'response7' - | 'response8'; + | 'response8' + | 'thermidor-schema-catalog'; const templateEvents = { response1: response1Events, @@ -27,6 +29,7 @@ const templateEvents = { response6: response6Events, response7: response7Events, response8: response8Events, + 'thermidor-schema-catalog': thermidorSchemaCatalogResponseEvents, } satisfies Record; const getTemplateEvents = (templateId: TemplateId): ConverseEvent[] => templateEvents[templateId]; diff --git a/packages/thermidor-contracts/package.json b/packages/thermidor-contracts/package.json new file mode 100644 index 00000000000..91eddf173eb --- /dev/null +++ b/packages/thermidor-contracts/package.json @@ -0,0 +1,42 @@ +{ + "name": "@coveo/thermidor-contracts", + "version": "0.0.1", + "description": "Generated Zod contracts for Thermidor controller and A2-UI schemas", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/coveo/ui-kit.git", + "directory": "packages/thermidor-contracts" + }, + "files": [ + "dist" + ], + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "tsdown", + "clean": "node ../../utils/ci/rm-rf.mjs dist" + }, + "dependencies": { + "zod": "catalog:" + }, + "devDependencies": { + "tsdown": "0.22.0", + "typescript": "catalog:" + }, + "engines": { + "node": "^22.11.0 || ^24.11.0" + } +} diff --git a/packages/thermidor-contracts/src/generated/catalog-contracts.ts b/packages/thermidor-contracts/src/generated/catalog-contracts.ts new file mode 100644 index 00000000000..5de184a4d45 --- /dev/null +++ b/packages/thermidor-contracts/src/generated/catalog-contracts.ts @@ -0,0 +1,161 @@ +/* + * This file is generated from the Thermidor A2-UI catalog and controller JSON Schemas. + * Run `npm run generate:thermidor-contracts` in integration/thermidor-schema after changing the catalog. + */ +import {z} from 'zod'; + +const productCarouselControllersSchema = z + .object({ + productListController: z + .object({ + controllerId: z.string().min(1), + controllerSchema: z.literal( + 'https://schema.thermidor.coveo.com/controllers/product-list.schema.json' + ), + }) + .strict(), + }) + .strict(); + +export const productCarouselPropsSchema = z + .object({ + controllers: productCarouselControllersSchema, + }) + .strict(); + +const cartControllersSchema = z + .object({ + cartController: z + .object({ + controllerId: z.string().min(1), + controllerSchema: z.literal( + 'https://schema.thermidor.coveo.com/controllers/cart.schema.json' + ), + }) + .strict(), + }) + .strict(); + +export const cartPropsSchema = z + .object({ + controllers: cartControllersSchema, + }) + .strict(); + +export interface Product { + permanentid: string; + ec_name: string; + ec_description?: string; + ec_shortdesc?: string; + ec_brand?: string; + ec_category?: string[]; + ec_price?: number; + ec_promo_price?: number; + ec_images?: string[]; + ec_thumbnails?: string[]; + ec_in_stock?: boolean; + ec_rating?: number | null; + ec_color?: string; + ec_item_group_id?: string; + ec_item_group_name?: string; + clickUri?: string; + additionalFields: Record; + children?: Product[]; +} + +export const productSchema: z.ZodType = z.lazy(() => + z + .object({ + permanentid: z.string(), + ec_name: z.string(), + ec_description: z.string().optional(), + ec_shortdesc: z.string().optional(), + ec_brand: z.string().optional(), + ec_category: z.array(z.string()).optional(), + ec_price: z.number().optional(), + ec_promo_price: z.number().optional(), + ec_images: z.array(z.string().url()).optional(), + ec_thumbnails: z.array(z.string().url()).optional(), + ec_in_stock: z.boolean().optional(), + ec_rating: z.number().min(0).max(5).nullable().optional(), + ec_color: z.string().optional(), + ec_item_group_id: z.string().optional(), + ec_item_group_name: z.string().optional(), + clickUri: z.string().optional(), + additionalFields: z.record(z.unknown()), + children: z.array(productSchema).optional(), + }) + .strict() +); + +export const cartItemSchema = z + .object({ + productId: z.string(), + name: z.string(), + price: z.number().gt(0), + quantity: z.number().int().min(1), + }) + .strict(); + +export type CartItem = z.infer; + +export const productListControllerStateSchema = z + .object({ + products: z.array(productSchema), + }) + .strict(); + +export type ProductListControllerState = z.infer; + +export type ProductListController = z.infer; + +export const productListControllerContract = z + .object({ + schemaId: z.literal('https://schema.thermidor.coveo.com/controllers/product-list.schema.json'), + state: productListControllerStateSchema, + }) + .strict(); + +export const cartControllerStateSchema = z + .object({ + items: z.array(cartItemSchema), + }) + .strict(); + +export type CartControllerState = z.infer; + +export const cartControllerContractSetItemsPayloadSchema = z + .object({ + items: z.array(cartItemSchema), + }) + .strict(); + +export type SetItemsPayload = z.infer; + +export const cartControllerContractUpdateItemQuantityPayloadSchema = z + .object({ + item: cartItemSchema, + }) + .strict(); + +export type UpdateItemQuantityPayload = z.infer< + typeof cartControllerContractUpdateItemQuantityPayloadSchema +>; + +export type CartController = z.infer; + +export const cartControllerContract = z + .object({ + schemaId: z.literal('https://schema.thermidor.coveo.com/controllers/cart.schema.json'), + state: cartControllerStateSchema, + setItems: cartControllerContractSetItemsPayloadSchema, + updateItemQuantity: cartControllerContractUpdateItemQuantityPayloadSchema, + }) + .strict(); + +export const controllerContracts = z.discriminatedUnion('schemaId', [ + productListControllerContract, + cartControllerContract, +]); + +export type ControllerContracts = z.infer; diff --git a/packages/thermidor-contracts/src/index.ts b/packages/thermidor-contracts/src/index.ts new file mode 100644 index 00000000000..7f850b75f41 --- /dev/null +++ b/packages/thermidor-contracts/src/index.ts @@ -0,0 +1 @@ +export * from './generated/catalog-contracts.js'; diff --git a/packages/thermidor-contracts/tsdown.config.ts b/packages/thermidor-contracts/tsdown.config.ts new file mode 100644 index 00000000000..de957210f94 --- /dev/null +++ b/packages/thermidor-contracts/tsdown.config.ts @@ -0,0 +1,11 @@ +import {defineConfig} from 'tsdown'; + +export default defineConfig({ + entry: ['src/index.ts'], + outDir: 'dist', + format: 'esm', + unbundle: true, + dts: true, + fixedExtension: false, + treeshake: false, +}); diff --git a/packages/thermidor/package.json b/packages/thermidor/package.json index 298242705ad..0facae1d946 100644 --- a/packages/thermidor/package.json +++ b/packages/thermidor/package.json @@ -35,7 +35,9 @@ }, "dependencies": { "@ag-ui/core": "0.0.57", - "@reduxjs/toolkit": "catalog:" + "@coveo/thermidor-contracts": "workspace:*", + "@reduxjs/toolkit": "catalog:", + "zod": "catalog:" }, "devDependencies": { "@microsoft/api-extractor": "7.58.12", diff --git a/packages/thermidor/src/index.ts b/packages/thermidor/src/index.ts index 25923c9c5b4..dd93e6d0e2f 100644 --- a/packages/thermidor/src/index.ts +++ b/packages/thermidor/src/index.ts @@ -39,7 +39,7 @@ export type { Supports, } from '@/src/internal/utils/index.js'; export type { - A2UISurface, + Activity, AgentMessage, AgentResponse, ReasoningMessageStep, diff --git a/packages/thermidor/src/internal/api/conversation/conversation-endpoint-types.ts b/packages/thermidor/src/internal/api/conversation/conversation-endpoint-types.ts index c58574b251b..e06daae31c9 100644 --- a/packages/thermidor/src/internal/api/conversation/conversation-endpoint-types.ts +++ b/packages/thermidor/src/internal/api/conversation/conversation-endpoint-types.ts @@ -7,13 +7,12 @@ export interface CoveoConversationCartItem { quantity: number; } -export interface CoveoConversationEndpointRequest { +export interface CoveoConversationEndpointRequestBase { trackingId?: string; language?: string; country?: string; currency?: string; clientId?: string; - message: string; context: { user: { userAgent?: string | null; @@ -33,6 +32,30 @@ export interface CoveoConversationEndpointRequest { facets?: Array<{facetId: string; selectedValues: string[]}>; } +export interface CoveoConversationMessageRequest extends CoveoConversationEndpointRequestBase { + message: string; +} + +/** + * A schema-derived mutation for one server-owned controller state entry. + * `controllerSchema` identifies the generated contract that defines `action` + * and validates `payload`; `controllerId` identifies its runtime snapshot key. + */ +export interface CoveoConversationControllerAction { + controllerId: string; + controllerSchema: string; + action: string; + payload: unknown; +} + +export interface CoveoConversationActionRequest extends CoveoConversationEndpointRequestBase { + action: CoveoConversationControllerAction; +} + +export type CoveoConversationEndpointRequest = + | CoveoConversationMessageRequest + | CoveoConversationActionRequest; + export interface CoveoConversationEndpointResponse { stream: ReadableStream; } diff --git a/packages/thermidor/src/internal/api/conversation/index.ts b/packages/thermidor/src/internal/api/conversation/index.ts index 2fea3504301..1fc70c63f47 100644 --- a/packages/thermidor/src/internal/api/conversation/index.ts +++ b/packages/thermidor/src/internal/api/conversation/index.ts @@ -7,8 +7,11 @@ export type { } from './conversation-endpoint-client.js'; export type { CoveoConversationCartItem, + CoveoConversationActionRequest, + CoveoConversationControllerAction, CoveoConversationEndpointRequest, CoveoConversationEndpointResponse, + CoveoConversationMessageRequest, } from './conversation-endpoint-types.js'; export {readConversationEventStream} from './conversation-event-stream.js'; export type { diff --git a/packages/thermidor/src/internal/api/generative/generative-runtime.test.ts b/packages/thermidor/src/internal/api/generative/generative-runtime.test.ts index 4e6c597b32d..1e184171c48 100644 --- a/packages/thermidor/src/internal/api/generative/generative-runtime.test.ts +++ b/packages/thermidor/src/internal/api/generative/generative-runtime.test.ts @@ -37,6 +37,7 @@ vi.mock('@/src/internal/utils/index.js', () => ({ function createMockStatePort(): GenerativeStatePort { return { + getActiveTurnId: vi.fn().mockReturnValue('active-turn-id'), createTurn: vi.fn(), setActiveTurnId: vi.fn(), replaceTurnId: vi.fn(), @@ -44,7 +45,8 @@ function createMockStatePort(): GenerativeStatePort { initAgentResponse: vi.fn(), startMessage: vi.fn(), appendMessageDelta: vi.fn(), - appendSurface: vi.fn(), + appendActivity: vi.fn(), + setStateSnapshot: vi.fn(), startToolCall: vi.fn(), appendToolCallArgs: vi.fn(), completeToolCall: vi.fn(), @@ -279,6 +281,57 @@ describe('GenerativeRuntime', () => { }); }); + describe('dispatchAction', () => { + it('posts a schema-derived action and applies its state snapshot to the active turn', async () => { + const config = createMockConfig(); + const engine = createMockEngine(); + const {mockClient} = setupSuccessfulStream([ + { + type: 'STATE_SNAPSHOT', + snapshot: {controllers: {'shopping-cart': {items: []}}}, + } as ConversationStreamEvent, + ]); + + const runtime = GenerativeRuntime.getInstance(engine, 'dispatch-action', config); + await runtime.dispatchAction({ + controllerId: 'shopping-cart', + controllerSchema: 'https://schema.thermidor.coveo.com/controllers/cart.schema.json', + action: 'updateItemQuantity', + payload: {item: {productId: 'p1', quantity: 2}}, + }); + + expect(mockClient.call).toHaveBeenCalledWith( + expect.objectContaining({ + action: { + controllerId: 'shopping-cart', + controllerSchema: 'https://schema.thermidor.coveo.com/controllers/cart.schema.json', + action: 'updateItemQuantity', + payload: {item: {productId: 'p1', quantity: 2}}, + }, + }), + expect.anything() + ); + expect(config.statePort.setStateSnapshot).toHaveBeenCalledWith('active-turn-id', { + controllers: {'shopping-cart': {items: []}}, + }); + }); + + it('rejects when no active turn can receive the resulting snapshot', async () => { + const config = createMockConfig(); + vi.mocked(config.statePort.getActiveTurnId).mockReturnValue(undefined); + const runtime = GenerativeRuntime.getInstance(createMockEngine(), 'no-active-turn', config); + + await expect( + runtime.dispatchAction({ + controllerId: 'shopping-cart', + controllerSchema: 'https://schema.thermidor.coveo.com/controllers/cart.schema.json', + action: 'updateItemQuantity', + payload: {}, + }) + ).rejects.toThrow('without an active conversation turn'); + }); + }); + describe('stream consumption', () => { it('fails the turn when stream ends without a terminal event', async () => { const config = createMockConfig(); @@ -453,16 +506,16 @@ describe('GenerativeRuntime', () => { ); }); - it('handles ACTIVITY_SNAPSHOT by appending surface', async () => { + it('normalizes ACTIVITY_SNAPSHOT before appending an activity', async () => { const config = createMockConfig(); const engine = createMockEngine(); - const surface = {component: 'product-card', data: {id: 'p1'}}; + const payload = {component: 'product-card', data: {id: 'p1'}}; setupSuccessfulStream([ { type: 'ACTIVITY_SNAPSHOT', messageId: 'm1', activityType: 'ui-surface', - content: surface, + content: payload, replace: false, } as ConversationStreamEvent, {type: 'turn_complete'} as ConversationStreamEvent, @@ -471,7 +524,12 @@ describe('GenerativeRuntime', () => { const runtime = GenerativeRuntime.getInstance(engine, 'activity', config); await runtime.submit('Hello'); - expect(config.statePort.appendSurface).toHaveBeenCalledWith('generated-id-1', surface); + expect(config.statePort.appendActivity).toHaveBeenCalledWith('generated-id-1', { + id: 'm1', + kind: 'ui-surface', + payload, + replace: false, + }); }); it('handles commerce_search_api_response by routing when hydration succeeds', async () => { @@ -632,17 +690,20 @@ describe('GenerativeRuntime', () => { expect(config.statePort.completeTurn).toHaveBeenCalledWith('generated-id-1'); }); - it('ignores STATE_SNAPSHOT events', async () => { + it('stores STATE_SNAPSHOT events as opaque Engine state', async () => { const config = createMockConfig(); const engine = createMockEngine(); + const snapshot = {controllers: {'featured-products': {products: []}}}; setupSuccessfulStream([ - {type: 'STATE_SNAPSHOT'} as ConversationStreamEvent, + {type: 'STATE_SNAPSHOT', snapshot} as ConversationStreamEvent, {type: 'turn_complete'} as ConversationStreamEvent, ]); const runtime = GenerativeRuntime.getInstance(engine, 'state-snap', config); await runtime.submit('Hello'); + expect(config.statePort.initAgentResponse).toHaveBeenCalledWith('generated-id-1'); + expect(config.statePort.setStateSnapshot).toHaveBeenCalledWith('generated-id-1', snapshot); expect(config.statePort.completeTurn).toHaveBeenCalled(); }); diff --git a/packages/thermidor/src/internal/api/generative/generative-runtime.ts b/packages/thermidor/src/internal/api/generative/generative-runtime.ts index 5533092715f..e88d8d546d2 100644 --- a/packages/thermidor/src/internal/api/generative/generative-runtime.ts +++ b/packages/thermidor/src/internal/api/generative/generative-runtime.ts @@ -1,6 +1,9 @@ import { readConversationEventStream, type ConversationStreamEvent, + type CoveoConversationActionRequest, + type CoveoConversationControllerAction, + type CoveoConversationMessageRequest, createConversationEndpointClient, } from '@/src/internal/api/conversation/index.js'; import type {FullEngine} from '@/src/internal/engine/index.js'; @@ -9,13 +12,14 @@ import {createConversationEndpointRequestSelector} from '@/src/internal/api/conv import {getOrCreateConfigurationSelectors} from '@/src/internal/features/configuration/index.js'; import {generateId} from '@/src/internal/utils/index.js'; import type { - A2UISurface, + Activity, RoutedUseCase, TurnStatus, UseCaseInterfaceMap, } from '@/src/internal/features/generative/index.js'; export interface GenerativeStatePort { + getActiveTurnId(): string | undefined; createTurn(payload: {id: string; prompt: string; status: TurnStatus}): void; setActiveTurnId(id: string): void; replaceTurnId(oldId: string, newId: string): void; @@ -23,7 +27,8 @@ export interface GenerativeStatePort { initAgentResponse(turnId: string): void; startMessage(turnId: string, role: string): void; appendMessageDelta(turnId: string, delta: string): void; - appendSurface(turnId: string, surface: A2UISurface): void; + appendActivity(turnId: string, activity: Activity): void; + setStateSnapshot(turnId: string, state: Record): void; startToolCall(turnId: string, toolCallId: string, toolName: string): void; appendToolCallArgs(turnId: string, toolCallId: string, delta: string): void; completeToolCall(turnId: string, toolCallId: string, result: string): void; @@ -118,30 +123,48 @@ export class GenerativeRuntime { await this.executeStream(turnId); } + /** + * Sends a schema-derived UI action through the same authenticated conversation + * transport as prompts. The gateway owns the mutation and replies with a + * stream, including the resulting `STATE_SNAPSHOT` for the active turn. + */ + async dispatchAction(action: CoveoConversationControllerAction): Promise { + const turnId = this.statePort.getActiveTurnId(); + if (!turnId) { + throw new Error('Cannot dispatch a controller action without an active conversation turn.'); + } + + const {message: _message, ...requestBase} = this.createConversationRequest(); + const request: CoveoConversationActionRequest = {...requestBase, action}; + const clientConfig = this.engine.read(this.configSelectors.getEndpointClientConfiguration); + const client = createConversationEndpointClient(); + const result = await client.call(request, clientConfig); + + if (!result.success) { + throw new Error(result.error); + } + + let streamError: unknown; + await readConversationEventStream({ + stream: result.data.stream, + onEvent: (event) => { + this.dispatchEvent(turnId, event); + }, + onError: (error) => { + streamError = error; + }, + }); + + if (streamError) { + throw streamError; + } + } + private async executeStream(turnId: string): Promise { try { - const {cart, ...fromState} = this.engine.read(this.buildRequest); - const navigatorContext = this.engine.getNavigatorContextProvider()?.(); const clientConfig = this.engine.read(this.configSelectors.getEndpointClientConfiguration); - - const request = { - ...fromState, - clientId: navigatorContext?.clientId ?? undefined, - context: { - user: { - userAgent: navigatorContext?.userAgent ?? null, - }, - view: { - url: navigatorContext?.location ?? null, - referrer: navigatorContext?.referrer ?? null, - }, - ...(cart ? {cart} : {}), - }, - targetEngine: 'AGENT_CORE' as const, - }; - const client = createConversationEndpointClient(); - const result = await client.call(request, clientConfig); + const result = await client.call(this.createConversationRequest(), clientConfig); if (!result.success) { this.statePort.failTurn(turnId, result.error); @@ -154,6 +177,27 @@ export class GenerativeRuntime { } } + private createConversationRequest(): CoveoConversationMessageRequest { + const {cart, ...fromState} = this.engine.read(this.buildRequest); + const navigatorContext = this.engine.getNavigatorContextProvider()?.(); + + return { + ...fromState, + clientId: navigatorContext?.clientId ?? undefined, + context: { + user: { + userAgent: navigatorContext?.userAgent ?? null, + }, + view: { + url: navigatorContext?.location ?? null, + referrer: navigatorContext?.referrer ?? null, + }, + ...(cart ? {cart} : {}), + }, + targetEngine: 'AGENT_CORE' as const, + }; + } + private async consumeStream(turnId: string, stream: ReadableStream): Promise { let activeTurnId = turnId; let terminalEventReceived = false; @@ -249,12 +293,19 @@ export class GenerativeRuntime { } case 'STATE_SNAPSHOT': { + this.ensureAgentResponse(turnId); + this.statePort.setStateSnapshot(turnId, asRecord(event.snapshot)); return {turnId, isTerminal: false}; } case 'ACTIVITY_SNAPSHOT': { this.ensureAgentResponse(turnId); - this.statePort.appendSurface(turnId, event.content as Record); + this.statePort.appendActivity(turnId, { + id: event.messageId, + kind: event.activityType, + payload: event.content as Record, + replace: event.replace, + }); return {turnId, isTerminal: false}; } @@ -306,6 +357,12 @@ export class GenerativeRuntime { } } +function asRecord(value: unknown): Record { + return value && typeof value === 'object' && !Array.isArray(value) + ? (value as Record) + : {}; +} + function getErrorMessage(error: unknown): string { if (error instanceof Error && error.message.trim()) { return error.message; diff --git a/packages/thermidor/src/internal/features/generative/generative-actions.ts b/packages/thermidor/src/internal/features/generative/generative-actions.ts index 20706c41204..fdc21346110 100644 --- a/packages/thermidor/src/internal/features/generative/generative-actions.ts +++ b/packages/thermidor/src/internal/features/generative/generative-actions.ts @@ -2,7 +2,7 @@ import {createAction} from '@reduxjs/toolkit'; import {type CacheKey, createCacheKey} from '@/src/internal/utils/index.js'; import {getInterfaceInternals} from '@/src/internal/utils/index.js'; import type {InterfaceHandle} from '@/src/internal/utils/index.js'; -import type {A2UISurface, GenerativeState, RoutedUseCase, TurnStatus} from './generative-types.js'; +import type {Activity, GenerativeState, RoutedUseCase, TurnStatus} from './generative-types.js'; type GenerativeActions = ReturnType; @@ -26,7 +26,10 @@ export function createGenerativeActions(interfaceId: string) { appendMessageDelta: createAction<{turnId: string; delta: string}>( `${prefix}/appendMessageDelta` ), - appendSurface: createAction<{turnId: string; surface: A2UISurface}>(`${prefix}/appendSurface`), + appendActivity: createAction<{turnId: string; activity: Activity}>(`${prefix}/appendActivity`), + setStateSnapshot: createAction<{turnId: string; state: Record}>( + `${prefix}/setStateSnapshot` + ), startToolCall: createAction<{ turnId: string; toolCallId: string; diff --git a/packages/thermidor/src/internal/features/generative/generative-slice.ts b/packages/thermidor/src/internal/features/generative/generative-slice.ts index a08047f698a..65766db74dd 100644 --- a/packages/thermidor/src/internal/features/generative/generative-slice.ts +++ b/packages/thermidor/src/internal/features/generative/generative-slice.ts @@ -61,8 +61,9 @@ export function createGenerativeSlice( const turn = state.turns.find((t) => t.id === payload.turnId); if (turn) { turn.agentResponse = { + state: {}, messages: [], - surfaces: [], + activities: [], reasoningSteps: [], }; } @@ -80,10 +81,16 @@ export function createGenerativeSlice( messages[messages.length - 1].content += payload.delta; } }) - .addCase(actions.appendSurface, (state, {payload}) => { + .addCase(actions.appendActivity, (state, {payload}) => { const turn = state.turns.find((t) => t.id === payload.turnId); if (turn?.agentResponse) { - turn.agentResponse.surfaces.push(payload.surface); + turn.agentResponse.activities.push(payload.activity); + } + }) + .addCase(actions.setStateSnapshot, (state, {payload}) => { + const turn = state.turns.find((t) => t.id === payload.turnId); + if (turn?.agentResponse) { + turn.agentResponse.state = payload.state; } }) .addCase(actions.startToolCall, (state, {payload}) => { diff --git a/packages/thermidor/src/internal/features/generative/generative-types.ts b/packages/thermidor/src/internal/features/generative/generative-types.ts index d91688607ab..bdb01a4d1b9 100644 --- a/packages/thermidor/src/internal/features/generative/generative-types.ts +++ b/packages/thermidor/src/internal/features/generative/generative-types.ts @@ -111,15 +111,25 @@ export type RoutedInterface = { }[RoutedUseCase]; export interface AgentResponse { + /** + * The latest server-owned AG-UI state snapshot for this turn. + * + * Thermidor retains this opaque object without coupling it to a UI protocol. + */ + state: Record; + /** * The ordered messages received from the agent during streaming. */ messages: AgentMessage[]; /** - * The opaque A2UI surfaces received during streaming. + * Structured activities emitted by the agent during streaming. + * + * Thermidor keeps each activity opaque; applications select and interpret the + * activity kinds they support. */ - surfaces: A2UISurface[]; + activities: Activity[]; /** * An ordered sequence of reasoning steps that preserves the temporal @@ -189,9 +199,18 @@ export interface AgentMessage { } /** - * Opaque surface data passed through from `/converse` without interpretation. + * A normalized, structured activity emitted by an agent. + * + * `kind` and `payload` deliberately do not prescribe a presentation protocol. + * Applications can use them for A2-UI, another UI protocol, or a non-visual + * integration without Thermidor taking a dependency on any of those choices. */ -export type A2UISurface = Record; +export interface Activity { + id: string | undefined; + kind: string | undefined; + payload: Record; + replace: boolean | undefined; +} export interface GenerativeState { /** diff --git a/packages/thermidor/src/internal/features/generative/index.ts b/packages/thermidor/src/internal/features/generative/index.ts index 5b6a3f8333d..5f5ee9b45ef 100644 --- a/packages/thermidor/src/internal/features/generative/index.ts +++ b/packages/thermidor/src/internal/features/generative/index.ts @@ -12,7 +12,7 @@ export { } from './routed-interface-registry.js'; export type {RoutedInterfaceEntry, RoutedInterfaceRegistry} from './routed-interface-registry.js'; export type { - A2UISurface, + Activity, AgentMessage, AgentResponse, GenerativeState, diff --git a/packages/thermidor/src/public/controllers/converse/converse-controller.test.ts b/packages/thermidor/src/public/controllers/converse/converse-controller.test.ts index bd7651177c4..72deae041b4 100644 --- a/packages/thermidor/src/public/controllers/converse/converse-controller.test.ts +++ b/packages/thermidor/src/public/controllers/converse/converse-controller.test.ts @@ -11,11 +11,13 @@ import { } from '@/src/public/interfaces/generative.js'; import {buildConverseController} from './converse-controller.js'; import type {SerializedConverseState} from './converse-controller-serialization.js'; +import type {RemoteControllerAction} from '../remote/remote-controller.js'; const TEST_ID = 'test-generative'; const mockSubmit = vi.fn<(prompt: string) => Promise>(); const mockResubmit = vi.fn<(turnId: string, prompt: string) => Promise>(); +const mockDispatchAction = vi.fn<(action: RemoteControllerAction) => Promise>(); const mockSetConversationSession = vi.fn<(sessionId: string | undefined, token: string | undefined) => void>(); const mockGetConversationSessionId = vi.fn<() => string | undefined>(); @@ -26,6 +28,7 @@ vi.mock('@/src/internal/api/generative/index.js', () => ({ getInstance: vi.fn(() => ({ submit: mockSubmit, resubmit: mockResubmit, + dispatchAction: mockDispatchAction, setConversationSession: mockSetConversationSession, getConversationSessionId: mockGetConversationSessionId, getConversationToken: mockGetConversationToken, @@ -53,11 +56,13 @@ describe('buildConverseController', () => { vi.clearAllMocks(); mockSubmit.mockReset(); mockResubmit.mockReset(); + mockDispatchAction.mockReset(); mockSetConversationSession.mockReset(); mockGetConversationSessionId.mockReset(); mockGetConversationToken.mockReset(); mockSubmit.mockResolvedValue(); mockResubmit.mockResolvedValue(); + mockDispatchAction.mockResolvedValue(); mockGetConversationSessionId.mockReturnValue(undefined); mockGetConversationToken.mockReturnValue(undefined); engine = createTestEngine(); @@ -195,6 +200,22 @@ describe('buildConverseController', () => { }); }); + describe('dispatchAction()', () => { + it('delegates a schema-derived controller action to the runtime', async () => { + const controller = buildController(); + const action = { + controllerId: 'shopping-cart', + controllerSchema: 'https://schema.thermidor.coveo.com/controllers/cart.schema.json', + action: 'updateItemQuantity', + payload: {item: {productId: 'p1', quantity: 2}}, + }; + + await controller.dispatchAction(action); + + expect(mockDispatchAction).toHaveBeenCalledWith(action); + }); + }); + describe('retry()', () => { it('calls runtime.resubmit when the turn exists and has error status', () => { const controller = buildController(); diff --git a/packages/thermidor/src/public/controllers/converse/converse-controller.ts b/packages/thermidor/src/public/controllers/converse/converse-controller.ts index 05181fe6256..ddb2b91db09 100644 --- a/packages/thermidor/src/public/controllers/converse/converse-controller.ts +++ b/packages/thermidor/src/public/controllers/converse/converse-controller.ts @@ -2,16 +2,18 @@ import type {GenerativeState, StateTurn, Turn} from '@/src/internal/features/gen import {GenerativeRuntime} from '@/src/internal/api/generative/index.js'; import { createHydrateSubInterface, + getOrCreateGenerativeActions, + getOrCreateGenerativeSelectors, getOrCreateRoutedInterfaceRegistry, mergeTurnsWithRegistry, } from '@/src/internal/features/generative/index.js'; -import {BaseController} from '@/src/internal/utils/index.js'; -import {createMemoizedStateSelector} from '@/src/internal/utils/index.js'; -import {getInterfaceInternals} from '@/src/internal/utils/index.js'; -import {getOrCreateGenerativeActions} from '@/src/internal/features/generative/index.js'; -import {getOrCreateGenerativeSelectors} from '@/src/internal/features/generative/index.js'; -import type {GenerativeInterface} from '@/src/internal/utils/index.js'; -import type {Controller} from '@/src/internal/utils/index.js'; +import { + BaseController, + createMemoizedStateSelector, + getInterfaceInternals, +} from '@/src/internal/utils/index.js'; +import type {Controller, GenerativeInterface} from '@/src/internal/utils/index.js'; +import type {RemoteControllerAction} from '../remote/remote-controller.js'; import {SerializedConverseState, SerializedTurn} from './converse-controller-serialization.js'; class ConverseControllerImpl extends BaseController { @@ -55,6 +57,9 @@ class ConverseControllerImpl extends BaseController { generativeInterface: options.interface, cartInterface: options.interface, statePort: { + getActiveTurnId: () => { + return this.engine.read(this.#selectors.getActiveTurnId); + }, createTurn: (payload) => { this.engine.mutate(this.#actions.createTurn(payload)); }, @@ -85,12 +90,11 @@ class ConverseControllerImpl extends BaseController { appendMessageDelta: (turnId, delta) => { this.engine.mutate(this.#actions.appendMessageDelta({turnId, delta})); }, - appendSurface: (turnId, surface) => { - this.engine.mutate(this.#actions.appendSurface({turnId, surface})); - const ops = (surface as {operations?: unknown[]}).operations; - if (Array.isArray(ops)) { - options.onSurfaceOperation?.(ops); - } + appendActivity: (turnId, activity) => { + this.engine.mutate(this.#actions.appendActivity({turnId, activity})); + }, + setStateSnapshot: (turnId, state) => { + this.engine.mutate(this.#actions.setStateSnapshot({turnId, state})); }, startToolCall: (turnId, toolCallId, toolName) => { this.engine.mutate(this.#actions.startToolCall({turnId, toolCallId, toolName})); @@ -198,6 +202,10 @@ class ConverseControllerImpl extends BaseController { } this.#runtime.resubmit(id, turn.prompt); } + + dispatchAction(action: RemoteControllerAction): Promise { + return this.#runtime.dispatchAction(action); + } } export const buildConverseController = (options: ConverseControllerOptions): ConverseController => @@ -210,6 +218,8 @@ export interface ConverseController extends Controller submit(options: {prompt: string}): void; selectTurn(options: {id: string}): void; retry(options: {id: string}): void; + /** Sends a schema-derived remote controller action to the AG-UI gateway. */ + dispatchAction(action: RemoteControllerAction): Promise; } export interface ConverseControllerState { @@ -221,7 +231,6 @@ export interface ConverseControllerState { export interface ConverseControllerOptions { interface: GenerativeInterface; conversationToRestore?: SerializedConverseState; - onSurfaceOperation?: (operations: unknown[]) => void; } function hydrateFromSerializedState(serialized: SerializedConverseState): GenerativeState { diff --git a/packages/thermidor/src/public/controllers/index.ts b/packages/thermidor/src/public/controllers/index.ts index 3c4a9fc918d..e560134a366 100644 --- a/packages/thermidor/src/public/controllers/index.ts +++ b/packages/thermidor/src/public/controllers/index.ts @@ -38,6 +38,19 @@ export type { ProductListControllerProduct, ProductListControllerState, } from './product-list/product-list-controller.js'; +export {buildRemoteController, selectRemoteControllerState} from './remote/remote-controller.js'; +export type { + RemoteController, + RemoteControllerAction, + RemoteControllerActionNameForSchema, + RemoteControllerActionPayloadForSchema, + AdvertisedRemoteController, + RemoteControllerOptions, + RemoteControllerSchemaId, + RemoteControllerSource, + RemoteControllerStateForSchema, + RemoteControllerActionsForSchema, +} from './remote/remote-controller.js'; export {buildPaginationController} from './pagination/pagination-controller.js'; export type { PaginationController, diff --git a/packages/thermidor/src/public/controllers/remote/remote-controller.test.ts b/packages/thermidor/src/public/controllers/remote/remote-controller.test.ts new file mode 100644 index 00000000000..39ea915cdfb --- /dev/null +++ b/packages/thermidor/src/public/controllers/remote/remote-controller.test.ts @@ -0,0 +1,102 @@ +import {describe, expect, it, vi} from 'vitest'; +import {cartControllerContract} from '@coveo/thermidor-contracts'; +import { + buildRemoteController, + selectRemoteControllerState, + type RemoteControllerSource, +} from './remote-controller.js'; + +const cartContract = cartControllerContract.shape.schemaId.value; +const cartItem = {productId: 'p1', name: 'Product', price: 10, quantity: 2}; + +describe('buildRemoteController', () => { + it('selects its server-owned state from the active conversation turn', () => { + const source = createSource({controllers: {cart: {items: [cartItem]}}}); + const controller = buildRemoteController({ + source, + controllerId: 'cart', + contract: cartContract, + }); + + expect(controller.state).toEqual({items: [cartItem]}); + }); + + it('notifies subscribers when its snapshot slice changes, but not for another controller', () => { + const cart = {items: []}; + const source = createSource({controllers: {cart, products: {products: []}}}); + const controller = buildRemoteController({ + source, + controllerId: 'cart', + contract: cartContract, + }); + const callback = vi.fn(); + + controller.subscribe(callback); + source.setSnapshot({controllers: {cart, products: {products: ['p1']}}}); + expect(callback).not.toHaveBeenCalled(); + + source.setSnapshot({controllers: {cart: {items: [cartItem]}}}); + expect(callback).toHaveBeenCalledWith({items: [cartItem]}); + }); + + it('dispatches schema-derived actions without locally changing server-owned state', async () => { + const source = createSource({controllers: {cart: {items: []}}}); + const controller = buildRemoteController({ + source, + controllerId: 'cart', + contract: cartContract, + }); + + await controller.dispatch('updateItemQuantity', {item: cartItem}); + + expect(source.dispatchAction).toHaveBeenCalledWith({ + controllerId: 'cart', + controllerSchema: 'https://schema.thermidor.coveo.com/controllers/cart.schema.json', + action: 'updateItemQuantity', + payload: {item: cartItem}, + }); + expect(controller.state).toEqual({items: []}); + }); + + it('returns undefined for an invalid snapshot and rejects an invalid action payload', async () => { + const controller = buildRemoteController({ + source: createSource({controllers: {cart: {items: 'invalid'}}}), + controllerId: 'cart', + contract: cartContract, + }); + + expect(controller.state).toBeUndefined(); + await expect( + controller.dispatch('updateItemQuantity', {item: {...cartItem, quantity: 0}}) + ).rejects.toThrow('Invalid payload'); + }); +}); + +describe('selectRemoteControllerState', () => { + it('returns the stable empty state when no matching snapshot entry exists', () => { + const state = {activeTurn: {agentResponse: {state: {controllers: {}}}}}; + + expect(selectRemoteControllerState(state as never, 'missing')).toEqual({}); + }); +}); + +function createSource(snapshot: Record) { + const listeners = new Set<() => void>(); + const source = { + state: {activeTurn: {agentResponse: {state: snapshot}}}, + dispatchAction: vi.fn().mockResolvedValue(undefined), + subscribe(listener: () => void) { + listeners.add(listener); + return () => listeners.delete(listener); + }, + setSnapshot(nextSnapshot: Record) { + source.state = {activeTurn: {agentResponse: {state: nextSnapshot}}}; + listeners.forEach((listener) => listener()); + }, + }; + + return source as unknown as RemoteControllerSource & { + dispatchAction: ReturnType; + setSnapshot(snapshot: Record): void; + }; +} diff --git a/packages/thermidor/src/public/controllers/remote/remote-controller.ts b/packages/thermidor/src/public/controllers/remote/remote-controller.ts new file mode 100644 index 00000000000..aa941907bf2 --- /dev/null +++ b/packages/thermidor/src/public/controllers/remote/remote-controller.ts @@ -0,0 +1,203 @@ +import type {z} from 'zod'; +import {controllerContracts, type ControllerContracts} from '@coveo/thermidor-contracts'; +import type {ConverseController} from '../converse/converse-controller.js'; +import type {Controller} from '../controller-types.js'; + +export interface RemoteControllerAction { + controllerId: string; + controllerSchema: string; + action: TAction; + payload: TPayload; +} + +export type RemoteControllerSchemaId = ControllerContracts['schemaId']; + +type ControllerContractSchema = (typeof controllerContracts)['options'][number]; + +type ControllerContractSchemaFor = Extract< + ControllerContractSchema, + {shape: {schemaId: {value: TSchema}}} +>; + +export type RemoteControllerActionNameForSchema = Exclude< + keyof Extract, + 'schemaId' | 'state' +> & + string; + +export type RemoteControllerActionPayloadForSchema< + TSchema extends RemoteControllerSchemaId, + TAction extends RemoteControllerActionNameForSchema, +> = + Extract extends Record + ? TPayload + : never; + +export type RemoteControllerStateForSchema = z.infer< + Extract< + (typeof controllerContracts)['options'][number], + {shape: {schemaId: {value: TSchema}}} + >['shape']['state'] +>; + +export type RemoteControllerActionsForSchema = Omit< + Extract, + 'schemaId' | 'state' +>; + +/** + * A controller state source backed by Thermidor's active conversation turn. + */ +export type RemoteControllerSource = Pick< + ConverseController, + 'state' | 'subscribe' | 'dispatchAction' +>; + +export interface RemoteController extends Controller< + RemoteControllerStateForSchema | undefined +> { + readonly controllerId: string; + dispatch>( + action: TAction, + payload: RemoteControllerActionPayloadForSchema + ): Promise; +} + +export type AdvertisedRemoteController = + RemoteController; + +export interface RemoteControllerOptions { + source: RemoteControllerSource; + controllerId: string; + /** The static controller schema ID advertised by the A2-UI component. */ + contract: TSchema; +} + +class RemoteControllerImpl< + TSchema extends RemoteControllerSchemaId, +> implements RemoteController { + readonly controllerId: string; + #lastRawState: unknown; + #lastValidatedState: RemoteControllerStateForSchema | undefined; + + constructor( + private readonly source: RemoteControllerSource, + controllerId: string, + private readonly contract: ControllerContractSchemaFor + ) { + this.controllerId = controllerId; + } + + get state(): RemoteControllerStateForSchema | undefined { + const rawState = selectRemoteControllerState(this.source.state, this.controllerId); + if (rawState === this.#lastRawState) { + return this.#lastValidatedState; + } + + this.#lastRawState = rawState; + const result = this.contract.shape.state.safeParse(rawState); + this.#lastValidatedState = + result.success && isRemoteControllerState(this.contract, result.data) + ? result.data + : undefined; + return this.#lastValidatedState; + } + + subscribe( + callback: (state: RemoteControllerStateForSchema | undefined) => void + ): () => void { + let previousState = this.state; + + return this.source.subscribe(() => { + const nextState = this.state; + if (nextState === previousState) { + return; + } + + previousState = nextState; + callback(nextState); + }); + } + + dispatch>( + action: TAction, + payload: RemoteControllerActionPayloadForSchema + ): Promise { + const actionSchema = Object.entries(this.contract.shape).find(([name]) => name === action)?.[1]; + if (!actionSchema) { + return Promise.reject(new Error(`Unknown controller action ${this.controllerId}/${action}.`)); + } + + const result = actionSchema.safeParse(payload); + if (!result.success) { + return Promise.reject( + new Error(`Invalid payload for controller action ${this.controllerId}/${action}.`) + ); + } + + return this.source.dispatchAction({ + controllerId: this.controllerId, + controllerSchema: this.contract.shape.schemaId.value, + action, + payload: result.data, + }); + } +} + +/** + * Creates a controller for one server-owned entry in the active AG-UI state + * snapshot. The controller never mutates its local state; action results arrive + * through a subsequent snapshot from the server. + */ + +export function buildRemoteController( + options: RemoteControllerOptions +): RemoteController { + const schema = findControllerContract(options.contract); + return new RemoteControllerImpl(options.source, options.controllerId, schema); +} + +function findControllerContract( + schemaId: TSchema +): ControllerContractSchemaFor { + const contract = controllerContracts.options.find( + (candidate): candidate is ControllerContractSchemaFor => + candidate.shape.schemaId.value === schemaId + ); + if (!contract) { + throw new Error(`Unknown controller contract ${schemaId}.`); + } + + return contract; +} + +function isRemoteControllerState( + contract: ControllerContractSchemaFor, + state: unknown +): state is RemoteControllerStateForSchema { + return contract.shape.state.safeParse(state).success; +} + +const EMPTY_REMOTE_CONTROLLER_STATE = {}; + +export function selectRemoteControllerState( + state: RemoteControllerSource['state'], + controllerId: string +): unknown { + const snapshot = state.activeTurn?.agentResponse?.state; + if (!isRecord(snapshot)) { + return EMPTY_REMOTE_CONTROLLER_STATE; + } + + const controllers = snapshot['controllers']; + if (!isRecord(controllers)) { + return EMPTY_REMOTE_CONTROLLER_STATE; + } + + const controllerState = controllers[controllerId]; + return isRecord(controllerState) ? controllerState : EMPTY_REMOTE_CONTROLLER_STATE; +} + +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e3018f51e91..4781defd70d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -219,6 +219,9 @@ catalogs: yaml: specifier: 2.9.0 version: 2.9.0 + zod: + specifier: 3.25.76 + version: 3.25.76 zone.js: specifier: 0.15.1 version: 0.15.1 @@ -1216,10 +1219,10 @@ importers: version: 1.60.0 '@salesforce/eslint-config-lwc': specifier: 3.7.2 - version: 3.7.2(@lwc/eslint-plugin-lwc@2.2.0(@babel/eslint-parser@7.25.9(@babel/core@7.29.7)(eslint@8.57.1))(eslint@8.57.1))(@salesforce/eslint-plugin-lightning@1.0.1(eslint@8.57.1))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.51.0(eslint@8.57.1)(typescript@6.0.3))(eslint@8.57.1))(eslint-plugin-jest@29.15.4(eslint@8.57.1)(jest@30.3.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3)))(typescript@6.0.3))(eslint@8.57.1) + version: 3.7.2(@lwc/eslint-plugin-lwc@2.2.0(@babel/eslint-parser@7.25.9(@babel/core@7.29.7)(eslint@8.57.1))(eslint@8.57.1))(@salesforce/eslint-plugin-lightning@1.0.1(eslint@8.57.1))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.51.0(eslint@8.57.1)(typescript@6.0.3))(eslint@8.57.1))(eslint-plugin-jest@29.15.4(eslint@8.57.1)(jest@30.3.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3)))(typescript@6.0.3))(eslint@8.57.1) '@salesforce/sfdx-lwc-jest': specifier: 6.0.0 - version: 6.0.0(@types/node@24.13.3)(eslint@8.57.1)(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3))(typescript@6.0.3) + version: 6.0.0(@types/node@26.1.0)(eslint@8.57.1)(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3))(typescript@6.0.3) '@types/wait-on': specifier: 5.3.4 version: 5.3.4 @@ -1240,7 +1243,7 @@ importers: version: 3.1.3 jest: specifier: 'catalog:' - version: 30.3.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3)) + version: 30.3.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3)) jest-junit: specifier: 16.0.0 version: 16.0.0 @@ -1258,7 +1261,7 @@ importers: version: 2.2.6(prettier@3.9.6) ts-node: specifier: 10.9.2 - version: 10.9.2(@types/node@24.13.3)(typescript@6.0.3) + version: 10.9.2(@types/node@26.1.0)(typescript@6.0.3) wait-on: specifier: 9.1.0 version: 9.1.0 @@ -1364,9 +1367,15 @@ importers: '@ag-ui/core': specifier: 0.0.57 version: 0.0.57 + '@coveo/thermidor-contracts': + specifier: workspace:* + version: link:../thermidor-contracts '@reduxjs/toolkit': specifier: 'catalog:' version: 2.12.0(react@19.2.7) + zod: + specifier: 'catalog:' + version: 3.25.76 devDependencies: '@microsoft/api-extractor': specifier: 7.58.12 @@ -1381,6 +1390,19 @@ importers: specifier: 'catalog:' version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.0)(@vitest/browser-playwright@4.1.10)(jsdom@28.1.0)(msw@2.15.0(@types/node@26.1.0)(typescript@6.0.3))(vite@8.1.5(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) + packages/thermidor-contracts: + dependencies: + zod: + specifier: 'catalog:' + version: 3.25.76 + devDependencies: + tsdown: + specifier: 0.22.0 + version: 0.22.0(oxc-resolver@11.24.2)(publint@0.3.22)(typescript@6.0.3) + typescript: + specifier: 6.0.3 + version: 6.0.3 + samples/atomic/commerce-react: dependencies: '@coveo/atomic-react': @@ -2244,6 +2266,46 @@ importers: specifier: 'catalog:' version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-playwright@4.1.10)(jsdom@28.1.0)(msw@2.15.0(@types/node@24.13.3)(typescript@6.0.3))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) + samples/thermidor/schema-contract-react: + dependencies: + '@copilotkit/a2ui-renderer': + specifier: 1.61.2 + version: 1.61.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@coveo/thermidor': + specifier: workspace:* + version: link:../../../packages/thermidor + '@coveo/thermidor-contracts': + specifier: workspace:* + version: link:../../../packages/thermidor-contracts + react: + specifier: 19.2.7 + version: 19.2.7 + react-dom: + specifier: 19.2.7 + version: 19.2.7(react@19.2.7) + zod: + specifier: 'catalog:' + version: 3.25.76 + devDependencies: + '@types/react': + specifier: 'catalog:' + version: 19.2.17 + '@types/react-dom': + specifier: 'catalog:' + version: 19.2.3(@types/react@19.2.17) + '@vitejs/plugin-react': + specifier: 'catalog:' + version: 6.0.4(vite@8.1.5(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: 'catalog:' + version: 8.1.5(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + vitest: + specifier: 'catalog:' + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.0)(@vitest/browser-playwright@4.1.10)(jsdom@28.1.0)(msw@2.15.0(@types/node@26.1.0)(typescript@6.0.3))(vite@8.1.5(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) + samples/thermidor/search-react: dependencies: '@coveo/thermidor': @@ -2325,6 +2387,9 @@ packages: '@75lb/nature': optional: true + '@a2ui/web_core@0.9.0': + resolution: {integrity: sha512-TsMWuEeuVDsScGIGPy/fWIZu+EOBRfhx6KwjKh3VwY1AwysRenQM8zDr8VrSk14Wck/aBgVxk2zWVrMCK2/s6A==} + '@acemir/cssom@0.9.31': resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==} @@ -2769,6 +2834,10 @@ packages: resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} engines: {node: '>=6.9.0'} + '@babel/generator@8.0.0-rc.6': + resolution: {integrity: sha512-6mIzgVK8DgEzvIapoQwhXTMnnkuE4STQmVv9H03i/tZ2ml8oev3TRvZJgTenK2Bsq0YWNtzOrFdTyNzCMFtjJQ==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/helper-annotate-as-pure@7.27.3': resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} engines: {node: '>=6.9.0'} @@ -2856,10 +2925,22 @@ packages: resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@8.0.0': + resolution: {integrity: sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/helper-validator-identifier@7.29.7': resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@8.0.0-rc.6': + resolution: {integrity: sha512-nVJ+1JcCgntv8d78rRo++o2wuODT0Irknx2BF8Np4Ft2CRgjLqIs4qzSZ8b66yGbBdMWGmZBO9WEZv1hhNiSpg==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/helper-validator-identifier@8.0.4': + resolution: {integrity: sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/helper-validator-option@7.29.7': resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} engines: {node: '>=6.9.0'} @@ -2881,6 +2962,16 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@8.0.0-rc.6': + resolution: {integrity: sha512-rOS8IpdO7mQELkTPlCsTgPejO0bFuZdEDCGQJouYbYf9e1FLTym7Fei2pEjq8q7MWbX0ravcd7QQYKs1TxOuog==} + engines: {node: ^22.18.0 || >=24.11.0} + hasBin: true + + '@babel/parser@8.0.4': + resolution: {integrity: sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g==} + engines: {node: ^22.18.0 || >=24.11.0} + hasBin: true + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.29.7': resolution: {integrity: sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w==} engines: {node: '>=6.9.0'} @@ -3435,6 +3526,10 @@ packages: resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} + '@babel/types@8.0.4': + resolution: {integrity: sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==} + engines: {node: ^22.18.0 || >=24.11.0} + '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} @@ -3529,6 +3624,17 @@ packages: '@colordx/core@5.5.0': resolution: {integrity: sha512-3PxTH8itZzltK0U9jTwVVnjLXvnDYuq3m+QXsHkENxWiPRh4WaoLcs1SQjqgZ55kS+QyirpH5BVwzP2gMVG6EQ==} + '@copilotkit/a2ui-renderer@1.61.2': + resolution: {integrity: sha512-NrW3R7gd7kL8Co6oirbUkV61m3V9JQ9ugT8YM2jJDC0Zr6ivkaU0g/6jF1aYK/9nmydQMWtIRib5YQvr/Svbdw==} + peerDependencies: + react: 19.2.7 + react-dom: 19.2.7 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + '@coveo/atomic-component-health-check@2.2.19': resolution: {integrity: sha512-n/llxt8mLYW5P9G8R0ozbrMrqtnDT03oMix7tQ796+F2AFwL6F3FcXyRPVMqsWuTnKH6jOwZ04HcAoP+868Ejw==} hasBin: true @@ -6534,6 +6640,9 @@ packages: '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + '@preact/signals-core@1.14.4': + resolution: {integrity: sha512-HNB6HYeYKhQbJ1aKl+YRjrS4+QWHLKX6qKoUsfS/m0vqzsVaEBiZiaKbG/e+NKk2ch5ALQr/ihWaMHxiCuuWHA==} + '@prettier-apex/apex-ast-serializer-darwin-arm64@2.2.6': resolution: {integrity: sha512-XzrGnEVQq/JH/rKPktpdL8/agocjDCnSrY4MuHxMs5V3OnV/4MJdMHp0frQhqOjbhnUIjewypX5XWcVi0xqRBQ==} cpu: [arm64] @@ -7943,6 +8052,9 @@ packages: '@types/jsdom@20.0.1': resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==} + '@types/jsesc@2.5.1': + resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -9052,6 +9164,10 @@ packages: resolution: {integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==} engines: {node: '>=0.10.0'} + ast-kit@3.0.0: + resolution: {integrity: sha512-8OG92q3R35qjC/4i6BLBMg8IB+fClWu/1PEwg2Z9Rn+BuNaiEgJzpzn+pxWOdHJWDCAwu2JP0wCDTozAM4QirQ==} + engines: {node: ^22.18.0 || >=24.11.0} + ast-types@0.13.4: resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} engines: {node: '>=4'} @@ -9300,6 +9416,9 @@ packages: bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + birpc@4.0.0: + resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -10045,6 +10164,9 @@ packages: dataloader@1.4.0: resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} + date-fns@4.4.0: + resolution: {integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==} + dateformat@4.6.3: resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} @@ -14778,6 +14900,25 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true + rolldown-plugin-dts@0.25.2: + resolution: {integrity: sha512-nMhN/R+vmR8GM45ZW1FWMSjRTSDDn/6w4GTf8RNrEFCBdl8B1kySWrU1ixPtbwzXoRlcO+R/S88VgXuJQwfdDg==} + engines: {node: ^22.18.0 || >=24.0.0} + peerDependencies: + '@ts-macro/tsc': ^0.3.6 + '@typescript/native-preview': '>=7.0.0-dev.20260325.1' + rolldown: ^1.0.0 + typescript: 6.0.3 + vue-tsc: ~3.2.0 + peerDependenciesMeta: + '@ts-macro/tsc': + optional: true + '@typescript/native-preview': + optional: true + typescript: + optional: true + vue-tsc: + optional: true + rolldown-plugin-dts@0.27.14: resolution: {integrity: sha512-ZvuDDwoIpRK9RPxDXratCpklFO9QZZWndf/sd0VBFb4LEj0jj07UcHK9OCh7V4XiFz2Z89ziyBC2K6tJiDjrbw==} engines: {node: ^22.18.0 || >=24.11.0} @@ -15810,6 +15951,40 @@ packages: tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + tsdown@0.22.0: + resolution: {integrity: sha512-FgW0hHb27nGQA/+F3d5+U9wKXkfilk9DVkc5+7x/ZqF03g+Hoz/eeApT32jqxATt9eRoR+1jxk7MUMON+O4CXw==} + engines: {node: ^22.18.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@arethetypeswrong/core': ^0.18.1 + '@tsdown/css': 0.22.0 + '@tsdown/exe': 0.22.0 + '@vitejs/devtools': '*' + publint: ^0.3.8 + tsx: '*' + typescript: 6.0.3 + unplugin-unused: ^0.5.0 + unrun: '*' + peerDependenciesMeta: + '@arethetypeswrong/core': + optional: true + '@tsdown/css': + optional: true + '@tsdown/exe': + optional: true + '@vitejs/devtools': + optional: true + publint: + optional: true + tsx: + optional: true + typescript: + optional: true + unplugin-unused: + optional: true + unrun: + optional: true + tsdown@0.22.14: resolution: {integrity: sha512-ule7Y+fsAN2iZbLDoo7C4KYljFJNJJ+fLshyn+9gozeTspVersWHxwdGB+Dm2hzA38s6muFnUTl0jK3vJm9ifQ==} engines: {node: ^22.18.0 || >=24.11.0} @@ -16826,6 +17001,13 @@ snapshots: lodash: 4.18.1 typical: 7.3.0 + '@a2ui/web_core@0.9.0': + dependencies: + '@preact/signals-core': 1.14.4 + date-fns: 4.4.0 + zod: 3.25.76 + zod-to-json-schema: 3.25.2(zod@3.25.76) + '@acemir/cssom@0.9.31': {} '@actions/github@9.1.1': @@ -17593,6 +17775,15 @@ snapshots: '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 + '@babel/generator@8.0.0-rc.6': + dependencies: + '@babel/parser': 8.0.0-rc.6 + '@babel/types': 8.0.4 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@types/jsesc': 2.5.1 + jsesc: 3.1.0 + '@babel/helper-annotate-as-pure@7.27.3': dependencies: '@babel/types': 7.29.7 @@ -17753,8 +17944,14 @@ snapshots: '@babel/helper-string-parser@7.29.7': {} + '@babel/helper-string-parser@8.0.0': {} + '@babel/helper-validator-identifier@7.29.7': {} + '@babel/helper-validator-identifier@8.0.0-rc.6': {} + + '@babel/helper-validator-identifier@8.0.4': {} + '@babel/helper-validator-option@7.29.7': {} '@babel/helper-wrap-function@7.29.7': @@ -17781,6 +17978,14 @@ snapshots: dependencies: '@babel/types': 7.29.7 + '@babel/parser@8.0.0-rc.6': + dependencies: + '@babel/types': 8.0.4 + + '@babel/parser@8.0.4': + dependencies: + '@babel/types': 8.0.4 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.29.7(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 @@ -18486,6 +18691,11 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 + '@babel/types@8.0.4': + dependencies: + '@babel/helper-string-parser': 8.0.0 + '@babel/helper-validator-identifier': 8.0.4 + '@bcoe/v8-coverage@0.2.3': {} '@blazediff/core@1.9.1': {} @@ -18686,6 +18896,17 @@ snapshots: '@colordx/core@5.5.0': {} + '@copilotkit/a2ui-renderer@1.61.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@a2ui/web_core': 0.9.0 + clsx: 2.1.1 + lit: 3.3.3 + zod: 3.25.76 + zod-to-json-schema: 3.25.2(zod@3.25.76) + optionalDependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + '@coveo/atomic-component-health-check@2.2.19': dependencies: chalk: 4.1.2 @@ -19832,7 +20053,7 @@ snapshots: jest-util: 30.3.0 slash: 3.0.0 - '@jest/core@29.7.0(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3))': + '@jest/core@29.7.0(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -19846,7 +20067,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3)) + jest-config: 29.7.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -19901,6 +20122,7 @@ snapshots: - esbuild-register - supports-color - ts-node + optional: true '@jest/core@30.3.0(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3))': dependencies: @@ -20482,33 +20704,33 @@ snapshots: globals: 13.24.0 minimatch: 9.0.9 - '@lwc/jest-preset@16.0.0(@lwc/compiler@7.1.2)(@lwc/engine-dom@7.1.2)(@lwc/engine-server@7.1.2)(@lwc/synthetic-shadow@7.1.2)(jest@29.7.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3)))': + '@lwc/jest-preset@16.0.0(@lwc/compiler@7.1.2)(@lwc/engine-dom@7.1.2)(@lwc/engine-server@7.1.2)(@lwc/synthetic-shadow@7.1.2)(jest@29.7.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3)))': dependencies: '@lwc/compiler': 7.1.2 '@lwc/engine-dom': 7.1.2 '@lwc/engine-server': 7.1.2 - '@lwc/jest-resolver': 16.0.0(jest@29.7.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3))) - '@lwc/jest-serializer': 16.0.0(jest@29.7.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3))) - '@lwc/jest-transformer': 16.0.0(@lwc/compiler@7.1.2)(jest@29.7.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3))) + '@lwc/jest-resolver': 16.0.0(jest@29.7.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3))) + '@lwc/jest-serializer': 16.0.0(jest@29.7.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3))) + '@lwc/jest-transformer': 16.0.0(@lwc/compiler@7.1.2)(jest@29.7.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3))) '@lwc/synthetic-shadow': 7.1.2 - jest: 29.7.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3)) + jest: 29.7.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3)) transitivePeerDependencies: - supports-color - '@lwc/jest-resolver@16.0.0(jest@29.7.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3)))': + '@lwc/jest-resolver@16.0.0(jest@29.7.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3)))': dependencies: '@lwc/jest-shared': 16.0.0 - jest: 29.7.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3)) + jest: 29.7.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3)) - '@lwc/jest-serializer@16.0.0(jest@29.7.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3)))': + '@lwc/jest-serializer@16.0.0(jest@29.7.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3)))': dependencies: '@lwc/jest-shared': 16.0.0 - jest: 29.7.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3)) + jest: 29.7.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3)) pretty-format: 29.7.0 '@lwc/jest-shared@16.0.0': {} - '@lwc/jest-transformer@16.0.0(@lwc/compiler@7.1.2)(jest@29.7.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3)))': + '@lwc/jest-transformer@16.0.0(@lwc/compiler@7.1.2)(jest@29.7.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3)))': dependencies: '@babel/core': 7.29.7 '@babel/plugin-proposal-dynamic-import': 7.18.6(@babel/core@7.29.7) @@ -20519,7 +20741,7 @@ snapshots: '@lwc/compiler': 7.1.2 '@lwc/jest-shared': 16.0.0 babel-preset-jest: 29.6.3(@babel/core@7.29.7) - jest: 29.7.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3)) + jest: 29.7.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3)) magic-string: 0.30.21 semver: 7.8.5 transitivePeerDependencies: @@ -21648,6 +21870,8 @@ snapshots: '@popperjs/core@2.11.8': {} + '@preact/signals-core@1.14.4': {} + '@prettier-apex/apex-ast-serializer-darwin-arm64@2.2.6': optional: true @@ -22320,7 +22544,7 @@ snapshots: dependencies: postcss: 8.5.24 - '@salesforce/eslint-config-lwc@3.7.2(@lwc/eslint-plugin-lwc@2.2.0(@babel/eslint-parser@7.25.9(@babel/core@7.29.7)(eslint@8.57.1))(eslint@8.57.1))(@salesforce/eslint-plugin-lightning@1.0.1(eslint@8.57.1))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.51.0(eslint@8.57.1)(typescript@6.0.3))(eslint@8.57.1))(eslint-plugin-jest@29.15.4(eslint@8.57.1)(jest@30.3.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3)))(typescript@6.0.3))(eslint@8.57.1)': + '@salesforce/eslint-config-lwc@3.7.2(@lwc/eslint-plugin-lwc@2.2.0(@babel/eslint-parser@7.25.9(@babel/core@7.29.7)(eslint@8.57.1))(eslint@8.57.1))(@salesforce/eslint-plugin-lightning@1.0.1(eslint@8.57.1))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.51.0(eslint@8.57.1)(typescript@6.0.3))(eslint@8.57.1))(eslint-plugin-jest@29.15.4(eslint@8.57.1)(jest@30.3.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3)))(typescript@6.0.3))(eslint@8.57.1)': dependencies: '@babel/core': 7.24.9 '@babel/eslint-parser': 7.24.8(@babel/core@7.24.9)(eslint@8.57.1) @@ -22328,7 +22552,7 @@ snapshots: '@salesforce/eslint-plugin-lightning': 1.0.1(eslint@8.57.1) eslint: 8.57.1 eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.51.0(eslint@8.57.1)(typescript@6.0.3))(eslint@8.57.1) - eslint-plugin-jest: 29.15.4(eslint@8.57.1)(jest@30.3.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3)))(typescript@6.0.3) + eslint-plugin-jest: 29.15.4(eslint@8.57.1)(jest@30.3.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3)))(typescript@6.0.3) eslint-restricted-globals: 0.2.0 semver: 7.8.5 transitivePeerDependencies: @@ -22338,21 +22562,21 @@ snapshots: dependencies: eslint: 8.57.1 - '@salesforce/sfdx-lwc-jest@6.0.0(@types/node@24.13.3)(eslint@8.57.1)(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3))(typescript@6.0.3)': + '@salesforce/sfdx-lwc-jest@6.0.0(@types/node@26.1.0)(eslint@8.57.1)(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3))(typescript@6.0.3)': dependencies: '@lwc/compiler': 7.1.2 '@lwc/engine-dom': 7.1.2 '@lwc/engine-server': 7.1.2 - '@lwc/jest-preset': 16.0.0(@lwc/compiler@7.1.2)(@lwc/engine-dom@7.1.2)(@lwc/engine-server@7.1.2)(@lwc/synthetic-shadow@7.1.2)(jest@29.7.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3))) - '@lwc/jest-resolver': 16.0.0(jest@29.7.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3))) - '@lwc/jest-serializer': 16.0.0(jest@29.7.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3))) - '@lwc/jest-transformer': 16.0.0(@lwc/compiler@7.1.2)(jest@29.7.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3))) + '@lwc/jest-preset': 16.0.0(@lwc/compiler@7.1.2)(@lwc/engine-dom@7.1.2)(@lwc/engine-server@7.1.2)(@lwc/synthetic-shadow@7.1.2)(jest@29.7.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3))) + '@lwc/jest-resolver': 16.0.0(jest@29.7.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3))) + '@lwc/jest-serializer': 16.0.0(jest@29.7.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3))) + '@lwc/jest-transformer': 16.0.0(@lwc/compiler@7.1.2)(jest@29.7.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3))) '@lwc/module-resolver': 7.1.2 '@lwc/synthetic-shadow': 7.1.2 '@lwc/wire-service': 7.1.2 '@salesforce/wire-service-jest-util': 4.1.4(@lwc/engine-dom@7.1.2)(eslint@8.57.1)(typescript@6.0.3) fast-glob: 3.3.3 - jest: 29.7.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3)) + jest: 29.7.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3)) jest-environment-jsdom: 29.7.0(patch_hash=b419a992476c3323e67ee6c86f3f9ecf6f4f073127cb572aa9af3b9c6550751d) yargs: 17.7.3 transitivePeerDependencies: @@ -22991,6 +23215,8 @@ snapshots: '@types/tough-cookie': 4.0.5 parse5: 7.3.0 + '@types/jsesc@2.5.1': {} + '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} @@ -23515,11 +23741,24 @@ snapshots: vite: 8.1.5(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) vue: 3.5.39(typescript@6.0.3) - '@vitest/browser-playwright@4.1.10(msw@2.15.0(@types/node@24.13.3)(typescript@6.0.3))(playwright@1.60.0)(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.48.0)(yaml@2.9.0))(vitest@4.1.10)': + '@vitest/browser-playwright@4.1.10(msw@2.15.0(@types/node@24.13.3)(typescript@6.0.3))(playwright@1.60.0)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vitest@4.1.10)': + dependencies: + '@vitest/browser': 4.1.10(msw@2.15.0(@types/node@24.13.3)(typescript@6.0.3))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vitest@4.1.10) + '@vitest/mocker': 4.1.10(msw@2.15.0(@types/node@24.13.3)(typescript@6.0.3))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) + playwright: 1.60.0 + tinyrainbow: 3.1.0 + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-playwright@4.1.10)(jsdom@28.1.0)(msw@2.15.0(@types/node@24.13.3)(typescript@6.0.3))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/browser-playwright@4.1.10(msw@2.15.0(@types/node@24.13.3)(typescript@6.0.3))(playwright@1.62.0-alpha-1783623505000)(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.48.0)(yaml@2.9.0))(vitest@4.1.10)': dependencies: '@vitest/browser': 4.1.10(msw@2.15.0(@types/node@24.13.3)(typescript@6.0.3))(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.48.0)(yaml@2.9.0))(vitest@4.1.10) '@vitest/mocker': 4.1.10(msw@2.15.0(@types/node@24.13.3)(typescript@6.0.3))(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.48.0)(yaml@2.9.0)) - playwright: 1.60.0 + playwright: 1.62.0-alpha-1783623505000 tinyrainbow: 3.1.0 vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-playwright@4.1.10)(jsdom@28.1.0)(msw@2.15.0(@types/node@24.13.3)(typescript@6.0.3))(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.48.0)(yaml@2.9.0)) transitivePeerDependencies: @@ -23529,11 +23768,11 @@ snapshots: - vite optional: true - '@vitest/browser-playwright@4.1.10(msw@2.15.0(@types/node@24.13.3)(typescript@6.0.3))(playwright@1.60.0)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.4.2)(sass@1.97.3)(terser@5.48.0)(yaml@2.9.0))(vitest@4.1.10)': + '@vitest/browser-playwright@4.1.10(msw@2.15.0(@types/node@24.13.3)(typescript@6.0.3))(playwright@1.62.0-alpha-1783623505000)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.4.2)(sass@1.97.3)(terser@5.48.0)(yaml@2.9.0))(vitest@4.1.10)': dependencies: '@vitest/browser': 4.1.10(msw@2.15.0(@types/node@24.13.3)(typescript@6.0.3))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.4.2)(sass@1.97.3)(terser@5.48.0)(yaml@2.9.0))(vitest@4.1.10) '@vitest/mocker': 4.1.10(msw@2.15.0(@types/node@24.13.3)(typescript@6.0.3))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.4.2)(sass@1.97.3)(terser@5.48.0)(yaml@2.9.0)) - playwright: 1.60.0 + playwright: 1.62.0-alpha-1783623505000 tinyrainbow: 3.1.0 vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-playwright@4.1.10)(jsdom@28.1.0)(msw@2.15.0(@types/node@24.13.3)(typescript@6.0.3))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.4.2)(sass@1.97.3)(terser@5.48.0)(yaml@2.9.0)) transitivePeerDependencies: @@ -23543,24 +23782,24 @@ snapshots: - vite optional: true - '@vitest/browser-playwright@4.1.10(msw@2.15.0(@types/node@24.13.3)(typescript@6.0.3))(playwright@1.60.0)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vitest@4.1.10)': + '@vitest/browser-playwright@4.1.10(msw@2.15.0(@types/node@26.1.0)(typescript@6.0.3))(playwright@1.60.0)(vite@8.1.5(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vitest@4.1.10)': dependencies: - '@vitest/browser': 4.1.10(msw@2.15.0(@types/node@24.13.3)(typescript@6.0.3))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vitest@4.1.10) - '@vitest/mocker': 4.1.10(msw@2.15.0(@types/node@24.13.3)(typescript@6.0.3))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) + '@vitest/browser': 4.1.10(msw@2.15.0(@types/node@26.1.0)(typescript@6.0.3))(vite@8.1.5(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vitest@4.1.10) + '@vitest/mocker': 4.1.10(msw@2.15.0(@types/node@26.1.0)(typescript@6.0.3))(vite@8.1.5(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) playwright: 1.60.0 tinyrainbow: 3.1.0 - vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-playwright@4.1.10)(jsdom@28.1.0)(msw@2.15.0(@types/node@24.13.3)(typescript@6.0.3))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.0)(@vitest/browser-playwright@4.1.10)(jsdom@28.1.0)(msw@2.15.0(@types/node@26.1.0)(typescript@6.0.3))(vite@8.1.5(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) transitivePeerDependencies: - bufferutil - msw - utf-8-validate - vite - '@vitest/browser-playwright@4.1.10(msw@2.15.0(@types/node@26.1.0)(typescript@6.0.3))(playwright@1.60.0)(vite@8.1.5(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vitest@4.1.10)': + '@vitest/browser-playwright@4.1.10(msw@2.15.0(@types/node@26.1.0)(typescript@6.0.3))(playwright@1.62.0-alpha-1783623505000)(vite@8.1.5(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vitest@4.1.10)': dependencies: '@vitest/browser': 4.1.10(msw@2.15.0(@types/node@26.1.0)(typescript@6.0.3))(vite@8.1.5(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vitest@4.1.10) '@vitest/mocker': 4.1.10(msw@2.15.0(@types/node@26.1.0)(typescript@6.0.3))(vite@8.1.5(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) - playwright: 1.60.0 + playwright: 1.62.0-alpha-1783623505000 tinyrainbow: 3.1.0 vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.0)(@vitest/browser-playwright@4.1.10)(jsdom@28.1.0)(msw@2.15.0(@types/node@26.1.0)(typescript@6.0.3))(vite@8.1.5(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) transitivePeerDependencies: @@ -23568,6 +23807,7 @@ snapshots: - msw - utf-8-validate - vite + optional: true '@vitest/browser@4.1.10(msw@2.15.0(@types/node@24.13.3)(typescript@6.0.3))(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.48.0)(yaml@2.9.0))(vitest@4.1.10)': dependencies: @@ -24260,6 +24500,12 @@ snapshots: assign-symbols@1.0.0: {} + ast-kit@3.0.0: + dependencies: + '@babel/parser': 8.0.4 + estree-walker: 3.0.3 + pathe: 2.0.3 + ast-types@0.13.4: dependencies: tslib: 2.8.1 @@ -24546,6 +24792,8 @@ snapshots: file-uri-to-path: 1.0.0 optional: true + birpc@4.0.0: {} + bl@4.1.0: dependencies: buffer: 5.7.1 @@ -25139,13 +25387,13 @@ snapshots: optionalDependencies: typescript: 6.0.3 - create-jest@29.7.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3)): + create-jest@29.7.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3)) + jest-config: 29.7.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -25426,6 +25674,8 @@ snapshots: dataloader@1.4.0: {} + date-fns@4.4.0: {} + dateformat@4.6.3: {} dayjs@1.11.21: {} @@ -25974,12 +26224,12 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-jest@29.15.4(eslint@8.57.1)(jest@30.3.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3)))(typescript@6.0.3): + eslint-plugin-jest@29.15.4(eslint@8.57.1)(jest@30.3.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3)))(typescript@6.0.3): dependencies: '@typescript-eslint/utils': 8.62.1(eslint@8.57.1)(typescript@6.0.3) eslint: 8.57.1 optionalDependencies: - jest: 30.3.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3)) + jest: 30.3.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3)) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -27677,16 +27927,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3)): + jest-cli@29.7.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3)) + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3)) + create-jest: 29.7.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3)) + jest-config: 29.7.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.3 @@ -27714,6 +27964,7 @@ snapshots: - esbuild-register - supports-color - ts-node + optional: true jest-cli@30.3.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3)): dependencies: @@ -27734,38 +27985,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3)): - dependencies: - '@babel/core': 7.29.7 - '@jest/test-sequencer': 29.7.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.29.7) - chalk: 4.1.2 - ci-info: 3.9.0 - deepmerge: 4.3.1 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-circus: 29.7.0 - jest-environment-node: 29.7.0 - jest-get-type: 29.6.3 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-runner: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - micromatch: 4.0.8 - parse-json: 5.2.0 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: - '@types/node': 24.13.3 - ts-node: 10.9.2(@types/node@24.13.3)(typescript@6.0.3) - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-config@29.7.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3)): + jest-config@29.7.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3)): dependencies: '@babel/core': 7.29.7 '@jest/test-sequencer': 29.7.0 @@ -27791,7 +28011,7 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 26.1.0 - ts-node: 10.9.2(@types/node@24.13.3)(typescript@6.0.3) + ts-node: 10.9.2(@types/node@26.1.0)(typescript@6.0.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -27827,6 +28047,7 @@ snapshots: transitivePeerDependencies: - babel-plugin-macros - supports-color + optional: true jest-config@30.3.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3)): dependencies: @@ -27859,6 +28080,7 @@ snapshots: transitivePeerDependencies: - babel-plugin-macros - supports-color + optional: true jest-config@30.3.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3)): dependencies: @@ -28394,12 +28616,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3)): + jest@29.7.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3)) + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3)) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@6.0.3)) + jest-cli: 29.7.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -28418,6 +28640,7 @@ snapshots: - esbuild-register - supports-color - ts-node + optional: true jest@30.3.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3)): dependencies: @@ -31691,6 +31914,22 @@ snapshots: dependencies: glob: 7.2.3 + rolldown-plugin-dts@0.25.2(oxc-resolver@11.24.2)(rolldown@1.2.0)(typescript@6.0.3): + dependencies: + '@babel/generator': 8.0.0-rc.6 + '@babel/helper-validator-identifier': 8.0.0-rc.6 + '@babel/parser': 8.0.0-rc.6 + ast-kit: 3.0.0 + birpc: 4.0.0 + dts-resolver: 3.0.0(oxc-resolver@11.24.2) + get-tsconfig: 5.0.0-beta.5 + obug: 2.1.4 + rolldown: 1.2.0 + optionalDependencies: + typescript: 6.0.3 + transitivePeerDependencies: + - oxc-resolver + rolldown-plugin-dts@0.27.14(oxc-resolver@11.24.2)(rolldown@1.2.0)(typescript@6.0.3): dependencies: dts-resolver: 3.0.0(oxc-resolver@11.24.2) @@ -32908,6 +33147,7 @@ snapshots: typescript: 6.0.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + optional: true ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3): dependencies: @@ -32926,7 +33166,6 @@ snapshots: typescript: 6.0.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 - optional: true ts-simple-type@2.0.0-next.0: {} @@ -32941,6 +33180,32 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 + tsdown@0.22.0(oxc-resolver@11.24.2)(publint@0.3.22)(typescript@6.0.3): + dependencies: + ansis: 4.3.1 + cac: 7.0.0 + defu: 6.1.7 + empathic: 2.0.1 + hookable: 6.1.1 + import-without-cache: 0.4.0 + obug: 2.1.4 + picomatch: 4.0.5 + rolldown: 1.2.0 + rolldown-plugin-dts: 0.25.2(oxc-resolver@11.24.2)(rolldown@1.2.0)(typescript@6.0.3) + semver: 7.8.5 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tree-kill: 1.2.2 + unconfig-core: 7.5.0 + optionalDependencies: + publint: 0.3.22 + typescript: 6.0.3 + transitivePeerDependencies: + - '@ts-macro/tsc' + - '@typescript/native-preview' + - oxc-resolver + - vue-tsc + tsdown@0.22.14(oxc-resolver@11.24.2)(publint@0.3.22)(typescript@6.0.3): dependencies: ansis: 4.3.1 @@ -33482,7 +33747,7 @@ snapshots: optionalDependencies: '@opentelemetry/api': 1.9.1 '@types/node': 24.13.3 - '@vitest/browser-playwright': 4.1.10(msw@2.15.0(@types/node@24.13.3)(typescript@6.0.3))(playwright@1.60.0)(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.48.0)(yaml@2.9.0))(vitest@4.1.10) + '@vitest/browser-playwright': 4.1.10(msw@2.15.0(@types/node@24.13.3)(typescript@6.0.3))(playwright@1.62.0-alpha-1783623505000)(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.48.0)(yaml@2.9.0))(vitest@4.1.10) jsdom: 28.1.0 transitivePeerDependencies: - msw @@ -33513,7 +33778,7 @@ snapshots: optionalDependencies: '@opentelemetry/api': 1.9.1 '@types/node': 24.13.3 - '@vitest/browser-playwright': 4.1.10(msw@2.15.0(@types/node@24.13.3)(typescript@6.0.3))(playwright@1.60.0)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.4.2)(sass@1.97.3)(terser@5.48.0)(yaml@2.9.0))(vitest@4.1.10) + '@vitest/browser-playwright': 4.1.10(msw@2.15.0(@types/node@24.13.3)(typescript@6.0.3))(playwright@1.62.0-alpha-1783623505000)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.4.2)(sass@1.97.3)(terser@5.48.0)(yaml@2.9.0))(vitest@4.1.10) jsdom: 28.1.0 transitivePeerDependencies: - msw @@ -33574,7 +33839,7 @@ snapshots: optionalDependencies: '@opentelemetry/api': 1.9.1 '@types/node': 26.1.0 - '@vitest/browser-playwright': 4.1.10(msw@2.15.0(@types/node@26.1.0)(typescript@6.0.3))(playwright@1.60.0)(vite@8.1.5(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vitest@4.1.10) + '@vitest/browser-playwright': 4.1.10(msw@2.15.0(@types/node@26.1.0)(typescript@6.0.3))(playwright@1.62.0-alpha-1783623505000)(vite@8.1.5(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vitest@4.1.10) jsdom: 28.1.0 transitivePeerDependencies: - msw @@ -34129,6 +34394,10 @@ snapshots: '@yuku-parser/binding-win32-arm64': 0.8.1 '@yuku-parser/binding-win32-x64': 0.8.1 + zod-to-json-schema@3.25.2(zod@3.25.76): + dependencies: + zod: 3.25.76 + zod-to-json-schema@3.25.2(zod@4.3.6): dependencies: zod: 4.3.6 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 77b7f6fe7b1..209c85bbeb7 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,6 +4,7 @@ packages: - packages/mock-converse-api - packages/headless - packages/thermidor + - packages/thermidor-contracts - packages/quantic - packages/headless-react - packages/atomic-hosted-page @@ -115,6 +116,7 @@ catalog: vite: 8.1.5 vitest: 4.1.10 yaml: 2.9.0 + zod: 3.25.76 zone.js: 0.15.1 minimumReleaseAge: 10080 diff --git a/samples/README.md b/samples/README.md index 732e17ed132..292a0ec10c7 100644 --- a/samples/README.md +++ b/samples/README.md @@ -51,11 +51,15 @@ Server-side rendering with Headless controllers for improved performance and SEO Samples using `@coveo/thermidor` for upcoming conversational and search experiences. -| Sample | Description | Framework | Use Case | -| ----------------------------------------------------- | ----------------------------------------------------------------- | ------------ | ------------ | -| [conversation-react](./thermidor/conversation-react/) | React + Vite conversational integration sample | React + Vite | Conversation | -| [commerce-react](./thermidor/commerce-react/) | React + Vite commerce sample with ProductList, Pagination | React + Vite | Commerce | -| [search-react](./thermidor/search-react/) | React + Vite search sample with SearchBox, ResultList, Pagination | React + Vite | Search | +| Sample | Description | Framework | Use Case | +| ----------------------------------------------------------- | ------------------------------------------------------------------ | ------------ | ------------ | +| [conversation-react](./thermidor/conversation-react/) | React + Vite conversational integration sample | React + Vite | Conversation | +| [commerce-react](./thermidor/commerce-react/) | React + Vite commerce sample with ProductList, Pagination | React + Vite | Commerce | +| [search-react](./thermidor/search-react/) | React + Vite search sample with SearchBox, ResultList, Pagination | React + Vite | Search | +| [generative-react](./thermidor/generative-react/) | React + Vite generative conversation and custom A2-UI sample | React + Vite | Conversation | +| [generative-angular](./thermidor/generative-angular/) | Angular generative conversation and custom A2-UI sample | Angular | Conversation | +| [demo-react](./thermidor/demo-react/) | React demo with landing, conversation, and routed search views | React + Vite | Conversation | +| [schema-contract-react](./thermidor/schema-contract-react/) | v0.9 Commerce Catalog contract rendered from normalized activities | React + Vite | Contract | ## 🚀 Quick Start diff --git a/samples/thermidor/generative-angular/src/app/a2ui-parser.ts b/samples/thermidor/generative-angular/src/app/a2ui-parser.ts index 7066744ab2f..e11a25ea6f6 100644 --- a/samples/thermidor/generative-angular/src/app/a2ui-parser.ts +++ b/samples/thermidor/generative-angular/src/app/a2ui-parser.ts @@ -6,8 +6,8 @@ * * Single public entry point: `parseSurfaces()` */ -import type {A2UISurface} from '@coveo/thermidor'; import type { + A2UISurface, A2UIOperation, ActivitySnapshotContent, BundleDisplayTier, @@ -328,7 +328,7 @@ function deduplicate(surfaces: RenderableCommerceSurface[]): RenderableCommerceS * Parse raw A2UI surface records into deduplicated, render-ready surfaces. */ export function parseSurfaces( - rawSurfaces: A2UISurface[] | undefined, + rawSurfaces: Record[] | undefined, options: {turnComplete: boolean} = {turnComplete: false} ): RenderableCommerceSurface[] { if (!rawSurfaces || rawSurfaces.length === 0) return []; diff --git a/samples/thermidor/generative-angular/src/app/models.ts b/samples/thermidor/generative-angular/src/app/models.ts index 89f0465dfe2..ecc036621ff 100644 --- a/samples/thermidor/generative-angular/src/app/models.ts +++ b/samples/thermidor/generative-angular/src/app/models.ts @@ -1,7 +1,9 @@ // Shared frontend contract types for the Angular sample. // These types re-export Thermidor's canonical conversation types and define // the sample-specific commerce surface shapes used across the app. -export type {Turn, A2UISurface, ReasoningStep, RoutedInterface} from '@coveo/thermidor'; +export type {Turn, ReasoningStep, RoutedInterface} from '@coveo/thermidor'; + +export type A2UISurface = Record; export type ProductRecord = { ec_product_id: string; diff --git a/samples/thermidor/generative-angular/src/app/services/conversation.service.ts b/samples/thermidor/generative-angular/src/app/services/conversation.service.ts index df9fd5dac4b..357b1521bfb 100644 --- a/samples/thermidor/generative-angular/src/app/services/conversation.service.ts +++ b/samples/thermidor/generative-angular/src/app/services/conversation.service.ts @@ -4,6 +4,7 @@ import { buildConverseController, type ConverseController, type ConverseControllerState, + type Activity, type SerializedConverseState, type ReasoningStep, } from '@coveo/thermidor'; @@ -82,8 +83,10 @@ export class ConversationService { let turnComplete = true; for (const turn of turns) { - if (turn.agentResponse?.surfaces?.length) { - latestSurfaces = turn.agentResponse.surfaces; + if (turn.agentResponse?.activities?.length) { + latestSurfaces = turn.agentResponse.activities.map( + (activity: Activity) => activity.payload + ); turnComplete = turn.status !== 'streaming'; } } diff --git a/samples/thermidor/generative-react/src/components/AgentResponse/AgentResponse.tsx b/samples/thermidor/generative-react/src/components/AgentResponse/AgentResponse.tsx index c65257e1427..0acfc6c9fbd 100644 --- a/samples/thermidor/generative-react/src/components/AgentResponse/AgentResponse.tsx +++ b/samples/thermidor/generative-react/src/components/AgentResponse/AgentResponse.tsx @@ -11,9 +11,9 @@ export interface AgentResponseProps { } export function AgentResponse({agentResponse, isStreaming, onAction}: AgentResponseProps) { - const {messages, reasoningSteps, surfaces} = agentResponse; + const {messages, reasoningSteps, activities} = agentResponse; - const hasContent = messages.length > 0 || reasoningSteps.length > 0 || surfaces.length > 0; + const hasContent = messages.length > 0 || reasoningSteps.length > 0 || activities.length > 0; if (!hasContent) { return null; @@ -25,7 +25,12 @@ export function AgentResponse({agentResponse, isStreaming, onAction}: AgentRespo )} {messages.length > 0 && } - {surfaces.length > 0 && } + {activities.length > 0 && ( + activity.payload)} + onAction={onAction} + /> + )} ); } diff --git a/samples/thermidor/schema-contract-react/.env b/samples/thermidor/schema-contract-react/.env new file mode 100644 index 00000000000..815a628fab0 --- /dev/null +++ b/samples/thermidor/schema-contract-react/.env @@ -0,0 +1,6 @@ +VITE_COVEO_ORGANIZATION_ID=your-organization-id +VITE_COVEO_ACCESS_TOKEN=your-access-token +VITE_COVEO_TRACKING_ID=thermidor-schema-contract-sample +VITE_COVEO_LANGUAGE=en +VITE_COVEO_COUNTRY=US +VITE_COVEO_CURRENCY=USD diff --git a/samples/thermidor/schema-contract-react/.env.example b/samples/thermidor/schema-contract-react/.env.example new file mode 100644 index 00000000000..815a628fab0 --- /dev/null +++ b/samples/thermidor/schema-contract-react/.env.example @@ -0,0 +1,6 @@ +VITE_COVEO_ORGANIZATION_ID=your-organization-id +VITE_COVEO_ACCESS_TOKEN=your-access-token +VITE_COVEO_TRACKING_ID=thermidor-schema-contract-sample +VITE_COVEO_LANGUAGE=en +VITE_COVEO_COUNTRY=US +VITE_COVEO_CURRENCY=USD diff --git a/samples/thermidor/schema-contract-react/README.md b/samples/thermidor/schema-contract-react/README.md new file mode 100644 index 00000000000..5e53ac16952 --- /dev/null +++ b/samples/thermidor/schema-contract-react/README.md @@ -0,0 +1,24 @@ +# Thermidor schema-contract React sample + +This sample proves the client-side path for the v0.9 Thermidor Commerce Catalog contract: + +`AG-UI STATE_SNAPSHOT` → Thermidor Engine state → advertised controller; `ACTIVITY_SNAPSHOT` → raw A2-UI messages → CopilotKit renderer/catalog. + +Thermidor normalizes AG-UI state snapshots into the active turn's Engine-backed `agentResponse.state` and retains A2-UI activities as opaque `kind` and `payload` values. The sample passes `a2ui-surface` operations to CopilotKit unchanged. Its local `ProductCarousel` and `Cart` renderers select the matching advertised `controllerId` slice from Thermidor Engine state and subscribe to future Engine updates. CopilotKit provides only renderer and catalog state; it does not replace Thermidor's conversational endpoint or runtime. + +## Run with the contract mock + +From `integration/ui-kit`, start the mock API in one terminal: + +```bash +pnpm --filter @coveo/mock-converse-api build +pnpm --filter @coveo/mock-converse-api start +``` + +Copy `.env.example` to `.env`, fill the Coveo configuration values, then start the sample in another terminal: + +```bash +pnpm --filter @samples/thermidor-schema-contract-react dev:mock +``` + +Submit **Show the Thermidor catalog**. The mock streams the catalog example from `thermidor-schema`, including the controller advertisements and concrete product-list/cart state. diff --git a/samples/thermidor/schema-contract-react/index.html b/samples/thermidor/schema-contract-react/index.html new file mode 100644 index 00000000000..5af6e8e7a36 --- /dev/null +++ b/samples/thermidor/schema-contract-react/index.html @@ -0,0 +1,12 @@ + + + + + + Thermidor schema contract + + +
+ + + diff --git a/samples/thermidor/schema-contract-react/package.json b/samples/thermidor/schema-contract-react/package.json new file mode 100644 index 00000000000..1b372176d45 --- /dev/null +++ b/samples/thermidor/schema-contract-react/package.json @@ -0,0 +1,29 @@ +{ + "name": "@samples/thermidor-schema-contract-react", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "dev:mock": "VITE_COVEO_ENDPOINT=http://localhost:3456 vite", + "build": "vite build", + "preview": "vite preview", + "test": "vitest run" + }, + "dependencies": { + "@copilotkit/a2ui-renderer": "1.61.2", + "@coveo/thermidor": "workspace:*", + "@coveo/thermidor-contracts": "workspace:*", + "react": "catalog:", + "react-dom": "catalog:", + "zod": "catalog:" + }, + "devDependencies": { + "@types/react": "catalog:", + "@types/react-dom": "catalog:", + "@vitejs/plugin-react": "catalog:", + "typescript": "catalog:", + "vite": "catalog:", + "vitest": "catalog:" + } +} diff --git a/samples/thermidor/schema-contract-react/src/App.tsx b/samples/thermidor/schema-contract-react/src/App.tsx new file mode 100644 index 00000000000..f06897ab65a --- /dev/null +++ b/samples/thermidor/schema-contract-react/src/App.tsx @@ -0,0 +1,102 @@ +import {useEffect, useMemo, useRef, useState} from 'react'; +import { + buildConverseController, + buildGenerativeInterface, + Engine, + type ConverseController, + type GenerativeInterface, +} from '@coveo/thermidor'; +import {A2UIProvider} from '@copilotkit/a2ui-renderer'; +import {createThermidorCatalog} from './a2ui/components.js'; +import {getA2UIMessages, ThermidorA2UISurfaces} from './a2ui/surfaces.js'; +import {getSampleConfiguration} from './env.js'; +import {useController} from './use-controller.js'; + +const CONTRACT_PROMPT = 'Show the Thermidor catalog'; + +export default function App() { + return ; +} + +function ContractSample() { + const engineRef = useRef(null); + engineRef.current ??= new Engine({ + configuration: getSampleConfiguration(), + navigatorContextProvider: getNavigatorContext, + }); + + const interfaceRef = useRef(null); + interfaceRef.current ??= buildGenerativeInterface({engine: engineRef.current}); + + const [controller, state] = useController(() => + buildConverseController({interface: interfaceRef.current!}) + ); + const catalogRef = useRef | null>(null); + catalogRef.current ??= createThermidorCatalog(controller); + const [prompt, setPrompt] = useState(CONTRACT_PROMPT); + const turn = state.activeTurn; + const a2uiMessages = useMemo( + () => getA2UIMessages(turn?.agentResponse?.activities ?? []), + [turn?.agentResponse?.activities] + ); + + useEffect(() => { + return () => { + interfaceRef.current?.dispose(); + engineRef.current?.dispose(); + }; + }, []); + + function submit(event: React.FormEvent) { + event.preventDefault(); + controller.submit({prompt}); + } + + return ( + +
+
+

Thermidor + v0.9 catalog contract

+

Server-owned commerce state, client-owned rendering.

+

+ Thermidor stays UI-less. This sample recognizes an a2ui-surface activity, + resolves the advertised controllers, and renders its local A2-UI component catalog. +

+
+ +
+ +
+ setPrompt(event.target.value)} + disabled={state.isStreaming} + /> + +
+
+ + {turn?.agentResponse?.messages.map((message, index) => ( +

+ {message.content} +

+ ))} + {turn?.status === 'error' &&

{turn.error}

} + + {!turn &&

Run the pre-filled prompt to render the schema example.

} +
+
+ ); +} + +function getNavigatorContext() { + return { + clientId: crypto.randomUUID(), + location: window.location.href, + referrer: document.referrer || null, + userAgent: navigator.userAgent || null, + }; +} diff --git a/samples/thermidor/schema-contract-react/src/a2ui/components.test.ts b/samples/thermidor/schema-contract-react/src/a2ui/components.test.ts new file mode 100644 index 00000000000..1011174b55e --- /dev/null +++ b/samples/thermidor/schema-contract-react/src/a2ui/components.test.ts @@ -0,0 +1,105 @@ +import {describe, expect, it} from 'vitest'; +import {thermidorCatalogDefinitions} from './components.js'; +import { + cartControllerContract, + cartItemSchema, + productListControllerContract, + productSchema, +} from '@coveo/thermidor-contracts'; + +describe('thermidorCatalogDefinitions', () => { + it('accepts the controller advertisements supplied by the catalog message', () => { + expect( + thermidorCatalogDefinitions.ProductCarousel.props.safeParse({ + controllers: { + productListController: { + controllerId: 'featured-products', + controllerSchema: + 'https://schema.thermidor.coveo.com/controllers/product-list.schema.json', + }, + }, + }).success + ).toBe(true); + expect( + thermidorCatalogDefinitions.Cart.props.safeParse({ + controllers: { + cartController: { + controllerId: 'shopping-cart', + controllerSchema: 'https://schema.thermidor.coveo.com/controllers/cart.schema.json', + }, + }, + }).success + ).toBe(true); + }); + + it('validates generated Product and CartItem values against their JSON Schema constraints', () => { + expect( + productSchema.safeParse({ + permanentid: 'p1', + ec_name: 'Trail shoes', + ec_rating: null, + additionalFields: {}, + children: [{permanentid: 'p1-blue', ec_name: 'Trail shoes', additionalFields: {}}], + }).success + ).toBe(true); + expect( + productSchema.safeParse({ + permanentid: 'p1', + ec_name: 'Trail shoes', + ec_rating: 6, + additionalFields: {}, + }).success + ).toBe(false); + expect( + cartItemSchema.safeParse({productId: 'p1', name: 'Trail shoes', price: 0, quantity: 1}) + .success + ).toBe(false); + expect( + cartItemSchema.safeParse({productId: 'p1', name: 'Trail shoes', price: 99.99, quantity: 1.5}) + .success + ).toBe(false); + }); + + it('enforces the controller contract literals and closed binding objects from JSON Schema', () => { + expect( + thermidorCatalogDefinitions.ProductCarousel.props.safeParse({ + controllers: { + productListController: { + controllerId: 'featured-products', + controllerSchema: 'https://schema.thermidor.coveo.com/controllers/cart.schema.json', + }, + }, + }).success + ).toBe(false); + expect( + thermidorCatalogDefinitions.Cart.props.safeParse({ + controllers: { + cartController: { + controllerId: 'shopping-cart', + controllerSchema: 'https://schema.thermidor.coveo.com/controllers/cart.schema.json', + unexpected: true, + }, + }, + }).success + ).toBe(false); + }); + + it('validates generated controller state and action contracts', () => { + expect( + productListControllerContract.shape.state.safeParse({ + products: [{permanentid: 'p1', ec_name: 'Trail shoes', additionalFields: {}}], + }).success + ).toBe(true); + expect(cartControllerContract.shape.state.safeParse({items: []}).success).toBe(true); + expect( + cartControllerContract.shape.setItems.safeParse({ + items: [{productId: 'p1', name: 'Trail shoes', price: 99.99, quantity: 1}], + }).success + ).toBe(true); + expect( + cartControllerContract.shape.updateItemQuantity.safeParse({ + item: {productId: 'p1', name: 'Trail shoes', price: 99.99, quantity: 0}, + }).success + ).toBe(false); + }); +}); diff --git a/samples/thermidor/schema-contract-react/src/a2ui/components.tsx b/samples/thermidor/schema-contract-react/src/a2ui/components.tsx new file mode 100644 index 00000000000..c563372cd35 --- /dev/null +++ b/samples/thermidor/schema-contract-react/src/a2ui/components.tsx @@ -0,0 +1,103 @@ +import { + createCatalog, + type CatalogDefinitions, + type CatalogRenderers, +} from '@copilotkit/a2ui-renderer'; +import {type EngineStateSource, useAdvertisedController} from './controllers.js'; +import {cartPropsSchema, productCarouselPropsSchema} from '@coveo/thermidor-contracts'; + +export const THERMIDOR_CATALOG_ID = 'https://schema.thermidor.coveo.com/a2-ui/catalog.json'; + +export const thermidorCatalogDefinitions = { + ProductCarousel: { + description: 'A responsive product carousel backed by a product-list controller.', + props: productCarouselPropsSchema, + }, + Cart: { + description: 'A shopping-cart summary backed by a cart controller.', + props: cartPropsSchema, + }, +} satisfies CatalogDefinitions; + +export function createThermidorCatalog(stateSource: EngineStateSource) { + const renderers = { + ProductCarousel: ({props}) => { + const controller = useAdvertisedController( + stateSource, + props.controllers.productListController + ); + const products = controller.state?.products ?? []; + + return ( +
+
+
+

ProductCarousel

+

Featured products

+
+ + controller: {props.controllers.productListController.controllerId} + +
+
+ {products.map((product) => { + const price = product.ec_promo_price ?? product.ec_price; + return ( +
+ +

{product.ec_brand}

+

{product.ec_name}

+

{product.ec_shortdesc}

+
+ + {price === undefined ? 'Price unavailable' : `$${price.toFixed(2)}`} + + + {product.ec_rating == null ? 'Not rated' : `★ ${product.ec_rating}`} + +
+ + {product.ec_in_stock === undefined + ? 'Availability unknown' + : product.ec_in_stock + ? 'In stock' + : 'Out of stock'} + +
+ ); + })} +
+
+ ); + }, + Cart: ({props}) => { + const controller = useAdvertisedController(stateSource, props.controllers.cartController); + const items = controller.state?.items ?? []; + const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0); + + return ( + + ); + }, + } satisfies CatalogRenderers; + + return createCatalog(thermidorCatalogDefinitions, renderers, { + catalogId: THERMIDOR_CATALOG_ID, + includeBasicCatalog: true, + }); +} diff --git a/samples/thermidor/schema-contract-react/src/a2ui/controllers.test.ts b/samples/thermidor/schema-contract-react/src/a2ui/controllers.test.ts new file mode 100644 index 00000000000..9cfc1f6b447 --- /dev/null +++ b/samples/thermidor/schema-contract-react/src/a2ui/controllers.test.ts @@ -0,0 +1,64 @@ +import {describe, expect, it, vi} from 'vitest'; +import { + buildRemoteController, + selectRemoteControllerState, + type RemoteControllerSource, +} from '@coveo/thermidor'; +import {cartControllerContract, type CartController} from '@coveo/thermidor-contracts'; + +describe('selectRemoteControllerState', () => { + it('selects the advertised controller slice from the active Thermidor turn', () => { + const state = { + activeTurn: { + agentResponse: { + state: {controllers: {'featured-products': {products: [{permanentid: 'p1'}]}}}, + }, + }, + } as unknown as Parameters[0]; + + expect(selectRemoteControllerState(state, 'featured-products')).toEqual({ + products: [{permanentid: 'p1'}], + }); + expect(selectRemoteControllerState(state, 'unknown-controller')).toEqual({}); + }); + + it('defines CartController as an inferred Zod contract object', () => { + const contract: CartController = { + schemaId: 'https://schema.thermidor.coveo.com/controllers/cart.schema.json', + state: {items: []}, + setItems: {items: []}, + updateItemQuantity: { + item: {productId: 'p1', name: 'Product', price: 10, quantity: 2}, + }, + }; + + expect(cartControllerContract.parse(contract)).toEqual(contract); + }); + + it('builds a remote controller from the advertised CartController schema ID', async () => { + const dispatchAction = vi.fn(); + const source = { + state: { + activeTurn: {agentResponse: {state: {controllers: {'shopping-cart': {items: []}}}}}, + }, + subscribe: () => () => undefined, + dispatchAction, + } as unknown as RemoteControllerSource; + const controller = buildRemoteController({ + source, + controllerId: 'shopping-cart', + contract: 'https://schema.thermidor.coveo.com/controllers/cart.schema.json', + }); + + await controller.dispatch('updateItemQuantity', { + item: {productId: 'p1', name: 'Product', price: 10, quantity: 2}, + }); + + expect(dispatchAction).toHaveBeenCalledWith({ + controllerId: 'shopping-cart', + controllerSchema: 'https://schema.thermidor.coveo.com/controllers/cart.schema.json', + action: 'updateItemQuantity', + payload: {item: {productId: 'p1', name: 'Product', price: 10, quantity: 2}}, + }); + }); +}); diff --git a/samples/thermidor/schema-contract-react/src/a2ui/controllers.tsx b/samples/thermidor/schema-contract-react/src/a2ui/controllers.tsx new file mode 100644 index 00000000000..1a94e6f9358 --- /dev/null +++ b/samples/thermidor/schema-contract-react/src/a2ui/controllers.tsx @@ -0,0 +1,28 @@ +import {useMemo} from 'react'; +import { + buildRemoteController, + type AdvertisedRemoteController, + type RemoteControllerSource, +} from '@coveo/thermidor'; +import type {ControllerContracts} from '@coveo/thermidor-contracts'; + +type ControllerSchemaId = ControllerContracts['schemaId']; + +export type ControllerAdvertisement = { + controllerId: string; + controllerSchema: TSchema; +}; + +export type EngineStateSource = RemoteControllerSource; + +type AdvertisedController = AdvertisedRemoteController; + +export function useAdvertisedController( + source: EngineStateSource, + {controllerId, controllerSchema: contract}: ControllerAdvertisement +): AdvertisedController { + return useMemo( + () => buildRemoteController({source, controllerId, contract}), + [controllerId, contract, source] + ); +} diff --git a/samples/thermidor/schema-contract-react/src/a2ui/surfaces.test.ts b/samples/thermidor/schema-contract-react/src/a2ui/surfaces.test.ts new file mode 100644 index 00000000000..266a256e6ae --- /dev/null +++ b/samples/thermidor/schema-contract-react/src/a2ui/surfaces.test.ts @@ -0,0 +1,26 @@ +import {describe, expect, it} from 'vitest'; +import {getA2UIMessages} from './surfaces.js'; + +describe('getA2UIMessages', () => { + it('passes raw A2-UI operations through and honors activity replacement', () => { + const previousOperation = {version: 'v0.9', createSurface: {surfaceId: 'old'}}; + const replacementOperation = {version: 'v0.9', createSurface: {surfaceId: 'catalog'}}; + + expect( + getA2UIMessages([ + { + id: 'old', + kind: 'a2ui-surface', + replace: false, + payload: {a2ui_operations: [previousOperation]}, + }, + { + id: 'catalog', + kind: 'a2ui-surface', + replace: true, + payload: {a2ui_operations: [replacementOperation]}, + }, + ]) + ).toEqual([replacementOperation]); + }); +}); diff --git a/samples/thermidor/schema-contract-react/src/a2ui/surfaces.tsx b/samples/thermidor/schema-contract-react/src/a2ui/surfaces.tsx new file mode 100644 index 00000000000..8ee4bd4038b --- /dev/null +++ b/samples/thermidor/schema-contract-react/src/a2ui/surfaces.tsx @@ -0,0 +1,76 @@ +import {useEffect, useMemo, useRef} from 'react'; +import {A2UIRenderer, useA2UI} from '@copilotkit/a2ui-renderer'; +import type {Activity} from '@coveo/thermidor'; + +type A2UIMessage = Record; + +/** Extracts opaque A2-UI messages without translating their protocol payloads. */ +export function getA2UIMessages(activities: Activity[]): A2UIMessage[] { + const messages: A2UIMessage[] = []; + + for (const activity of activities) { + if (activity.kind !== 'a2ui-surface' || !isRecord(activity.payload)) { + continue; + } + const operations = activity.payload['a2ui_operations']; + if (!Array.isArray(operations)) { + continue; + } + if (activity.replace) { + messages.length = 0; + } + messages.push(...operations.filter(isRecord)); + } + + return messages; +} + +export function ThermidorA2UISurfaces({messages}: {messages: A2UIMessage[]}) { + const {clearSurfaces, processMessages} = useA2UI(); + const serializedMessages = useMemo(() => JSON.stringify(messages), [messages]); + const surfaceIds = useMemo(() => getSurfaceIds(messages), [messages]); + const actionsRef = useRef({clearSurfaces, processMessages}); + actionsRef.current = {clearSurfaces, processMessages}; + + useEffect(() => { + const {clearSurfaces, processMessages} = actionsRef.current; + clearSurfaces(); + if (serializedMessages !== '[]') { + processMessages(JSON.parse(serializedMessages) as A2UIMessage[]); + } + }, [serializedMessages]); + + return ( + <> + {surfaceIds.map((surfaceId) => ( +
+ +
+ ))} + + ); +} + +function getSurfaceIds(messages: A2UIMessage[]): string[] { + const surfaceIds = new Set(); + for (const message of messages) { + const createSurface = message['createSurface']; + if (isRecord(createSurface) && typeof createSurface['surfaceId'] === 'string') { + surfaceIds.add(createSurface['surfaceId']); + continue; + } + const deleteSurface = message['deleteSurface']; + if (isRecord(deleteSurface) && typeof deleteSurface['surfaceId'] === 'string') { + surfaceIds.delete(deleteSurface['surfaceId']); + } + } + return [...surfaceIds]; +} + +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} diff --git a/samples/thermidor/schema-contract-react/src/env.ts b/samples/thermidor/schema-contract-react/src/env.ts new file mode 100644 index 00000000000..f8344f500a5 --- /dev/null +++ b/samples/thermidor/schema-contract-react/src/env.ts @@ -0,0 +1,30 @@ +const requiredKeys = [ + 'VITE_COVEO_ORGANIZATION_ID', + 'VITE_COVEO_ACCESS_TOKEN', + 'VITE_COVEO_TRACKING_ID', + 'VITE_COVEO_LANGUAGE', + 'VITE_COVEO_COUNTRY', + 'VITE_COVEO_CURRENCY', +] as const; + +function required(key: (typeof requiredKeys)[number]): string { + const value = import.meta.env[key]?.trim(); + if (!value) { + throw new Error(`Missing required environment variable: ${key}`); + } + return value; +} + +export function getSampleConfiguration() { + return { + organizationId: required('VITE_COVEO_ORGANIZATION_ID'), + accessToken: required('VITE_COVEO_ACCESS_TOKEN'), + trackingId: required('VITE_COVEO_TRACKING_ID'), + language: required('VITE_COVEO_LANGUAGE'), + country: required('VITE_COVEO_COUNTRY'), + currency: required('VITE_COVEO_CURRENCY'), + endpoint: import.meta.env.DEV + ? window.location.origin + : import.meta.env.VITE_COVEO_ENDPOINT?.trim(), + }; +} diff --git a/samples/thermidor/schema-contract-react/src/main.tsx b/samples/thermidor/schema-contract-react/src/main.tsx new file mode 100644 index 00000000000..656ef2ed1cb --- /dev/null +++ b/samples/thermidor/schema-contract-react/src/main.tsx @@ -0,0 +1,5 @@ +import {createRoot} from 'react-dom/client'; +import App from './App.js'; +import './styles.css'; + +createRoot(document.getElementById('root')!).render(); diff --git a/samples/thermidor/schema-contract-react/src/styles.css b/samples/thermidor/schema-contract-react/src/styles.css new file mode 100644 index 00000000000..e2730e297ab --- /dev/null +++ b/samples/thermidor/schema-contract-react/src/styles.css @@ -0,0 +1,201 @@ +:root { + color: #17202a; + background: #f6f8fb; + font-family: Inter, ui-sans-serif, system-ui, sans-serif; +} +* { + box-sizing: border-box; +} +body { + margin: 0; +} +button, +input { + font: inherit; +} +.page-shell { + width: min(1120px, calc(100% - 32px)); + margin: 0 auto; + padding: 56px 0 80px; +} +.hero { + max-width: 760px; +} +.eyebrow { + margin: 0 0 8px; + color: #3867d6; + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.09em; + text-transform: uppercase; +} +h1, +h2, +h3, +p { + margin-top: 0; +} +h1 { + margin-bottom: 16px; + font-size: clamp(2.2rem, 5vw, 4rem); + line-height: 1.05; +} +.hero > p:last-child { + color: #4d5968; + font-size: 1.1rem; + line-height: 1.6; +} +code { + color: #254da3; +} +.prompt { + display: grid; + gap: 8px; + margin: 36px 0 20px; +} +.prompt label { + font-weight: 700; +} +.prompt div { + display: flex; + gap: 8px; +} +.prompt input { + flex: 1; + min-width: 0; + border: 1px solid #b9c4d4; + border-radius: 8px; + padding: 12px 14px; + background: white; +} +.prompt button { + border: 0; + border-radius: 8px; + padding: 12px 18px; + background: #244db6; + color: white; + font-weight: 700; + cursor: pointer; +} +.prompt button:disabled { + cursor: wait; + opacity: 0.65; +} +.agent-message, +.hint, +.error { + padding: 14px 16px; + border-radius: 8px; + background: #e7edf7; + line-height: 1.5; +} +.error { + color: #9c1c1c; + background: #fbe7e7; +} +.catalog-surface { + display: grid; + grid-template-columns: minmax(0, 1fr) 280px; + gap: 24px; + align-items: start; + margin-top: 24px; +} +.product-carousel, +.cart { + border: 1px solid #dce3ef; + border-radius: 14px; + padding: 22px; + background: white; + box-shadow: 0 10px 30px rgba(30, 48, 79, 0.06); +} +.section-heading { + display: flex; + align-items: start; + justify-content: space-between; + gap: 16px; +} +.section-heading h2, +.cart h2 { + margin-bottom: 20px; +} +.controller-id { + color: #5b687a; + font-size: 0.8rem; +} +.product-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; +} +.product-card { + overflow: hidden; + border: 1px solid #e1e7f0; + border-radius: 10px; + padding: 14px; +} +.product-card img { + width: 100%; + height: 132px; + border-radius: 6px; + background: #e7ecf3; + object-fit: cover; +} +.product-card h3 { + margin-bottom: 8px; + font-size: 1.05rem; +} +.product-card p:not(.brand) { + min-height: 42px; + margin-bottom: 14px; + color: #596779; + font-size: 0.9rem; + line-height: 1.4; +} +.brand { + margin: 12px 0 6px; + color: #64748b; + font-size: 0.78rem; + font-weight: 700; + text-transform: uppercase; +} +.product-meta, +.cart-line, +.cart-total { + display: flex; + justify-content: space-between; + gap: 12px; +} +.product-meta { + margin-bottom: 8px; +} +.product-card small { + color: #167544; + font-weight: 700; +} +.cart-line { + padding: 12px 0; + border-bottom: 1px solid #e6ebf2; + line-height: 1.4; +} +.cart-total { + margin-top: 18px; + font-size: 1.1rem; +} +.unsupported-component { + color: #8b5c00; +} +@media (max-width: 760px) { + .catalog-surface { + grid-template-columns: 1fr; + } + .product-grid { + grid-template-columns: 1fr; + } + .section-heading { + display: block; + } + .controller-id { + display: block; + margin-bottom: 16px; + } +} diff --git a/samples/thermidor/schema-contract-react/src/use-controller.ts b/samples/thermidor/schema-contract-react/src/use-controller.ts new file mode 100644 index 00000000000..bfd5424f155 --- /dev/null +++ b/samples/thermidor/schema-contract-react/src/use-controller.ts @@ -0,0 +1,20 @@ +import {useCallback, useRef, useSyncExternalStore} from 'react'; +import type {Controller} from '@coveo/thermidor'; + +type StateOf = T extends Controller ? TState : never; + +export function useController>( + factory: () => TController +): [TController, StateOf] { + const controllerRef = useRef(null); + controllerRef.current ??= factory(); + const controller = controllerRef.current; + + const subscribe = useCallback( + (onStoreChange: () => void) => controller.subscribe(onStoreChange), + [controller] + ); + const getSnapshot = useCallback(() => controller.state as StateOf, [controller]); + + return [controller, useSyncExternalStore(subscribe, getSnapshot, getSnapshot)]; +} diff --git a/samples/thermidor/schema-contract-react/tsconfig.json b/samples/thermidor/schema-contract-react/tsconfig.json new file mode 100644 index 00000000000..52cd885c82a --- /dev/null +++ b/samples/thermidor/schema-contract-react/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "isolatedModules": true, + "jsx": "react-jsx", + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "Node16", + "moduleResolution": "Node16", + "noEmit": true, + "types": ["vitest/globals", "vite/client"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/samples/thermidor/schema-contract-react/vite.config.ts b/samples/thermidor/schema-contract-react/vite.config.ts new file mode 100644 index 00000000000..d39de1f8a93 --- /dev/null +++ b/samples/thermidor/schema-contract-react/vite.config.ts @@ -0,0 +1,22 @@ +import react from '@vitejs/plugin-react'; +import {defineConfig, loadEnv} from 'vite'; + +export default defineConfig(({mode}) => { + const endpoint = loadEnv(mode, process.cwd(), '').VITE_COVEO_ENDPOINT?.trim(); + + return { + plugins: [react()], + server: { + ...(endpoint + ? { + proxy: { + '/rest': { + target: endpoint, + changeOrigin: true, + }, + }, + } + : {}), + }, + }; +});