Skip to content

Commit 82f2ed4

Browse files
authored
Add a GULF client prototype (#317)
1 parent bb4468f commit 82f2ed4

File tree

138 files changed

+6517
-6
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

138 files changed

+6517
-6
lines changed
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
# **`gulf_client` Design Document**
2+
3+
## **1. Overview**
4+
5+
This document outlines the detailed design for a new Flutter package, `gulf_client`. The purpose of this package is to provide a client-side implementation for the "GULF Streaming UI Protocol". It will be responsible for parsing a JSONL stream of UI commands, managing state, and rendering a dynamic Flutter UI.
6+
7+
The design is heavily inspired by the existing `gulf_client` package but is architected from the ground up to specifically support the semantics and schema of the GULF protocol. The core goal is to create a robust, maintainable, and performant client that enables progressive UI rendering directly from an AI-generated stream.
8+
9+
## **2. Detailed Analysis of the Goal**
10+
11+
The primary goal is to build a Flutter package that can interpret and render a UI defined a simplified GULF protocol. This protocol differs significantly from the GenUI Streaming Protocol (GULF) implemented by the `gulf_client`.
12+
13+
### **Key Requirements & Protocol Features**
14+
15+
- **JSONL Stream Processing:** The client must be able to consume a stream of JSONL objects, parsing each line as a distinct message.
16+
- **Progressive Rendering:** The UI should be rendered incrementally as component and data model definitions arrive. The client should not wait for the entire stream to finish before displaying the UI.
17+
- **LLM-Friendly "Property Bag" Schema:** The client's data models must conform to the GULF protocol's schema, which uses a single "property bag" structure for all components and a discriminator field (`type` or `messageType`) to differentiate them.
18+
- **Decoupled UI and Data:** The protocol separates the UI structure (`components`) from the application data (`dataModelNodes`). The client must manage these two models independently and link them via data bindings.
19+
- **Flattened Adjacency List Model:** Both the UI tree and the data model tree are represented as flattened maps of nodes, where relationships are defined by ID references. The client must be able to reconstruct these hierarchical relationships.
20+
- **Data Binding:** The client must resolve data bindings specified in component properties (e.g., `value: { "path": "/user/name" }`) by looking up the corresponding data in the data model tree.
21+
22+
### **Comparison with `gulf_client`**
23+
24+
Understanding the differences with the existing `gulf_client` is crucial for this refactor:
25+
26+
| Feature | `gulf_client` (GULF) | `gulf_client` (GULF Protocol) | Rationale for New Implementation |
27+
| :------------------- | :------------------------------------------------- | :------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------- |
28+
| **UI Definition** | Client-defined `WidgetCatalog` sent to server. | Server-streamed `Component` definitions. | The fundamental contract is inverted. GULF has no concept of a client-side catalog, making the `gulf_client`'s core logic incompatible. |
29+
| **Component Schema** | Each widget has a unique, strictly defined schema. | A single "property bag" schema for all components. | Requires a completely different approach to deserialization and property resolution. |
30+
| **State/Data Model** | A single, simple `initialState` JSON object. | A flattened, tree-like `dataModelNodes` structure. | The data model is more complex and requires a dedicated engine to traverse and resolve paths. |
31+
| **Message Types** | `Layout`, `LayoutRoot`, `StateUpdate`. | `ComponentUpdate`, `DataModelUpdate`, `UIRoot`. | The stream's message semantics are different, requiring new parsing and handling logic. |
32+
33+
Given these fundamental differences, a new, purpose-built implementation is necessary to ensure a clean, maintainable, and accurate client for the GULF protocol.
34+
35+
## **3. Alternatives Considered**
36+
37+
### **Alternative 1: Adapt the existing `gulf_client`**
38+
39+
- **Description:** Modify the `gulf_client` to support both GULF and the GULF protocol, likely through extensive conditional logic.
40+
- **Pros:** Potential for some code reuse in the widget building and rendering layers.
41+
- **Cons:** The core architectural differences (especially the catalog-based approach vs. the streamed-component approach) are too significant. The codebase would become convoluted with `if (protocol == 'SGULF')` checks, making it difficult to maintain and debug. The data model and message parsing logic are entirely different and would require separate pathways anyway.
42+
- **Decision:** Rejected. The cost of maintaining a complex, multi-protocol client outweighs the benefits of minimal code reuse. A clean slate is preferable.
43+
44+
### **Alternative 2: Use a generic JSON-to-Widget library**
45+
46+
- **Description:** Find a third-party library that can render Flutter widgets from a generic JSON schema and build an adaptation layer on top of it.
47+
- **Pros:** Could potentially save time on the widget-building logic itself.
48+
- **Cons:** No existing library is designed to handle the specific JSONL streaming, progressive rendering, and flattened data model semantics of the GULF protocol. The adaptation layer would need to manage the stream, buffer nodes, reconstruct the tree, and resolve data bindings, effectively becoming a custom interpreter anyway. This adds an unnecessary dependency and couples our implementation to the limitations of the generic library.
49+
- **Decision:** Rejected. The GULF protocol is specialized enough to warrant a bespoke client implementation for a clean and efficient result.
50+
51+
## **4. Detailed Design**
52+
53+
The `gulf_client` will be architected around a few core components that work together to process the stream and render the UI.
54+
55+
### **4.1. Project Structure**
56+
57+
The package will be organized as follows:
58+
59+
```txt
60+
packages/spikes/gulf_client/
61+
├── lib/
62+
│ ├── gulf_client.dart # Main library file, exports public APIs
63+
│ ├── src/
64+
│ │ ├── core/
65+
│ │ │ ├── interpreter.dart # GulfInterpreter class
66+
│ │ │ └── widget_registry.dart # WidgetRegistry class
67+
│ │ ├── models/
68+
│ │ │ ├── component.dart # Component data model
69+
│ │ │ ├── data_node.dart # DataModelNode data model
70+
│ │ │ └── stream_message.dart # GulfStreamMessage and related classes
71+
│ │ └── widgets/
72+
│ │ ├── gulf_provider.dart # InheritedWidget for event handling
73+
│ │ └── gulf_view.dart # Main rendering widget
74+
│ └── pubspec.yaml
75+
└── example/
76+
└── ... (A simple example app similar to gulf_client's)
77+
```
78+
79+
### **4.2. Core Components & Data Flow**
80+
81+
The data will flow from the stream through the `GulfInterpreter`, which will then be consumed by the `GulfView` to build the widget tree.
82+
83+
```mermaid
84+
sequenceDiagram
85+
participant StreamSource
86+
participant GulfInterpreter
87+
participant GulfView
88+
participant WidgetRegistry
89+
participant FlutterEngine
90+
91+
StreamSource->>+GulfInterpreter: JSONL Stream (line by line)
92+
GulfInterpreter->>GulfInterpreter: Parse JSON into StreamMessage
93+
GulfInterpreter->>GulfInterpreter: Handle message (e.g., ComponentUpdate)
94+
GulfInterpreter->>GulfInterpreter: Update internal component/data buffers
95+
GulfInterpreter-->>-GulfView: notifyListeners()
96+
GulfView->>+GulfInterpreter: Get rootComponentId
97+
GulfView->>GulfView: Start building widget tree from root
98+
loop For each component in tree
99+
GulfView->>+GulfInterpreter: Get Component object by ID
100+
GulfView->>GulfView: Resolve data bindings against Data Model
101+
GulfView->>+WidgetRegistry: Get builder for component.type
102+
WidgetRegistry-->>-GulfView: Return WidgetBuilder function
103+
GulfView->>GulfView: Call builder with resolved properties
104+
end
105+
GulfView-->>-FlutterEngine: Return final Widget tree for rendering
106+
```
107+
108+
### **4.3. Class Definitions**
109+
110+
#### **`GulfInterpreter` (`interpreter.dart`)**
111+
112+
This will be the heart of the client. It consumes the raw JSONL stream and makes sense of it.
113+
114+
- **Class:** `class GulfInterpreter with ChangeNotifier`
115+
- **Inputs:** `Stream<String> stream`
116+
- **State:**
117+
- `Map<String, Component> components = {}`
118+
- `Map<String, DataModelNode> dataModelNodes = {}`
119+
- `String? rootComponentId`
120+
- `String? dataModelRootId`
121+
- `bool isReadyToRender = false`
122+
- **Logic:**
123+
- The constructor will listen to the input stream.
124+
- A `processMessage(String jsonl)` method will parse the JSON and deserialize it into an `GulfStreamMessage` object.
125+
- It will use a `switch` statement on `message.messageType` to delegate to private handler methods:
126+
- `_handleComponentUpdate(message)`: Iterates through `message.components` and adds them to the `_components` map.
127+
- `_handleDataModelUpdate(message)`: Iterates through `message.nodes` and adds them to the `_dataModelNodes` map.
128+
- `_handleUIRoot(message)`: Sets `_rootComponentId` and `_dataModelRootId`. Sets `isReadyToRender = true`.
129+
- After any state change, it will call `notifyListeners()`.
130+
- **Public API:**
131+
- `Component? getComponent(String id)`
132+
- `DataModelNode? getDataNode(String id)`
133+
- `Object? resolveDataBinding(String path)`: A crucial method that traverses the data model tree starting from `dataModelRootId` to find the value at the given path.
134+
135+
#### **Data Models (`models/*.dart`)**
136+
137+
These will be simple, immutable data classes created using the `freezed` package to represent the JSON structures from the protocol. This provides value equality, `copyWith`, and exhaustive `when` methods for free.
138+
139+
- **`GulfStreamMessage`**: A freezed union type to represent the different message types.
140+
141+
```dart
142+
@freezed
143+
class GulfStreamMessage with _$GulfStreamMessage {
144+
const factory GulfStreamMessage.streamHeader({required String version}) = _StreamHeader;
145+
const factory GulfStreamMessage.componentUpdate({required List<Component> components}) = _ComponentUpdate;
146+
// ... etc.
147+
}
148+
```
149+
150+
- **`Component`**: A freezed class representing the component "property bag". All properties from the schema will be fields here, most of them nullable.
151+
- **`DataModelNode`**: A freezed class representing a node in the data model tree.
152+
153+
#### **`WidgetRegistry` (`widget_registry.dart`)**
154+
155+
This class maps a component `type` string to a function that builds a Flutter `Widget`.
156+
157+
- **Class:** `class WidgetRegistry`
158+
- **State:** `Map<String, CatalogWidgetBuilder> _builders = {}`
159+
- **Logic:**
160+
- `register(String type, CatalogWidgetBuilder builder)`: Adds a builder to the map.
161+
- `getBuilder(String type)`: Retrieves a builder.
162+
- **Note:** Unlike `gulf_client`, this registry does _not_ build a `WidgetCatalog` object, as that concept doesn't exist in the GULF protocol. It is purely a client-side mapping.
163+
164+
#### **`GulfView` (`gulf_view.dart`)**
165+
166+
This is the main `StatefulWidget` that developers will use. It orchestrates the rendering process.
167+
168+
- **Class:** `class GulfView extends StatefulWidget`
169+
- **Inputs:**
170+
- `GulfInterpreter interpreter`
171+
- `WidgetRegistry registry`
172+
- `ValueChanged<Event>? onEvent`
173+
- **Logic:**
174+
- Its `State` object will listen to the `interpreter`. When the interpreter notifies, `setState` is called to trigger a rebuild.
175+
- The `build` method will check `interpreter.isReadyToRender`. If false, it shows a `CircularProgressIndicator`.
176+
- If ready, it will start the recursive build process, beginning with `_buildNode(context, interpreter.rootComponentId)`.
177+
- It will wrap the entire tree in an `GulfProvider` to make the `onEvent` callback available to descendant widgets (like buttons).
178+
179+
#### **Layout and Data Binding Engine (Private methods in `_GulfViewState`)**
180+
181+
- **`_buildNode(BuildContext context, String componentId)`**:
182+
1. Gets the `Component` object from the interpreter.
183+
2. Gets the `WidgetBuilder` from the registry.
184+
3. Resolves all properties for the component. This involves checking for data bindings in properties like `value`.
185+
4. Recursively builds all child widgets specified by ID in properties like `child` or `children.explicitList`.
186+
5. Calls the retrieved `WidgetBuilder` with the resolved properties and built children.
187+
- **`_resolveProperties(Component component)`**:
188+
1. Creates a mutable copy of the component's properties.
189+
2. For each property, checks if it's a data binding (e.g., `value.path` is not null).
190+
3. If it is, it calls `interpreter.resolveDataBinding(path)` to get the real value.
191+
4. It replaces the binding object with the resolved value in the property map.
192+
5. Returns the fully resolved map of properties.
193+
194+
## **5. Summary of Design**
195+
196+
The proposed design establishes a clean, reactive architecture for a Flutter client that implements the GULF Streaming UI Protocol.
197+
198+
- **`GulfInterpreter`** acts as the "brain", processing the stream and managing the canonical UI and data state.
199+
- **`GulfView`** acts as the "renderer", listening to the interpreter and translating its state into a Flutter widget tree.
200+
- **`WidgetRegistry`** provides the necessary mapping from abstract component types to concrete Flutter widgets.
201+
- **Immutable Data Models** (using `freezed`) ensure predictable state management and reduce bugs.
202+
203+
This approach directly addresses the requirements of the GULF protocol, including its streaming nature, LLM-friendly schema, and decoupled data model, while following Dart and Flutter best practices.
204+
205+
## **6. References**
206+
207+
- [GenUI Streaming Protocol](./packages/spikes/gulf_client/docs/GenUI_Streaming_Protocol.md)
208+
- [freezed package](https://pub.dev/packages/freezed)
209+
- [State management in Flutter](https://docs.flutter.dev/data-and-backend/state-mgmt/simple)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# gulf_client
2+
3+
A Flutter package that acts as a client for the GULF Streaming UI Protocol.
4+
5+
## Usage
6+
7+
This package is intended to be used with a server that provides a JSONL stream conforming to the GULF protocol. The main entry point is the `GulfView` widget, which takes an `GulfInterpreter` and a `WidgetRegistry`.
8+
9+
The `example` directory contains a simple application that demonstrates how to use the package.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Copyright 2025 The Flutter Authors.
2+
# Use of this source code is governed by a BSD-style license that can be
3+
# found in the LICENSE file.
4+
5+
include: package:dart_flutter_team_lints/analysis_options.yaml
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Miscellaneous
2+
*.class
3+
*.log
4+
*.pyc
5+
*.swp
6+
.DS_Store
7+
.atom/
8+
.build/
9+
.buildlog/
10+
.history
11+
.svn/
12+
.swiftpm/
13+
migrate_working_dir/
14+
15+
# IntelliJ related
16+
*.iml
17+
*.ipr
18+
*.iws
19+
.idea/
20+
21+
# The .vscode folder contains launch configuration and tasks you configure in
22+
# VS Code which you may wish to be included in version control, so this line
23+
# is commented out by default.
24+
#.vscode/
25+
26+
# Flutter/Dart/Pub related
27+
**/doc/api/
28+
**/ios/Flutter/.last_build_id
29+
.dart_tool/
30+
.flutter-plugins-dependencies
31+
.pub-cache/
32+
.pub/
33+
/build/
34+
/coverage/
35+
36+
# Symbolication related
37+
app.*.symbols
38+
39+
# Obfuscation related
40+
app.*.map.json
41+
42+
# Android Studio will place build artifacts here
43+
/android/app/debug
44+
/android/app/profile
45+
/android/app/release
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# This file tracks properties of this Flutter project.
2+
# Used by Flutter tool to assess capabilities and perform upgrades etc.
3+
#
4+
# This file should be version controlled and should not be manually edited.
5+
6+
version:
7+
revision: "d2ac0210ee05a56415bf309a41722d0a10eacfdb"
8+
channel: "main"
9+
10+
project_type: app
11+
12+
# Tracks metadata for the flutter migrate command
13+
migration:
14+
platforms:
15+
- platform: root
16+
create_revision: d2ac0210ee05a56415bf309a41722d0a10eacfdb
17+
base_revision: d2ac0210ee05a56415bf309a41722d0a10eacfdb
18+
- platform: android
19+
create_revision: d2ac0210ee05a56415bf309a41722d0a10eacfdb
20+
base_revision: d2ac0210ee05a56415bf309a41722d0a10eacfdb
21+
- platform: ios
22+
create_revision: d2ac0210ee05a56415bf309a41722d0a10eacfdb
23+
base_revision: d2ac0210ee05a56415bf309a41722d0a10eacfdb
24+
- platform: linux
25+
create_revision: d2ac0210ee05a56415bf309a41722d0a10eacfdb
26+
base_revision: d2ac0210ee05a56415bf309a41722d0a10eacfdb
27+
- platform: macos
28+
create_revision: d2ac0210ee05a56415bf309a41722d0a10eacfdb
29+
base_revision: d2ac0210ee05a56415bf309a41722d0a10eacfdb
30+
- platform: web
31+
create_revision: d2ac0210ee05a56415bf309a41722d0a10eacfdb
32+
base_revision: d2ac0210ee05a56415bf309a41722d0a10eacfdb
33+
- platform: windows
34+
create_revision: d2ac0210ee05a56415bf309a41722d0a10eacfdb
35+
base_revision: d2ac0210ee05a56415bf309a41722d0a10eacfdb
36+
37+
# User provided section
38+
39+
# List of Local paths (relative to this file) that should be
40+
# ignored by the migrate tool.
41+
#
42+
# Files that are not part of the templates will be ignored by default.
43+
unmanaged_files:
44+
- 'lib/main.dart'
45+
- 'ios/Runner.xcodeproj/project.pbxproj'
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# example
2+
3+
A new Flutter project.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Copyright 2025 The Flutter Authors.
2+
# Use of this source code is governed by a BSD-style license that can be
3+
# found in the LICENSE file.
4+
5+
include: package:dart_flutter_team_lints/analysis_options.yaml
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
gradle-wrapper.jar
2+
/.gradle
3+
/captures/
4+
/gradlew
5+
/gradlew.bat
6+
/local.properties
7+
GeneratedPluginRegistrant.java
8+
.cxx/
9+
10+
# Remember to never publicly share your keystore.
11+
# See https://flutter.dev/to/reference-keystore
12+
key.properties
13+
**/*.keystore
14+
**/*.jks
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
plugins {
2+
id("com.android.application")
3+
id("kotlin-android")
4+
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
5+
id("dev.flutter.flutter-gradle-plugin")
6+
}
7+
8+
android {
9+
namespace = "dev.flutter.example"
10+
compileSdk = flutter.compileSdkVersion
11+
ndkVersion = flutter.ndkVersion
12+
13+
compileOptions {
14+
sourceCompatibility = JavaVersion.VERSION_11
15+
targetCompatibility = JavaVersion.VERSION_11
16+
}
17+
18+
kotlinOptions {
19+
jvmTarget = JavaVersion.VERSION_11.toString()
20+
}
21+
22+
defaultConfig {
23+
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
24+
applicationId = "dev.flutter.example"
25+
// You can update the following values to match your application needs.
26+
// For more information, see: https://flutter.dev/to/review-gradle-config.
27+
minSdk = flutter.minSdkVersion
28+
targetSdk = flutter.targetSdkVersion
29+
versionCode = flutter.versionCode
30+
versionName = flutter.versionName
31+
}
32+
33+
buildTypes {
34+
release {
35+
// TODO: Add your own signing config for the release build.
36+
// Signing with the debug keys for now, so `flutter run --release` works.
37+
signingConfig = signingConfigs.getByName("debug")
38+
}
39+
}
40+
}
41+
42+
flutter {
43+
source = "../.."
44+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<!-- Copyright 2025 The Flutter Authors.
2+
Use of this source code is governed by a BSD-style license that can be
3+
found in the LICENSE file. -->
4+
5+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
6+
<!-- The INTERNET permission is required for development. Specifically,
7+
the Flutter tool needs it to communicate with the running application
8+
to allow setting breakpoints, to provide hot reload, etc.
9+
-->
10+
<uses-permission android:name="android.permission.INTERNET"/>
11+
</manifest>

0 commit comments

Comments
 (0)