Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
The widget communication layer previously used a generic
UIElementMessageNotification with an untyped message dict. This made it
difficult to understand what data was being sent and didn't leverage
msgspec's tagged union support.
This introduces `ModelLifecycleNotification` with typed inner messages
(`ModelOpen`, `ModelUpdate`, `ModelCustom`, `ModelClose`) that mirror
the Jupyter widget comm protocol.
On the command side, `ModelCommand` replaces `UpdateWidgetModelCommand`
with a similar tagged union for `ModelUpdateMessage` and
`ModelCustomMessage`.
The new notification format:
```json
{
"op": "model-lifecycle",
"model_id": "abc123",
"message": {
"method": "update",
"state": {"count": 5},
"buffer_paths": [],
"buffers": []
}
}
```
Widget messages now use a dedicated `model-lifecycle` notification type instead of piggy-backing on `send-ui-element-message` with runtime type checks. The `handleWidgetMessage` function accepts the raw notification and decodes buffers internally, simplifying call sites. The Model class now uses a `MarimoComm` interface, and AnyWidgetPlugin receives a `model_id` reference instead of full state props. Command type renamed from `"update-widget-model"` to `"model"`.
|
Breaking changes detected in the OpenAPI specification! |
for more information, see https://pre-commit.ci
|
Breaking changes detected in the OpenAPI specification! |
|
Breaking changes detected in the OpenAPI specification! |
|
Breaking changes detected in the OpenAPI specification! |
|
Breaking changes detected in the OpenAPI specification! |
|
Breaking changes detected in the OpenAPI specification! |
There was a problem hiding this comment.
Pull request overview
This PR decouples the AnyWidget model and view architecture by sending models via ModelLifecycleNotification before rendering views, which then retrieve models by ID. The changes include performance optimizations (manual loops for base64 conversion, removing structuredClone), type safety improvements with zod schemas, and a cleaner separation between model state management and view rendering.
Changes:
- Renamed
UpdateWidgetModelCommandtoModelCommandwith support for both update and custom messages - Introduced
ModelLifecycleNotificationmirroring Jupyter's comm protocol (open/update/custom/close) - Refactored frontend Model class with internal API separation and AFM-compliant public interface
- Performance improvements in buffer serialization using manual loops instead of
Array.from - Model state now sent before cell notifications to ensure availability for views
Reviewed changes
Copilot reviewed 44 out of 44 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
marimo/_runtime/commands.py |
Renamed command structure and added ModelCustomMessage support |
marimo/_runtime/runtime.py |
Updated handler to trigger cell re-runs on model updates |
marimo/_plugins/ui/_impl/comm.py |
Refactored to broadcast ModelLifecycleNotifications |
marimo/_plugins/ui/_impl/from_anywidget.py |
Changed initial value to model_id reference instead of full state |
marimo/_session/state/session_view.py |
Ensured model messages sent before cell notifications |
frontend/src/plugins/impl/anywidget/model.ts |
Major refactor with internal API isolation and lifecycle management |
frontend/src/plugins/impl/anywidget/serialization.ts |
Removed structuredClone for performance, now mutates input |
frontend/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx |
Simplified to use model_id lookups |
frontend/src/utils/json/base64.ts |
Added native API support with fallbacks for base64 conversion |
tests/_plugins/ui/_impl/test_anywidget.py |
Updated tests but introduced breaking issues |
Comments suppressed due to low confidence (2)
tests/_plugins/ui/_impl/anywidget/test_anywidget_utils.py:30
- All tests for
insert_buffer_pathsandis_model_messagefunctions have been removed. These utility functions were also removed from the codebase. While this may be intentional as part of the refactoring, it represents a loss of test coverage for buffer handling logic. Ensure that equivalent functionality is tested elsewhere, particularly the buffer insertion/extraction logic which is critical for binary data handling.
frontend/src/plugins/impl/anywidget/serialization.ts:101 - This removes the
structuredClonecall which meansdecodeFromWirenow mutates the inputstateobject directly. This is a significant behavioral change that could cause subtle bugs if callers expect the input to remain unchanged. The comment mentions this is for performance, but it breaks immutability assumptions. Consider documenting this clearly in the function's JSDoc, or use a shallow copy for the top-level object to avoid surprising mutations of caller's data.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| wrapped._update({"a": 10}) | ||
| assert wrapped.value == {"a": 10, "b": 2, "c": 3} | ||
|
|
||
| # Test multiple partial updates | ||
| wrapped._update({"b": 20}) | ||
| assert wrapped.value == {"a": 10, "b": 20, "c": 3} | ||
|
|
||
| # Test updating all traits | ||
| wrapped._update({"a": 100, "b": 200, "c": 300}) | ||
| assert wrapped.value == {"a": 100, "b": 200, "c": 300} |
There was a problem hiding this comment.
The _update method has been removed from the anywidget wrapper class. Tests in test_partial_state_updates call wrapped._update(...) which will now fail with AttributeError since this method no longer exists. Either the method needs to be restored or the tests need to be updated to use a different approach (e.g., direct widget attribute access).
The comm layer was manually building nested dicts to match the ipywidgets handle_msg payload format, duplicating knowledge of each message type's shape across both the command structs and the comm manager. This also required a set of TypedDicts that were difficult to keep in sync and were causing type errors (discriminated union fields that didn't match actual usage). Instead, each message struct now owns its serialization via `into_comm_payload_content()`, and `ModelCommand.into_comm_payload()` wraps the envelope. This lets `receive_comm_message` accept the whole `ModelCommand` directly, removing the manual dict construction and all the TypedDict definitions that supported it.
|
Breaking changes detected in the OpenAPI specification! |
|
Breaking changes detected in the OpenAPI specification! |
|
Breaking changes detected in the OpenAPI specification! |
|
Breaking changes detected in the OpenAPI specification! |
marimo 0.19.8 refactors anywidget model-view communication (marimo-team/marimo#8124). The key changes include: - New `model-lifecycle` WebSocket notification replaces piggybacking on `send-ui-element-message` for widget state - Renamed types: `UpdateWidgetModelRequest` -> `ModelRequest`, `UpdateWidgetModelCommand` -> `ModelCommand` - Command discriminator changed from `"update-widget-model"` to `"model"` - Widget messages now use a tagged union protocol (`open`, `update`, `custom`, `close`) via msgspec These type changes require the LSP to stay in sync with the marimo frontend/backend messaging protocol.
marimo 0.19.8 (marimo-team/marimo#8124) decouples anywidget model communication from the `send-ui-element-message` notification. Previously widget state was piggybacked on UI element messages and filtered with `isMessageWidgetState`. Now widget messages flow through a dedicated `model-lifecycle` notification with a tagged union protocol (open, update, custom, close). This updates the LSP extension to match the new protocol. The renderer now handles `model-lifecycle` as a separate notification, forwarding it to `handleWidgetMessage(MODEL_MANAGER, msg)` which accepts the new two-argument signature. The old `isMessageWidgetState` guard and the widget branch inside `handleSendUiElementMessage` are removed since that notification no longer carries model state. The new protocol also means the frontend's request client needs a `sendModelValue` method so widgets can send state updates and custom messages back to the kernel. This is wired through a new `set-model-value` renderer command that routes to the LSP's `marimo.api`, which forwards it as a `ModelCommand` to the marimo session.
marimo 0.19.8 (marimo-team/marimo#8124) decouples anywidget model communication from the `send-ui-element-message` notification. Previously widget state was piggybacked on UI element messages and filtered with `isMessageWidgetState`. Now widget messages flow through a dedicated `model-lifecycle` notification with a tagged union protocol (open, update, custom, close). This updates the LSP extension to match the new protocol. The renderer now handles `model-lifecycle` as a separate notification, forwarding it to `handleWidgetMessage(MODEL_MANAGER, msg)` which accepts the new two-argument signature. The old `isMessageWidgetState` guard and the widget branch inside `handleSendUiElementMessage` are removed since that notification no longer carries model state. The new protocol also means the frontend's request client needs a `sendModelValue` method so widgets can send state updates and custom messages back to the kernel. This is wired through a new `set-model-value` renderer command that routes to the LSP's `marimo.api`, which forwards it as a `ModelCommand` to the marimo session. Test fixtures are regenerated to match marimo 0.19.8's updated codegen which adds blank lines before `return` in cells with import statements.
The model/view decoupling in #8124 moved widget state updates from the UI element value path to a dedicated model message path. The old path ran inside an execution context, but the new one didn't. When a model update from the frontend triggers Python-side callbacks (e.g. observe handlers), those callbacks may call mo.state setters which require an active execution context. Without one, the state update silently fails and downstream cells never re-run. Wraps `receive_comm_message` in an execution context derived from the model's owning cell, so that any callbacks fired during message processing have the same context they had before the refactor.
The model/view decoupling in #8124 moved widget state updates from the UI element value path to a dedicated model message path. The old path ran inside an execution context, but the new one didn't. When a model update from the frontend triggers Python-side callbacks (e.g. observe handlers), those callbacks may call mo.state setters which require an active execution context. Without one, the state update silently fails and downstream cells never re-run. Wraps `receive_comm_message` in an execution context derived from the model's owning cell, so that any callbacks fired during message processing have the same context they had before the refactor.
Code code-written with @manzt
These changes decouple the AnyWidget model and view. Previously, models and views were created together. The plugin received full widget state as its value, created the model at render time, and widget messages were piggybacked on the
send-ui-element-messagenotification with runtime type-checks to distinguish them from normal UI element messages.This meant models couldn't exist before their views rendered, and
initialize()was incorrectly called on every mount rather than once per model lifetime (violating the AFM spec).Now, models are sent before rendering the anywidget view via a dedicated
model-lifecycleWebSocket notification (open/update/custom/close). The view receives just amodel_idand grabs the pre-existing model from a global model manager. This properly separates the model lifecycle from the view lifecycle, matching the AFM spec whereinitialize()runs once per model andrender()runs once per view.These changes also include cleanups and removal of old code paths:
into_comm_payload_content()), removing manual dict construction in the comm managerencode_to_wire/decode_from_wire,_prev_statetracking,send_to_widgetRPC, wire format handling in the plugin, andisMessageWidgetStatefilteringUint8Array.fromBase64/.toBase64when available (~7-25x faster), removedstructuredClonein wire format decoding, eliminated double encode/decode round-trip through the UIElement value systemCloses #7686