Frontend → Python events#
The embedded viewer sends one of seven closed event records to Python. The
browser posts JSON to POST /_cellucid/events; the server authenticates
viewerId and viewerToken, removes the token, and dispatches the record to
the matching viewer.
Hook entry points#
Use the event-specific decorators for interaction events:
@viewer.on_selection
def handle_selection(event):
print(event["cells"], event["source"])
@viewer.on_hover
def handle_hover(event):
print(event["cell"], event["position"])
@viewer.on_click
def handle_click(event):
print(event["cell"], event["button"])
@viewer.on_ready
def handle_ready(event):
print(event["n_cells"], event["dimensions"])
@viewer.on_message receives every accepted event in a common envelope:
@viewer.on_message
def handle_current_event(event):
print(event["event"], event)
The envelope contains event plus the exact payload fields below. Routing
fields are never exposed to hook code.
Closed event catalog#
Unknown event types and every record with missing or undeclared fields are rejected before any hook runs. Integers are native, non-negative JSON integers; booleans are native JSON booleans.
selection#
Posted record:
{
"type": "selection",
"viewerId": "<viewer-id>",
"viewerToken": "<viewer-token>",
"cells": [0, 10, 42],
"source": "lasso"
}
Hook payload:
{"cells": [0, 10, 42], "source": "lasso"}
cells contains unique row indices. source is one exact non-empty token.
hover#
Posted record:
{
"type": "hover",
"viewerId": "<viewer-id>",
"viewerToken": "<viewer-token>",
"cell": 42,
"position": {"x": 0.1, "y": 0.2, "z": -0.3}
}
cell and position are both null when no cell is hovered. A non-null
position contains exactly finite x, y, and z numbers.
click#
Posted record:
{
"type": "click",
"viewerId": "<viewer-id>",
"viewerToken": "<viewer-token>",
"cell": 42,
"button": 0,
"shift": false,
"ctrl": true
}
button is exactly 0, 1, or 2.
ready#
Posted record:
{
"type": "ready",
"viewerId": "<viewer-id>",
"viewerToken": "<viewer-token>",
"n_cells": 12345,
"dimensions": 2
}
dimensions is exactly 1, 2, or 3.
pong#
pong is the response to the internal ping diagnostic:
{"requestId": "exact-request-id", "t": 1722000000000}
debug_snapshot#
debug_snapshot reports the iframe environment for
viewer.debug_connection():
{
"requestId": "exact-request-id",
"ts": "2026-07-26T12:00:00.000Z",
"locationHref": "https://viewer.example/?jupyter=true",
"origin": "https://viewer.example",
"serverUrl": "https://notebook.example/cellucid",
"connected": True,
"parentOrigin": "https://notebook.example",
"userAgent": "Browser identification",
}
userAgent may be None; every other field is required.
session_bundle#
This record is generated by the Python upload endpoint after it has validated, stored, authenticated, and consumed one pending bundle request:
{
"requestId": "exact-request-id",
"status": "ok",
"bytes": 123456,
"path": "/temporary/path/session.cellucid-session",
}
There is no error-shaped event. An invalid upload is rejected by the HTTP endpoint and does not dispatch a hook.
Delivery and failure semantics#
The endpoint accepts at most 1 MiB of UTF-8 JSON.
The viewer ID must be registered and the token must match.
The event name, full field set, and every payload value are validated before hook dispatch.
@viewer.on_messagehandlers run before the event-specific handlers.If any hook raises, the event is not published to
viewer.state, event waiters are not notified, and the HTTP request returns a callback failure.A successful hook sequence is recorded once and wakes
viewer.wait_for_event(...).
Hook callbacks run on the server request thread. Keep them short and move expensive analysis out of the callback path.
Troubleshooting#
If no event arrives:
Run
viewer.debug_connection().Confirm
server_health,frontend_roundtrip, andfrontend_debug_snapshot.Check the browser network panel for
POST /_cellucid/events.Confirm the notebook still owns the displayed viewer; stop stale viewers with
viewer.stop().
See Troubleshooting (hooks) for the full diagnostic sequence.