Fix/anywidget refresh value#9454
Conversation
…of being dropped.
… events after widget render completes.
…ckend updates arrive before render listeners attached, it doesn't cause a state mismatch.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
1 issue found across 6 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="marimo/_plugins/ui/_impl/comm.py">
<violation number="1" location="marimo/_plugins/ui/_impl/comm.py:123">
P3: Update `_create_model_message` docstring: it still says `echo_update` is skipped, but this branch now emits a `ModelUpdate`.</violation>
</file>
Architecture diagram
sequenceDiagram
participant UI as Widget UI (FE)
participant Plugin as AnyWidgetPlugin
participant Model as Frontend Model
participant Comm as Marimo Comm (BE)
participant Kernel as Python Kernel
Note over UI, Kernel: Phase 1: State Capture (During Interaction)
UI->>Model: model.set("count", 8)
Model->>Comm: sendUpdate({ method: "echo_update", state: {count: 8} })
rect rgb(240, 240, 240)
Note over Comm: CHANGED: Handle "echo_update"
Comm->>Comm: Create ModelUpdate message
Note right of Comm: Prevents dropping state<br/>on acknowledgement path
end
Comm-->>Kernel: Broadcast state to all clients
Note over UI, Kernel: Phase 2: State Hydration (Upon Notebook Reload)
Kernel->>Comm: Replay stored state
Comm->>Model: updateAndEmitDiffs({ count: 8 })
Plugin->>Plugin: bind(widgetDef, model)
Plugin->>UI: render(element, model)
alt Widget Initialization
UI->>Model: model.on("change:count", listener)
Note left of UI: Widget defaults to "5"<br/>(Internal initial state)
end
rect rgb(240, 240, 240)
Note over Plugin, Model: NEW: Re-hydration Flow
Plugin->>Model: getMarimoInternal(model).reemitState()
loop For each key in state
Model->>UI: emit("change:count", 8)
end
Model->>UI: emit("change")
end
UI->>UI: listener updates UI to "8"
Partial review: This PR has more than 50 files, so cubic reviewed the highest-priority files first. During the trial, paid plans get a higher file limit.
You can try an ultrareview to bypass the file limit, comment @cubic-dev-ai ultrareview. Learn more.
Fix all with cubic.
| elif method == "echo_update": | ||
| # echo_update is for multi-client sync acknowledgment, skip it | ||
| return None | ||
| # Preserve frontend-driven trait changes for reconnect replay. |
There was a problem hiding this comment.
P3: Update _create_model_message docstring: it still says echo_update is skipped, but this branch now emits a ModelUpdate.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At marimo/_plugins/ui/_impl/comm.py, line 123:
<comment>Update `_create_model_message` docstring: it still says `echo_update` is skipped, but this branch now emits a `ModelUpdate`.</comment>
<file context>
@@ -120,8 +120,14 @@ def _create_model_message(
elif method == "echo_update":
- # echo_update is for multi-client sync acknowledgment, skip it
- return None
+ # Preserve frontend-driven trait changes for reconnect replay.
+ # anywidget/ipywidgets can emit echo_update as the synchronisation
+ # acknowledgement path; dropping it causes stale replay state.
</file context>
There was a problem hiding this comment.
Pull request overview
Fixes anywidget model state hydration on browser refresh/reconnect by ensuring frontend-originated state changes are preserved for session replay and by re-emitting the current model state after a widget view attaches listeners.
Changes:
- Backend: treat
echo_updatemessages asModelUpdateso they are included in replayable model state. - Frontend: add an internal
reemitState()API to re-dispatch current model values aschange:*events afterrender(). - Tests: add backend and frontend test coverage for
echo_updatereplay contribution and late listener hydration.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
marimo/_plugins/ui/_impl/comm.py |
Converts echo_update into ModelUpdate to preserve state for reconnect replay. |
tests/_plugins/ui/_impl/test_comm.py |
Adds coverage ensuring echo_update is broadcast as a state-bearing update. |
frontend/src/plugins/impl/anywidget/model.ts |
Adds internal reemitState() that emits change:* events for current state. |
frontend/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx |
Calls reemitState() after render() so late listeners observe hydrated state. |
frontend/src/plugins/impl/anywidget/__tests__/model.test.ts |
Tests reemitState() emits field and aggregate change events. |
frontend/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx |
Adds regression test for hydration when listener attaches after initial state is set. |
| elif method == "echo_update": | ||
| # echo_update is for multi-client sync acknowledgment, skip it | ||
| return None | ||
| # Preserve frontend-driven trait changes for reconnect replay. | ||
| # anywidget/ipywidgets can emit echo_update as the synchronisation | ||
| # acknowledgement path; dropping it causes stale replay state. | ||
| return ModelUpdate( |
📝 Summary
The Counter anywidget was always defaulting to a default value of
5upon reloading the notebook as the state wasn't being reattached to the FE. This fixes the issue by sending anecho_updateto BE and upon reload reconnects to get the correct state. FE callsreemitStateensure that all listeners receive the state even if one was rendered before the other.Closes #9420
Screen.Recording.2026-05-04.at.17.29.29.mov
📋 Pre-Review Checklist
✅ Merge Checklist