|
| 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) |
0 commit comments