From 86fdfcb6426f4c3c982ad0db154d6b14d13a1c5b Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Tue, 31 Mar 2026 16:55:14 -0700 Subject: [PATCH 1/4] Fix tests --- examples/eval/test/simple_chat_test.dart | 4 ++-- packages/genui/lib/src/engine/surface_controller.dart | 4 ---- packages/genui/lib/src/facade/prompt_builder.dart | 4 ++++ .../create_only_with_dataModel_false.txt | 2 ++ .../create_only_with_dataModel_true.txt | 2 ++ 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/examples/eval/test/simple_chat_test.dart b/examples/eval/test/simple_chat_test.dart index cd7c7c2a9..be731f0a2 100644 --- a/examples/eval/test/simple_chat_test.dart +++ b/examples/eval/test/simple_chat_test.dart @@ -117,8 +117,8 @@ class _ChatSessionTester { reporter.expect(errors.isEmpty, 'No errors should occur'); reporter.expect( - updated.isEmpty, - 'In chat setup surfaces should not be updated', + updated.length == created.length, + 'In chat setup surfaces should not be updated after initial creation', ); } diff --git a/packages/genui/lib/src/engine/surface_controller.dart b/packages/genui/lib/src/engine/surface_controller.dart index 51f011218..3c61a591b 100644 --- a/packages/genui/lib/src/engine/surface_controller.dart +++ b/packages/genui/lib/src/engine/surface_controller.dart @@ -234,10 +234,6 @@ interface class SurfaceController implements SurfaceHost, A2uiMessageSink { final DataModel model = _store.getDataModel(surfaceId); model.update(path, value); - // Trigger generic update on surface to refresh UI - final SurfaceDefinition current = _registry.getSurface(surfaceId)!; - _registry.updateSurface(surfaceId, current); - case DeleteSurface(:final surfaceId): _pendingUpdates.remove(surfaceId); _pendingUpdateTimers.remove(surfaceId)?.cancel(); diff --git a/packages/genui/lib/src/facade/prompt_builder.dart b/packages/genui/lib/src/facade/prompt_builder.dart index fbef8f403..13b956009 100644 --- a/packages/genui/lib/src/facade/prompt_builder.dart +++ b/packages/genui/lib/src/facade/prompt_builder.dart @@ -286,6 +286,10 @@ You can control the UI by outputting valid A2UI JSON messages wrapped in markdow To create a new UI: 1. Output a ${ProtocolMessages.createSurface.tickedName} message with a unique `surfaceId` and `catalogId` (use the catalog ID provided in system instructions). 2. Output an ${ProtocolMessages.updateComponents.tickedName} message with the `surfaceId` and the component definitions. +''', + if (!update) + ''' +IMPORTANT: DO NOT update or modify surfaces created in previous turns. If the UI needs to change, you MUST create a NEW surface with a new unique `surfaceId`. You may only use `${ProtocolMessages.updateComponents.name}` to populate the components of a freshly created surface. ''', if (update) ''' diff --git a/packages/genui/test/facade/prompt_builder_test.golden/create_only_with_dataModel_false.txt b/packages/genui/test/facade/prompt_builder_test.golden/create_only_with_dataModel_false.txt index e97f8940f..d34366070 100644 --- a/packages/genui/test/facade/prompt_builder_test.golden/create_only_with_dataModel_false.txt +++ b/packages/genui/test/facade/prompt_builder_test.golden/create_only_with_dataModel_false.txt @@ -107,6 +107,8 @@ One component MUST have `id: "root"`. To create a new UI: 1. Output a `createSurface` message with a unique `surfaceId` and `catalogId` (use the catalog ID provided in system instructions). 2. Output an `updateComponents` message with the `surfaceId` and the component definitions. + +IMPORTANT: DO NOT update or modify surfaces created in previous turns. If the UI needs to change, you MUST create a NEW surface with a new unique `surfaceId`. You may only use `updateComponents` to populate the components of a freshly created surface. -----CONTROLLING_THE_UI_END----- ------------------------------------- diff --git a/packages/genui/test/facade/prompt_builder_test.golden/create_only_with_dataModel_true.txt b/packages/genui/test/facade/prompt_builder_test.golden/create_only_with_dataModel_true.txt index 31a468da0..3aca4e49d 100644 --- a/packages/genui/test/facade/prompt_builder_test.golden/create_only_with_dataModel_true.txt +++ b/packages/genui/test/facade/prompt_builder_test.golden/create_only_with_dataModel_true.txt @@ -109,6 +109,8 @@ One component MUST have `id: "root"`. To create a new UI: 1. Output a `createSurface` message with a unique `surfaceId` and `catalogId` (use the catalog ID provided in system instructions). 2. Output an `updateComponents` message with the `surfaceId` and the component definitions. + +IMPORTANT: DO NOT update or modify surfaces created in previous turns. If the UI needs to change, you MUST create a NEW surface with a new unique `surfaceId`. You may only use `updateComponents` to populate the components of a freshly created surface. -----CONTROLLING_THE_UI_END----- ------------------------------------- From c4337c8fff42dbc033583877812cc89626858751 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Tue, 31 Mar 2026 17:45:21 -0700 Subject: [PATCH 2/4] Update packages/genui/lib/src/engine/surface_controller.dart Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- packages/genui/lib/src/engine/surface_controller.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/genui/lib/src/engine/surface_controller.dart b/packages/genui/lib/src/engine/surface_controller.dart index 3c61a591b..88c83a9f8 100644 --- a/packages/genui/lib/src/engine/surface_controller.dart +++ b/packages/genui/lib/src/engine/surface_controller.dart @@ -234,6 +234,9 @@ interface class SurfaceController implements SurfaceHost, A2uiMessageSink { final DataModel model = _store.getDataModel(surfaceId); model.update(path, value); + // Note: We don't trigger a surface update here to avoid full UI refreshes + // on data changes. Components should listen to the DataModel directly. + case DeleteSurface(:final surfaceId): _pendingUpdates.remove(surfaceId); _pendingUpdateTimers.remove(surfaceId)?.cancel(); From 7d54ba51e2d06abc89573155749749d798f39fd7 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Tue, 31 Mar 2026 17:46:08 -0700 Subject: [PATCH 3/4] Update packages/genui/lib/src/facade/prompt_builder.dart Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- packages/genui/lib/src/facade/prompt_builder.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/genui/lib/src/facade/prompt_builder.dart b/packages/genui/lib/src/facade/prompt_builder.dart index 13b956009..e93f0b52c 100644 --- a/packages/genui/lib/src/facade/prompt_builder.dart +++ b/packages/genui/lib/src/facade/prompt_builder.dart @@ -287,10 +287,7 @@ To create a new UI: 1. Output a ${ProtocolMessages.createSurface.tickedName} message with a unique `surfaceId` and `catalogId` (use the catalog ID provided in system instructions). 2. Output an ${ProtocolMessages.updateComponents.tickedName} message with the `surfaceId` and the component definitions. ''', - if (!update) - ''' -IMPORTANT: DO NOT update or modify surfaces created in previous turns. If the UI needs to change, you MUST create a NEW surface with a new unique `surfaceId`. You may only use `${ProtocolMessages.updateComponents.name}` to populate the components of a freshly created surface. -''', +IMPORTANT: DO NOT update or modify surfaces created in previous turns. If the UI needs to change, you MUST create a NEW surface with a new unique `surfaceId`. You may only use ${ProtocolMessages.updateComponents.tickedName} to populate the components of a freshly created surface. if (update) ''' To update an existing UI: From b5f3853ba59b3cc3fdd91238b940738429a39fc5 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Tue, 31 Mar 2026 18:00:02 -0700 Subject: [PATCH 4/4] Add comment, test turn counts --- examples/eval/test/simple_chat_test.dart | 33 +++++++++++++++++++ .../lib/src/engine/surface_controller.dart | 4 +-- .../genui/lib/src/facade/conversation.dart | 3 +- .../genui/lib/src/facade/prompt_builder.dart | 11 ++++--- .../all_operations_with_dataModel_false.txt | 4 +-- .../all_operations_with_dataModel_true.txt | 4 +-- ...create_and_update_with_dataModel_false.txt | 4 +-- .../create_and_update_with_dataModel_true.txt | 4 +-- .../create_only_with_dataModel_false.txt | 4 +-- .../create_only_with_dataModel_true.txt | 4 +-- .../update_only_with_dataModel_false.txt | 2 +- .../update_only_with_dataModel_true.txt | 2 +- 12 files changed, 58 insertions(+), 21 deletions(-) diff --git a/examples/eval/test/simple_chat_test.dart b/examples/eval/test/simple_chat_test.dart index be731f0a2..4781ee955 100644 --- a/examples/eval/test/simple_chat_test.dart +++ b/examples/eval/test/simple_chat_test.dart @@ -90,22 +90,48 @@ class _ChatSessionTester { var content = 0; var waiting = 0; final errors = []; + + var currentTurnCreates = 0; + var currentTurnUpdates = 0; + var turnCount = 0; + + void verifyTurn() { + if (turnCount > 0) { + reporter.expect( + currentTurnCreates <= 1, + 'Turn $turnCount should create at most 1 surface', + ); + reporter.expect( + currentTurnUpdates == currentTurnCreates, + 'Turn $turnCount should have matching creates ($currentTurnCreates) ' + 'and updates ($currentTurnUpdates)', + ); + } + } + for (final ConversationEvent event in events) { switch (event) { case ConversationSurfaceAdded(): created.add(event.surfaceId); + currentTurnCreates++; case ConversationComponentsUpdated(): updated.add(event.surfaceId); + currentTurnUpdates++; case ConversationSurfaceRemoved(): removed.add(event.surfaceId); case ConversationContentReceived(): content++; case ConversationWaiting(): + verifyTurn(); + turnCount++; waiting++; + currentTurnCreates = 0; + currentTurnUpdates = 0; case ConversationError(): errors.add(event.error.toString()); } } + verifyTurn(); print('Conversation summary:'); print(' Created surfaces: $created'); @@ -120,6 +146,13 @@ class _ChatSessionTester { updated.length == created.length, 'In chat setup surfaces should not be updated after initial creation', ); + for (final id in created) { + final int updateCount = updated.where((u) => u == id).length; + reporter.expect( + updateCount == 1, + 'Surface $id should be updated exactly once', + ); + } } void failIfIssuesFound() => reporter.failIfIssuesFound(); diff --git a/packages/genui/lib/src/engine/surface_controller.dart b/packages/genui/lib/src/engine/surface_controller.dart index 88c83a9f8..69731d290 100644 --- a/packages/genui/lib/src/engine/surface_controller.dart +++ b/packages/genui/lib/src/engine/surface_controller.dart @@ -234,8 +234,8 @@ interface class SurfaceController implements SurfaceHost, A2uiMessageSink { final DataModel model = _store.getDataModel(surfaceId); model.update(path, value); - // Note: We don't trigger a surface update here to avoid full UI refreshes - // on data changes. Components should listen to the DataModel directly. + // Note: We don't trigger a surface update here to avoid full UI refreshes + // on data changes. Components should listen to the DataModel directly. case DeleteSurface(:final surfaceId): _pendingUpdates.remove(surfaceId); diff --git a/packages/genui/lib/src/facade/conversation.dart b/packages/genui/lib/src/facade/conversation.dart index 0b73fccd3..5b09ff07d 100644 --- a/packages/genui/lib/src/facade/conversation.dart +++ b/packages/genui/lib/src/facade/conversation.dart @@ -57,7 +57,8 @@ final class ConversationContentReceived extends ConversationEvent { final String text; } -/// Fired when the conversation is waiting for a response. +/// Fired when a request is sent to the LLM and the conversation is waiting +/// for an AI response. final class ConversationWaiting extends ConversationEvent {} /// Fired when an error occurs during the conversation. diff --git a/packages/genui/lib/src/facade/prompt_builder.dart b/packages/genui/lib/src/facade/prompt_builder.dart index e93f0b52c..3a0e1c5ae 100644 --- a/packages/genui/lib/src/facade/prompt_builder.dart +++ b/packages/genui/lib/src/facade/prompt_builder.dart @@ -22,7 +22,7 @@ abstract class PromptFragments { /// [prefix] is a prefix to be added to the prompt. /// Is useful when you want to emphasize the importance of this fragment. static String acknowledgeUser({String prefix = ''}) => - ''' + ''' ${prefix}Your responses should contain acknowledgment of the user message. ''' .trim(); @@ -131,7 +131,7 @@ enum ProtocolMessages { explanation: 'Creates a new surface.', properties: ''' Requires `surfaceId` (you must always use a unique ID for each created surface), -`catalogId` (use the catalog ID provided in system instructions), +`catalogId` (use the catalog ID provided in system instructions), and `sendDataModel: true`. ''', // TODO: figure out why we instruct AI to always set sendDataModel: true, @@ -142,7 +142,7 @@ and `sendDataModel: true`. name: 'updateComponents', explanation: 'Updates components in a surface.', properties: ''' -Requires `surfaceId` and a list of `components`. +Requires `surfaceId` and a list of `components`. One component MUST have `id: "root"`. ''', ), @@ -150,7 +150,7 @@ One component MUST have `id: "root"`. name: 'updateDataModel', explanation: 'Updates the data model.', properties: ''' -Requires `surfaceId`, `path` and `value`. +Requires `surfaceId`, `path` and `value`. ''', ), deleteSurface( @@ -287,7 +287,10 @@ To create a new UI: 1. Output a ${ProtocolMessages.createSurface.tickedName} message with a unique `surfaceId` and `catalogId` (use the catalog ID provided in system instructions). 2. Output an ${ProtocolMessages.updateComponents.tickedName} message with the `surfaceId` and the component definitions. ''', + if (!update) + ''' IMPORTANT: DO NOT update or modify surfaces created in previous turns. If the UI needs to change, you MUST create a NEW surface with a new unique `surfaceId`. You may only use ${ProtocolMessages.updateComponents.tickedName} to populate the components of a freshly created surface. +''', if (update) ''' To update an existing UI: diff --git a/packages/genui/test/facade/prompt_builder_test.golden/all_operations_with_dataModel_false.txt b/packages/genui/test/facade/prompt_builder_test.golden/all_operations_with_dataModel_false.txt index e8677efdb..3888111e3 100644 --- a/packages/genui/test/facade/prompt_builder_test.golden/all_operations_with_dataModel_false.txt +++ b/packages/genui/test/facade/prompt_builder_test.golden/all_operations_with_dataModel_false.txt @@ -100,9 +100,9 @@ Supported messages are: `createSurface`, `updateComponents`, `deleteSurface`. Properties: - `createSurface`: Requires `surfaceId` (you must always use a unique ID for each created surface), -`catalogId` (use the catalog ID provided in system instructions), +`catalogId` (use the catalog ID provided in system instructions), and `sendDataModel: true`. -- `updateComponents`: Requires `surfaceId` and a list of `components`. +- `updateComponents`: Requires `surfaceId` and a list of `components`. One component MUST have `id: "root"`. - `deleteSurface`: Requires `surfaceId`. diff --git a/packages/genui/test/facade/prompt_builder_test.golden/all_operations_with_dataModel_true.txt b/packages/genui/test/facade/prompt_builder_test.golden/all_operations_with_dataModel_true.txt index 5e8179ebf..8778192dd 100644 --- a/packages/genui/test/facade/prompt_builder_test.golden/all_operations_with_dataModel_true.txt +++ b/packages/genui/test/facade/prompt_builder_test.golden/all_operations_with_dataModel_true.txt @@ -101,9 +101,9 @@ Supported messages are: `createSurface`, `updateComponents`, `deleteSurface`, `u Properties: - `createSurface`: Requires `surfaceId` (you must always use a unique ID for each created surface), -`catalogId` (use the catalog ID provided in system instructions), +`catalogId` (use the catalog ID provided in system instructions), and `sendDataModel: true`. -- `updateComponents`: Requires `surfaceId` and a list of `components`. +- `updateComponents`: Requires `surfaceId` and a list of `components`. One component MUST have `id: "root"`. - `deleteSurface`: Requires `surfaceId`. - `updateDataModel`: Requires `surfaceId`, `path` and `value`. diff --git a/packages/genui/test/facade/prompt_builder_test.golden/create_and_update_with_dataModel_false.txt b/packages/genui/test/facade/prompt_builder_test.golden/create_and_update_with_dataModel_false.txt index 9b0b36e04..4d6716c4b 100644 --- a/packages/genui/test/facade/prompt_builder_test.golden/create_and_update_with_dataModel_false.txt +++ b/packages/genui/test/facade/prompt_builder_test.golden/create_and_update_with_dataModel_false.txt @@ -99,9 +99,9 @@ Supported messages are: `createSurface`, `updateComponents`. Properties: - `createSurface`: Requires `surfaceId` (you must always use a unique ID for each created surface), -`catalogId` (use the catalog ID provided in system instructions), +`catalogId` (use the catalog ID provided in system instructions), and `sendDataModel: true`. -- `updateComponents`: Requires `surfaceId` and a list of `components`. +- `updateComponents`: Requires `surfaceId` and a list of `components`. One component MUST have `id: "root"`. To create a new UI: diff --git a/packages/genui/test/facade/prompt_builder_test.golden/create_and_update_with_dataModel_true.txt b/packages/genui/test/facade/prompt_builder_test.golden/create_and_update_with_dataModel_true.txt index 2415cb23f..6dcd4d3de 100644 --- a/packages/genui/test/facade/prompt_builder_test.golden/create_and_update_with_dataModel_true.txt +++ b/packages/genui/test/facade/prompt_builder_test.golden/create_and_update_with_dataModel_true.txt @@ -100,9 +100,9 @@ Supported messages are: `createSurface`, `updateComponents`, `updateDataModel`. Properties: - `createSurface`: Requires `surfaceId` (you must always use a unique ID for each created surface), -`catalogId` (use the catalog ID provided in system instructions), +`catalogId` (use the catalog ID provided in system instructions), and `sendDataModel: true`. -- `updateComponents`: Requires `surfaceId` and a list of `components`. +- `updateComponents`: Requires `surfaceId` and a list of `components`. One component MUST have `id: "root"`. - `updateDataModel`: Requires `surfaceId`, `path` and `value`. diff --git a/packages/genui/test/facade/prompt_builder_test.golden/create_only_with_dataModel_false.txt b/packages/genui/test/facade/prompt_builder_test.golden/create_only_with_dataModel_false.txt index d34366070..5a746e597 100644 --- a/packages/genui/test/facade/prompt_builder_test.golden/create_only_with_dataModel_false.txt +++ b/packages/genui/test/facade/prompt_builder_test.golden/create_only_with_dataModel_false.txt @@ -99,9 +99,9 @@ Supported messages are: `createSurface`, `updateComponents`. Properties: - `createSurface`: Requires `surfaceId` (you must always use a unique ID for each created surface), -`catalogId` (use the catalog ID provided in system instructions), +`catalogId` (use the catalog ID provided in system instructions), and `sendDataModel: true`. -- `updateComponents`: Requires `surfaceId` and a list of `components`. +- `updateComponents`: Requires `surfaceId` and a list of `components`. One component MUST have `id: "root"`. To create a new UI: diff --git a/packages/genui/test/facade/prompt_builder_test.golden/create_only_with_dataModel_true.txt b/packages/genui/test/facade/prompt_builder_test.golden/create_only_with_dataModel_true.txt index 3aca4e49d..8538fc1a3 100644 --- a/packages/genui/test/facade/prompt_builder_test.golden/create_only_with_dataModel_true.txt +++ b/packages/genui/test/facade/prompt_builder_test.golden/create_only_with_dataModel_true.txt @@ -100,9 +100,9 @@ Supported messages are: `createSurface`, `updateComponents`, `updateDataModel`. Properties: - `createSurface`: Requires `surfaceId` (you must always use a unique ID for each created surface), -`catalogId` (use the catalog ID provided in system instructions), +`catalogId` (use the catalog ID provided in system instructions), and `sendDataModel: true`. -- `updateComponents`: Requires `surfaceId` and a list of `components`. +- `updateComponents`: Requires `surfaceId` and a list of `components`. One component MUST have `id: "root"`. - `updateDataModel`: Requires `surfaceId`, `path` and `value`. diff --git a/packages/genui/test/facade/prompt_builder_test.golden/update_only_with_dataModel_false.txt b/packages/genui/test/facade/prompt_builder_test.golden/update_only_with_dataModel_false.txt index 32e59a090..3b6ca3e37 100644 --- a/packages/genui/test/facade/prompt_builder_test.golden/update_only_with_dataModel_false.txt +++ b/packages/genui/test/facade/prompt_builder_test.golden/update_only_with_dataModel_false.txt @@ -97,7 +97,7 @@ Supported messages are: `updateComponents`. Properties: -- `updateComponents`: Requires `surfaceId` and a list of `components`. +- `updateComponents`: Requires `surfaceId` and a list of `components`. One component MUST have `id: "root"`. To update an existing UI: diff --git a/packages/genui/test/facade/prompt_builder_test.golden/update_only_with_dataModel_true.txt b/packages/genui/test/facade/prompt_builder_test.golden/update_only_with_dataModel_true.txt index ccc7d09cd..0ed23463a 100644 --- a/packages/genui/test/facade/prompt_builder_test.golden/update_only_with_dataModel_true.txt +++ b/packages/genui/test/facade/prompt_builder_test.golden/update_only_with_dataModel_true.txt @@ -98,7 +98,7 @@ Supported messages are: `updateComponents`, `updateDataModel`. Properties: -- `updateComponents`: Requires `surfaceId` and a list of `components`. +- `updateComponents`: Requires `surfaceId` and a list of `components`. One component MUST have `id: "root"`. - `updateDataModel`: Requires `surfaceId`, `path` and `value`.