Skip to content

fix: decouple anywidget model and view#8124

Merged
mscolnick merged 20 commits intomainfrom
ms/widget-model
Feb 5, 2026
Merged

fix: decouple anywidget model and view#8124
mscolnick merged 20 commits intomainfrom
ms/widget-model

Conversation

@mscolnick
Copy link
Contributor

@mscolnick mscolnick commented Feb 4, 2026

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-message notification 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-lifecycle WebSocket notification (open/update/custom/close). The view receives just a model_id and grabs the pre-existing model from a global model manager. This properly separates the model lifecycle from the view lifecycle, matching the AFM spec where initialize() runs once per model and render() runs once per view.

These changes also include cleanups and removal of old code paths:

  • Typed model lifecycle protocol using msgspec tagged unions on the backend and zod schemas on the frontend, replacing untyped message dicts and TypedDicts that were hard to keep in sync
  • Each message type owns its serialization (into_comm_payload_content()), removing manual dict construction in the comm manager
  • Removed encode_to_wire/decode_from_wire, _prev_state tracking, send_to_widget RPC, wire format handling in the plugin, and isMessageWidgetState filtering
  • Few perf improvements: native Uint8Array.fromBase64/.toBase64 when available (~7-25x faster), removed structuredClone in wire format decoding, eliminated double encode/decode round-trip through the UIElement value system

Closes #7686

@vercel
Copy link

vercel bot commented Feb 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
marimo-docs Ready Ready Preview, Comment Feb 5, 2026 8:25pm

Request Review

@github-actions github-actions bot added the bash-focus Area to focus on during release bug bash label Feb 4, 2026
manzt added 2 commits February 4, 2026 18:34
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"`.
@github-actions
Copy link

github-actions bot commented Feb 4, 2026

Breaking changes detected in the OpenAPI specification!

==========================================================================
==                            API CHANGE LOG                            ==
==========================================================================
                                marimo API                                
--------------------------------------------------------------------------
--                            What's Changed                            --
--------------------------------------------------------------------------
- POST   /api/kernel/set_model_value
  Request:
        - Changed application/json
          Schema: Broken compatibility
          Missing property: message.bufferPaths (object)
          Missing property: message.state (object)
--------------------------------------------------------------------------
--                                Result                                --
--------------------------------------------------------------------------
                 API changes broke backward compatibility                 
--------------------------------------------------------------------------

@github-actions
Copy link

github-actions bot commented Feb 4, 2026

Breaking changes detected in the OpenAPI specification!

==========================================================================
==                            API CHANGE LOG                            ==
==========================================================================
                                marimo API                                
--------------------------------------------------------------------------
--                            What's Changed                            --
--------------------------------------------------------------------------
- POST   /api/kernel/set_model_value
  Request:
        - Changed application/json
          Schema: Broken compatibility
          Missing property: message.bufferPaths (object)
          Missing property: message.state (object)
--------------------------------------------------------------------------
--                                Result                                --
--------------------------------------------------------------------------
                 API changes broke backward compatibility                 
--------------------------------------------------------------------------

@github-actions
Copy link

github-actions bot commented Feb 4, 2026

Breaking changes detected in the OpenAPI specification!

==========================================================================
==                            API CHANGE LOG                            ==
==========================================================================
                                marimo API                                
--------------------------------------------------------------------------
--                            What's Changed                            --
--------------------------------------------------------------------------
- POST   /api/kernel/set_model_value
  Request:
        - Changed application/json
          Schema: Broken compatibility
          Missing property: message.bufferPaths (object)
          Missing property: message.state (object)
--------------------------------------------------------------------------
--                                Result                                --
--------------------------------------------------------------------------
                 API changes broke backward compatibility                 
--------------------------------------------------------------------------

@github-actions
Copy link

github-actions bot commented Feb 4, 2026

Breaking changes detected in the OpenAPI specification!

==========================================================================
==                            API CHANGE LOG                            ==
==========================================================================
                                marimo API                                
--------------------------------------------------------------------------
--                            What's Changed                            --
--------------------------------------------------------------------------
- POST   /api/kernel/set_model_value
  Request:
        - Changed application/json
          Schema: Broken compatibility
          Missing property: message.bufferPaths (object)
          Missing property: message.state (object)
--------------------------------------------------------------------------
--                                Result                                --
--------------------------------------------------------------------------
                 API changes broke backward compatibility                 
--------------------------------------------------------------------------

@github-actions
Copy link

github-actions bot commented Feb 5, 2026

Breaking changes detected in the OpenAPI specification!

==========================================================================
==                            API CHANGE LOG                            ==
==========================================================================
                                marimo API                                
--------------------------------------------------------------------------
--                            What's Changed                            --
--------------------------------------------------------------------------
- POST   /api/kernel/set_model_value
  Request:
        - Changed application/json
          Schema: Broken compatibility
          Missing property: message.bufferPaths (object)
          Missing property: message.state (object)
--------------------------------------------------------------------------
--                                Result                                --
--------------------------------------------------------------------------
                 API changes broke backward compatibility                 
--------------------------------------------------------------------------

@github-actions
Copy link

github-actions bot commented Feb 5, 2026

Breaking changes detected in the OpenAPI specification!

==========================================================================
==                            API CHANGE LOG                            ==
==========================================================================
                                marimo API                                
--------------------------------------------------------------------------
--                            What's Changed                            --
--------------------------------------------------------------------------
- POST   /api/kernel/set_model_value
  Request:
        - Changed application/json
          Schema: Broken compatibility
          Missing property: message.bufferPaths (object)
          Missing property: message.state (object)
--------------------------------------------------------------------------
--                                Result                                --
--------------------------------------------------------------------------
                 API changes broke backward compatibility                 
--------------------------------------------------------------------------

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 UpdateWidgetModelCommand to ModelCommand with support for both update and custom messages
  • Introduced ModelLifecycleNotification mirroring 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_paths and is_model_message functions 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 structuredClone call which means decodeFromWire now mutates the input state object 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.

Comment on lines 513 to 522
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}
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

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).

Copilot uses AI. Check for mistakes.
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.
@github-actions
Copy link

github-actions bot commented Feb 5, 2026

Breaking changes detected in the OpenAPI specification!

==========================================================================
==                            API CHANGE LOG                            ==
==========================================================================
                                marimo API                                
--------------------------------------------------------------------------
--                            What's Changed                            --
--------------------------------------------------------------------------
- POST   /api/kernel/set_model_value
  Request:
        - Changed application/json
          Schema: Broken compatibility
          Missing property: message.bufferPaths (object)
          Missing property: message.state (object)
--------------------------------------------------------------------------
--                                Result                                --
--------------------------------------------------------------------------
                 API changes broke backward compatibility                 
--------------------------------------------------------------------------

@github-actions
Copy link

github-actions bot commented Feb 5, 2026

Breaking changes detected in the OpenAPI specification!

==========================================================================
==                            API CHANGE LOG                            ==
==========================================================================
                                marimo API                                
--------------------------------------------------------------------------
--                            What's Changed                            --
--------------------------------------------------------------------------
- POST   /api/kernel/set_model_value
  Request:
        - Changed application/json
          Schema: Broken compatibility
          Missing property: message.bufferPaths (object)
          Missing property: message.state (object)
--------------------------------------------------------------------------
--                                Result                                --
--------------------------------------------------------------------------
                 API changes broke backward compatibility                 
--------------------------------------------------------------------------

@github-actions
Copy link

github-actions bot commented Feb 5, 2026

Breaking changes detected in the OpenAPI specification!

==========================================================================
==                            API CHANGE LOG                            ==
==========================================================================
                                marimo API                                
--------------------------------------------------------------------------
--                            What's Changed                            --
--------------------------------------------------------------------------
- POST   /api/kernel/set_model_value
  Request:
        - Changed application/json
          Schema: Broken compatibility
          Missing property: message.bufferPaths (object)
          Missing property: message.state (object)
--------------------------------------------------------------------------
--                                Result                                --
--------------------------------------------------------------------------
                 API changes broke backward compatibility                 
--------------------------------------------------------------------------

@github-actions
Copy link

github-actions bot commented Feb 5, 2026

Breaking changes detected in the OpenAPI specification!

==========================================================================
==                            API CHANGE LOG                            ==
==========================================================================
                                marimo API                                
--------------------------------------------------------------------------
--                            What's Changed                            --
--------------------------------------------------------------------------
- POST   /api/kernel/set_model_value
  Request:
        - Changed application/json
          Schema: Broken compatibility
          Missing property: message.bufferPaths (object)
          Missing property: message.state (object)
--------------------------------------------------------------------------
--                                Result                                --
--------------------------------------------------------------------------
                 API changes broke backward compatibility                 
--------------------------------------------------------------------------

@mscolnick mscolnick merged commit 973eefc into main Feb 5, 2026
38 of 55 checks passed
@mscolnick mscolnick deleted the ms/widget-model branch February 5, 2026 20:36
manzt added a commit to marimo-team/marimo-lsp that referenced this pull request Feb 5, 2026
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.
manzt added a commit to marimo-team/marimo-lsp that referenced this pull request Feb 5, 2026
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.
manzt added a commit to marimo-team/marimo-lsp that referenced this pull request Feb 5, 2026
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.
manzt added a commit that referenced this pull request Feb 9, 2026
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.
manzt added a commit that referenced this pull request Feb 9, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bash-focus Area to focus on during release bug bash bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Undefined anywidget state when switching between edit and app modes (0.18.4)

3 participants