Skip to content

fix: anywidget refresh value#9454

Merged
mscolnick merged 34 commits into
marimo-team:mainfrom
Shamik-07:fix/anywidget_refresh_value
Jun 29, 2026
Merged

fix: anywidget refresh value#9454
mscolnick merged 34 commits into
marimo-team:mainfrom
Shamik-07:fix/anywidget_refresh_value

Conversation

@Shamik-07

@Shamik-07 Shamik-07 commented May 4, 2026

Copy link
Copy Markdown
Contributor

📝 Summary

The Counter anywidget was always defaulting to a default value of 5 upon reloading the notebook as the state wasn't being reattached to the FE. This fixes the issue by sending an echo_update to BE and upon reload reconnects to get the correct state. FE calls reemitState ensure 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

  • For large changes, or changes that affect the public API: this change was discussed or approved through an issue, on Discord, or the community discussions (Please provide a link if applicable).
  • Any AI generated code has been reviewed line-by-line by the human PR author, who stands by it.
  • Video or media evidence is provided for any visual changes (optional).

✅ Merge Checklist

  • I have read the contributor guidelines.
  • Documentation has been updated where applicable, including docstrings for API changes.
  • Tests have been added for the changes made.

@vercel

vercel Bot commented May 4, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
marimo-docs Ready Ready Preview, Comment Jun 3, 2026 9:13pm

Request Review

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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"
Loading

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.

Comment thread marimo/_plugins/ui/_impl/comm.py

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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_update messages as ModelUpdate so they are included in replayable model state.
  • Frontend: add an internal reemitState() API to re-dispatch current model values as change:* events after render().
  • Tests: add backend and frontend test coverage for echo_update replay 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.

Comment on lines 122 to +126
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(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Agreed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hello @manzt

If you could take a look at this, before it becomes stale, that would be great.

Thank you

@Shamik-07

Copy link
Copy Markdown
Contributor Author

@manzt Just a gentle nudge.

@kirangadhave kirangadhave added the internal A refactor or improvement that is not user facing label May 30, 2026
@kirangadhave

Copy link
Copy Markdown
Member

@cubic-dev-ai

@cubic-dev-ai

cubic-dev-ai Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai

@kirangadhave I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No issues found across 7 files

Architecture diagram
sequenceDiagram
    participant AnyWidget as AnyWidget Frontend
    participant Model as Model (State Manager)
    participant BE as Backend (Comm)
    participant Broadcast as Broadcast System

    Note over AnyWidget,Broadcast: AnyWidget Refresh / Reconnect Flow

    loop On notebook reload / reconnect
        AnyWidget->>Model: get() initial state
        Model-->>AnyWidget: default values (e.g., count=5)
        AnyWidget->>BE: send echo_update (frontend traits)
        BE->>Broadcast: broadcast Notification (ModelUpdate)
        Broadcast-->>AnyWidget: state event
        Note over AnyWidget,Model: state may arrive before render listener is attached
    end

    alt Late-attached render listener
        AnyWidget->>Model: reemitState()
        Model->>Model: #reemitState: emit change:key events for each current value
        Model--)AnyWidget: change:count (e.g., count=8)
        Note over AnyWidget: Listener receives hydrated state even if late
    else Normal path (listener already attached during connect)
        AnyWidget->>Model: reemitState()
        Model->>Model: #reemitState
        Model--)AnyWidget: change events (duplicate but harmless)
    end

    Note over AnyWidget,Broadcast: echo_update on BE side

    BE->>BE: _create_model_message(method="echo_update")
    alt echo_update method
        BE->>BE: Convert to ModelUpdate (preserves state)
        BE->>Broadcast: broadcast with state
    else Unknown method
        BE->>BE: Return None (skip)
    end
Loading

Re-trigger cubic

@mscolnick
mscolnick merged commit f7b1571 into marimo-team:main Jun 29, 2026
40 checks passed
manzt added a commit that referenced this pull request Jul 14, 2026
Closes #10173, MO-6686

Since 0.23.12 (#9454), the kernel coerced ipywidget's `echo_update` into
an ordinary update and broadcast it, feeding a client its own write
back. These messages re-fired `change:` listeners for traits that did
not change and also looped widgets that call `save_changes` in such a
listener.

The echo existed only to get frontend-driven trait changes into the
server replay state (#9420). Every client write already reaches the
server as a `ModelCommand`, so `SessionView` now records it into replay
state directly (stripping `_esm`/`_css`, since replayed state is served
to future viewers), and the echo is dropped.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

internal A refactor or improvement that is not user facing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

anywidget model.get() returns default value in render() on browser refresh

5 participants