Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a24693d
feat(genui): support functions in prompts and verify rendering
gspencergoog May 14, 2026
4134cb3
feat(genui): refactor PromptBuilder to use spec schemas and async API
gspencergoog May 14, 2026
f9f9e21
Merge branch 'main' into add_prompt_functions
gspencergoog May 15, 2026
1affaab
fix(genui): remove duplicate 'component' in required list in prompt
gspencergoog May 15, 2026
cb629b6
feat(genui): use $refs in A2uiSchemas and add custom component test
gspencergoog May 15, 2026
fb4a577
Merge branch 'main' into add_prompt_functions
gspencergoog May 15, 2026
9c47110
Fix formatting
gspencergoog May 15, 2026
ab3593a
Merge branch 'main' into add_prompt_functions
gspencergoog May 15, 2026
b7bba17
fix(genui): resolve schema references in tests and fix async hangs in…
gspencergoog May 18, 2026
341789f
Fix copyright
gspencergoog May 18, 2026
6d21e7b
refactor(genui): use platform-agnostic URIs for schema reference reso…
gspencergoog May 18, 2026
5fe5754
fix(genui): use type check instead of unsafe type cast for required p…
gspencergoog May 18, 2026
6a4ad57
refactor(genui): compose enum constraints with DynamicString schema t…
gspencergoog May 18, 2026
8aff417
feat(genui): implement secure error boundaries to prevent system stac…
gspencergoog May 18, 2026
f5c991b
fix(genui): add theme, anyComponent, and anyFunction schemas to catal…
gspencergoog May 18, 2026
ea68855
refactor(genui): extract duplicated schema asset loading into a stati…
gspencergoog May 18, 2026
c6858ab
refactor(genui): centralize A2UI schema URIs, asset keys, and local p…
gspencergoog May 18, 2026
fc669bd
Merge remote-tracking branch 'upstream/main' into add_prompt_functions
gspencergoog May 18, 2026
f27595c
refactor(genui): link schema assets to root submodule and support agn…
gspencergoog May 18, 2026
0520535
chore(genui): remove obsolete nested submodule path exclusion from an…
gspencergoog May 18, 2026
c7fd032
Merge branch 'main' into add_prompt_functions
gspencergoog Jun 3, 2026
0f94f83
feat(genui)!: change PromptBuilder to load schemas asynchronously and…
gspencergoog Jun 3, 2026
47379fe
refactor(genui): restrict public exports of primitives constants and …
gspencergoog Jun 3, 2026
7bc9578
refactor(genui): move show filter clauses to src/primitives.dart barr…
gspencergoog Jun 3, 2026
49d8733
docs(genui): document restricted primitives exports breaking change i…
gspencergoog Jun 3, 2026
ed852c5
ci: enable checkout_submodules in publish workflow
gspencergoog Jun 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
sdk: beta # version of dart sdk to use for publishing
use-flutter: true
write-comments: false
checkout_submodules: false
checkout_submodules: true
permissions:
id-token: write
pull-requests: write
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)

list(APPEND FLUTTER_FFI_PLUGIN_LIST
jni
)

set(PLUGIN_BUNDLED_LIBRARIES)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ import video_player_avfoundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
VideoPlayerPlugin.register(with: registry.registrar(forPlugin: "VideoPlayerPlugin"))
}
2 changes: 1 addition & 1 deletion dev_tools/catalog_gallery/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ dependencies:
file: ^7.0.1
flutter:
sdk: flutter
genui: ^0.9.0
genui: ^0.10.0
json_schema_builder: ^0.1.3
yaml: ^3.1.3

Expand Down
2 changes: 1 addition & 1 deletion dev_tools/composer/lib/create_tab.dart
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class _CreateTabState extends State<CreateTab> {
transport: transport,
);

final promptBuilder = PromptBuilder.chat(
final promptBuilder = await PromptBuilder.createChat(
catalog: catalog,
systemPromptFragments: [
'You are a UI generator. The user will describe a UI they want. '
Expand Down
1 change: 1 addition & 0 deletions dev_tools/composer/linux/flutter/generated_plugins.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)

list(APPEND FLUTTER_FFI_PLUGIN_LIST
jni
)

set(PLUGIN_BUNDLED_LIBRARIES)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
VideoPlayerPlugin.register(with: registry.registrar(forPlugin: "VideoPlayerPlugin"))
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
}
2 changes: 1 addition & 1 deletion dev_tools/composer/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ dependencies:
sdk: flutter
flutter_code_editor: ^0.3.5
flutter_highlight: ^0.7.0
genui: ^0.9.0
genui: ^0.10.0
highlight: ^0.7.0
logging: ^1.3.0
window_manager: ^0.5.1
Expand Down
1 change: 1 addition & 0 deletions dev_tools/composer/windows/flutter/generated_plugins.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)

list(APPEND FLUTTER_FFI_PLUGIN_LIST
jni
)

set(PLUGIN_BUNDLED_LIBRARIES)
Expand Down
34 changes: 17 additions & 17 deletions examples/simple_chat/lib/chat_session.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,20 @@ final Catalog _customCatalog = _basicCatalog.copyWith(
newItems: [climbingLocationItem],
);

PromptBuilder _promptBuilderFor(Catalog catalog) => PromptBuilder.chat(
catalog: catalog,
systemPromptFragments: [
Prompts.summary,
PromptFragments.acknowledgeUser(),
PromptFragments.requireAtLeastOneSubmitElement(
prefix: PromptBuilder.defaultImportancePrefix,
),
PromptFragments.uiGenerationRestriction(
prefix: PromptBuilder.defaultImportancePrefix,
),
],
);
Future<PromptBuilder> _promptBuilderFor(Catalog catalog) async =>
await PromptBuilder.createChat(
catalog: catalog,
systemPromptFragments: [
Prompts.summary,
PromptFragments.acknowledgeUser(),
PromptFragments.requireAtLeastOneSubmitElement(
prefix: PromptBuilder.defaultImportancePrefix,
),
PromptFragments.uiGenerationRestriction(
prefix: PromptBuilder.defaultImportancePrefix,
),
],
);

sealed class ChatSession extends ChangeNotifier {
ChatSession._();
Expand Down Expand Up @@ -188,7 +189,7 @@ class A2uiChatSession extends ChatSession {
late final StreamSubscription<ChatMessage> _submitSub;
late final StreamSubscription<SurfaceUpdate> _surfaceSub;

void _init() {
Future<void> _init() async {
_messageSub = _transport.incomingMessages.listen(
_surfaceController.handleMessage,
);
Expand All @@ -198,9 +199,8 @@ class A2uiChatSession extends ChatSession {
);
_surfaceSub = _surfaceController.surfaceUpdates.listen(_onSurfaceUpdate);

_transport.addSystemMessage(
_promptBuilderFor(_catalog).systemPromptJoined(),
);
final PromptBuilder pb = await _promptBuilderFor(_catalog);
_transport.addSystemMessage(pb.systemPromptJoined());
}

void _onSurfaceUpdate(SurfaceUpdate update) {
Expand Down
1 change: 1 addition & 0 deletions examples/simple_chat/linux/flutter/generated_plugins.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)

list(APPEND FLUTTER_FFI_PLUGIN_LIST
jni
)

set(PLUGIN_BUNDLED_LIBRARIES)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ import video_player_avfoundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
VideoPlayerPlugin.register(with: registry.registrar(forPlugin: "VideoPlayerPlugin"))
}
2 changes: 1 addition & 1 deletion examples/simple_chat/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ dependencies:
flutter:
sdk: flutter
flutter_markdown_plus: ^1.0.7
genui: ^0.9.0
genui: ^0.10.0
json_schema_builder: ^0.1.3
logging: ^1.3.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)

list(APPEND FLUTTER_FFI_PLUGIN_LIST
jni
)

set(PLUGIN_BUNDLED_LIBRARIES)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)

list(APPEND FLUTTER_FFI_PLUGIN_LIST
jni
)

set(PLUGIN_BUNDLED_LIBRARIES)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
VideoPlayerPlugin.register(with: registry.registrar(forPlugin: "VideoPlayerPlugin"))
}
2 changes: 1 addition & 1 deletion examples/verdure/client/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ dependencies:
sdk: flutter
flutter_riverpod: ^3.1.0
flutter_svg: ^2.2.2
genui: ^0.9.0
genui: ^0.10.0
genui_a2a: ^0.9.0
go_router: ^17.0.0
image_picker: ^1.2.0
Expand Down
10 changes: 10 additions & 0 deletions packages/genui/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# `genui` Changelog

## 0.10.0

- **BREAKING**: Changed `PromptBuilder.chat` and `PromptBuilder.custom` from synchronous factory constructors to asynchronous static methods (`createChat` and `createCustom`) to support asynchronous asset loading.
- **BREAKING**: Changed `_loadSchemas` return type to a named record structure.
- **BREAKING**: Restricted public API surface of low-level `primitives` exports. Only `CancellationException`, `CancellationSignal`, `JsonMap`, `basicCatalogId`, `configureLogging`, `genUiLogger`, and `generateId` are now exported from `package:genui/genui.dart`.
- **Refactor**: Extracted exception mapping logic to a private helper `_errorToMap` in `SurfaceController`.
- **Refactor**: Centralized and shared common schema registry initialization helper.
- **Refactor**: Extracted mock binary messenger asset setup to a shared helper for test reuse.
- **Fix**: Sanitized raw error messages exposed from `ArgumentError` in `Button` press handlers.

## 0.9.2

- **Feature**: Updated example/README.md.
Expand Down
1 change: 1 addition & 0 deletions packages/genui/assets/schemas/common_types.json
1 change: 1 addition & 0 deletions packages/genui/assets/schemas/server_to_client.json
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Diff is weird on this file; is this a symlink to a git submodule?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QQ, do we need to modify the test runners to clone this package with submodules recursively, or is that the default?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the asset files are symbolic links pointing to the spec schemas inside the submodules/a2ui directory to keep from duplicating the schemas in multiple places.

When publishing to pub.dev with dart pub publish, pub automatically resolves the symlinks and bundles the actual file contents, so users of the package will get the actual files and don't need to check out the submodules.

For CI testing, the test runners in flutter_packages.yaml are already configured to checkout and update submodules recursively (submodules: recursive), so they are there during testing and validation.

38 changes: 36 additions & 2 deletions packages/genui/lib/src/catalog/basic_catalog_widgets/button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:json_schema_builder/json_schema_builder.dart';

import '../../model/a2ui_exceptions.dart';
import '../../model/a2ui_schemas.dart';
import '../../model/catalog_item.dart';
import '../../model/data_model.dart';
Expand Down Expand Up @@ -231,9 +234,40 @@ Future<void> _handlePress(
funcMap,
);
try {
await resultStream.first;
await resultStream.first.timeout(
const Duration(seconds: 10),
onTimeout: () => throw TimeoutException(
'Function execution for $callName timed out',
),
);
} catch (exception, stackTrace) {
itemContext.reportError(exception, stackTrace);
genUiLogger.severe(
'Error executing function call "$callName" on button press',
exception,
stackTrace,
);

if (exception is A2uiFunctionException) {
Comment thread
gspencergoog marked this conversation as resolved.
itemContext.reportError(exception, stackTrace);
} else if (exception is TimeoutException) {
itemContext.reportError(
A2uiFunctionException(
'Function execution timed out.',
functionName: callName,
cause: exception,
),
stackTrace,
);
} else {
itemContext.reportError(
A2uiFunctionException(
'Function execution failed. Please check arguments and try again.',
functionName: callName,
cause: exception,
),
stackTrace,
);
}
}
} else {
genUiLogger.warning(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -340,9 +340,7 @@ final dateTimeInput = CatalogItem(
{
"id": "root",
"component": "DateTimeInput",
"value": {
"path": "/myDateTime"
}
"value": "2026-05-15"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did we need to change this example?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because DateTimeInput also allows raw string inputs, and I wanted to add an example that covered that. We already had examples that covered data model values.

}
]
''',
Expand All @@ -354,7 +352,7 @@ final dateTimeInput = CatalogItem(
"value": {
"path": "/myDate"
},
"enableTime": false
"variant": "date"
}
]
''',
Expand All @@ -366,7 +364,7 @@ final dateTimeInput = CatalogItem(
"value": {
"path": "/myTime"
},
"enableDate": false
"variant": "time"
}
]
''',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ final CatalogItem image = CatalogItem(
{
"id": "root",
"component": "Image",
"url": "https://storage.googleapis.com/cms-storage-bucket/lockup_flutter_horizontal.c823e53b3a1a7b0d36a9.png",
"url": {
"path": "/imageUrl"
},
"variant": "mediumFeature"
}
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,7 @@ final text = CatalogItem(
{
"id": "root",
"component": "Text",
"text": "Hello World",
"variant": "h1"
"text": "Hello World"
}
]
''',
Expand Down
45 changes: 29 additions & 16 deletions packages/genui/lib/src/engine/surface_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import '../interfaces/a2ui_message_sink.dart';
import '../interfaces/surface_context.dart';
import '../interfaces/surface_host.dart';
import '../model/a2ui_client_capabilities.dart';
import '../model/a2ui_exceptions.dart';
import '../model/a2ui_message.dart';
import '../model/catalog.dart';
import '../model/chat_message.dart';
Expand Down Expand Up @@ -120,33 +121,45 @@ interface class SurfaceController implements SurfaceHost, A2uiMessageSink {

/// Reports an error to the AI service.
void reportError(Object error, StackTrace? stack) {
var errorCode = 'RUNTIME_ERROR';
var message = error.toString();
final Map<String, Object> errorMsg = {
'version': 'v0.9',
'error': _errorToMap(error),
};
if (!_onSubmit.isClosed) {
_onSubmit.add(
ChatMessage.user(
'',
parts: [UiInteractionPart.create(jsonEncode(errorMsg))],
),
);
}
}

Map<String, Object> _errorToMap(Object error) {
var errorCode = 'INTERNAL_ERROR';
var message = 'An unexpected system error occurred.';
String? surfaceId;
String? path;
String? functionName;

if (error is A2uiValidationException) {
errorCode = 'VALIDATION_FAILED';
message = error.message;
surfaceId = error.surfaceId;
path = error.path;
} else if (error is A2uiFunctionException) {
errorCode = 'FUNCTION_EXECUTION_FAILED';
message = error.message;
functionName = error.functionName;
}

final Map<String, Object> errorMsg = {
'version': 'v0.9',
'error': {
'code': errorCode,
'surfaceId': ?surfaceId,
'path': ?path,
'message': message,
},
return {
'code': errorCode,
'surfaceId': ?surfaceId,
'path': ?path,
'functionName': ?functionName,
'message': message,
};
Comment thread
gspencergoog marked this conversation as resolved.
_onSubmit.add(
ChatMessage.user(
'',
parts: [UiInteractionPart.create(jsonEncode(errorMsg))],
),
);
}

void _handleMessageInternal(A2uiMessage message) {
Expand Down
Loading
Loading