From e145a9b7e59d5f93327a856314747001c8f4ac42 Mon Sep 17 00:00:00 2001
From: Louis Bompart
Date: Mon, 27 Jul 2026 22:25:50 +0200
Subject: [PATCH 1/5] wip-demo
---
packages/mock-converse-api/src/server.test.ts | 69 ++++
.../src/converse/generate-response.ts | 4 +
.../src/converse/templates/response9.ts | 122 +++++++
.../src/converse/templates/templates.ts | 5 +-
packages/thermidor/src/index.ts | 2 +-
.../api/generative/generative-runtime.test.ts | 15 +-
.../api/generative/generative-runtime.ts | 11 +-
.../features/generative/generative-actions.ts | 4 +-
.../features/generative/generative-slice.ts | 6 +-
.../features/generative/generative-types.ts | 20 +-
.../src/internal/features/generative/index.ts | 2 +-
.../converse/converse-controller.ts | 9 +-
pnpm-lock.yaml | 298 ++++++++++++------
samples/README.md | 14 +-
.../generative-angular/src/app/a2ui-parser.ts | 4 +-
.../generative-angular/src/app/models.ts | 4 +-
.../src/app/services/conversation.service.ts | 7 +-
.../AgentResponse/AgentResponse.tsx | 11 +-
samples/thermidor/schema-contract-react/.env | 6 +
.../schema-contract-react/.env.example | 6 +
.../thermidor/schema-contract-react/README.md | 24 ++
.../schema-contract-react/index.html | 12 +
.../schema-contract-react/package.json | 26 ++
.../schema-contract-react/src/App.tsx | 92 ++++++
.../src/a2ui/adapter.test.ts | 73 +++++
.../schema-contract-react/src/a2ui/adapter.ts | 111 +++++++
.../src/a2ui/components.tsx | 112 +++++++
.../schema-contract-react/src/env.ts | 30 ++
.../schema-contract-react/src/main.tsx | 5 +
.../schema-contract-react/src/styles.css | 201 ++++++++++++
.../src/use-controller.ts | 20 ++
.../schema-contract-react/tsconfig.json | 15 +
.../schema-contract-react/vite.config.ts | 22 ++
33 files changed, 1233 insertions(+), 129 deletions(-)
create mode 100644 packages/platform-mock-api/src/converse/templates/response9.ts
create mode 100644 samples/thermidor/schema-contract-react/.env
create mode 100644 samples/thermidor/schema-contract-react/.env.example
create mode 100644 samples/thermidor/schema-contract-react/README.md
create mode 100644 samples/thermidor/schema-contract-react/index.html
create mode 100644 samples/thermidor/schema-contract-react/package.json
create mode 100644 samples/thermidor/schema-contract-react/src/App.tsx
create mode 100644 samples/thermidor/schema-contract-react/src/a2ui/adapter.test.ts
create mode 100644 samples/thermidor/schema-contract-react/src/a2ui/adapter.ts
create mode 100644 samples/thermidor/schema-contract-react/src/a2ui/components.tsx
create mode 100644 samples/thermidor/schema-contract-react/src/env.ts
create mode 100644 samples/thermidor/schema-contract-react/src/main.tsx
create mode 100644 samples/thermidor/schema-contract-react/src/styles.css
create mode 100644 samples/thermidor/schema-contract-react/src/use-controller.ts
create mode 100644 samples/thermidor/schema-contract-react/tsconfig.json
create mode 100644 samples/thermidor/schema-contract-react/vite.config.ts
diff --git a/packages/mock-converse-api/src/server.test.ts b/packages/mock-converse-api/src/server.test.ts
index 64bdc6be809..6d3122cee0b 100644
--- a/packages/mock-converse-api/src/server.test.ts
+++ b/packages/mock-converse-api/src/server.test.ts
@@ -125,6 +125,75 @@ 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');
+
+ 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: [{component: 'ProductCarousel'}, {component: 'Cart'}],
+ },
+ },
+ {
+ version: 'v0.9',
+ updateDataModel: {
+ surfaceId: 'commerce-catalog-example',
+ },
+ },
+ ],
+ },
+ });
+
+ const content = activity?.['content'] as {a2ui_operations: Array>};
+ const dataModelOperation = content.a2ui_operations.find(
+ (operation) => 'updateDataModel' in operation
+ )!['updateDataModel'] as {
+ value: {
+ controllers: Record;
+ };
+ };
+
+ expect(dataModelOperation.value.controllers['featured-products'].products).toEqual(
+ expect.arrayContaining([expect.objectContaining({permanentid: 'trail-running-shoes-001'})])
+ );
+ expect(dataModelOperation.value.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..1daee0cff0d
--- /dev/null
+++ b/packages/platform-mock-api/src/converse/templates/response9.ts
@@ -0,0 +1,122 @@
+import {
+ ActivitySnapshot,
+ RunFinished,
+ RunStarted,
+ TurnComplete,
+ TurnStarted,
+ textMessage,
+ type ConverseEvent,
+} from '../events.js';
+
+const thermidorSchemaCatalogResponseEvents: ConverseEvent[] = [
+ TurnStarted(),
+ RunStarted(),
+ ...textMessage(
+ 'thermidor-schema-catalog-message',
+ 'Here are featured products and the current cart from the Thermidor catalog contract.'
+ ),
+ 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',
+ sendDataModel: true,
+ },
+ },
+ {
+ version: 'v0.9',
+ updateComponents: {
+ surfaceId: 'commerce-catalog-example',
+ components: [
+ {
+ 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',
+ },
+ },
+ },
+ ],
+ },
+ },
+ {
+ version: 'v0.9',
+ updateDataModel: {
+ surfaceId: 'commerce-catalog-example',
+ value: {
+ 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,
+ },
+ ],
+ },
+ },
+ },
+ },
+ },
+ ],
+ },
+ }),
+ 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 a2567904d6b..906fe2c8f66 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/src/index.ts b/packages/thermidor/src/index.ts
index 89328378f10..d6631066899 100644
--- a/packages/thermidor/src/index.ts
+++ b/packages/thermidor/src/index.ts
@@ -35,7 +35,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/generative/generative-runtime.test.ts b/packages/thermidor/src/internal/api/generative/generative-runtime.test.ts
index 0cc70b6ee57..e67684d5ca1 100644
--- a/packages/thermidor/src/internal/api/generative/generative-runtime.test.ts
+++ b/packages/thermidor/src/internal/api/generative/generative-runtime.test.ts
@@ -44,7 +44,7 @@ function createMockStatePort(): GenerativeStatePort {
initAgentResponse: vi.fn(),
startMessage: vi.fn(),
appendMessageDelta: vi.fn(),
- appendSurface: vi.fn(),
+ appendActivity: vi.fn(),
startToolCall: vi.fn(),
appendToolCallArgs: vi.fn(),
completeToolCall: vi.fn(),
@@ -453,16 +453,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 +471,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 () => {
diff --git a/packages/thermidor/src/internal/api/generative/generative-runtime.ts b/packages/thermidor/src/internal/api/generative/generative-runtime.ts
index 4dae1e84240..f36c1b334a5 100644
--- a/packages/thermidor/src/internal/api/generative/generative-runtime.ts
+++ b/packages/thermidor/src/internal/api/generative/generative-runtime.ts
@@ -9,7 +9,7 @@ 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,
@@ -23,7 +23,7 @@ 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;
startToolCall(turnId: string, toolCallId: string, toolName: string): void;
appendToolCallArgs(turnId: string, toolCallId: string, delta: string): void;
completeToolCall(turnId: string, toolCallId: string, result: string): void;
@@ -253,7 +253,12 @@ export class GenerativeRuntime {
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};
}
diff --git a/packages/thermidor/src/internal/features/generative/generative-actions.ts b/packages/thermidor/src/internal/features/generative/generative-actions.ts
index 122f4f59c72..94782ea509c 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 {getHandleInternals} 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,7 @@ 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`),
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 aeedc9ec770..c070d35ae02 100644
--- a/packages/thermidor/src/internal/features/generative/generative-slice.ts
+++ b/packages/thermidor/src/internal/features/generative/generative-slice.ts
@@ -57,7 +57,7 @@ export function createGenerativeSlice(
if (turn) {
turn.agentResponse = {
messages: [],
- surfaces: [],
+ activities: [],
reasoningSteps: [],
};
}
@@ -75,10 +75,10 @@ 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.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..fce91337799 100644
--- a/packages/thermidor/src/internal/features/generative/generative-types.ts
+++ b/packages/thermidor/src/internal/features/generative/generative-types.ts
@@ -117,9 +117,12 @@ export interface AgentResponse {
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 +192,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.ts b/packages/thermidor/src/public/controllers/converse/converse-controller.ts
index 7de201a8d9a..a8816c08a02 100644
--- a/packages/thermidor/src/public/controllers/converse/converse-controller.ts
+++ b/packages/thermidor/src/public/controllers/converse/converse-controller.ts
@@ -97,12 +97,8 @@ 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}));
},
startToolCall: (turnId, toolCallId, toolName) => {
this.engine.mutate(this.#actions.startToolCall({turnId, toolCallId, toolName}));
@@ -238,5 +234,4 @@ export interface ConverseControllerState {
export interface ConverseControllerOptions {
interface: GenerativeInterface;
conversationToRestore?: SerializedConverseState;
- onSurfaceOperation?: (operations: unknown[]) => void;
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index cb99f32a0f2..ae51ec881e9 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -483,7 +483,7 @@ importers:
devDependencies:
'@angular-devkit/build-angular':
specifier: 'catalog:'
- version: 21.2.17(7e6d0542722e3ce068cf3323a964064c)
+ version: 21.2.17(80d26e843c839fab20396dae6982b00b)
'@angular/cli':
specifier: 'catalog:'
version: 21.2.17(@types/node@24.12.4)(chokidar@5.0.0)
@@ -2101,6 +2101,37 @@ importers:
specifier: 'catalog:'
version: 4.1.9(@types/node@24.12.4)(@vitest/browser-playwright@4.1.9)(jsdom@28.1.0)(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.2(@types/node@24.12.4)(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:
+ '@coveo/thermidor':
+ specifier: workspace:*
+ version: link:../../../packages/thermidor
+ react:
+ specifier: 19.2.7
+ version: 19.2.7
+ react-dom:
+ specifier: 19.2.7
+ version: 19.2.7(react@19.2.7)
+ 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.3(vite@8.1.2(@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.2(@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.9(@types/node@26.1.0)(@vitest/browser-playwright@4.1.9)(jsdom@28.1.0)(msw@2.14.6(@types/node@26.1.0)(typescript@6.0.3))(vite@8.1.2(@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':
@@ -16801,13 +16832,13 @@ snapshots:
transitivePeerDependencies:
- chokidar
- '@angular-devkit/build-angular@21.2.17(7e6d0542722e3ce068cf3323a964064c)':
+ '@angular-devkit/build-angular@21.2.17(80d26e843c839fab20396dae6982b00b)':
dependencies:
'@ampproject/remapping': 2.3.0
'@angular-devkit/architect': 0.2102.17(chokidar@5.0.0)
'@angular-devkit/build-webpack': 0.2102.17(chokidar@5.0.0)(webpack-dev-server@5.2.5(tslib@2.8.1)(webpack@5.105.2(esbuild@0.28.1)(postcss@8.5.12)))(webpack@5.105.2(esbuild@0.28.1)(postcss@8.5.12))
'@angular-devkit/core': 21.2.17(chokidar@5.0.0)
- '@angular/build': 21.2.17(2579aedd5157518c28138f5f823a7c91)
+ '@angular/build': 21.2.17(6133a8bd1a48a56825f649b47a4a697e)
'@angular/compiler-cli': 21.2.17(@angular/compiler@21.2.17)(typescript@6.0.3)
'@babel/core': 7.29.7
'@babel/generator': 7.29.1
@@ -16864,7 +16895,7 @@ snapshots:
'@angular/platform-browser': 21.2.17(@angular/animations@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1))
'@angular/platform-server': 21.2.17(@angular/common@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@21.2.17)(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@21.2.17(@angular/animations@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)
esbuild: 0.28.1
- jest: 30.3.0(@types/node@24.12.4)
+ jest: 30.3.0(@types/node@24.12.4)(ts-node@10.9.2(@types/node@24.12.4)(typescript@6.0.3))
ng-packagr: 20.3.2(@angular/compiler-cli@21.2.17(@angular/compiler@21.2.17)(typescript@6.0.3))(tailwindcss@4.3.2)(tslib@2.8.1)(typescript@6.0.3)
tailwindcss: 4.3.2
transitivePeerDependencies:
@@ -16934,7 +16965,7 @@ snapshots:
'@angular/core': 21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1)
tslib: 2.8.1
- '@angular/build@21.2.17(2579aedd5157518c28138f5f823a7c91)':
+ '@angular/build@21.2.17(5b911479a7812c42761d62528d4640b4)':
dependencies:
'@ampproject/remapping': 2.3.0
'@angular-devkit/architect': 0.2102.17(chokidar@5.0.0)
@@ -16944,7 +16975,7 @@ snapshots:
'@babel/helper-annotate-as-pure': 7.27.3
'@babel/helper-split-export-declaration': 7.24.7
'@inquirer/confirm': 5.1.21(@types/node@24.12.4)
- '@vitejs/plugin-basic-ssl': 2.1.4(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.9.0))
+ '@vitejs/plugin-basic-ssl': 2.1.4(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.48.0)(yaml@2.9.0))
beasties: 0.4.1
browserslist: 4.28.5
esbuild: 0.28.1
@@ -16957,7 +16988,7 @@ snapshots:
parse5-html-rewriting-stream: 8.0.0
picomatch: 4.0.4
piscina: 5.2.0
- rolldown: 1.0.0-rc.4(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)
+ rolldown: 1.0.0-rc.4(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)
sass: 1.97.3
semver: 7.7.4
source-map-support: 0.5.21
@@ -16965,18 +16996,17 @@ snapshots:
tslib: 2.8.1
typescript: 6.0.3
undici: 7.24.4
- vite: 7.3.5(@types/node@24.12.4)(jiti@2.7.0)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.9.0)
+ vite: 7.3.5(@types/node@24.12.4)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.48.0)(yaml@2.9.0)
watchpack: 2.5.1
optionalDependencies:
'@angular/core': 21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1)
'@angular/platform-browser': 21.2.17(@angular/animations@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1))
'@angular/platform-server': 21.2.17(@angular/common@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@21.2.17)(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@21.2.17(@angular/animations@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)
- less: 4.4.2
+ less: 4.6.7
lmdb: 3.5.1
- ng-packagr: 20.3.2(@angular/compiler-cli@21.2.17(@angular/compiler@21.2.17)(typescript@6.0.3))(tailwindcss@4.3.2)(tslib@2.8.1)(typescript@6.0.3)
- postcss: 8.5.12
+ postcss: 8.5.16
tailwindcss: 4.3.2
- vitest: 4.1.9(@types/node@24.12.4)(@vitest/browser-playwright@4.1.9)(jsdom@28.1.0)(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.3(@types/node@24.12.4)(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.9(@types/node@24.12.4)(@vitest/browser-playwright@4.1.9)(jsdom@28.1.0)(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@7.3.5(@types/node@24.12.4)(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:
- '@emnapi/core'
- '@emnapi/runtime'
@@ -16992,7 +17022,7 @@ snapshots:
- tsx
- yaml
- '@angular/build@21.2.17(5b911479a7812c42761d62528d4640b4)':
+ '@angular/build@21.2.17(6133a8bd1a48a56825f649b47a4a697e)':
dependencies:
'@ampproject/remapping': 2.3.0
'@angular-devkit/architect': 0.2102.17(chokidar@5.0.0)
@@ -17002,7 +17032,7 @@ snapshots:
'@babel/helper-annotate-as-pure': 7.27.3
'@babel/helper-split-export-declaration': 7.24.7
'@inquirer/confirm': 5.1.21(@types/node@24.12.4)
- '@vitejs/plugin-basic-ssl': 2.1.4(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.48.0)(yaml@2.9.0))
+ '@vitejs/plugin-basic-ssl': 2.1.4(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.9.0))
beasties: 0.4.1
browserslist: 4.28.5
esbuild: 0.28.1
@@ -17015,7 +17045,7 @@ snapshots:
parse5-html-rewriting-stream: 8.0.0
picomatch: 4.0.4
piscina: 5.2.0
- rolldown: 1.0.0-rc.4(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)
+ rolldown: 1.0.0-rc.4(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)
sass: 1.97.3
semver: 7.7.4
source-map-support: 0.5.21
@@ -17023,17 +17053,18 @@ snapshots:
tslib: 2.8.1
typescript: 6.0.3
undici: 7.24.4
- vite: 7.3.5(@types/node@24.12.4)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.48.0)(yaml@2.9.0)
+ vite: 7.3.5(@types/node@24.12.4)(jiti@2.7.0)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.9.0)
watchpack: 2.5.1
optionalDependencies:
'@angular/core': 21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1)
'@angular/platform-browser': 21.2.17(@angular/animations@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1))
'@angular/platform-server': 21.2.17(@angular/common@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@21.2.17)(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@21.2.17(@angular/animations@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)
- less: 4.6.7
+ less: 4.4.2
lmdb: 3.5.1
- postcss: 8.5.16
+ ng-packagr: 20.3.2(@angular/compiler-cli@21.2.17(@angular/compiler@21.2.17)(typescript@6.0.3))(tailwindcss@4.3.2)(tslib@2.8.1)(typescript@6.0.3)
+ postcss: 8.5.12
tailwindcss: 4.3.2
- vitest: 4.1.9(@types/node@24.12.4)(@vitest/browser-playwright@4.1.9)(jsdom@28.1.0)(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.3(@types/node@24.12.4)(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.9(@types/node@24.12.4)(@vitest/browser-playwright@4.1.9)(jsdom@28.1.0)(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.3(@types/node@24.12.4)(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:
- '@emnapi/core'
- '@emnapi/runtime'
@@ -17090,7 +17121,7 @@ snapshots:
lmdb: 3.5.1
postcss: 8.5.16
tailwindcss: 4.3.2
- vitest: 4.1.9(@types/node@26.1.0)(@vitest/browser-playwright@4.1.9)(jsdom@28.1.0)(msw@2.14.6(@types/node@26.1.0)(typescript@6.0.3))(vite@7.3.5(@types/node@26.1.0)(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.9(@types/node@26.1.0)(@vitest/browser-playwright@4.1.9)(jsdom@28.1.0)(msw@2.14.6(@types/node@26.1.0)(typescript@6.0.3))(vite@8.1.3(@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:
- '@emnapi/core'
- '@emnapi/runtime'
@@ -19717,6 +19748,42 @@ snapshots:
- supports-color
- ts-node
+ '@jest/core@30.3.0(ts-node@10.9.2(@types/node@24.12.4)(typescript@6.0.3))':
+ dependencies:
+ '@jest/console': 30.3.0
+ '@jest/pattern': 30.0.1
+ '@jest/reporters': 30.3.0
+ '@jest/test-result': 30.3.0
+ '@jest/transform': 30.3.0
+ '@jest/types': 30.3.0
+ '@types/node': 26.1.0
+ ansi-escapes: 4.3.2
+ chalk: 4.1.2
+ ci-info: 4.4.0
+ exit-x: 0.2.2
+ graceful-fs: 4.2.11
+ jest-changed-files: 30.3.0
+ jest-config: 30.3.0(@types/node@26.1.0)(ts-node@10.9.2(@types/node@24.12.4)(typescript@6.0.3))
+ jest-haste-map: 30.3.0
+ jest-message-util: 30.3.0
+ jest-regex-util: 30.0.1
+ jest-resolve: 30.3.0
+ jest-resolve-dependencies: 30.3.0
+ jest-runner: 30.3.0
+ jest-runtime: 30.3.0
+ jest-snapshot: 30.3.0
+ jest-util: 30.3.0
+ jest-validate: 30.3.0
+ jest-watcher: 30.3.0
+ pretty-format: 30.3.0
+ slash: 3.0.0
+ transitivePeerDependencies:
+ - babel-plugin-macros
+ - 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:
'@jest/console': 30.3.0
@@ -21763,9 +21830,9 @@ snapshots:
'@rolldown/binding-openharmony-arm64@1.1.4':
optional: true
- '@rolldown/binding-wasm32-wasi@1.0.0-rc.4(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)':
+ '@rolldown/binding-wasm32-wasi@1.0.0-rc.4(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)':
dependencies:
- '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)
+ '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)
transitivePeerDependencies:
- '@emnapi/core'
- '@emnapi/runtime'
@@ -23388,13 +23455,13 @@ snapshots:
- utf-8-validate
- vite
- '@vitest/browser-playwright@4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(playwright@1.60.0)(vite@8.1.3(@types/node@24.12.4)(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.9)':
+ '@vitest/browser-playwright@4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(playwright@1.62.0-alpha-2026-06-29)(vite@7.3.5(@types/node@24.12.4)(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.9)':
dependencies:
- '@vitest/browser': 4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.3(@types/node@24.12.4)(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.9)
- '@vitest/mocker': 4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.3(@types/node@24.12.4)(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
+ '@vitest/browser': 4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@7.3.5(@types/node@24.12.4)(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.9)
+ '@vitest/mocker': 4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@7.3.5(@types/node@24.12.4)(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.62.0-alpha-2026-06-29
tinyrainbow: 3.1.0
- vitest: 4.1.9(@types/node@24.12.4)(@vitest/browser-playwright@4.1.9)(jsdom@28.1.0)(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.3(@types/node@24.12.4)(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.9(@types/node@24.12.4)(@vitest/browser-playwright@4.1.9)(jsdom@28.1.0)(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@7.3.5(@types/node@24.12.4)(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:
- bufferutil
- msw
@@ -23416,32 +23483,32 @@ snapshots:
- vite
optional: true
- '@vitest/browser-playwright@4.1.9(msw@2.14.6(@types/node@26.1.0)(typescript@6.0.3))(playwright@1.60.0)(vite@8.1.3(@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.9)':
+ '@vitest/browser-playwright@4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(playwright@1.62.0-alpha-2026-06-29)(vite@8.1.3(@types/node@24.12.4)(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.9)':
dependencies:
- '@vitest/browser': 4.1.9(msw@2.14.6(@types/node@26.1.0)(typescript@6.0.3))(vite@8.1.3(@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.9)
- '@vitest/mocker': 4.1.9(msw@2.14.6(@types/node@26.1.0)(typescript@6.0.3))(vite@8.1.3(@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
+ '@vitest/browser': 4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.3(@types/node@24.12.4)(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.9)
+ '@vitest/mocker': 4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.3(@types/node@24.12.4)(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.62.0-alpha-2026-06-29
tinyrainbow: 3.1.0
- vitest: 4.1.9(@types/node@26.1.0)(@vitest/browser-playwright@4.1.9)(jsdom@28.1.0)(msw@2.14.6(@types/node@26.1.0)(typescript@6.0.3))(vite@8.1.3(@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.9(@types/node@24.12.4)(@vitest/browser-playwright@4.1.9)(jsdom@28.1.0)(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.3(@types/node@24.12.4)(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
+ optional: true
- '@vitest/browser-playwright@4.1.9(msw@2.14.6(@types/node@26.1.0)(typescript@6.0.3))(playwright@1.62.0-alpha-2026-06-29)(vite@7.3.5(@types/node@26.1.0)(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.9)':
+ '@vitest/browser-playwright@4.1.9(msw@2.14.6(@types/node@26.1.0)(typescript@6.0.3))(playwright@1.60.0)(vite@8.1.3(@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.9)':
dependencies:
- '@vitest/browser': 4.1.9(msw@2.14.6(@types/node@26.1.0)(typescript@6.0.3))(vite@7.3.5(@types/node@26.1.0)(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.9)
- '@vitest/mocker': 4.1.9(msw@2.14.6(@types/node@26.1.0)(typescript@6.0.3))(vite@7.3.5(@types/node@26.1.0)(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.62.0-alpha-2026-06-29
+ '@vitest/browser': 4.1.9(msw@2.14.6(@types/node@26.1.0)(typescript@6.0.3))(vite@8.1.3(@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.9)
+ '@vitest/mocker': 4.1.9(msw@2.14.6(@types/node@26.1.0)(typescript@6.0.3))(vite@8.1.3(@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.9(@types/node@26.1.0)(@vitest/browser-playwright@4.1.9)(jsdom@28.1.0)(msw@2.14.6(@types/node@26.1.0)(typescript@6.0.3))(vite@7.3.5(@types/node@26.1.0)(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.9(@types/node@26.1.0)(@vitest/browser-playwright@4.1.9)(jsdom@28.1.0)(msw@2.14.6(@types/node@26.1.0)(typescript@6.0.3))(vite@8.1.3(@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
- optional: true
'@vitest/browser-playwright@4.1.9(msw@2.14.6(@types/node@26.1.0)(typescript@6.0.3))(playwright@1.62.0-alpha-2026-06-29)(vite@8.1.2(@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.9)':
dependencies:
@@ -23471,51 +23538,51 @@ snapshots:
- vite
optional: true
- '@vitest/browser@4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.2(@types/node@24.12.4)(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.9)':
+ '@vitest/browser@4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@7.3.5(@types/node@24.12.4)(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.9)':
dependencies:
'@blazediff/core': 1.9.1
- '@vitest/mocker': 4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.2(@types/node@24.12.4)(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/mocker': 4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@7.3.5(@types/node@24.12.4)(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/utils': 4.1.9
magic-string: 0.30.21
pngjs: 7.0.0
sirv: 3.0.2
tinyrainbow: 3.1.0
- vitest: 4.1.9(@types/node@24.12.4)(@vitest/browser-playwright@4.1.9)(jsdom@28.1.0)(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.2(@types/node@24.12.4)(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.9(@types/node@24.12.4)(@vitest/browser-playwright@4.1.9)(jsdom@28.1.0)(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.48.0)(yaml@2.9.0))
ws: 8.21.0
transitivePeerDependencies:
- bufferutil
- msw
- utf-8-validate
- vite
+ optional: true
- '@vitest/browser@4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.3(@types/node@24.12.4)(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.9)':
+ '@vitest/browser@4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.2(@types/node@24.12.4)(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.9)':
dependencies:
'@blazediff/core': 1.9.1
- '@vitest/mocker': 4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.3(@types/node@24.12.4)(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/mocker': 4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.2(@types/node@24.12.4)(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/utils': 4.1.9
magic-string: 0.30.21
pngjs: 7.0.0
sirv: 3.0.2
tinyrainbow: 3.1.0
- vitest: 4.1.9(@types/node@24.12.4)(@vitest/browser-playwright@4.1.9)(jsdom@28.1.0)(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.3(@types/node@24.12.4)(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.9(@types/node@24.12.4)(@vitest/browser-playwright@4.1.9)(jsdom@28.1.0)(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.2(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))
ws: 8.21.0
transitivePeerDependencies:
- bufferutil
- msw
- utf-8-validate
- vite
- optional: true
- '@vitest/browser@4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.3(@types/node@24.12.4)(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.9)':
+ '@vitest/browser@4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.3(@types/node@24.12.4)(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.9)':
dependencies:
'@blazediff/core': 1.9.1
- '@vitest/mocker': 4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.3(@types/node@24.12.4)(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/mocker': 4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.3(@types/node@24.12.4)(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/utils': 4.1.9
magic-string: 0.30.21
pngjs: 7.0.0
sirv: 3.0.2
tinyrainbow: 3.1.0
- vitest: 4.1.9(@types/node@24.12.4)(@vitest/browser-playwright@4.1.9)(jsdom@28.1.0)(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.3(@types/node@24.12.4)(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.9(@types/node@24.12.4)(@vitest/browser-playwright@4.1.9)(jsdom@28.1.0)(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.3(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(less@4.4.2)(sass@1.97.3)(terser@5.48.0)(yaml@2.9.0))
ws: 8.21.0
transitivePeerDependencies:
- bufferutil
@@ -23524,16 +23591,16 @@ snapshots:
- vite
optional: true
- '@vitest/browser@4.1.9(msw@2.14.6(@types/node@26.1.0)(typescript@6.0.3))(vite@7.3.5(@types/node@26.1.0)(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.9)':
+ '@vitest/browser@4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.3(@types/node@24.12.4)(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.9)':
dependencies:
'@blazediff/core': 1.9.1
- '@vitest/mocker': 4.1.9(msw@2.14.6(@types/node@26.1.0)(typescript@6.0.3))(vite@7.3.5(@types/node@26.1.0)(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/mocker': 4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.3(@types/node@24.12.4)(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/utils': 4.1.9
magic-string: 0.30.21
pngjs: 7.0.0
sirv: 3.0.2
tinyrainbow: 3.1.0
- vitest: 4.1.9(@types/node@26.1.0)(@vitest/browser-playwright@4.1.9)(jsdom@28.1.0)(msw@2.14.6(@types/node@26.1.0)(typescript@6.0.3))(vite@7.3.5(@types/node@26.1.0)(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.9(@types/node@24.12.4)(@vitest/browser-playwright@4.1.9)(jsdom@28.1.0)(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.3(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))
ws: 8.21.0
transitivePeerDependencies:
- bufferutil
@@ -23594,6 +23661,16 @@ snapshots:
chai: 6.2.2
tinyrainbow: 3.1.0
+ '@vitest/mocker@4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.48.0)(yaml@2.9.0))':
+ dependencies:
+ '@vitest/spy': 4.1.9
+ estree-walker: 3.0.3
+ magic-string: 0.30.21
+ optionalDependencies:
+ msw: 2.14.6(@types/node@24.12.4)(typescript@6.0.3)
+ vite: 7.3.5(@types/node@24.12.4)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.48.0)(yaml@2.9.0)
+ optional: true
+
'@vitest/mocker@4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.2(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))':
dependencies:
'@vitest/spy': 4.1.9
@@ -23622,16 +23699,6 @@ snapshots:
msw: 2.14.6(@types/node@24.12.4)(typescript@6.0.3)
vite: 8.1.3(@types/node@24.12.4)(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/mocker@4.1.9(msw@2.14.6(@types/node@26.1.0)(typescript@6.0.3))(vite@7.3.5(@types/node@26.1.0)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.48.0)(yaml@2.9.0))':
- dependencies:
- '@vitest/spy': 4.1.9
- estree-walker: 3.0.3
- magic-string: 0.30.21
- optionalDependencies:
- msw: 2.14.6(@types/node@26.1.0)(typescript@6.0.3)
- vite: 7.3.5(@types/node@26.1.0)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.48.0)(yaml@2.9.0)
- optional: true
-
'@vitest/mocker@4.1.9(msw@2.14.6(@types/node@26.1.0)(typescript@6.0.3))(vite@8.1.2(@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))':
dependencies:
'@vitest/spy': 4.1.9
@@ -27573,15 +27640,15 @@ snapshots:
- supports-color
- ts-node
- jest-cli@30.3.0(@types/node@24.12.4):
+ jest-cli@30.3.0(@types/node@24.12.4)(ts-node@10.9.2(@types/node@24.12.4)(typescript@6.0.3)):
dependencies:
- '@jest/core': 30.3.0(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3))
+ '@jest/core': 30.3.0(ts-node@10.9.2(@types/node@24.12.4)(typescript@6.0.3))
'@jest/test-result': 30.3.0
'@jest/types': 30.3.0
chalk: 4.1.2
exit-x: 0.2.2
import-local: 3.2.0
- jest-config: 30.3.0(@types/node@24.12.4)
+ jest-config: 30.3.0(@types/node@24.12.4)(ts-node@10.9.2(@types/node@24.12.4)(typescript@6.0.3))
jest-util: 30.3.0
jest-validate: 30.3.0
yargs: 17.7.3
@@ -27643,7 +27710,7 @@ snapshots:
- babel-plugin-macros
- supports-color
- jest-config@30.3.0(@types/node@24.12.4):
+ jest-config@30.3.0(@types/node@24.12.4)(ts-node@10.9.2(@types/node@24.12.4)(typescript@6.0.3)):
dependencies:
'@babel/core': 7.29.7
'@jest/get-type': 30.1.0
@@ -27670,6 +27737,40 @@ snapshots:
strip-json-comments: 3.1.1
optionalDependencies:
'@types/node': 24.12.4
+ ts-node: 10.9.2(@types/node@24.12.4)(typescript@6.0.3)
+ 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.12.4)(typescript@6.0.3)):
+ dependencies:
+ '@babel/core': 7.29.7
+ '@jest/get-type': 30.1.0
+ '@jest/pattern': 30.0.1
+ '@jest/test-sequencer': 30.3.0
+ '@jest/types': 30.3.0
+ babel-jest: 30.3.0(@babel/core@7.29.7)
+ chalk: 4.1.2
+ ci-info: 4.4.0
+ deepmerge: 4.3.1
+ glob: 10.5.0
+ graceful-fs: 4.2.11
+ jest-circus: 30.3.0
+ jest-docblock: 30.2.0
+ jest-environment-node: 30.3.0
+ jest-regex-util: 30.0.1
+ jest-resolve: 30.3.0
+ jest-runner: 30.3.0
+ jest-util: 30.3.0
+ jest-validate: 30.3.0
+ parse-json: 5.2.0
+ pretty-format: 30.3.0
+ slash: 3.0.0
+ strip-json-comments: 3.1.1
+ optionalDependencies:
+ '@types/node': 26.1.0
+ ts-node: 10.9.2(@types/node@24.12.4)(typescript@6.0.3)
transitivePeerDependencies:
- babel-plugin-macros
- supports-color
@@ -28221,12 +28322,12 @@ snapshots:
- supports-color
- ts-node
- jest@30.3.0(@types/node@24.12.4):
+ jest@30.3.0(@types/node@24.12.4)(ts-node@10.9.2(@types/node@24.12.4)(typescript@6.0.3)):
dependencies:
- '@jest/core': 30.3.0(ts-node@10.9.2(@types/node@26.1.0)(typescript@6.0.3))
+ '@jest/core': 30.3.0(ts-node@10.9.2(@types/node@24.12.4)(typescript@6.0.3))
'@jest/types': 30.3.0
import-local: 3.2.0
- jest-cli: 30.3.0(@types/node@24.12.4)
+ jest-cli: 30.3.0(@types/node@24.12.4)(ts-node@10.9.2(@types/node@24.12.4)(typescript@6.0.3))
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
@@ -31524,7 +31625,7 @@ snapshots:
transitivePeerDependencies:
- oxc-resolver
- rolldown@1.0.0-rc.4(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0):
+ rolldown@1.0.0-rc.4(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1):
dependencies:
'@oxc-project/types': 0.113.0
'@rolldown/pluginutils': 1.0.0-rc.4
@@ -31539,7 +31640,7 @@ snapshots:
'@rolldown/binding-linux-x64-gnu': 1.0.0-rc.4
'@rolldown/binding-linux-x64-musl': 1.0.0-rc.4
'@rolldown/binding-openharmony-arm64': 1.0.0-rc.4
- '@rolldown/binding-wasm32-wasi': 1.0.0-rc.4(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)
+ '@rolldown/binding-wasm32-wasi': 1.0.0-rc.4(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)
'@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.4
'@rolldown/binding-win32-x64-msvc': 1.0.0-rc.4
transitivePeerDependencies:
@@ -32726,6 +32827,25 @@ snapshots:
lit-analyzer: 2.0.3
web-component-analyzer: 2.0.0
+ ts-node@10.9.2(@types/node@24.12.4)(typescript@6.0.3):
+ dependencies:
+ '@cspotcode/source-map-support': 0.8.1
+ '@tsconfig/node10': 1.0.12
+ '@tsconfig/node12': 1.0.11
+ '@tsconfig/node14': 1.0.3
+ '@tsconfig/node16': 1.0.4
+ '@types/node': 24.12.4
+ acorn: 8.17.0
+ acorn-walk: 8.3.5
+ arg: 4.1.3
+ create-require: 1.1.1
+ diff: 4.0.4
+ make-error: 1.3.6
+ 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:
'@cspotcode/source-map-support': 0.8.1
@@ -33348,10 +33468,10 @@ snapshots:
terser: 5.48.0
yaml: 2.9.0
- vitest@4.1.9(@types/node@24.12.4)(@vitest/browser-playwright@4.1.9)(jsdom@28.1.0)(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.2(@types/node@24.12.4)(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.9(@types/node@24.12.4)(@vitest/browser-playwright@4.1.9)(jsdom@28.1.0)(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.48.0)(yaml@2.9.0)):
dependencies:
'@vitest/expect': 4.1.9
- '@vitest/mocker': 4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.2(@types/node@24.12.4)(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/mocker': 4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@7.3.5(@types/node@24.12.4)(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/pretty-format': 4.1.9
'@vitest/runner': 4.1.9
'@vitest/snapshot': 4.1.9
@@ -33368,19 +33488,20 @@ snapshots:
tinyexec: 1.2.4
tinyglobby: 0.2.17
tinyrainbow: 3.1.0
- vite: 8.1.2(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)
+ vite: 7.3.5(@types/node@24.12.4)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.48.0)(yaml@2.9.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 24.12.4
- '@vitest/browser-playwright': 4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(playwright@1.60.0)(vite@8.1.2(@types/node@24.12.4)(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.9)
+ '@vitest/browser-playwright': 4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(playwright@1.62.0-alpha-2026-06-29)(vite@7.3.5(@types/node@24.12.4)(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.9)
jsdom: 28.1.0
transitivePeerDependencies:
- msw
+ optional: true
- vitest@4.1.9(@types/node@24.12.4)(@vitest/browser-playwright@4.1.9)(jsdom@28.1.0)(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.3(@types/node@24.12.4)(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.9(@types/node@24.12.4)(@vitest/browser-playwright@4.1.9)(jsdom@28.1.0)(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.2(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)):
dependencies:
'@vitest/expect': 4.1.9
- '@vitest/mocker': 4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.3(@types/node@24.12.4)(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/mocker': 4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.2(@types/node@24.12.4)(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/pretty-format': 4.1.9
'@vitest/runner': 4.1.9
'@vitest/snapshot': 4.1.9
@@ -33397,20 +33518,19 @@ snapshots:
tinyexec: 1.2.4
tinyglobby: 0.2.17
tinyrainbow: 3.1.0
- vite: 8.1.3(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(less@4.4.2)(sass@1.97.3)(terser@5.48.0)(yaml@2.9.0)
+ vite: 8.1.2(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 24.12.4
- '@vitest/browser-playwright': 4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(playwright@1.62.0-alpha-2026-06-29)(vite@8.1.3(@types/node@24.12.4)(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.9)
+ '@vitest/browser-playwright': 4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(playwright@1.60.0)(vite@8.1.2(@types/node@24.12.4)(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.9)
jsdom: 28.1.0
transitivePeerDependencies:
- msw
- optional: true
- vitest@4.1.9(@types/node@24.12.4)(@vitest/browser-playwright@4.1.9)(jsdom@28.1.0)(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.3(@types/node@24.12.4)(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.9(@types/node@24.12.4)(@vitest/browser-playwright@4.1.9)(jsdom@28.1.0)(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.3(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(less@4.4.2)(sass@1.97.3)(terser@5.48.0)(yaml@2.9.0)):
dependencies:
'@vitest/expect': 4.1.9
- '@vitest/mocker': 4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.3(@types/node@24.12.4)(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/mocker': 4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.3(@types/node@24.12.4)(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/pretty-format': 4.1.9
'@vitest/runner': 4.1.9
'@vitest/snapshot': 4.1.9
@@ -33427,19 +33547,20 @@ snapshots:
tinyexec: 1.2.4
tinyglobby: 0.2.17
tinyrainbow: 3.1.0
- vite: 8.1.3(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)
+ vite: 8.1.3(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(less@4.4.2)(sass@1.97.3)(terser@5.48.0)(yaml@2.9.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 24.12.4
- '@vitest/browser-playwright': 4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(playwright@1.60.0)(vite@8.1.3(@types/node@24.12.4)(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.9)
+ '@vitest/browser-playwright': 4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(playwright@1.62.0-alpha-2026-06-29)(vite@8.1.3(@types/node@24.12.4)(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.9)
jsdom: 28.1.0
transitivePeerDependencies:
- msw
+ optional: true
- vitest@4.1.9(@types/node@26.1.0)(@vitest/browser-playwright@4.1.9)(jsdom@28.1.0)(msw@2.14.6(@types/node@26.1.0)(typescript@6.0.3))(vite@7.3.5(@types/node@26.1.0)(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.9(@types/node@24.12.4)(@vitest/browser-playwright@4.1.9)(jsdom@28.1.0)(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.3(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)):
dependencies:
'@vitest/expect': 4.1.9
- '@vitest/mocker': 4.1.9(msw@2.14.6(@types/node@26.1.0)(typescript@6.0.3))(vite@7.3.5(@types/node@26.1.0)(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/mocker': 4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.3(@types/node@24.12.4)(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/pretty-format': 4.1.9
'@vitest/runner': 4.1.9
'@vitest/snapshot': 4.1.9
@@ -33456,15 +33577,14 @@ snapshots:
tinyexec: 1.2.4
tinyglobby: 0.2.17
tinyrainbow: 3.1.0
- vite: 7.3.5(@types/node@26.1.0)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.48.0)(yaml@2.9.0)
+ vite: 8.1.3(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)
why-is-node-running: 2.3.0
optionalDependencies:
- '@types/node': 26.1.0
- '@vitest/browser-playwright': 4.1.9(msw@2.14.6(@types/node@26.1.0)(typescript@6.0.3))(playwright@1.62.0-alpha-2026-06-29)(vite@7.3.5(@types/node@26.1.0)(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.9)
+ '@types/node': 24.12.4
+ '@vitest/browser-playwright': 4.1.9(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(playwright@1.62.0-alpha-2026-06-29)(vite@8.1.3(@types/node@24.12.4)(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.9)
jsdom: 28.1.0
transitivePeerDependencies:
- msw
- optional: true
vitest@4.1.9(@types/node@26.1.0)(@vitest/browser-playwright@4.1.9)(jsdom@28.1.0)(msw@2.14.6(@types/node@26.1.0)(typescript@6.0.3))(vite@8.1.2(@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)):
dependencies:
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 026f3b44d2e..36802b8cadd 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..d0a010d4a42
--- /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:
+
+`ConverseController` → normalized Thermidor `Activity` → sample-owned A2-UI adapter → sample-owned component catalog.
+
+Thermidor only exposes opaque activity `kind` and `payload` values. The sample is the layer that recognizes the `a2ui-surface` kind and renders the `ProductCarousel` and `Cart` components advertised by the schema catalog.
+
+## 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..0a63d0a4289
--- /dev/null
+++ b/samples/thermidor/schema-contract-react/package.json
@@ -0,0 +1,26 @@
+{
+ "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": {
+ "@coveo/thermidor": "workspace:*",
+ "react": "catalog:",
+ "react-dom": "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..cb878792d97
--- /dev/null
+++ b/samples/thermidor/schema-contract-react/src/App.tsx
@@ -0,0 +1,92 @@
+import {useEffect, useRef, useState} from 'react';
+import {
+ buildConverseController,
+ buildGenerativeInterface,
+ Engine,
+ type ConverseController,
+ type GenerativeInterface,
+} from '@coveo/thermidor';
+import {CatalogSurfaceRenderer} from './a2ui/components.js';
+import {toCatalogSurfaces} from './a2ui/adapter.js';
+import {getSampleConfiguration} from './env.js';
+import {useController} from './use-controller.js';
+
+const CONTRACT_PROMPT = 'Show the Thermidor catalog';
+
+export default function App() {
+ 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 [prompt, setPrompt] = useState(CONTRACT_PROMPT);
+ const turn = state.activeTurn;
+ const surfaces = toCatalogSurfaces(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.
+
+
+
+
+
+ {turn?.agentResponse?.messages.map((message, index) => (
+
+ {message.content}
+
+ ))}
+ {turn?.status === 'error' && {turn.error}
}
+ {surfaces.map((surface) => (
+
+ ))}
+ {!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/adapter.test.ts b/samples/thermidor/schema-contract-react/src/a2ui/adapter.test.ts
new file mode 100644
index 00000000000..3eaa0b8a217
--- /dev/null
+++ b/samples/thermidor/schema-contract-react/src/a2ui/adapter.test.ts
@@ -0,0 +1,73 @@
+import {describe, expect, it} from 'vitest';
+import {toCatalogSurfaces} from './adapter.js';
+
+describe('toCatalogSurfaces', () => {
+ it('keeps advertised components bound to their externally owned controller state', () => {
+ const [surface] = toCatalogSurfaces([
+ {
+ id: 'catalog',
+ kind: 'a2ui-surface',
+ replace: true,
+ payload: {
+ a2ui_operations: [
+ {
+ version: 'v0.9',
+ createSurface: {surfaceId: 'catalog', catalogId: 'catalog.json'},
+ },
+ {
+ version: 'v0.9',
+ updateComponents: {
+ surfaceId: 'catalog',
+ components: [
+ {
+ id: 'featured-products',
+ component: 'ProductCarousel',
+ controllers: {
+ productListController: {
+ controllerId: 'featured-products',
+ controllerSchema: 'product-list.schema.json',
+ },
+ },
+ },
+ ],
+ },
+ },
+ {
+ version: 'v0.9',
+ updateDataModel: {
+ surfaceId: 'catalog',
+ value: {controllers: {'featured-products': {products: [{permanentid: 'p1'}]}}},
+ },
+ },
+ ],
+ },
+ },
+ ]);
+
+ expect(surface).toMatchObject({
+ id: 'catalog',
+ catalogId: 'catalog.json',
+ components: [{component: 'ProductCarousel'}],
+ controllers: {'featured-products': {products: [{permanentid: 'p1'}]}},
+ });
+ });
+
+ it('resets prior surfaces when an activity declares replacement semantics', () => {
+ const surfaces = toCatalogSurfaces([
+ {
+ id: 'old',
+ kind: 'a2ui-surface',
+ replace: false,
+ payload: {a2ui_operations: [{version: 'v0.9', createSurface: {surfaceId: 'old'}}]},
+ },
+ {
+ id: 'new',
+ kind: 'a2ui-surface',
+ replace: true,
+ payload: {a2ui_operations: [{version: 'v0.9', createSurface: {surfaceId: 'new'}}]},
+ },
+ ]);
+
+ expect(surfaces.map(({id}) => id)).toEqual(['new']);
+ });
+});
diff --git a/samples/thermidor/schema-contract-react/src/a2ui/adapter.ts b/samples/thermidor/schema-contract-react/src/a2ui/adapter.ts
new file mode 100644
index 00000000000..7cecda710e5
--- /dev/null
+++ b/samples/thermidor/schema-contract-react/src/a2ui/adapter.ts
@@ -0,0 +1,111 @@
+import type {Activity} from '@coveo/thermidor';
+
+type ControllerAdvertisement = {
+ controllerId: string;
+ controllerSchema: string;
+};
+
+type CatalogComponent = {
+ id: string;
+ component: string;
+ controllers: Record;
+};
+
+export type CatalogSurface = {
+ id: string;
+ catalogId: string;
+ components: CatalogComponent[];
+ controllers: Record>;
+};
+
+type MutableCatalogSurface = CatalogSurface;
+
+function isRecord(value: unknown): value is Record {
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
+}
+
+function getOperations(activity: Activity): Record[] {
+ if (activity.kind !== 'a2ui-surface' || !isRecord(activity.payload)) {
+ return [];
+ }
+
+ const operations = activity.payload['a2ui_operations'];
+ return Array.isArray(operations) ? operations.filter(isRecord) : [];
+}
+
+function getOrCreateSurface(
+ surfaces: Map,
+ id: string,
+ catalogId = ''
+): MutableCatalogSurface {
+ const existing = surfaces.get(id);
+ if (existing) {
+ return existing;
+ }
+
+ const surface = {id, catalogId, components: [], controllers: {}};
+ surfaces.set(id, surface);
+ return surface;
+}
+
+/**
+ * This is the sample's A2-UI boundary. It interprets only the v0.9 catalog
+ * operations and keeps the application-facing result independent of Thermidor.
+ */
+export function toCatalogSurfaces(activities: Activity[]): CatalogSurface[] {
+ const surfaces = new Map();
+
+ for (const activity of activities) {
+ const operations = getOperations(activity);
+ if (operations.length === 0) {
+ continue;
+ }
+ if (activity.replace) {
+ surfaces.clear();
+ }
+
+ for (const operation of operations) {
+ const createSurface = operation['createSurface'];
+ if (isRecord(createSurface) && typeof createSurface['surfaceId'] === 'string') {
+ getOrCreateSurface(
+ surfaces,
+ createSurface['surfaceId'],
+ typeof createSurface['catalogId'] === 'string' ? createSurface['catalogId'] : ''
+ );
+ continue;
+ }
+
+ const updateComponents = operation['updateComponents'];
+ if (isRecord(updateComponents) && typeof updateComponents['surfaceId'] === 'string') {
+ const surface = getOrCreateSurface(surfaces, updateComponents['surfaceId']);
+ surface.components = Array.isArray(updateComponents['components'])
+ ? updateComponents['components'].filter(isCatalogComponent)
+ : [];
+ continue;
+ }
+
+ const updateDataModel = operation['updateDataModel'];
+ if (!isRecord(updateDataModel) || typeof updateDataModel['surfaceId'] !== 'string') {
+ continue;
+ }
+ const value = updateDataModel['value'];
+ const controllers =
+ isRecord(value) && isRecord(value['controllers']) ? value['controllers'] : {};
+ const surface = getOrCreateSurface(surfaces, updateDataModel['surfaceId']);
+ surface.controllers = Object.fromEntries(
+ Object.entries(controllers).filter(([, state]) => isRecord(state))
+ ) as Record>;
+ }
+ }
+
+ return [...surfaces.values()];
+}
+
+function isCatalogComponent(value: unknown): value is CatalogComponent {
+ return (
+ isRecord(value) &&
+ typeof value['id'] === 'string' &&
+ typeof value['component'] === 'string' &&
+ isRecord(value['controllers'])
+ );
+}
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..9fe1b840706
--- /dev/null
+++ b/samples/thermidor/schema-contract-react/src/a2ui/components.tsx
@@ -0,0 +1,112 @@
+import type {ComponentType} from 'react';
+import type {CatalogSurface} from './adapter.js';
+
+type ComponentBinding = {
+ component: CatalogSurface['components'][number];
+ controllers: CatalogSurface['controllers'];
+};
+
+type Product = {
+ permanentid: string;
+ ec_name: string;
+ ec_shortdesc: string;
+ ec_brand: string;
+ ec_price: number;
+ ec_promo_price?: number;
+ ec_images: string[];
+ ec_in_stock: boolean;
+ ec_rating: number;
+};
+
+type CartItem = {
+ productId: string;
+ name: string;
+ price: number;
+ quantity: number;
+};
+
+const A2UI_COMPONENTS: Record> = {
+ ProductCarousel,
+ Cart,
+};
+
+export function CatalogSurfaceRenderer({surface}: {surface: CatalogSurface}) {
+ return (
+
+ {surface.components.map((component) => {
+ const Component = A2UI_COMPONENTS[component.component];
+ return Component ? (
+
+ ) : (
+
+ No sample renderer is registered for {component.component}.
+
+ );
+ })}
+
+ );
+}
+
+function ProductCarousel({component, controllers}: ComponentBinding) {
+ const controllerId = component.controllers['productListController']?.controllerId;
+ const products = controllerId ? asArray(controllers[controllerId]?.['products']) : [];
+
+ return (
+
+
+
+
ProductCarousel
+
Featured products
+
+
controller: {controllerId}
+
+
+ {products.map((product) => {
+ const price = product.ec_promo_price ?? product.ec_price;
+ return (
+
+
+ {product.ec_brand}
+ {product.ec_name}
+ {product.ec_shortdesc}
+
+ ${price.toFixed(2)}
+ ★ {product.ec_rating}
+
+ {product.ec_in_stock ? 'In stock' : 'Out of stock'}
+
+ );
+ })}
+
+
+ );
+}
+
+function Cart({component, controllers}: ComponentBinding) {
+ const controllerId = component.controllers['cartController']?.controllerId;
+ const items = controllerId ? asArray(controllers[controllerId]?.['items']) : [];
+ const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
+
+ return (
+
+ Cart
+ Your cart
+ {items.map((item) => (
+
+
+ {item.quantity} × {item.name}
+
+ ${(item.price * item.quantity).toFixed(2)}
+
+ ))}
+
+ Total
+ ${total.toFixed(2)}
+
+
+ );
+}
+
+function asArray(value: unknown): T[] {
+ return Array.isArray(value) ? (value as T[]) : [];
+}
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,
+ },
+ },
+ }
+ : {}),
+ },
+ };
+});
From 73b0b034c0147e9f1400c18679eb7071a6d670c2 Mon Sep 17 00:00:00 2001
From: Louis Bompart
Date: Mon, 27 Jul 2026 23:02:26 +0200
Subject: [PATCH 2/5] close-2-native
---
packages/mock-converse-api/src/server.test.ts | 6 +-
.../src/converse/templates/response9.ts | 5 +
pnpm-lock.yaml | 103 ++++++---
.../thermidor/schema-contract-react/README.md | 4 +-
.../schema-contract-react/package.json | 4 +-
.../schema-contract-react/src/App.tsx | 27 ++-
.../src/a2ui/adapter.test.ts | 73 ------
.../schema-contract-react/src/a2ui/adapter.ts | 111 ---------
.../src/a2ui/components.test.ts | 27 +++
.../src/a2ui/components.tsx | 211 +++++++++---------
.../src/a2ui/controllers.test.ts | 48 ++++
.../src/a2ui/controllers.tsx | 176 +++++++++++++++
.../src/a2ui/surfaces.test.ts | 26 +++
.../src/a2ui/surfaces.tsx | 76 +++++++
14 files changed, 573 insertions(+), 324 deletions(-)
delete mode 100644 samples/thermidor/schema-contract-react/src/a2ui/adapter.test.ts
delete mode 100644 samples/thermidor/schema-contract-react/src/a2ui/adapter.ts
create mode 100644 samples/thermidor/schema-contract-react/src/a2ui/components.test.ts
create mode 100644 samples/thermidor/schema-contract-react/src/a2ui/controllers.test.ts
create mode 100644 samples/thermidor/schema-contract-react/src/a2ui/controllers.tsx
create mode 100644 samples/thermidor/schema-contract-react/src/a2ui/surfaces.test.ts
create mode 100644 samples/thermidor/schema-contract-react/src/a2ui/surfaces.tsx
diff --git a/packages/mock-converse-api/src/server.test.ts b/packages/mock-converse-api/src/server.test.ts
index 6d3122cee0b..164243cdc38 100644
--- a/packages/mock-converse-api/src/server.test.ts
+++ b/packages/mock-converse-api/src/server.test.ts
@@ -162,7 +162,11 @@ describe('createMockConverseServer', () => {
{
version: 'v0.9',
updateComponents: {
- components: [{component: 'ProductCarousel'}, {component: 'Cart'}],
+ components: [
+ {id: 'root', component: 'Column', children: ['featured-products', 'cart']},
+ {component: 'ProductCarousel'},
+ {component: 'Cart'},
+ ],
},
},
{
diff --git a/packages/platform-mock-api/src/converse/templates/response9.ts b/packages/platform-mock-api/src/converse/templates/response9.ts
index 1daee0cff0d..161ef2c8d5b 100644
--- a/packages/platform-mock-api/src/converse/templates/response9.ts
+++ b/packages/platform-mock-api/src/converse/templates/response9.ts
@@ -34,6 +34,11 @@ const thermidorSchemaCatalogResponseEvents: ConverseEvent[] = [
updateComponents: {
surfaceId: 'commerce-catalog-example',
components: [
+ {
+ id: 'root',
+ component: 'Column',
+ children: ['featured-products', 'cart'],
+ },
{
id: 'featured-products',
component: 'ProductCarousel',
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ae51ec881e9..3590cf96f00 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -483,7 +483,7 @@ importers:
devDependencies:
'@angular-devkit/build-angular':
specifier: 'catalog:'
- version: 21.2.17(80d26e843c839fab20396dae6982b00b)
+ version: 21.2.17(1928eecfa89eed155901caad367f4cb8)
'@angular/cli':
specifier: 'catalog:'
version: 21.2.17(@types/node@24.12.4)(chokidar@5.0.0)
@@ -2103,6 +2103,9 @@ importers:
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
@@ -2112,6 +2115,9 @@ importers:
react-dom:
specifier: 19.2.7
version: 19.2.7(react@19.2.7)
+ zod:
+ specifier: 3.25.76
+ version: 3.25.76
devDependencies:
'@types/react':
specifier: 'catalog:'
@@ -2210,6 +2216,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==}
@@ -3418,6 +3427,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
@@ -6490,6 +6510,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]
@@ -9875,6 +9898,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==}
@@ -16671,9 +16697,6 @@ packages:
zod@4.3.6:
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
- zod@4.4.3:
- resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
-
zone.js@0.15.1:
resolution: {integrity: sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==}
@@ -16687,6 +16710,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':
@@ -16832,13 +16862,13 @@ snapshots:
transitivePeerDependencies:
- chokidar
- '@angular-devkit/build-angular@21.2.17(80d26e843c839fab20396dae6982b00b)':
+ '@angular-devkit/build-angular@21.2.17(1928eecfa89eed155901caad367f4cb8)':
dependencies:
'@ampproject/remapping': 2.3.0
'@angular-devkit/architect': 0.2102.17(chokidar@5.0.0)
'@angular-devkit/build-webpack': 0.2102.17(chokidar@5.0.0)(webpack-dev-server@5.2.5(tslib@2.8.1)(webpack@5.105.2(esbuild@0.28.1)(postcss@8.5.12)))(webpack@5.105.2(esbuild@0.28.1)(postcss@8.5.12))
'@angular-devkit/core': 21.2.17(chokidar@5.0.0)
- '@angular/build': 21.2.17(6133a8bd1a48a56825f649b47a4a697e)
+ '@angular/build': 21.2.17(2579aedd5157518c28138f5f823a7c91)
'@angular/compiler-cli': 21.2.17(@angular/compiler@21.2.17)(typescript@6.0.3)
'@babel/core': 7.29.7
'@babel/generator': 7.29.1
@@ -16965,7 +16995,7 @@ snapshots:
'@angular/core': 21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1)
tslib: 2.8.1
- '@angular/build@21.2.17(5b911479a7812c42761d62528d4640b4)':
+ '@angular/build@21.2.17(2579aedd5157518c28138f5f823a7c91)':
dependencies:
'@ampproject/remapping': 2.3.0
'@angular-devkit/architect': 0.2102.17(chokidar@5.0.0)
@@ -16975,7 +17005,7 @@ snapshots:
'@babel/helper-annotate-as-pure': 7.27.3
'@babel/helper-split-export-declaration': 7.24.7
'@inquirer/confirm': 5.1.21(@types/node@24.12.4)
- '@vitejs/plugin-basic-ssl': 2.1.4(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.48.0)(yaml@2.9.0))
+ '@vitejs/plugin-basic-ssl': 2.1.4(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.9.0))
beasties: 0.4.1
browserslist: 4.28.5
esbuild: 0.28.1
@@ -16988,7 +17018,7 @@ snapshots:
parse5-html-rewriting-stream: 8.0.0
picomatch: 4.0.4
piscina: 5.2.0
- rolldown: 1.0.0-rc.4(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)
+ rolldown: 1.0.0-rc.4(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)
sass: 1.97.3
semver: 7.7.4
source-map-support: 0.5.21
@@ -16996,17 +17026,18 @@ snapshots:
tslib: 2.8.1
typescript: 6.0.3
undici: 7.24.4
- vite: 7.3.5(@types/node@24.12.4)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.48.0)(yaml@2.9.0)
+ vite: 7.3.5(@types/node@24.12.4)(jiti@2.7.0)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.9.0)
watchpack: 2.5.1
optionalDependencies:
'@angular/core': 21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1)
'@angular/platform-browser': 21.2.17(@angular/animations@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1))
'@angular/platform-server': 21.2.17(@angular/common@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@21.2.17)(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@21.2.17(@angular/animations@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)
- less: 4.6.7
+ less: 4.4.2
lmdb: 3.5.1
- postcss: 8.5.16
+ ng-packagr: 20.3.2(@angular/compiler-cli@21.2.17(@angular/compiler@21.2.17)(typescript@6.0.3))(tailwindcss@4.3.2)(tslib@2.8.1)(typescript@6.0.3)
+ postcss: 8.5.12
tailwindcss: 4.3.2
- vitest: 4.1.9(@types/node@24.12.4)(@vitest/browser-playwright@4.1.9)(jsdom@28.1.0)(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@7.3.5(@types/node@24.12.4)(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.9(@types/node@24.12.4)(@vitest/browser-playwright@4.1.9)(jsdom@28.1.0)(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.3(@types/node@24.12.4)(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:
- '@emnapi/core'
- '@emnapi/runtime'
@@ -17022,7 +17053,7 @@ snapshots:
- tsx
- yaml
- '@angular/build@21.2.17(6133a8bd1a48a56825f649b47a4a697e)':
+ '@angular/build@21.2.17(5b911479a7812c42761d62528d4640b4)':
dependencies:
'@ampproject/remapping': 2.3.0
'@angular-devkit/architect': 0.2102.17(chokidar@5.0.0)
@@ -17032,7 +17063,7 @@ snapshots:
'@babel/helper-annotate-as-pure': 7.27.3
'@babel/helper-split-export-declaration': 7.24.7
'@inquirer/confirm': 5.1.21(@types/node@24.12.4)
- '@vitejs/plugin-basic-ssl': 2.1.4(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.9.0))
+ '@vitejs/plugin-basic-ssl': 2.1.4(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.48.0)(yaml@2.9.0))
beasties: 0.4.1
browserslist: 4.28.5
esbuild: 0.28.1
@@ -17045,7 +17076,7 @@ snapshots:
parse5-html-rewriting-stream: 8.0.0
picomatch: 4.0.4
piscina: 5.2.0
- rolldown: 1.0.0-rc.4(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)
+ rolldown: 1.0.0-rc.4(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)
sass: 1.97.3
semver: 7.7.4
source-map-support: 0.5.21
@@ -17053,18 +17084,17 @@ snapshots:
tslib: 2.8.1
typescript: 6.0.3
undici: 7.24.4
- vite: 7.3.5(@types/node@24.12.4)(jiti@2.7.0)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.9.0)
+ vite: 7.3.5(@types/node@24.12.4)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.48.0)(yaml@2.9.0)
watchpack: 2.5.1
optionalDependencies:
'@angular/core': 21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1)
'@angular/platform-browser': 21.2.17(@angular/animations@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1))
'@angular/platform-server': 21.2.17(@angular/common@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@21.2.17)(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@21.2.17(@angular/animations@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)
- less: 4.4.2
+ less: 4.6.7
lmdb: 3.5.1
- ng-packagr: 20.3.2(@angular/compiler-cli@21.2.17(@angular/compiler@21.2.17)(typescript@6.0.3))(tailwindcss@4.3.2)(tslib@2.8.1)(typescript@6.0.3)
- postcss: 8.5.12
+ postcss: 8.5.16
tailwindcss: 4.3.2
- vitest: 4.1.9(@types/node@24.12.4)(@vitest/browser-playwright@4.1.9)(jsdom@28.1.0)(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@8.1.3(@types/node@24.12.4)(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.9(@types/node@24.12.4)(@vitest/browser-playwright@4.1.9)(jsdom@28.1.0)(msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3))(vite@7.3.5(@types/node@24.12.4)(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:
- '@emnapi/core'
- '@emnapi/runtime'
@@ -18562,6 +18592,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
@@ -21557,6 +21598,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
@@ -21830,9 +21873,9 @@ snapshots:
'@rolldown/binding-openharmony-arm64@1.1.4':
optional: true
- '@rolldown/binding-wasm32-wasi@1.0.0-rc.4(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)':
+ '@rolldown/binding-wasm32-wasi@1.0.0-rc.4(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)':
dependencies:
- '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)
+ '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)
transitivePeerDependencies:
- '@emnapi/core'
- '@emnapi/runtime'
@@ -25399,6 +25442,8 @@ snapshots:
dataloader@1.4.0: {}
+ date-fns@4.4.0: {}
+
dateformat@4.6.3: {}
dayjs@1.11.21: {}
@@ -28554,7 +28599,7 @@ snapshots:
tinyglobby: 0.2.17
unbash: 4.0.2
yaml: 2.9.0
- zod: 4.4.3
+ zod: 4.3.6
koa-bodyparser@4.4.1:
dependencies:
@@ -31625,7 +31670,7 @@ snapshots:
transitivePeerDependencies:
- oxc-resolver
- rolldown@1.0.0-rc.4(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1):
+ rolldown@1.0.0-rc.4(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0):
dependencies:
'@oxc-project/types': 0.113.0
'@rolldown/pluginutils': 1.0.0-rc.4
@@ -31640,7 +31685,7 @@ snapshots:
'@rolldown/binding-linux-x64-gnu': 1.0.0-rc.4
'@rolldown/binding-linux-x64-musl': 1.0.0-rc.4
'@rolldown/binding-openharmony-arm64': 1.0.0-rc.4
- '@rolldown/binding-wasm32-wasi': 1.0.0-rc.4(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)
+ '@rolldown/binding-wasm32-wasi': 1.0.0-rc.4(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)
'@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.4
'@rolldown/binding-win32-x64-msvc': 1.0.0-rc.4
transitivePeerDependencies:
@@ -34149,6 +34194,10 @@ snapshots:
yoctocolors@2.1.2: {}
+ 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
@@ -34159,8 +34208,6 @@ snapshots:
zod@4.3.6: {}
- zod@4.4.3: {}
-
zone.js@0.15.1: {}
zwitch@2.0.4: {}
diff --git a/samples/thermidor/schema-contract-react/README.md b/samples/thermidor/schema-contract-react/README.md
index d0a010d4a42..d038c7e1cef 100644
--- a/samples/thermidor/schema-contract-react/README.md
+++ b/samples/thermidor/schema-contract-react/README.md
@@ -2,9 +2,9 @@
This sample proves the client-side path for the v0.9 Thermidor Commerce Catalog contract:
-`ConverseController` → normalized Thermidor `Activity` → sample-owned A2-UI adapter → sample-owned component catalog.
+`ConverseController` → normalized Thermidor `Activity` → raw A2-UI messages → CopilotKit A2-UI renderer/catalog.
-Thermidor only exposes opaque activity `kind` and `payload` values. The sample is the layer that recognizes the `a2ui-surface` kind and renders the `ProductCarousel` and `Cart` components advertised by the schema catalog.
+Thermidor only exposes opaque activity `kind` and `payload` values. The sample passes the `a2ui-surface` operations to CopilotKit unchanged. Its local `ProductCarousel` and `Cart` components create their advertised controllers from `updateComponents`, hydrate them from `updateDataModel`, and subscribe to later data-model updates. CopilotKit provides only renderer and catalog state; it does not replace Thermidor's conversational endpoint or runtime.
## Run with the contract mock
diff --git a/samples/thermidor/schema-contract-react/package.json b/samples/thermidor/schema-contract-react/package.json
index 0a63d0a4289..d6a30a162fd 100644
--- a/samples/thermidor/schema-contract-react/package.json
+++ b/samples/thermidor/schema-contract-react/package.json
@@ -11,9 +11,11 @@
"test": "vitest run"
},
"dependencies": {
+ "@copilotkit/a2ui-renderer": "1.61.2",
"@coveo/thermidor": "workspace:*",
"react": "catalog:",
- "react-dom": "catalog:"
+ "react-dom": "catalog:",
+ "zod": "3.25.76"
},
"devDependencies": {
"@types/react": "catalog:",
diff --git a/samples/thermidor/schema-contract-react/src/App.tsx b/samples/thermidor/schema-contract-react/src/App.tsx
index cb878792d97..900f6de6726 100644
--- a/samples/thermidor/schema-contract-react/src/App.tsx
+++ b/samples/thermidor/schema-contract-react/src/App.tsx
@@ -1,4 +1,4 @@
-import {useEffect, useRef, useState} from 'react';
+import {useEffect, useMemo, useRef, useState} from 'react';
import {
buildConverseController,
buildGenerativeInterface,
@@ -6,14 +6,24 @@ import {
type ConverseController,
type GenerativeInterface,
} from '@coveo/thermidor';
-import {CatalogSurfaceRenderer} from './a2ui/components.js';
-import {toCatalogSurfaces} from './a2ui/adapter.js';
+import {A2UIProvider} from '@copilotkit/a2ui-renderer';
+import {thermidorCatalog} from './a2ui/components.js';
+import {ThermidorControllerProvider} from './a2ui/controllers.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(),
@@ -28,7 +38,10 @@ export default function App() {
);
const [prompt, setPrompt] = useState(CONTRACT_PROMPT);
const turn = state.activeTurn;
- const surfaces = toCatalogSurfaces(turn?.agentResponse?.activities ?? []);
+ const a2uiMessages = useMemo(
+ () => getA2UIMessages(turn?.agentResponse?.activities ?? []),
+ [turn?.agentResponse?.activities]
+ );
useEffect(() => {
return () => {
@@ -74,9 +87,9 @@ export default function App() {
))}
{turn?.status === 'error' && {turn.error}
}
- {surfaces.map((surface) => (
-
- ))}
+
+
+
{!turn && Run the pre-filled prompt to render the schema example.
}
);
diff --git a/samples/thermidor/schema-contract-react/src/a2ui/adapter.test.ts b/samples/thermidor/schema-contract-react/src/a2ui/adapter.test.ts
deleted file mode 100644
index 3eaa0b8a217..00000000000
--- a/samples/thermidor/schema-contract-react/src/a2ui/adapter.test.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import {describe, expect, it} from 'vitest';
-import {toCatalogSurfaces} from './adapter.js';
-
-describe('toCatalogSurfaces', () => {
- it('keeps advertised components bound to their externally owned controller state', () => {
- const [surface] = toCatalogSurfaces([
- {
- id: 'catalog',
- kind: 'a2ui-surface',
- replace: true,
- payload: {
- a2ui_operations: [
- {
- version: 'v0.9',
- createSurface: {surfaceId: 'catalog', catalogId: 'catalog.json'},
- },
- {
- version: 'v0.9',
- updateComponents: {
- surfaceId: 'catalog',
- components: [
- {
- id: 'featured-products',
- component: 'ProductCarousel',
- controllers: {
- productListController: {
- controllerId: 'featured-products',
- controllerSchema: 'product-list.schema.json',
- },
- },
- },
- ],
- },
- },
- {
- version: 'v0.9',
- updateDataModel: {
- surfaceId: 'catalog',
- value: {controllers: {'featured-products': {products: [{permanentid: 'p1'}]}}},
- },
- },
- ],
- },
- },
- ]);
-
- expect(surface).toMatchObject({
- id: 'catalog',
- catalogId: 'catalog.json',
- components: [{component: 'ProductCarousel'}],
- controllers: {'featured-products': {products: [{permanentid: 'p1'}]}},
- });
- });
-
- it('resets prior surfaces when an activity declares replacement semantics', () => {
- const surfaces = toCatalogSurfaces([
- {
- id: 'old',
- kind: 'a2ui-surface',
- replace: false,
- payload: {a2ui_operations: [{version: 'v0.9', createSurface: {surfaceId: 'old'}}]},
- },
- {
- id: 'new',
- kind: 'a2ui-surface',
- replace: true,
- payload: {a2ui_operations: [{version: 'v0.9', createSurface: {surfaceId: 'new'}}]},
- },
- ]);
-
- expect(surfaces.map(({id}) => id)).toEqual(['new']);
- });
-});
diff --git a/samples/thermidor/schema-contract-react/src/a2ui/adapter.ts b/samples/thermidor/schema-contract-react/src/a2ui/adapter.ts
deleted file mode 100644
index 7cecda710e5..00000000000
--- a/samples/thermidor/schema-contract-react/src/a2ui/adapter.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-import type {Activity} from '@coveo/thermidor';
-
-type ControllerAdvertisement = {
- controllerId: string;
- controllerSchema: string;
-};
-
-type CatalogComponent = {
- id: string;
- component: string;
- controllers: Record;
-};
-
-export type CatalogSurface = {
- id: string;
- catalogId: string;
- components: CatalogComponent[];
- controllers: Record>;
-};
-
-type MutableCatalogSurface = CatalogSurface;
-
-function isRecord(value: unknown): value is Record {
- return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
-}
-
-function getOperations(activity: Activity): Record[] {
- if (activity.kind !== 'a2ui-surface' || !isRecord(activity.payload)) {
- return [];
- }
-
- const operations = activity.payload['a2ui_operations'];
- return Array.isArray(operations) ? operations.filter(isRecord) : [];
-}
-
-function getOrCreateSurface(
- surfaces: Map,
- id: string,
- catalogId = ''
-): MutableCatalogSurface {
- const existing = surfaces.get(id);
- if (existing) {
- return existing;
- }
-
- const surface = {id, catalogId, components: [], controllers: {}};
- surfaces.set(id, surface);
- return surface;
-}
-
-/**
- * This is the sample's A2-UI boundary. It interprets only the v0.9 catalog
- * operations and keeps the application-facing result independent of Thermidor.
- */
-export function toCatalogSurfaces(activities: Activity[]): CatalogSurface[] {
- const surfaces = new Map();
-
- for (const activity of activities) {
- const operations = getOperations(activity);
- if (operations.length === 0) {
- continue;
- }
- if (activity.replace) {
- surfaces.clear();
- }
-
- for (const operation of operations) {
- const createSurface = operation['createSurface'];
- if (isRecord(createSurface) && typeof createSurface['surfaceId'] === 'string') {
- getOrCreateSurface(
- surfaces,
- createSurface['surfaceId'],
- typeof createSurface['catalogId'] === 'string' ? createSurface['catalogId'] : ''
- );
- continue;
- }
-
- const updateComponents = operation['updateComponents'];
- if (isRecord(updateComponents) && typeof updateComponents['surfaceId'] === 'string') {
- const surface = getOrCreateSurface(surfaces, updateComponents['surfaceId']);
- surface.components = Array.isArray(updateComponents['components'])
- ? updateComponents['components'].filter(isCatalogComponent)
- : [];
- continue;
- }
-
- const updateDataModel = operation['updateDataModel'];
- if (!isRecord(updateDataModel) || typeof updateDataModel['surfaceId'] !== 'string') {
- continue;
- }
- const value = updateDataModel['value'];
- const controllers =
- isRecord(value) && isRecord(value['controllers']) ? value['controllers'] : {};
- const surface = getOrCreateSurface(surfaces, updateDataModel['surfaceId']);
- surface.controllers = Object.fromEntries(
- Object.entries(controllers).filter(([, state]) => isRecord(state))
- ) as Record>;
- }
- }
-
- return [...surfaces.values()];
-}
-
-function isCatalogComponent(value: unknown): value is CatalogComponent {
- return (
- isRecord(value) &&
- typeof value['id'] === 'string' &&
- typeof value['component'] === 'string' &&
- isRecord(value['controllers'])
- );
-}
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..97e975d8530
--- /dev/null
+++ b/samples/thermidor/schema-contract-react/src/a2ui/components.test.ts
@@ -0,0 +1,27 @@
+import {describe, expect, it} from 'vitest';
+import {thermidorCatalogDefinitions} from './components.js';
+
+describe('thermidorCatalogDefinitions', () => {
+ it('accepts the controller advertisements supplied by the catalog message', () => {
+ expect(
+ thermidorCatalogDefinitions.ProductCarousel.props.safeParse({
+ controllers: {
+ productListController: {
+ controllerId: 'featured-products',
+ controllerSchema: 'product-list.schema.json',
+ },
+ },
+ }).success
+ ).toBe(true);
+ expect(
+ thermidorCatalogDefinitions.Cart.props.safeParse({
+ controllers: {
+ cartController: {
+ controllerId: 'shopping-cart',
+ controllerSchema: 'cart.schema.json',
+ },
+ },
+ }).success
+ ).toBe(true);
+ });
+});
diff --git a/samples/thermidor/schema-contract-react/src/a2ui/components.tsx b/samples/thermidor/schema-contract-react/src/a2ui/components.tsx
index 9fe1b840706..28fb7aa525c 100644
--- a/samples/thermidor/schema-contract-react/src/a2ui/components.tsx
+++ b/samples/thermidor/schema-contract-react/src/a2ui/components.tsx
@@ -1,112 +1,121 @@
-import type {ComponentType} from 'react';
-import type {CatalogSurface} from './adapter.js';
+import {
+ createCatalog,
+ type CatalogDefinitions,
+ type CatalogRenderers,
+} from '@copilotkit/a2ui-renderer';
+import {z} from 'zod';
+import {useAdvertisedController} from './controllers.js';
-type ComponentBinding = {
- component: CatalogSurface['components'][number];
- controllers: CatalogSurface['controllers'];
-};
+export const THERMIDOR_CATALOG_ID = 'https://schema.thermidor.coveo.com/a2-ui/catalog.json';
-type Product = {
- permanentid: string;
- ec_name: string;
- ec_shortdesc: string;
- ec_brand: string;
- ec_price: number;
- ec_promo_price?: number;
- ec_images: string[];
- ec_in_stock: boolean;
- ec_rating: number;
-};
+const controllerAdvertisement = z.object({
+ controllerId: z.string(),
+ controllerSchema: z.string(),
+});
-type CartItem = {
- productId: string;
- name: string;
- price: number;
- quantity: number;
-};
+const product = z.object({
+ permanentid: z.string(),
+ ec_name: z.string(),
+ ec_shortdesc: z.string(),
+ ec_brand: z.string(),
+ ec_price: z.number(),
+ ec_promo_price: z.number().optional(),
+ ec_images: z.array(z.string()),
+ ec_in_stock: z.boolean(),
+ ec_rating: z.number(),
+});
-const A2UI_COMPONENTS: Record> = {
- ProductCarousel,
- Cart,
-};
+const cartItem = z.object({
+ productId: z.string(),
+ name: z.string(),
+ price: z.number(),
+ quantity: z.number(),
+});
-export function CatalogSurfaceRenderer({surface}: {surface: CatalogSurface}) {
- return (
-
- {surface.components.map((component) => {
- const Component = A2UI_COMPONENTS[component.component];
- return Component ? (
-
- ) : (
-
- No sample renderer is registered for {component.component}.
-
- );
- })}
-
- );
-}
+type Product = z.infer;
+type CartItem = z.infer;
-function ProductCarousel({component, controllers}: ComponentBinding) {
- const controllerId = component.controllers['productListController']?.controllerId;
- const products = controllerId ? asArray(controllers[controllerId]?.['products']) : [];
+export const thermidorCatalogDefinitions = {
+ ProductCarousel: {
+ description: 'A responsive product carousel backed by a product-list controller.',
+ props: z.object({
+ controllers: z.object({productListController: controllerAdvertisement}),
+ }),
+ },
+ Cart: {
+ description: 'A shopping-cart summary backed by a cart controller.',
+ props: z.object({
+ controllers: z.object({cartController: controllerAdvertisement}),
+ }),
+ },
+} satisfies CatalogDefinitions;
- return (
-
-
-
-
ProductCarousel
-
Featured products
-
-
controller: {controllerId}
-
-
- {products.map((product) => {
- const price = product.ec_promo_price ?? product.ec_price;
- return (
-
-
- {product.ec_brand}
- {product.ec_name}
- {product.ec_shortdesc}
-
- ${price.toFixed(2)}
- ★ {product.ec_rating}
-
- {product.ec_in_stock ? 'In stock' : 'Out of stock'}
-
- );
- })}
-
-
- );
-}
-
-function Cart({component, controllers}: ComponentBinding) {
- const controllerId = component.controllers['cartController']?.controllerId;
- const items = controllerId ? asArray(controllers[controllerId]?.['items']) : [];
- const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
+const renderers = {
+ ProductCarousel: ({props}) => {
+ const state = useAdvertisedController<{products?: Product[]}>(
+ props.controllers.productListController
+ );
+ const products = Array.isArray(state.products) ? state.products : [];
- return (
-
- Cart
- Your cart
- {items.map((item) => (
-
-
- {item.quantity} × {item.name}
+ return (
+
+
+
+
ProductCarousel
+
Featured products
+
+
+ controller: {props.controllers.productListController.controllerId}
-
${(item.price * item.quantity).toFixed(2)}
- ))}
-
- Total
- ${total.toFixed(2)}
-
-
- );
-}
+
+ {products.map((product) => {
+ const price = product.ec_promo_price ?? product.ec_price;
+ return (
+
+
+ {product.ec_brand}
+ {product.ec_name}
+ {product.ec_shortdesc}
+
+ ${price.toFixed(2)}
+ ★ {product.ec_rating}
+
+ {product.ec_in_stock ? 'In stock' : 'Out of stock'}
+
+ );
+ })}
+
+
+ );
+ },
+ Cart: ({props}) => {
+ const state = useAdvertisedController<{items?: CartItem[]}>(props.controllers.cartController);
+ const items = Array.isArray(state.items) ? state.items : [];
+ const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
+
+ return (
+
+ Cart
+ Your cart
+ {items.map((item) => (
+
+
+ {item.quantity} × {item.name}
+
+ ${(item.price * item.quantity).toFixed(2)}
+
+ ))}
+
+ Total
+ ${total.toFixed(2)}
+
+
+ );
+ },
+} satisfies CatalogRenderers;
-function asArray(value: unknown): T[] {
- return Array.isArray(value) ? (value as T[]) : [];
-}
+export const thermidorCatalog = 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..8b97cd6b75a
--- /dev/null
+++ b/samples/thermidor/schema-contract-react/src/a2ui/controllers.test.ts
@@ -0,0 +1,48 @@
+import {describe, expect, it, vi} from 'vitest';
+import {ThermidorControllerRegistry} from './controllers.js';
+
+describe('ThermidorControllerRegistry', () => {
+ it('hydrates a component-created controller and notifies it about subsequent data-model updates', () => {
+ const registry = new ThermidorControllerRegistry();
+ const advertisement = {
+ controllerId: 'featured-products',
+ controllerSchema: 'product-list.schema.json',
+ };
+ registry.synchronize([
+ {
+ version: 'v0.9',
+ updateDataModel: {
+ surfaceId: 'catalog',
+ value: {controllers: {'featured-products': {products: [{permanentid: 'p1'}]}}},
+ },
+ },
+ ]);
+
+ const controller = registry.getOrCreate(advertisement);
+ expect(controller.snapshot).toEqual({products: [{permanentid: 'p1'}]});
+
+ const listener = vi.fn();
+ const unsubscribe = controller.subscribe(listener);
+ registry.synchronize([
+ {
+ version: 'v0.9',
+ updateDataModel: {
+ surfaceId: 'catalog',
+ value: {controllers: {'featured-products': {products: [{permanentid: 'p1'}]}}},
+ },
+ },
+ {
+ version: 'v0.9',
+ updateDataModel: {
+ surfaceId: 'catalog',
+ path: '/controllers/featured-products/products',
+ value: [{permanentid: 'p2'}],
+ },
+ },
+ ]);
+
+ expect(controller.snapshot).toEqual({products: [{permanentid: 'p2'}]});
+ expect(listener).toHaveBeenCalledOnce();
+ unsubscribe();
+ });
+});
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..f57a840e60f
--- /dev/null
+++ b/samples/thermidor/schema-contract-react/src/a2ui/controllers.tsx
@@ -0,0 +1,176 @@
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useRef,
+ useSyncExternalStore,
+ type ReactNode,
+} from 'react';
+
+export type ControllerAdvertisement = {
+ controllerId: string;
+ controllerSchema: string;
+};
+
+type A2UIMessage = Record;
+type ControllerState = Record;
+
+class AdvertisedController {
+ private listeners = new Set<() => void>();
+ private serializedState: string;
+
+ public constructor(
+ public readonly advertisement: ControllerAdvertisement,
+ private state: ControllerState
+ ) {
+ this.serializedState = JSON.stringify(state);
+ }
+
+ public get snapshot(): ControllerState {
+ return this.state;
+ }
+
+ public hydrate(state: ControllerState): void {
+ const serializedState = JSON.stringify(state);
+ if (serializedState === this.serializedState) {
+ return;
+ }
+
+ this.state = state;
+ this.serializedState = serializedState;
+ this.listeners.forEach((listener) => listener());
+ }
+
+ public subscribe(listener: () => void): () => void {
+ this.listeners.add(listener);
+ return () => this.listeners.delete(listener);
+ }
+}
+
+export class ThermidorControllerRegistry {
+ private controllers = new Map();
+ private controllerStates = new Map();
+
+ public synchronize(messages: A2UIMessage[]): void {
+ const dataModels = new Map();
+
+ for (const message of messages) {
+ const updateDataModel = message['updateDataModel'];
+ if (!isRecord(updateDataModel) || typeof updateDataModel['surfaceId'] !== 'string') {
+ continue;
+ }
+
+ const surfaceId = updateDataModel['surfaceId'];
+ dataModels.set(
+ surfaceId,
+ applyDataModelUpdate(
+ dataModels.get(surfaceId),
+ typeof updateDataModel['path'] === 'string' ? updateDataModel['path'] : '/',
+ updateDataModel['value']
+ )
+ );
+ }
+
+ const controllerStates = new Map();
+ for (const dataModel of dataModels.values()) {
+ const advertisedStates =
+ isRecord(dataModel) && isRecord(dataModel['controllers']) ? dataModel['controllers'] : {};
+ for (const [controllerId, state] of Object.entries(advertisedStates)) {
+ if (isRecord(state)) {
+ controllerStates.set(controllerId, state);
+ }
+ }
+ }
+
+ this.controllerStates = controllerStates;
+ for (const controller of this.controllers.values()) {
+ controller.hydrate(controllerStates.get(controller.advertisement.controllerId) ?? {});
+ }
+ }
+
+ public getOrCreate(advertisement: ControllerAdvertisement): AdvertisedController {
+ const existing = this.controllers.get(advertisement.controllerId);
+ if (existing && existing.advertisement.controllerSchema === advertisement.controllerSchema) {
+ return existing;
+ }
+
+ const controller = new AdvertisedController(
+ advertisement,
+ this.controllerStates.get(advertisement.controllerId) ?? {}
+ );
+ this.controllers.set(advertisement.controllerId, controller);
+ return controller;
+ }
+}
+
+const ControllerRegistryContext = createContext(null);
+
+export function ThermidorControllerProvider({
+ children,
+ messages,
+}: {
+ children: ReactNode;
+ messages: A2UIMessage[];
+}) {
+ const registryRef = useRef(null);
+ registryRef.current ??= new ThermidorControllerRegistry();
+ const serializedMessages = useMemo(() => JSON.stringify(messages), [messages]);
+
+ useEffect(() => {
+ registryRef.current!.synchronize(JSON.parse(serializedMessages) as A2UIMessage[]);
+ }, [serializedMessages]);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useAdvertisedController(
+ advertisement: ControllerAdvertisement
+): T {
+ const registry = useContext(ControllerRegistryContext);
+ if (!registry) {
+ throw new Error('Advertised controllers require ThermidorControllerProvider.');
+ }
+
+ const controller = useMemo(
+ () => registry.getOrCreate(advertisement),
+ [advertisement.controllerId, advertisement.controllerSchema, registry]
+ );
+ const subscribe = useCallback(
+ (listener: () => void) => controller.subscribe(listener),
+ [controller]
+ );
+ const getSnapshot = useCallback(() => controller.snapshot as T, [controller]);
+
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
+}
+
+function applyDataModelUpdate(dataModel: unknown, path: string, value: unknown): unknown {
+ const segments = path
+ .split('/')
+ .filter(Boolean)
+ .map((segment) => segment.replaceAll('~1', '/').replaceAll('~0', '~'));
+ if (segments.length === 0) {
+ return value;
+ }
+
+ const root = isRecord(dataModel) ? {...dataModel} : {};
+ let target: Record = root;
+ for (const segment of segments.slice(0, -1)) {
+ const current = target[segment];
+ const next = isRecord(current) ? {...current} : {};
+ target[segment] = next;
+ target = next;
+ }
+ target[segments.at(-1)!] = value;
+ return root;
+}
+
+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/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);
+}
From 6a80abe751564e0ff2657012965fbc999187cd8a Mon Sep 17 00:00:00 2001
From: Louis Bompart
Date: Mon, 27 Jul 2026 23:41:17 +0200
Subject: [PATCH 3/5] generative-ctrls
---
packages/mock-converse-api/src/server.test.ts | 20 +-
.../src/converse/templates/response9.ts | 102 +++++-----
.../api/generative/generative-runtime.test.ts | 8 +-
.../api/generative/generative-runtime.ts | 9 +
.../features/generative/generative-actions.ts | 3 +
.../features/generative/generative-slice.ts | 7 +
.../features/generative/generative-types.ts | 7 +
.../converse/converse-controller.ts | 3 +
.../thermidor/src/public/controllers/index.ts | 9 +
.../remote/remote-controller.test.ts | 91 +++++++++
.../controllers/remote/remote-controller.ts | 130 +++++++++++++
.../thermidor/schema-contract-react/README.md | 4 +-
.../schema-contract-react/src/App.tsx | 77 ++++----
.../src/a2ui/components.tsx | 148 ++++++++-------
.../src/a2ui/controllers.test.ts | 55 ++----
.../src/a2ui/controllers.tsx | 179 ++----------------
16 files changed, 474 insertions(+), 378 deletions(-)
create mode 100644 packages/thermidor/src/public/controllers/remote/remote-controller.test.ts
create mode 100644 packages/thermidor/src/public/controllers/remote/remote-controller.ts
diff --git a/packages/mock-converse-api/src/server.test.ts b/packages/mock-converse-api/src/server.test.ts
index 164243cdc38..c7610273001 100644
--- a/packages/mock-converse-api/src/server.test.ts
+++ b/packages/mock-converse-api/src/server.test.ts
@@ -144,6 +144,7 @@ describe('createMockConverseServer', () => {
(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',
@@ -169,29 +170,18 @@ describe('createMockConverseServer', () => {
],
},
},
- {
- version: 'v0.9',
- updateDataModel: {
- surfaceId: 'commerce-catalog-example',
- },
- },
],
},
});
- const content = activity?.['content'] as {a2ui_operations: Array>};
- const dataModelOperation = content.a2ui_operations.find(
- (operation) => 'updateDataModel' in operation
- )!['updateDataModel'] as {
- value: {
- controllers: Record;
- };
+ const snapshot = stateSnapshot?.['snapshot'] as {
+ controllers: Record;
};
- expect(dataModelOperation.value.controllers['featured-products'].products).toEqual(
+ expect(snapshot.controllers['featured-products'].products).toEqual(
expect.arrayContaining([expect.objectContaining({permanentid: 'trail-running-shoes-001'})])
);
- expect(dataModelOperation.value.controllers['shopping-cart'].items).toEqual(
+ expect(snapshot.controllers['shopping-cart'].items).toEqual(
expect.arrayContaining([
expect.objectContaining({productId: 'trail-running-shoes-001', quantity: 1}),
])
diff --git a/packages/platform-mock-api/src/converse/templates/response9.ts b/packages/platform-mock-api/src/converse/templates/response9.ts
index 161ef2c8d5b..8ed8a22a309 100644
--- a/packages/platform-mock-api/src/converse/templates/response9.ts
+++ b/packages/platform-mock-api/src/converse/templates/response9.ts
@@ -2,12 +2,59 @@ 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(),
@@ -15,6 +62,7 @@ const thermidorSchemaCatalogResponseEvents: ConverseEvent[] = [
'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',
@@ -26,7 +74,6 @@ const thermidorSchemaCatalogResponseEvents: ConverseEvent[] = [
createSurface: {
surfaceId: 'commerce-catalog-example',
catalogId: 'https://schema.thermidor.coveo.com/a2-ui/catalog.json',
- sendDataModel: true,
},
},
{
@@ -64,59 +111,6 @@ const thermidorSchemaCatalogResponseEvents: ConverseEvent[] = [
],
},
},
- {
- version: 'v0.9',
- updateDataModel: {
- surfaceId: 'commerce-catalog-example',
- value: {
- 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,
- },
- ],
- },
- },
- },
- },
- },
],
},
}),
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 e67684d5ca1..075e166b880 100644
--- a/packages/thermidor/src/internal/api/generative/generative-runtime.test.ts
+++ b/packages/thermidor/src/internal/api/generative/generative-runtime.test.ts
@@ -45,6 +45,7 @@ function createMockStatePort(): GenerativeStatePort {
startMessage: vi.fn(),
appendMessageDelta: vi.fn(),
appendActivity: vi.fn(),
+ setStateSnapshot: vi.fn(),
startToolCall: vi.fn(),
appendToolCallArgs: vi.fn(),
completeToolCall: vi.fn(),
@@ -636,17 +637,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 f36c1b334a5..1421d1e855e 100644
--- a/packages/thermidor/src/internal/api/generative/generative-runtime.ts
+++ b/packages/thermidor/src/internal/api/generative/generative-runtime.ts
@@ -24,6 +24,7 @@ export interface GenerativeStatePort {
startMessage(turnId: string, role: string): void;
appendMessageDelta(turnId: string, delta: string): 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;
@@ -248,6 +249,8 @@ export class GenerativeRuntime {
}
case 'STATE_SNAPSHOT': {
+ this.ensureAgentResponse(turnId);
+ this.statePort.setStateSnapshot(turnId, asRecord(event.snapshot));
return {turnId, isTerminal: false};
}
@@ -309,6 +312,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 94782ea509c..c898a5d978b 100644
--- a/packages/thermidor/src/internal/features/generative/generative-actions.ts
+++ b/packages/thermidor/src/internal/features/generative/generative-actions.ts
@@ -27,6 +27,9 @@ export function createGenerativeActions(interfaceId: string) {
`${prefix}/appendMessageDelta`
),
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 c070d35ae02..5a97093376a 100644
--- a/packages/thermidor/src/internal/features/generative/generative-slice.ts
+++ b/packages/thermidor/src/internal/features/generative/generative-slice.ts
@@ -56,6 +56,7 @@ export function createGenerativeSlice(
const turn = state.turns.find((t) => t.id === payload.turnId);
if (turn) {
turn.agentResponse = {
+ state: {},
messages: [],
activities: [],
reasoningSteps: [],
@@ -81,6 +82,12 @@ export function createGenerativeSlice(
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}) => {
const turn = state.turns.find((t) => t.id === payload.turnId);
if (turn?.agentResponse) {
diff --git a/packages/thermidor/src/internal/features/generative/generative-types.ts b/packages/thermidor/src/internal/features/generative/generative-types.ts
index fce91337799..bdb01a4d1b9 100644
--- a/packages/thermidor/src/internal/features/generative/generative-types.ts
+++ b/packages/thermidor/src/internal/features/generative/generative-types.ts
@@ -111,6 +111,13 @@ 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.
*/
diff --git a/packages/thermidor/src/public/controllers/converse/converse-controller.ts b/packages/thermidor/src/public/controllers/converse/converse-controller.ts
index a8816c08a02..42952c735a9 100644
--- a/packages/thermidor/src/public/controllers/converse/converse-controller.ts
+++ b/packages/thermidor/src/public/controllers/converse/converse-controller.ts
@@ -100,6 +100,9 @@ class ConverseControllerImpl extends BaseController {
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}));
},
diff --git a/packages/thermidor/src/public/controllers/index.ts b/packages/thermidor/src/public/controllers/index.ts
index 8c799d08eb9..4615ba13fbe 100644
--- a/packages/thermidor/src/public/controllers/index.ts
+++ b/packages/thermidor/src/public/controllers/index.ts
@@ -38,6 +38,15 @@ export type {
ProductListControllerProduct,
ProductListControllerState,
} from './product-list/product-list-controller.js';
+export {buildRemoteController, selectRemoteControllerState} from './remote/remote-controller.js';
+export type {
+ RemoteController,
+ RemoteControllerAction,
+ RemoteControllerActionDispatcher,
+ RemoteControllerOptions,
+ RemoteControllerSource,
+ RemoteControllerState,
+} 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..e3a6be8fe03
--- /dev/null
+++ b/packages/thermidor/src/public/controllers/remote/remote-controller.test.ts
@@ -0,0 +1,91 @@
+import {describe, expect, it, vi} from 'vitest';
+import {
+ buildRemoteController,
+ selectRemoteControllerState,
+ type RemoteControllerSource,
+} from './remote-controller.js';
+
+describe('buildRemoteController', () => {
+ it('selects its server-owned state from the active conversation turn', () => {
+ const source = createSource({controllers: {cart: {items: [{productId: 'p1'}]}}});
+ const controller = buildRemoteController({
+ source,
+ controllerId: 'cart',
+ dispatchAction: vi.fn(),
+ });
+
+ expect(controller.state).toEqual({items: [{productId: 'p1'}]});
+ });
+
+ it('notifies subscribers when its snapshot slice changes, but not for another controller', () => {
+ const source = createSource({controllers: {cart: {items: []}, products: {products: []}}});
+ const controller = buildRemoteController({
+ source,
+ controllerId: 'cart',
+ dispatchAction: vi.fn(),
+ });
+ const callback = vi.fn();
+
+ controller.subscribe(callback);
+ source.setSnapshot({controllers: {cart: controller.state, products: {products: ['p1']}}});
+ expect(callback).not.toHaveBeenCalled();
+
+ source.setSnapshot({controllers: {cart: {items: [{productId: 'p1'}]}}});
+ expect(callback).toHaveBeenCalledWith({items: [{productId: 'p1'}]});
+ });
+
+ it('dispatches controller actions without locally changing server-owned state', async () => {
+ const dispatchAction = vi.fn();
+ const controller = buildRemoteController({
+ source: createSource({controllers: {cart: {items: []}}}),
+ controllerId: 'cart',
+ dispatchAction,
+ });
+
+ await controller.dispatch('updateItemQuantity', {item: {productId: 'p1', quantity: 2}});
+
+ expect(dispatchAction).toHaveBeenCalledWith({
+ controllerId: 'cart',
+ action: 'updateItemQuantity',
+ payload: {item: {productId: 'p1', quantity: 2}},
+ });
+ expect(controller.state).toEqual({items: []});
+ });
+
+ it('rejects an unnamed action', async () => {
+ const controller = buildRemoteController({
+ source: createSource({controllers: {}}),
+ controllerId: 'cart',
+ dispatchAction: vi.fn(),
+ });
+
+ await expect(controller.dispatch(' ', {})).rejects.toThrow('action name is required');
+ });
+});
+
+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}}},
+ 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 & {
+ 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..3bdf0ee1032
--- /dev/null
+++ b/packages/thermidor/src/public/controllers/remote/remote-controller.ts
@@ -0,0 +1,130 @@
+import type {Controller} from '../controller-types.js';
+import type {ConverseController, ConverseControllerState} from '../converse/converse-controller.js';
+
+export type RemoteControllerState = Record;
+
+export interface RemoteControllerAction {
+ controllerId: string;
+ action: string;
+ payload: TPayload;
+}
+
+/**
+ * Delivers a controller action to the application's server transport.
+ *
+ * Thermidor deliberately does not prescribe that transport: the server owns the
+ * controller state and must acknowledge a mutation by publishing a later
+ * `STATE_SNAPSHOT`.
+ */
+export type RemoteControllerActionDispatcher = (
+ action: RemoteControllerAction
+) => void | Promise;
+
+/**
+ * A controller state source backed by Thermidor's active conversation turn.
+ */
+export type RemoteControllerSource = Pick;
+
+class RemoteControllerImpl<
+ TState extends RemoteControllerState,
+> implements RemoteController {
+ readonly controllerId: string;
+
+ constructor(
+ private readonly source: RemoteControllerSource,
+ controllerId: string,
+ private readonly dispatchAction: RemoteControllerActionDispatcher
+ ) {
+ this.controllerId = controllerId;
+ }
+
+ get state(): TState {
+ return selectRemoteControllerState(this.source.state, this.controllerId) as TState;
+ }
+
+ subscribe(callback: (state: TState) => void): () => void {
+ let previousState = this.state;
+
+ return this.source.subscribe(() => {
+ const nextState = this.state;
+ if (nextState === previousState) {
+ return;
+ }
+
+ previousState = nextState;
+ callback(nextState);
+ });
+ }
+
+ dispatch(action: string, payload: TPayload): Promise {
+ if (!action.trim()) {
+ return Promise.reject(new Error('A controller action name is required.'));
+ }
+
+ return Promise.resolve().then(() =>
+ this.dispatchAction({
+ controllerId: this.controllerId,
+ action,
+ payload,
+ })
+ );
+ }
+}
+
+/**
+ * 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 {
+ return new RemoteControllerImpl(
+ options.source,
+ options.controllerId,
+ options.dispatchAction
+ );
+}
+
+export interface RemoteController<
+ TState extends RemoteControllerState = RemoteControllerState,
+> extends Controller {
+ /** The runtime key used to select this controller's state from `controllers`. */
+ readonly controllerId: string;
+
+ /**
+ * Emits an action for this controller through the configured server transport.
+ * State remains server-owned and changes only when the server sends a snapshot.
+ */
+ dispatch(action: string, payload: TPayload): Promise;
+}
+
+export interface RemoteControllerOptions {
+ source: RemoteControllerSource;
+ controllerId: string;
+ dispatchAction: RemoteControllerActionDispatcher;
+}
+
+const EMPTY_REMOTE_CONTROLLER_STATE: RemoteControllerState = {};
+
+export function selectRemoteControllerState(
+ state: ConverseControllerState,
+ controllerId: string
+): RemoteControllerState {
+ 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/samples/thermidor/schema-contract-react/README.md b/samples/thermidor/schema-contract-react/README.md
index d038c7e1cef..5e53ac16952 100644
--- a/samples/thermidor/schema-contract-react/README.md
+++ b/samples/thermidor/schema-contract-react/README.md
@@ -2,9 +2,9 @@
This sample proves the client-side path for the v0.9 Thermidor Commerce Catalog contract:
-`ConverseController` → normalized Thermidor `Activity` → raw A2-UI messages → CopilotKit A2-UI renderer/catalog.
+`AG-UI STATE_SNAPSHOT` → Thermidor Engine state → advertised controller; `ACTIVITY_SNAPSHOT` → raw A2-UI messages → CopilotKit renderer/catalog.
-Thermidor only exposes opaque activity `kind` and `payload` values. The sample passes the `a2ui-surface` operations to CopilotKit unchanged. Its local `ProductCarousel` and `Cart` components create their advertised controllers from `updateComponents`, hydrate them from `updateDataModel`, and subscribe to later data-model updates. CopilotKit provides only renderer and catalog state; it does not replace Thermidor's conversational endpoint or runtime.
+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
diff --git a/samples/thermidor/schema-contract-react/src/App.tsx b/samples/thermidor/schema-contract-react/src/App.tsx
index 900f6de6726..f06897ab65a 100644
--- a/samples/thermidor/schema-contract-react/src/App.tsx
+++ b/samples/thermidor/schema-contract-react/src/App.tsx
@@ -7,8 +7,7 @@ import {
type GenerativeInterface,
} from '@coveo/thermidor';
import {A2UIProvider} from '@copilotkit/a2ui-renderer';
-import {thermidorCatalog} from './a2ui/components.js';
-import {ThermidorControllerProvider} from './a2ui/controllers.js';
+import {createThermidorCatalog} from './a2ui/components.js';
import {getA2UIMessages, ThermidorA2UISurfaces} from './a2ui/surfaces.js';
import {getSampleConfiguration} from './env.js';
import {useController} from './use-controller.js';
@@ -16,11 +15,7 @@ import {useController} from './use-controller.js';
const CONTRACT_PROMPT = 'Show the Thermidor catalog';
export default function App() {
- return (
-
-
-
- );
+ return ;
}
function ContractSample() {
@@ -36,6 +31,8 @@ function ContractSample() {
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(
@@ -56,42 +53,42 @@ function ContractSample() {
}
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.
-
-
+
+
+
+ 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.
+
+
-
+
- {turn?.agentResponse?.messages.map((message, index) => (
-
- {message.content}
-
- ))}
- {turn?.status === 'error' && {turn.error}
}
-
+ {turn?.agentResponse?.messages.map((message, index) => (
+
+ {message.content}
+
+ ))}
+ {turn?.status === 'error' && {turn.error}
}
-
- {!turn && Run the pre-filled prompt to render the schema example.
}
-
+ {!turn && Run the pre-filled prompt to render the schema example.
}
+
+
);
}
diff --git a/samples/thermidor/schema-contract-react/src/a2ui/components.tsx b/samples/thermidor/schema-contract-react/src/a2ui/components.tsx
index 28fb7aa525c..4b57e393242 100644
--- a/samples/thermidor/schema-contract-react/src/a2ui/components.tsx
+++ b/samples/thermidor/schema-contract-react/src/a2ui/components.tsx
@@ -3,8 +3,9 @@ import {
type CatalogDefinitions,
type CatalogRenderers,
} from '@copilotkit/a2ui-renderer';
+import type {RemoteControllerActionDispatcher} from '@coveo/thermidor';
import {z} from 'zod';
-import {useAdvertisedController} from './controllers.js';
+import {type EngineStateSource, useAdvertisedController} from './controllers.js';
export const THERMIDOR_CATALOG_ID = 'https://schema.thermidor.coveo.com/a2-ui/catalog.json';
@@ -50,72 +51,89 @@ export const thermidorCatalogDefinitions = {
},
} satisfies CatalogDefinitions;
-const renderers = {
- ProductCarousel: ({props}) => {
- const state = useAdvertisedController<{products?: Product[]}>(
- props.controllers.productListController
- );
- const products = Array.isArray(state.products) ? state.products : [];
+export function createThermidorCatalog(
+ stateSource: EngineStateSource,
+ dispatchAction: RemoteControllerActionDispatcher = rejectUnhandledControllerAction
+) {
+ const renderers = {
+ ProductCarousel: ({props}) => {
+ const [, state] = useAdvertisedController<{products?: Product[]}>(
+ stateSource,
+ props.controllers.productListController,
+ dispatchAction
+ );
+ const products = Array.isArray(state.products) ? state.products : [];
- return (
-
-
-
-
ProductCarousel
-
Featured products
+ return (
+
+
+
+
ProductCarousel
+
Featured products
+
+
+ controller: {props.controllers.productListController.controllerId}
+
-
- 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.toFixed(2)}
- ★ {product.ec_rating}
-
- {product.ec_in_stock ? 'In stock' : 'Out of stock'}
-
- );
- })}
-
-
- );
- },
- Cart: ({props}) => {
- const state = useAdvertisedController<{items?: CartItem[]}>(props.controllers.cartController);
- const items = Array.isArray(state.items) ? state.items : [];
- const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
+
+ {products.map((product) => {
+ const price = product.ec_promo_price ?? product.ec_price;
+ return (
+
+
+ {product.ec_brand}
+ {product.ec_name}
+ {product.ec_shortdesc}
+
+ ${price.toFixed(2)}
+ ★ {product.ec_rating}
+
+ {product.ec_in_stock ? 'In stock' : 'Out of stock'}
+
+ );
+ })}
+
+
+ );
+ },
+ Cart: ({props}) => {
+ const [, state] = useAdvertisedController<{items?: CartItem[]}>(
+ stateSource,
+ props.controllers.cartController,
+ dispatchAction
+ );
+ const items = Array.isArray(state.items) ? state.items : [];
+ const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
- return (
-