mirror of
https://github.com/wassname/jsteer.git
synced 2026-09-09 11:25:03 +08:00
wip
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
---
|
||||
name: marimo-pair
|
||||
description: >-
|
||||
Drive a live marimo notebook as a workspace: run Python in the same kernel
|
||||
the user does, inspect live notebook state, and commit durable notebook
|
||||
changes. Use when the user wants to start a marimo notebook or pair on an
|
||||
active marimo session.
|
||||
allowed-tools: Bash(bash **/scripts/discover-servers.sh *), Bash(bash **/scripts/execute-code.sh *), Read
|
||||
---
|
||||
|
||||
marimo is a reactive Python runtime for building reproducible Python programs
|
||||
(marimo notebooks). Cells are connected by the variables they define and
|
||||
reference. Running a cell re-executes dependents in dataflow order. The active
|
||||
runtime holds the kernel namespace, cell state, and dataflow graph. The
|
||||
notebook (`.py` file) is the artifact the kernel writes from that state while a
|
||||
session is running.
|
||||
|
||||
A user interacts with the same runtime via a notebook UI with cells, outputs,
|
||||
and widgets.
|
||||
|
||||
**WARNING. The active runtime is the source of truth.** During a session, you
|
||||
SHOULD NOT modify the associated `.py` file directly. File edits WILL NOT reach
|
||||
the active kernel or user, and the kernel may overwrite them on save. Use
|
||||
`marimo._code_mode` (`cm`) for notebook changes. Reading disk is fine, but
|
||||
prefer `ctx.cells[...].code` for current cell code.
|
||||
|
||||
## Connect to a Notebook
|
||||
|
||||
Use the bundled script (`bash scripts/execute-code.sh`) or MCP
|
||||
(`execute_code(...)`) to run Python in a live marimo kernel.
|
||||
|
||||
If the user provides a notebook URL, target it directly:
|
||||
|
||||
```bash
|
||||
bash scripts/execute-code.sh --url http://localhost:2718 -c "print('connected')"
|
||||
```
|
||||
|
||||
Use `-c` only for short one-liners. For multiline code or code containing
|
||||
quotes, backticks, `$`, or braces, use a single-quoted heredoc:
|
||||
|
||||
```bash
|
||||
bash scripts/execute-code.sh --url http://localhost:2718 <<'PY'
|
||||
import marimo._code_mode as cm
|
||||
|
||||
async with cm.get_context() as ctx:
|
||||
cid = ctx.create_cell("x = df.head()")
|
||||
ctx.run_cell(cid)
|
||||
PY
|
||||
```
|
||||
|
||||
When code already lives in a file, pass the file path:
|
||||
|
||||
```bash
|
||||
bash scripts/execute-code.sh --url http://localhost:2718 /tmp/code.py
|
||||
```
|
||||
|
||||
If no target is provided, find or start a session. First look for a running
|
||||
session with `bash scripts/discover-servers.sh`, MCP `list_sessions()`, or
|
||||
local process context. When multiple sessions are possible, target with
|
||||
`--url`, `--port`, or `--session`.
|
||||
|
||||
If no server is running and the user wants a notebook, start marimo with
|
||||
`--no-token` (and without `--headless`) so it auto-registers for discovery. The
|
||||
notebook UI must be open before there is an active session for `execute-code`
|
||||
to target. The right way to invoke marimo depends on context (project tooling,
|
||||
global install, sandbox mode). If the notebook file contains a PEP 723 `#
|
||||
/// script` header, it MUST be opened with `--sandbox` — otherwise marimo
|
||||
ignores the inline dependencies. See
|
||||
[finding-marimo.md](reference/finding-marimo.md) for the full decision tree and
|
||||
[execution-context.md](reference/execution-context.md) for scripts, MCP, and
|
||||
shell quoting.
|
||||
|
||||
## Scratchpad Scope
|
||||
|
||||
`execute-code` evaluates Python in marimo's scratchpad: a temporary namespace
|
||||
with a shallow copy of the kernel globals. Notebook variables are available by
|
||||
name, but new top-level bindings and rebindings are discarded after each call.
|
||||
In-place mutations to notebook-owned objects can persist because those names
|
||||
still reference live objects.
|
||||
|
||||
Each call reports stdout and stderr from the scratchpad, plus console output
|
||||
from notebook cells it causes to run, including reactive descendants.
|
||||
|
||||
### Ordinary Python
|
||||
|
||||
Use ordinary Python in the scratchpad to inspect variables, sample data, test
|
||||
transformations, probe APIs, check imports, and read widget state.
|
||||
|
||||
```python
|
||||
print(df.head())
|
||||
|
||||
x = 10
|
||||
print(x)
|
||||
```
|
||||
|
||||
Here `df` comes from notebook globals, while `x` is a scratchpad-local binding.
|
||||
`x` exists for this call only and WILL NOT be added to notebook globals.
|
||||
|
||||
### Persist with `cm`
|
||||
|
||||
Top-level scratchpad assignments and rebindings are temporary. To persist work,
|
||||
including new variables, you MUST submit changes through `marimo._code_mode`
|
||||
(`cm`).
|
||||
|
||||
`marimo._code_mode` is a PRIVATE, UNSTABLE agent API (note the leading
|
||||
underscore). It exists for tools like this skill to drive a live kernel from
|
||||
the scratchpad. DO NOT import it from notebook cells, library code, or
|
||||
anything a user would run — methods can change or disappear across marimo
|
||||
versions and kernels. Treat every `import marimo._code_mode as cm` as
|
||||
scratchpad-only.
|
||||
|
||||
At session start, inspect what `cm` exposes in the active kernel:
|
||||
|
||||
```python
|
||||
import marimo._code_mode as cm
|
||||
|
||||
help(cm)
|
||||
```
|
||||
|
||||
Open a code-mode context to queue notebook changes.
|
||||
|
||||
```python
|
||||
import marimo._code_mode as cm
|
||||
|
||||
async with cm.get_context() as ctx:
|
||||
cid = ctx.create_cell("x = df.head()")
|
||||
ctx.run_cell(cid)
|
||||
```
|
||||
|
||||
The scratchpad supports top-level async code. Use `async with` directly;
|
||||
wrapping it in `asyncio.run(...)` is unnecessary and can conflict with the
|
||||
kernel's event loop.
|
||||
|
||||
After this block exits and the new cell runs, `x` is notebook state. Later
|
||||
scratchpad calls can read `x` by name. Code later in the same scratchpad call
|
||||
should read `ctx.globals["x"]`, because the scratchpad namespace was copied
|
||||
before the cell ran.
|
||||
|
||||
Inside the context, queued mutation methods are synchronous. Call them
|
||||
directly; do not `await` them. Each call queues an operation for marimo to
|
||||
apply when the context exits normally. If the block raises, the queue is
|
||||
discarded.
|
||||
|
||||
On clean exit, marimo applies packages, validates and applies structural cell
|
||||
changes, runs queued cells, then may run dependents. Validation is only
|
||||
structural since queued cell runs can still error. `create_cell` and
|
||||
`edit_cell` change notebook structure only. Use `run_cell` to execute.
|
||||
|
||||
`create_cell` currently defaults to `hide_code=True`, which collapses the code
|
||||
editor in the UI. Pass `hide_code=False` if the user wants created cells to
|
||||
be visible without manually expanding them.
|
||||
|
||||
|
||||
## Marimo Rules
|
||||
|
||||
marimo imposes a small contract on notebook code so it can keep the notebook as
|
||||
a directed acyclic graph (DAG):
|
||||
|
||||
- **No cycles** - cells cannot depend on each other in a cycle.
|
||||
- **No public redefinitions across cells** - each name has one owning cell.
|
||||
- **No wildcard imports** - `import *` prevents static analysis of definitions.
|
||||
|
||||
These rules keep the kernel, UI, and saved artifact consistent.
|
||||
|
||||
When `cm` submits a cell body, marimo parses its top-level definitions and
|
||||
references. A top-level name enters the graph unless it is private with a
|
||||
leading underscore.
|
||||
|
||||
```python
|
||||
# Public definitions: values, total, i, value, mean
|
||||
values = np.array([1, 2, 3])
|
||||
total = 0
|
||||
for i, value in enumerate(values):
|
||||
total += value
|
||||
mean = total / len(values)
|
||||
mean
|
||||
```
|
||||
|
||||
```python
|
||||
# Public definition: mean
|
||||
_values = np.array([1, 2, 3])
|
||||
_total = 0
|
||||
for _i, _value in enumerate(_values):
|
||||
_total += _value
|
||||
mean = _total / len(_values)
|
||||
mean
|
||||
```
|
||||
|
||||
Use private names for intermediates that no other cell should read. Public
|
||||
names define the notebook-level dataflow. If a `cm` edit violates the contract,
|
||||
marimo rejects the structural change and returns the validation error.
|
||||
|
||||
## The Notebook's Shape
|
||||
|
||||
A notebook is an ordered collection of cells. `ctx.cells` is the document view
|
||||
and `ctx.graph` is the dataflow view.
|
||||
|
||||
```python
|
||||
for cell in ctx.cells:
|
||||
cell # .id, .code, .name, .config, .status, .errors
|
||||
|
||||
ctx.cells["setup"] # by name
|
||||
ctx.cells[0] # by position
|
||||
list(ctx.cells.keys()) # all IDs, in notebook order
|
||||
```
|
||||
|
||||
Cell IDs are opaque strings which can be queried from the notebook or captured
|
||||
from `cm` return values:
|
||||
|
||||
```python
|
||||
cid = ctx.create_cell("df = pd.read_csv('data.csv')")
|
||||
print(cid) # e.g. 'Hbol'
|
||||
```
|
||||
|
||||
Alternatively, cells can be assigned and referenced by `name`. The graph can be
|
||||
used to understand its role in the dataflow.
|
||||
|
||||
```python
|
||||
for cid, impl in ctx.graph.cells.items():
|
||||
impl # .defs, .refs (sets of public names)
|
||||
|
||||
ctx.graph.descendants(cid) # cells that re-run when this one changes
|
||||
ctx.graph.ancestors(cid) # cells this one depends on
|
||||
```
|
||||
|
||||
In marimo, deletes are *destructive* so it can be useful to query the
|
||||
descendants prior to deleting to understand it's impact.
|
||||
|
||||
## Writing Notebook Changes
|
||||
|
||||
The graph contract keeps marimo able to run and save the notebook. Passing
|
||||
those checks alone does not guarantee a useful artifact. Committed cells should
|
||||
still be readable, rerunnable, and editable.
|
||||
|
||||
Make durable edits that reuse the notebook's existing names, imports,
|
||||
dependencies, and UI model. Don't be lazy. Avoid one-off workarounds that pass
|
||||
`cm` validation but leave a brittle notebook.
|
||||
|
||||
### Cell Bodies
|
||||
|
||||
Submit the code that belongs in the cell.
|
||||
|
||||
- **Submit cell contents** - `create_cell` and `edit_cell` take cell contents,
|
||||
not saved-file `@app.cell` wrappers.
|
||||
- **Read before replacing** - for now, another editor may change a cell between
|
||||
scratchpad calls. Before `edit_cell`, read the current body from
|
||||
`ctx.cells[...]` and submit the full replacement.
|
||||
- **Reuse notebook imports** - if `np` already exists, use it or edit the owning
|
||||
import cell. DO NOT add `import numpy as _np` just to bypass the graph.
|
||||
- **Define public names intentionally** - use public names for values later
|
||||
cells should reference. Use private `_name` bindings or function locals for
|
||||
same-cell intermediates.
|
||||
- **Define each public name once** - a public name has one owning cell.
|
||||
Reassigning it in another cell fails with `Multiply-defined names`; edit the
|
||||
owning cell or give the result a new name. See
|
||||
[gotchas.md](reference/gotchas.md).
|
||||
- **Run cells deliberately** - `create_cell` and `edit_cell` change structure
|
||||
only. Queue `ctx.run_cell(...)` when the cell should execute.
|
||||
|
||||
### Prefer `cm`-Managed Changes
|
||||
|
||||
Use `cm` APIs when they exist. Avoid direct file edits, shell package commands,
|
||||
and scratchpad-only state for changes that should persist.
|
||||
|
||||
- **Do not edit the `.py` artifact** - DO NOT use `Edit`, `Write`, or
|
||||
`NotebookEdit` on the notebook file during a live session. Use
|
||||
`ctx.edit_cell(...)` even for small changes.
|
||||
- **Manage packages through `cm`** - use `ctx.packages.add()` or
|
||||
`ctx.packages.remove()` instead of direct `uv` or `pip`; confirm
|
||||
non-obvious dependency changes.
|
||||
- **Avoid transient paths** - persisted cells should not depend on `/tmp/...`
|
||||
unless the work is intentionally transient.
|
||||
- **Delete deliberately** - deleting a cell removes globals it defines. Reuse
|
||||
empty cells when convenient and delete cells left empty after edits.
|
||||
|
||||
### UI and Widgets
|
||||
|
||||
Inspect the object before changing it. Different UI objects update through
|
||||
different paths.
|
||||
|
||||
- **Set `mo.ui.*` through `cm`** - use `ctx.set_ui_value(element, value)` inside
|
||||
`cm.get_context()`.
|
||||
- **Set anywidget traitlets directly** - synced traitlets are Python
|
||||
attributes, for example `widget.value = 5`.
|
||||
|
||||
For designing custom visual or interactive output, see
|
||||
[rich-representations.md](reference/rich-representations.md).
|
||||
|
||||
## References
|
||||
|
||||
- [execution-context.md](reference/execution-context.md) — scripts, MCP, auth, startup, and shell quoting
|
||||
- [finding-marimo.md](reference/finding-marimo.md) — choosing the right marimo invocation
|
||||
- [gotchas.md](reference/gotchas.md) — name redefinition, cached module proxies, and notebook traps
|
||||
- [rich-representations.md](reference/rich-representations.md) — custom widgets and visualizations
|
||||
- [notebook-improvements.md](reference/notebook-improvements.md) — improving existing notebooks
|
||||
@@ -0,0 +1,62 @@
|
||||
# Connection Troubleshooting
|
||||
|
||||
Use this reference when `execute-code.sh` or MCP cannot reach the intended
|
||||
marimo session, cannot select a session, or fails because code was passed
|
||||
incorrectly.
|
||||
|
||||
## Targeting
|
||||
|
||||
Use explicit targets when possible.
|
||||
|
||||
- `--url` connects to a known marimo server or notebook URL.
|
||||
- `--port` selects a local marimo server from the registry.
|
||||
- `--session` selects one notebook session on a server.
|
||||
|
||||
If multiple servers or sessions are available, do not guess. Ask for the URL or
|
||||
session, or inspect local context.
|
||||
|
||||
## Auth
|
||||
|
||||
For token-authenticated servers, prefer `MARIMO_TOKEN`.
|
||||
|
||||
```bash
|
||||
MARIMO_TOKEN=... bash scripts/execute-code.sh --url http://localhost:2718 -c "1 + 1"
|
||||
```
|
||||
|
||||
`--token` also works, but may expose the token in process listings. If both are
|
||||
present, `--token` overrides `MARIMO_TOKEN`. The script sends the token as
|
||||
`Authorization: Bearer ...` on session discovery and code execution requests.
|
||||
|
||||
## Quoting
|
||||
|
||||
Use `-c` only for short one-liners. Use a single-quoted heredoc or file input
|
||||
for multiline code or shell-sensitive characters.
|
||||
|
||||
```bash
|
||||
bash scripts/execute-code.sh --url http://localhost:2718 <<'PY'
|
||||
print(df.head())
|
||||
PY
|
||||
```
|
||||
|
||||
```bash
|
||||
bash scripts/execute-code.sh --url http://localhost:2718 /tmp/code.py
|
||||
```
|
||||
|
||||
## Common Script Errors
|
||||
|
||||
- **No running marimo instances found** - use an explicit `--url`, or start
|
||||
marimo with the project's normal tooling.
|
||||
- **Multiple instances found** - rerun with `--port` or `--url`.
|
||||
- **No active sessions on the server** - open the notebook in the browser or
|
||||
provide `--session`.
|
||||
- **Multiple sessions on server** - rerun with `--session`.
|
||||
- **Failed to connect** - check the URL, token, and whether the server is still
|
||||
running.
|
||||
- **SyntaxError** - the submitted Python was malformed; use a heredoc or file.
|
||||
- **ImportError** - diagnose in the notebook kernel. Install packages through
|
||||
`cm` when needed.
|
||||
|
||||
## Starting marimo
|
||||
|
||||
Discover first. If no server is running and the user wants a notebook, use
|
||||
[finding-marimo.md](finding-marimo.md).
|
||||
@@ -0,0 +1,99 @@
|
||||
# Finding and Invoking marimo
|
||||
|
||||
Only servers started with `--no-token` register in the local server registry
|
||||
and are auto-discoverable — starting without a token makes discovery easier.
|
||||
If a server has a token, set the `MARIMO_TOKEN` environment variable before
|
||||
calling the execute script (avoids leaking the token in process listings).
|
||||
|
||||
```sh
|
||||
marimo edit notebook.py --no-token [--sandbox]
|
||||
```
|
||||
|
||||
Start marimo in edit mode without `--headless` unless the user asks for a
|
||||
headless server. The notebook UI must be open in a browser before marimo has an
|
||||
active session for `execute-code` to target. If running headless, give the user
|
||||
the local URL and wait for them to open it before executing code.
|
||||
|
||||
How you invoke `marimo` depends on context — find the right way to run it.
|
||||
|
||||
## Notebooks with PEP 723 metadata require `--sandbox`
|
||||
|
||||
Before picking a runner, check the notebook file for a PEP 723 header:
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "marimo",
|
||||
# "polars",
|
||||
# ]
|
||||
# ///
|
||||
```
|
||||
|
||||
If the block is present, the notebook was authored as a self-contained
|
||||
sandboxed script and **SHOULD be opened with `--sandbox`**. Without the flag
|
||||
marimo runs in the ambient environment and silently ignores the inline
|
||||
dependencies. Imports will fail or, worse, resolve to a different version than
|
||||
the author pinned.
|
||||
|
||||
`--sandbox` works regardless of project context: inside a uv project, `uv run
|
||||
marimo edit notebook.py --no-token --sandbox` still creates the isolated env
|
||||
from the PEP 723 block rather than the project's `.venv`.
|
||||
|
||||
## Inside a Python project
|
||||
|
||||
If there's a `pyproject.toml` in cwd or a parent directory, check that marimo
|
||||
is actually in the dependencies before using the project's runner. Look for
|
||||
`marimo` in:
|
||||
|
||||
- `[project.dependencies]`
|
||||
- `[project.optional-dependencies]` or `[dependency-groups]` (dev deps)
|
||||
- `[tool.pixi.dependencies]`
|
||||
- The project's `.venv` (`uv pip show marimo` or check `.venv/bin/marimo`)
|
||||
|
||||
If marimo is in a named dependency group (not the default), you need to
|
||||
specify it:
|
||||
|
||||
```sh
|
||||
# marimo is in [dependency-groups] → "notebooks" group
|
||||
uv run --group notebooks marimo edit notebook.py --no-token
|
||||
```
|
||||
|
||||
Once you know marimo is available, use whatever CLI runner the project uses:
|
||||
|
||||
```sh
|
||||
# uv-managed project
|
||||
uv run marimo edit notebook.py --no-token
|
||||
# pixi-managed project
|
||||
pixi run marimo edit notebook.py --no-token
|
||||
```
|
||||
|
||||
Skip `--sandbox` here — the project already manages dependencies.
|
||||
|
||||
If `pyproject.toml` exists but marimo is **not** in the deps, treat this as
|
||||
"outside a project" (see below).
|
||||
|
||||
## Outside a Python project
|
||||
|
||||
Prefer `--sandbox`. Sandbox mode creates an isolated environment for the
|
||||
notebook and writes dependencies into the script itself as inline PEP 723
|
||||
metadata — so the notebook stays self-contained and reproducible.
|
||||
|
||||
```sh
|
||||
# With uv available (preferred)
|
||||
uvx marimo@latest edit notebook.py --no-token --sandbox
|
||||
|
||||
# With marimo installed globally
|
||||
marimo edit notebook.py --no-token --sandbox
|
||||
```
|
||||
|
||||
## Global marimo install
|
||||
|
||||
If marimo is installed globally, check the version — code mode shipped in
|
||||
v0.21.1. If the installed version is older, prompt the user to upgrade before
|
||||
proceeding.
|
||||
|
||||
## Nothing found
|
||||
|
||||
If no project marimo, no `uv`/`uvx`, and no global `marimo` on PATH, tell the
|
||||
user to install `uv` (<https://docs.astral.sh/uv/getting-started/installation/>).
|
||||
@@ -0,0 +1,88 @@
|
||||
# Gotchas
|
||||
|
||||
## Private variables are cell-scoped
|
||||
|
||||
Variables with a `_` prefix are **private to the cell that defines them** in
|
||||
marimo. They cannot be referenced from other cells — you'll get a `NameError`.
|
||||
|
||||
This matters when building notebooks programmatically. A common mistake:
|
||||
|
||||
```python
|
||||
# Cell A
|
||||
_df = pd.DataFrame(results) # _df is private to this cell
|
||||
|
||||
# Cell B — FAILS
|
||||
mo.ui.table(_df) # NameError: name '_df' is not defined
|
||||
```
|
||||
|
||||
**Fix:** Either merge both into one cell, or use a non-private name (`df`).
|
||||
|
||||
## Redefining a public name across cells
|
||||
|
||||
Each public name has one owning cell. Defining it again in another cell fails
|
||||
with `Multiply-defined names`. This is easy to hit when building a notebook
|
||||
incrementally — a second cell reassigns `df`, `results`, `data`, etc.
|
||||
|
||||
```python
|
||||
# Cell A
|
||||
df = pd.read_csv("data.csv")
|
||||
|
||||
# Cell B — FAILS: df already defined in Cell A
|
||||
df = df.dropna() # Multiply-defined names: df
|
||||
```
|
||||
|
||||
**Fix — pick one:**
|
||||
|
||||
- **Edit the owning cell** if the step belongs there (`ctx.edit_cell`).
|
||||
- **Use a new name** when later cells need the result (`clean = df.dropna()`).
|
||||
- **Use a private `_` name** for a throwaway intermediate (`_clean = df.dropna()`).
|
||||
|
||||
`ctx.graph.cells[cid].defs` shows what a cell already owns.
|
||||
|
||||
## Duplicate public imports across cells
|
||||
|
||||
The same single-definition rule applies to imports: a public name (like `pd`)
|
||||
can only be defined in one cell. If two cells both `import pandas as pd`, you
|
||||
get a `Multiply-defined names` error at validation.
|
||||
|
||||
**Fix:** Use a `_` prefix on the second import (`import pandas as _pd`) or
|
||||
consolidate imports into a shared cell.
|
||||
|
||||
## `inspect.getsource()` on methods is indented
|
||||
|
||||
`inspect.getsource()` on a class method preserves the original indentation.
|
||||
Passing this to `ast.parse()` fails with `IndentationError`.
|
||||
|
||||
```python
|
||||
# FAILS
|
||||
src = inspect.getsource(SomeClass.some_method)
|
||||
tree = ast.parse(src) # IndentationError: unexpected indent
|
||||
|
||||
# FIX
|
||||
import textwrap
|
||||
src = textwrap.dedent(inspect.getsource(SomeClass.some_method))
|
||||
tree = ast.parse(src)
|
||||
```
|
||||
|
||||
## Cached module availability
|
||||
|
||||
Some libraries cache optional-dependency availability at import time. Installing
|
||||
a package mid-session via `ctx.packages.add()` won't update those caches.
|
||||
The user may need to restart the kernel — but try known workarounds first.
|
||||
|
||||
### Polars + pyarrow
|
||||
|
||||
`df.to_pandas()` fails with `ModuleNotFoundError: pa.Table requires 'pyarrow'`.
|
||||
|
||||
**Workaround** — if this error occurs after installing pyarrow mid-session,
|
||||
run the following via `execute-code` (scratchpad), NOT in a cell. The patch
|
||||
mutates the cached module object in the running kernel, so it doesn't need to
|
||||
persist in the notebook.
|
||||
|
||||
```python
|
||||
import pyarrow as _pa
|
||||
import polars.dataframe.frame as _frame_mod
|
||||
_frame_mod.pa = _pa
|
||||
```
|
||||
|
||||
Then re-run the failing cell.
|
||||
@@ -0,0 +1,97 @@
|
||||
# Notebook Improvements
|
||||
|
||||
When the user asks to improve, optimize, or clean up their notebook, scan the
|
||||
current cells for these opportunities. Use your judgment — don't over-apply,
|
||||
and if you're unsure whether a change is worthwhile, ask the user.
|
||||
|
||||
## Cell names
|
||||
|
||||
Low priority unless the user asks. `setup` and cells defining
|
||||
functions/classes are auto-named by marimo. Beyond that, naming is optional.
|
||||
Note that naming markdown cells clutters the UI by showing the cell header
|
||||
that's normally hidden.
|
||||
|
||||
## Setup cell
|
||||
|
||||
A setup cell is named `"setup"` and is guaranteed to run before all other
|
||||
cells. It's the place for module imports. Consolidating imports here keeps
|
||||
the notebook clean and ensures every cell can rely on those modules being
|
||||
available.
|
||||
|
||||
**The setup cell cannot reference other cells' variables.** It runs first, so
|
||||
it must be self-contained: imports, constants, and definitions that depend only
|
||||
on each other. Reading a name defined elsewhere (e.g. `df`, a UI element) fails
|
||||
with `The setup cell cannot have references`.
|
||||
|
||||
First check if the notebook already has a cell named `"setup"`. If not, create
|
||||
one and hoist scattered imports into it. `name="setup"` auto-positions the cell
|
||||
first — no `before`/`after` needed:
|
||||
|
||||
```python
|
||||
cid = ctx.create_cell('''import polars as pl
|
||||
import marimo as mo
|
||||
import anywidget
|
||||
import traitlets''', name="setup")
|
||||
ctx.run_cell(cid)
|
||||
```
|
||||
|
||||
If a setup cell already exists, `create_cell(name="setup")` raises `ValueError`;
|
||||
use `ctx.edit_cell("setup", code=...)` and `ctx.run_cell("setup")` instead.
|
||||
|
||||
## Lift reusable functions into their own cells
|
||||
|
||||
When a cell contains a single function or class that doesn't reference
|
||||
variables from other cells, marimo treats it specially — it can be written as
|
||||
a standalone definition and reused outside the notebook. These functions can
|
||||
use modules from the setup cell.
|
||||
|
||||
Look for functions that **could belong in a library**: data loading, transforms,
|
||||
parsers, domain logic, custom widgets. If someone might reasonably `import` it
|
||||
from another module, it's a good candidate to lift into its own cell.
|
||||
|
||||
Don't lift everything — notebook-specific wiring (UI layout, display logic,
|
||||
cell-level orchestration) should stay where it is. Use `_prefix` for
|
||||
cell-internal helpers that aren't meant to be reused.
|
||||
|
||||
```python
|
||||
# before: useful logic buried in a larger cell
|
||||
objects = pl.read_csv("https://example.com/objects.csv")
|
||||
artists = pl.read_csv("https://example.com/artists.csv")
|
||||
|
||||
def top_counts(df, col, n=5):
|
||||
return df.group_by(col).len().sort("len", descending=True).head(n)
|
||||
|
||||
result = top_counts(objects.join(artists, on="id"), "category")
|
||||
```
|
||||
|
||||
```python
|
||||
# after: top_counts is general-purpose — give it its own cell
|
||||
|
||||
# cell 1
|
||||
def top_counts(df, col, n=5):
|
||||
return df.group_by(col).len().sort("len", descending=True).head(n)
|
||||
```
|
||||
|
||||
```python
|
||||
# cell 2
|
||||
result = top_counts(df, "category")
|
||||
```
|
||||
|
||||
## `mo.persistent_cache`
|
||||
|
||||
`@mo.persistent_cache` caches a function's result to disk so it isn't
|
||||
recomputed on subsequent runs. The cache persists across kernel restarts.
|
||||
|
||||
```python
|
||||
@mo.persistent_cache
|
||||
def load_data():
|
||||
objects = pl.read_csv("https://example.com/objects.csv")
|
||||
artists = pl.read_csv("https://example.com/artists.csv")
|
||||
return objects.join(artists, on="id", how="left")
|
||||
|
||||
df = load_data()
|
||||
```
|
||||
|
||||
Good candidates: data loading, ETL, expensive computation that rarely changes.
|
||||
Don't over-optimize — if you're unsure, suggest it to the user rather than
|
||||
applying it.
|
||||
@@ -0,0 +1,303 @@
|
||||
# Rich Representations
|
||||
|
||||
Custom visual encodings for data that go beyond standard charts and tables.
|
||||
|
||||
## Guiding principles
|
||||
|
||||
**Visualization matters.** Helping users build custom visual representations
|
||||
is one of the highest-impact things the agent can do. A bespoke encoding
|
||||
tailored to the task — labeling, batch review, comparing variants — lets
|
||||
users *see* their data in ways that tables and numbers never will. marimo
|
||||
is an environment where users create their own views, not just consume
|
||||
library charts. Help them imagine what's possible, then build it.
|
||||
|
||||
**Use modern web APIs.** Models may default to older browser patterns; prefer
|
||||
modern HTML, CSS, and JavaScript that are supported in current browsers. Avoid
|
||||
build steps unless the task clearly needs them.
|
||||
|
||||
**Prefer compact output.** marimo clips cell output at ~610px and scrolls.
|
||||
Avoid hitting that limit; if you need more space, manage your own scrolling
|
||||
inside a fixed-height container.
|
||||
|
||||
**Keep it thin, make it compose.** A widget is a thin layer over data, not
|
||||
an application. One clear purpose, few traitlets, small `_esm`. Build small
|
||||
pieces that compose in the notebook — combine with other cells, UI elements,
|
||||
and views. Don't over-engineer.
|
||||
|
||||
## Decision tree
|
||||
|
||||
| Need | Approach |
|
||||
|------|----------|
|
||||
| Custom output or interaction | **anywidget** — flexible enough to grow from display-only to interactive |
|
||||
| Tiny static HTML representation | `_display_()` or `mo.Html` |
|
||||
| Built-in control used as-is (slider, dropdown) | `mo.ui.*` |
|
||||
|
||||
For custom representations, prefer anywidget unless the output is clearly a
|
||||
small static one-off.
|
||||
|
||||
## anywidget
|
||||
|
||||
[anywidget](https://anywidget.dev) bridges Python and JavaScript via
|
||||
traitlets. `.tag(sync=True)` makes a traitlet bidirectional — Python sets a
|
||||
value → JS sees it; JS calls `model.set()` + `model.save_changes()` →
|
||||
Python sees it. `_css` is optional global CSS.
|
||||
|
||||
**marimo does not render traditional Jupyter widgets.** Libraries like jscatter,
|
||||
ipyvolume, etc. often have a top-level object whose default representation is a
|
||||
Jupyter widget (`application/vnd.jupyter.widget-view+json`). marimo cannot
|
||||
display these — you need to find the underlying **anywidget** instance, which
|
||||
marimo *does* support.
|
||||
|
||||
Common pattern: look for a `.widget` attribute on the library object:
|
||||
|
||||
```python
|
||||
# jscatter example — Scatter is not renderable, but .widget is an anywidget
|
||||
scatter = jscatter.Scatter(data=df, x="x", y="y")
|
||||
scatter.widget # <-- use this in the cell output
|
||||
```
|
||||
|
||||
When unsure, check in the scratchpad:
|
||||
|
||||
```python
|
||||
import anywidget
|
||||
obj = scatter.widget # or whatever accessor the library provides
|
||||
print(isinstance(obj, anywidget.AnyWidget)) # True = marimo can render it
|
||||
```
|
||||
|
||||
### `_esm` lifecycle
|
||||
|
||||
**Render only** (most widgets):
|
||||
|
||||
```js
|
||||
function render({ model, el }) { /* ... */ }
|
||||
export default { render };
|
||||
```
|
||||
|
||||
**Initialize + render** (shared state across views, one-time setup):
|
||||
|
||||
```js
|
||||
export default () => {
|
||||
return {
|
||||
initialize({ model }) {
|
||||
// Once per widget instance — timers, connections, shared handlers
|
||||
return () => { /* cleanup */ };
|
||||
},
|
||||
render({ model, el }) {
|
||||
// Once per view — display in 3 cells = 3 renders
|
||||
return () => { /* cleanup DOM listeners */ };
|
||||
}
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
- `model.on()` is auto-cleaned when a view is removed
|
||||
- DOM `addEventListener` is **not** — clean up with `AbortController`
|
||||
|
||||
### Timer example (initialize + render)
|
||||
|
||||
`initialize` owns one interval; each `render` view displays it.
|
||||
|
||||
```python
|
||||
import anywidget
|
||||
import traitlets
|
||||
|
||||
_TIMER_ESM = """
|
||||
export default () => {
|
||||
return {
|
||||
initialize({ model }) {
|
||||
const id = setInterval(() => {
|
||||
if (model.get("running")) {
|
||||
model.set("seconds", model.get("seconds") + 1);
|
||||
model.save_changes();
|
||||
}
|
||||
}, 1000);
|
||||
return () => clearInterval(id);
|
||||
},
|
||||
render({ model, el }) {
|
||||
const controller = new AbortController();
|
||||
const { signal } = controller;
|
||||
|
||||
const span = document.createElement("span");
|
||||
span.style.cssText = "font: 24px monospace;";
|
||||
|
||||
const btn = document.createElement("button");
|
||||
btn.style.cssText = "margin-left: 8px; cursor: pointer;";
|
||||
|
||||
function update() {
|
||||
const s = model.get("seconds");
|
||||
const mm = String(Math.floor(s / 60)).padStart(2, "0");
|
||||
const ss = String(s % 60).padStart(2, "0");
|
||||
span.textContent = `${mm}:${ss}`;
|
||||
btn.textContent = model.get("running") ? "⏸" : "▶";
|
||||
}
|
||||
|
||||
model.on("change:seconds", update);
|
||||
model.on("change:running", update);
|
||||
|
||||
btn.addEventListener("click", () => {
|
||||
model.set("running", !model.get("running"));
|
||||
model.save_changes();
|
||||
}, { signal });
|
||||
|
||||
update();
|
||||
el.append(span, btn);
|
||||
return () => controller.abort();
|
||||
}
|
||||
};
|
||||
};
|
||||
"""
|
||||
|
||||
class Timer(anywidget.AnyWidget):
|
||||
seconds = traitlets.Int(0).tag(sync=True)
|
||||
running = traitlets.Bool(True).tag(sync=True)
|
||||
_esm = _TIMER_ESM
|
||||
```
|
||||
|
||||
### Composing with the notebook
|
||||
|
||||
Widgets become reactive notebook citizens when you bridge a traitlet to
|
||||
`mo.state`. This is a two-cell pattern — create the widget and wire up the
|
||||
observer in one cell, read the value in another:
|
||||
|
||||
```python
|
||||
# Cell 1 — widget + observer
|
||||
timer = Timer()
|
||||
|
||||
get_seconds, set_seconds = mo.state(timer.seconds)
|
||||
timer.observe(lambda _: set_seconds(timer.seconds), names=["seconds"])
|
||||
|
||||
timer # display the widget
|
||||
```
|
||||
|
||||
```python
|
||||
# Cell 2 — reacts to changes
|
||||
seconds = get_seconds()
|
||||
mo.md(f"Timer is at **{seconds}s** — {'running' if seconds > 0 else 'stopped'}")
|
||||
```
|
||||
|
||||
The common pattern is `mo.state(widget.trait)` for the initial value,
|
||||
`.observe()` on the specific trait name, and reading with the getter in a
|
||||
downstream cell. See [Reactive anywidgets](#reactive-anywidgets-in-marimo)
|
||||
for the details.
|
||||
|
||||
### CDN dependencies
|
||||
|
||||
Import JS libraries from [esm.sh](https://esm.sh) — no build step:
|
||||
|
||||
```js
|
||||
import * as d3 from "https://esm.sh/d3@7";
|
||||
import { tableFromIPC } from "https://esm.sh/@uwdata/flechette@2";
|
||||
```
|
||||
|
||||
### DataFrames and binary data
|
||||
|
||||
**Prefer reducing data on the Python side.** Aggregate, filter, sample —
|
||||
send the widget only what it needs. Most widgets should receive a small,
|
||||
pre-processed payload via simple traitlets (lists, dicts). This keeps the
|
||||
widget simple and avoids extra dependencies.
|
||||
|
||||
**For large tabular data (>2k rows)** where the widget genuinely needs
|
||||
row-level access, send Arrow IPC bytes instead of JSON. This adds
|
||||
complexity and dependencies, so only reach for it when the data volume
|
||||
justifies it.
|
||||
|
||||
**Python — serialize:**
|
||||
|
||||
```python
|
||||
# Polars (native, no pyarrow needed)
|
||||
_ipc=df.write_ipc(None).getvalue()
|
||||
|
||||
# Any __arrow_c_stream__ source (pandas, narwhals, pyarrow, etc.)
|
||||
import io, pyarrow as pa, pyarrow.feather as feather
|
||||
|
||||
def to_arrow_ipc(data) -> bytes:
|
||||
table = pa.RecordBatchReader.from_stream(data).read_all()
|
||||
sink = io.BytesIO()
|
||||
feather.write_feather(table, sink, compression="uncompressed")
|
||||
return sink.getvalue()
|
||||
```
|
||||
|
||||
**JS — deserialize with `@uwdata/flechette`:**
|
||||
|
||||
```js
|
||||
import { tableFromIPC } from "https://esm.sh/@uwdata/flechette@2";
|
||||
const table = tableFromIPC(new Uint8Array(model.get("_ipc").buffer));
|
||||
// table.numRows, table.numCols, table.get(i), table.getChild("col_name")
|
||||
```
|
||||
|
||||
Use `traitlets.Any().tag(sync=True)` for the IPC bytes traitlet.
|
||||
|
||||
## Reactive anywidgets in marimo
|
||||
|
||||
When an anywidget trait (selection, value, zoom, etc.) should drive a
|
||||
downstream marimo cell, use `mo.state()` + `.observe()` on the **specific
|
||||
trait**. This is the preferred pattern:
|
||||
|
||||
```python
|
||||
# In the cell that creates the widget:
|
||||
get_selection, set_selection = mo.state(widget.selection)
|
||||
widget.observe(
|
||||
lambda _: set_selection(widget.selection),
|
||||
names=["selection"],
|
||||
)
|
||||
|
||||
# In a downstream cell — re-executes when selection changes:
|
||||
selection = get_selection()
|
||||
```
|
||||
|
||||
Initialize `mo.state()` with the widget's current trait value — not a
|
||||
hardcoded default. Read the trait directly off the widget in the lambda.
|
||||
Do **not** use `change["new"]` or `allow_self_loops=True`.
|
||||
|
||||
### `mo.state` + `.observe()` vs `mo.ui.anywidget()`
|
||||
|
||||
Two strategies for reactive anywidgets. Choose one per widget — don't mix them.
|
||||
|
||||
| Strategy | Reactivity | Best for |
|
||||
|----------|-----------|----------|
|
||||
| `mo.state` + `.observe()` | Specific traits you pick | Precision — only named traits trigger downstream cells |
|
||||
| `mo.ui.anywidget(widget)` | All synced traits as one `.value` dict | Convenience — observe everything at once |
|
||||
|
||||
### Programmatic widget control (scratchpad)
|
||||
|
||||
Read widget state or set UI controls from the scratchpad — no clicking:
|
||||
|
||||
```python
|
||||
print(timer.seconds) # read
|
||||
timer.seconds = 0 # set — frontend updates automatically
|
||||
```
|
||||
|
||||
`mo.ui.*` elements need `ctx.set_ui_value(...)` from code mode; anywidgets use
|
||||
direct assignment.
|
||||
|
||||
## `_display_()` protocol
|
||||
|
||||
Any object with a `_display_()` method renders richly in marimo. Return
|
||||
anything marimo can render — `mo.Html`, `mo.md()`, a chart, a string.
|
||||
|
||||
Precedence: `_display_()` > built-in formatters > `_mime_()` > IPython
|
||||
`_repr_*_()` methods.
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
import marimo as mo
|
||||
|
||||
@dataclass
|
||||
class ColorSwatch:
|
||||
colors: list[str]
|
||||
|
||||
def _display_(self):
|
||||
divs = "".join(
|
||||
f'<div style="width:40px;height:40px;background:{c};border-radius:4px;"></div>'
|
||||
for c in self.colors
|
||||
)
|
||||
return mo.Html(f'<div style="display:flex;gap:8px;">{divs}</div>')
|
||||
```
|
||||
|
||||
For inline `<script>` tags, use `document.currentScript.previousElementSibling`
|
||||
to scope to the element — never hardcode IDs (breaks with multiple instances).
|
||||
|
||||
## Minimize CLS (Cumulative Layout Shift)
|
||||
|
||||
Use `min-height` or `aspect-ratio` on the outer container so the widget
|
||||
reserves space before content loads or when toggling between states.
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
# List running marimo instances from the server registry.
|
||||
# Cleans up stale entries (dead PIDs) and outputs live servers as JSON.
|
||||
# No marimo installation required.
|
||||
set -euo pipefail
|
||||
|
||||
# Locate the servers directory
|
||||
is_windows=false
|
||||
if [[ "$OSTYPE" == msys* || "$OSTYPE" == cygwin* ]]; then
|
||||
is_windows=true
|
||||
servers_dir="$HOME/.marimo/servers"
|
||||
else
|
||||
servers_dir="${XDG_STATE_HOME:-$HOME/.local/state}/marimo/servers"
|
||||
fi
|
||||
|
||||
if [[ ! -d "$servers_dir" ]]; then
|
||||
echo "[]"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Liveness check. On POSIX, `kill -0 $pid` is cheap and reliable. On Windows
|
||||
# (Git Bash/MSYS2) `kill` operates on Cygwin PIDs, not the native Windows PIDs
|
||||
# marimo writes, so fall back to an HTTP probe against marimo's /health.
|
||||
check_live() {
|
||||
local f="$1"
|
||||
if [[ "$is_windows" == false ]]; then
|
||||
local pid
|
||||
pid=$(jq -r '.pid' "$f" 2>/dev/null) || return 1
|
||||
kill -0 "$pid" 2>/dev/null
|
||||
else
|
||||
local host port base_url
|
||||
host=$(jq -r '.host' "$f" 2>/dev/null) || return 1
|
||||
port=$(jq -r '.port' "$f" 2>/dev/null) || return 1
|
||||
base_url=$(jq -r '.base_url' "$f" 2>/dev/null) || return 1
|
||||
curl -sf --max-time 1 "http://${host}:${port}${base_url}/health" >/dev/null 2>&1
|
||||
fi
|
||||
}
|
||||
|
||||
results="[]"
|
||||
for f in "$servers_dir"/*.json; do
|
||||
[[ -e "$f" ]] || continue
|
||||
|
||||
if ! check_live "$f"; then
|
||||
# On Windows the HTTP probe can fail transiently (slow start, busy server),
|
||||
# so keep the entry; only POSIX `kill -0` is reliable enough to delete on.
|
||||
[[ "$is_windows" == false ]] && rm -f "$f"
|
||||
continue
|
||||
fi
|
||||
|
||||
entry=$(jq '.' "$f" 2>/dev/null) || continue
|
||||
results=$(echo "$results" | jq --argjson e "$entry" '. + [$e]')
|
||||
done
|
||||
|
||||
echo "$results" | jq .
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env bash
|
||||
# Execute code in a running marimo session's scratchpad.
|
||||
# No marimo installation required — talks directly to the HTTP API.
|
||||
# Usage:
|
||||
# execute-code.sh [--port PORT] [--session ID] -c "code" # inline code
|
||||
# execute-code.sh [--port PORT] [--session ID] script.py # code from file
|
||||
# execute-code.sh [--port PORT] [--session ID] <<< "code" # stdin (here-string)
|
||||
# execute-code.sh [--port PORT] [--session ID] <<'EOF' # stdin (heredoc)
|
||||
# code
|
||||
# EOF
|
||||
# execute-code.sh --url URL [--session ID] -c "code" # skip discovery, hit URL directly
|
||||
#
|
||||
# Auth: set MARIMO_TOKEN env var (preferred) or pass --token TOKEN (visible in ps).
|
||||
set -euo pipefail
|
||||
|
||||
# Optional eval logging: set EXECUTE_CODE_LOG to a file path to record each call
|
||||
if [[ -n "${EXECUTE_CODE_LOG:-}" ]]; then
|
||||
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$EXECUTE_CODE_LOG"
|
||||
fi
|
||||
|
||||
port=""
|
||||
code=""
|
||||
url=""
|
||||
token="${MARIMO_TOKEN:-}"
|
||||
session=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--port) port="$2"; shift 2 ;;
|
||||
--url) url="$2"; shift 2 ;;
|
||||
--token) token="$2"; shift 2 ;;
|
||||
--session) session="$2"; shift 2 ;;
|
||||
-c) code="$2"; shift 2 ;;
|
||||
-*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||
*) break ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -n "$code" ]]; then
|
||||
: # set via -c
|
||||
elif [[ $# -gt 0 ]]; then
|
||||
code=$(cat "$1")
|
||||
elif [[ ! -t 0 ]]; then
|
||||
code=$(cat)
|
||||
else
|
||||
echo "Usage: execute-code.sh [--port PORT | --url URL] -c 'code'" >&2
|
||||
echo " execute-code.sh [--port PORT | --url URL] script.py" >&2
|
||||
echo " echo 'code' | execute-code.sh [--port PORT | --url URL]" >&2
|
||||
echo "Auth: set MARIMO_TOKEN env var (preferred) or pass --token TOKEN" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "$url" ]]; then
|
||||
base="${url%/}"
|
||||
# Warn when connecting to a non-local server (data exfiltration risk)
|
||||
url_host="${url#*://}"
|
||||
url_host="${url_host%%[:/]*}"
|
||||
case "$url_host" in
|
||||
localhost|127.0.0.1|::1|0.0.0.0) ;;
|
||||
*) echo "Warning: connecting to non-local server '${url_host}'. Ensure this is trusted." >&2 ;;
|
||||
esac
|
||||
else
|
||||
# Locate the servers directory
|
||||
is_windows=false
|
||||
if [[ "$OSTYPE" == msys* || "$OSTYPE" == cygwin* ]]; then
|
||||
is_windows=true
|
||||
servers_dir="$HOME/.marimo/servers"
|
||||
else
|
||||
servers_dir="${XDG_STATE_HOME:-$HOME/.local/state}/marimo/servers"
|
||||
fi
|
||||
|
||||
# Liveness check. On POSIX, `kill -0 $pid` is cheap and reliable. On Windows
|
||||
# (Git Bash/MSYS2) `kill` operates on Cygwin PIDs, not the native Windows PIDs
|
||||
# marimo writes, so fall back to an HTTP probe against marimo's /health.
|
||||
check_live() {
|
||||
local f="$1"
|
||||
if [[ "$is_windows" == false ]]; then
|
||||
local pid
|
||||
pid=$(jq -r '.pid' "$f" 2>/dev/null) || return 1
|
||||
kill -0 "$pid" 2>/dev/null
|
||||
else
|
||||
local host port base_url
|
||||
host=$(jq -r '.host' "$f" 2>/dev/null) || return 1
|
||||
port=$(jq -r '.port' "$f" 2>/dev/null) || return 1
|
||||
base_url=$(jq -r '.base_url' "$f" 2>/dev/null) || return 1
|
||||
curl -sf --max-time 1 "http://${host}:${port}${base_url}/health" >/dev/null 2>&1
|
||||
fi
|
||||
}
|
||||
|
||||
# Find a live registry entry
|
||||
entry=""
|
||||
count=0
|
||||
for f in "$servers_dir"/*.json; do
|
||||
[[ -e "$f" ]] || continue
|
||||
|
||||
if ! check_live "$f"; then
|
||||
# On Windows the HTTP probe can fail transiently (slow start, busy server),
|
||||
# so keep the entry; only POSIX `kill -0` is reliable enough to delete on.
|
||||
[[ "$is_windows" == false ]] && rm -f "$f"
|
||||
continue
|
||||
fi
|
||||
|
||||
e=$(cat "$f")
|
||||
|
||||
if [[ -n "$port" ]]; then
|
||||
e_port=$(echo "$e" | jq -r '.port')
|
||||
if [[ "$e_port" == "$port" ]]; then
|
||||
entry="$e"
|
||||
count=1
|
||||
break
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
|
||||
entry="$e"
|
||||
count=$((count + 1))
|
||||
done
|
||||
|
||||
if [[ $count -eq 0 ]]; then
|
||||
echo "No running marimo instances found." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $count -gt 1 ]]; then
|
||||
echo "Multiple instances found. Use --port to specify:" >&2
|
||||
for f in "$servers_dir"/*.json; do
|
||||
[[ -e "$f" ]] || continue
|
||||
check_live "$f" || continue
|
||||
jq -r '.server_id' "$f" >&2
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
|
||||
host=$(echo "$entry" | jq -r '.host')
|
||||
e_port=$(echo "$entry" | jq -r '.port')
|
||||
base_url=$(echo "$entry" | jq -r '.base_url')
|
||||
base="http://${host}:${e_port}${base_url}"
|
||||
fi
|
||||
|
||||
# Build optional auth header
|
||||
auth_args=()
|
||||
if [[ -n "$token" ]]; then
|
||||
auth_args+=(-H "Authorization: Bearer ${token}")
|
||||
fi
|
||||
|
||||
# Discover session ID
|
||||
if [[ -n "$session" ]]; then
|
||||
session_id="$session"
|
||||
else
|
||||
sessions_resp=$(curl -sf "${auth_args[@]+"${auth_args[@]}"}" "${base}/api/sessions") || {
|
||||
echo "Failed to connect to marimo server at ${base}" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
session_ids=$(echo "$sessions_resp" | jq -r 'keys[]')
|
||||
|
||||
if [[ -z "$session_ids" ]]; then
|
||||
echo "No active sessions on the server. Make sure a notebook is open in the browser." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
session_count=$(echo "$session_ids" | wc -l | tr -d ' ')
|
||||
|
||||
if [[ $session_count -gt 1 ]]; then
|
||||
echo "Multiple sessions on server. Cannot auto-select:" >&2
|
||||
echo "$sessions_resp" | jq -r 'to_entries[] | "\(.key) \(.value.filename // "")"' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
session_id=$(echo "$session_ids" | head -1)
|
||||
fi
|
||||
|
||||
# Execute code via SSE stream
|
||||
# Events: stdout/stderr stream as JSON {"data":"..."}, done is final result.
|
||||
exit_code=0
|
||||
current_event=""
|
||||
done_received=false
|
||||
while IFS= read -r line && [[ "$done_received" == false ]]; do
|
||||
case "$line" in
|
||||
event:*)
|
||||
current_event="${line#event: }"
|
||||
;;
|
||||
data:*)
|
||||
payload="${line#data: }"
|
||||
case "$current_event" in
|
||||
stdout)
|
||||
echo "$payload" | jq -jr '.data'
|
||||
;;
|
||||
stderr)
|
||||
echo "$payload" | jq -jr '.data' >&2
|
||||
;;
|
||||
done)
|
||||
if echo "$payload" | jq -e '.success == false' >/dev/null 2>&1; then
|
||||
echo "$payload" | jq -r '.error.msg' >&2
|
||||
exit_code=1
|
||||
else
|
||||
echo "$payload" | jq -r '.output.data // empty'
|
||||
fi
|
||||
done_received=true
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
done < <(curl -sN -X POST "${base}/api/kernel/execute" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Marimo-Session-Id: ${session_id}" \
|
||||
${auth_args[@]+"${auth_args[@]}"} \
|
||||
-d "$(jq -n --arg c "$code" '{code: $c}')" \
|
||||
)
|
||||
|
||||
exit "$exit_code"
|
||||
@@ -0,0 +1,141 @@
|
||||
---
|
||||
name: retro-marimo-pair
|
||||
description: >-
|
||||
Session retrospective for improving marimo-pair and marimo._code_mode.
|
||||
Use when the user wants to analyze friction from a pairing session, identify
|
||||
what went wrong, and brainstorm improvements to the skill docs or the
|
||||
underlying API. Trigger on: "retro", "what went wrong", "improve the skill",
|
||||
"session review", "friction", or /retro-marimo-pair.
|
||||
---
|
||||
|
||||
# Session Retrospective
|
||||
|
||||
You are helping a **marimo team member** review a pairing session to find
|
||||
friction and turn it into improvements. The target is always one or both of:
|
||||
|
||||
1. **The marimo-pair skill** (`github://marimo-team/marimo-pair`)
|
||||
2. **`marimo._code_mode`** — the underlying notebook metaprogramming API
|
||||
|
||||
This is a **conversation**, not an automated report. You surface findings,
|
||||
the user steers which ones matter, and together you decide what to do about
|
||||
them.
|
||||
|
||||
## Guard Rails
|
||||
|
||||
- **NEVER** edit files in `github://marimo-team/marimo-pair` without explicit
|
||||
user approval.
|
||||
- **ALWAYS** start with session analysis (Step 1) — do not jump to solutions.
|
||||
- **Present friction points before root causes** — let the user choose which
|
||||
ones to dig into.
|
||||
- If the user invoked with a specific complaint, focus your analysis there but
|
||||
still scan for other friction in the background.
|
||||
|
||||
## Step 1: Session Analysis
|
||||
|
||||
Review the current conversation and identify friction. Look for:
|
||||
|
||||
| Signal | What to look for |
|
||||
|--------|-----------------|
|
||||
| **User frustration** | Corrections ("no not that"), repeated attempts, backtracking, confusion, tone shifts |
|
||||
| **Inefficiency** | Multiple rounds for a one-step task, over-engineering, wrong API usage |
|
||||
| **Errors** | Compile-check failures, runtime errors, silent failures, wrong output |
|
||||
| **Workarounds** | User or Claude working around a limitation instead of doing it directly |
|
||||
| **Context loss** | Claude forgetting instructions from earlier, re-asking things the skill covers |
|
||||
|
||||
Present a numbered summary of friction points found. For each, note:
|
||||
- What happened (brief)
|
||||
- Where in the conversation it occurred (quote or paraphrase)
|
||||
- Initial category guess (skill structure / skill gap / API issue / etc.)
|
||||
|
||||
Then ask: **"Which of these should we dig into? Or is there something I missed?"**
|
||||
|
||||
## Step 2: Root Cause Discussion
|
||||
|
||||
For each friction point the user selects, work through these lenses:
|
||||
|
||||
| Lens | Question | Example improvement |
|
||||
|------|----------|-------------------|
|
||||
| **Skill structure** | Was the right info in the skill but hard to find? Buried in reference/ when it should be in SKILL.md? | Promote to guard rail, restructure progressive disclosure |
|
||||
| **Skill gap** | Was information missing entirely from the skill? | Add new section, example, or anti-pattern |
|
||||
| **Misleading docs** | Did the skill say something that led Claude astray? | Correct the docs, add clarifying examples |
|
||||
| **API ergonomics** | Was `_code_mode` clunky or unintuitive for this task? | Propose API improvement (better defaults, clearer errors) |
|
||||
| **Missing API** | Is there something `_code_mode` simply can't do that it should? | Design a new API surface |
|
||||
| **API bug** | Did `_code_mode` behave incorrectly? | Characterize the bug, propose fix or workaround |
|
||||
| **Context window** | Did Claude forget instructions due to long context? | Shorter, more prominent guard rails |
|
||||
|
||||
Discuss each lens briefly, then converge on the most likely root cause with the
|
||||
user. It's okay to have multiple contributing causes.
|
||||
|
||||
## Step 3: Diagnose & Capture
|
||||
|
||||
The goal of a retro is **diagnosis**, not a contribution. Based on the root
|
||||
cause, write up a clear diagnosis the team can act on — don't jump to proposing
|
||||
or authoring a fix.
|
||||
|
||||
For each friction point, produce:
|
||||
|
||||
- **Diagnosis** — What went wrong and why it was frustrating, in plain terms
|
||||
- **Contributing factors** — Skill structure, gap, misleading docs, API
|
||||
ergonomics, missing API, API bug, context window (from Step 2)
|
||||
- **Considerations** — Trade-offs, open questions, or things that would need to
|
||||
be true for a fix to make sense. Note possible directions here, but frame
|
||||
them as considerations rather than committed solutions.
|
||||
|
||||
The default next step is to **capture the diagnosis as an issue or discussion**
|
||||
so the team can weigh it — not to immediately make a contribution. Concrete
|
||||
code changes (skill edits, API designs) come *after* an issue/discussion
|
||||
exists and the user explicitly chooses to go further.
|
||||
|
||||
Present the diagnosis and ask: **"Want me to draft this as an issue or
|
||||
discussion?"**
|
||||
|
||||
## Step 4: File or Follow Up
|
||||
|
||||
### Default: file an issue / discussion
|
||||
|
||||
Write it up clearly for the marimo team to triage:
|
||||
|
||||
- **Problem:** What happened and why it's painful
|
||||
- **Current behavior:** What the skill or `_code_mode` does today
|
||||
- **Considerations:** Trade-offs and open questions (not a committed solution)
|
||||
- **Example:** A minimal snippet or quote from the session, if helpful
|
||||
|
||||
Leave the actual filing to the user — do not auto-file. This is the preferred
|
||||
outcome: surface friction for the team rather than ship a fix from the retro.
|
||||
|
||||
### Only if the user explicitly wants to go further
|
||||
|
||||
A skill edit or API change should follow an issue/discussion, not replace it.
|
||||
If — and only if — the user explicitly asks to draft a change now:
|
||||
|
||||
1. Read the target file in `github://marimo-team/marimo-pair`
|
||||
2. Show the proposed diff to the user
|
||||
3. Only apply after explicit sign-off
|
||||
4. After applying, verify SKILL.md stays under 500 lines (reference/ files
|
||||
have no limit)
|
||||
|
||||
### Wrapping up
|
||||
|
||||
After completing the cycle for the selected friction points, ask if the user
|
||||
wants to revisit any remaining items from Step 1, or if the retro is done.
|
||||
|
||||
## Key Files Reference
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `github://marimo-team/marimo-pair/SKILL.md` | Main skill instructions |
|
||||
| `github://marimo-team/marimo-pair/reference/execute-code.md` | Scratchpad & cell operation recipes |
|
||||
| `github://marimo-team/marimo-pair/reference/rich-representations.md` | Widget & display patterns |
|
||||
| `github://marimo-team/marimo-pair/scripts/` | Bundled discovery & execution scripts |
|
||||
|
||||
To inspect the live `_code_mode` API surface during a retro, the user can
|
||||
run in their notebook scratchpad:
|
||||
|
||||
```python
|
||||
import marimo._code_mode as cm
|
||||
|
||||
async with cm.get_context() as ctx:
|
||||
# List all public methods/attributes
|
||||
print([x for x in dir(ctx) if not x.startswith('_')])
|
||||
help(ctx)
|
||||
```
|
||||
@@ -0,0 +1,62 @@
|
||||
# `marimo._code_mode` API Surface
|
||||
|
||||
> This is a **point-in-time snapshot** for retro discussions. The live API may
|
||||
> differ — always verify with `dir(ctx)` and `help()` in the running session.
|
||||
|
||||
## Entry Point
|
||||
|
||||
```python
|
||||
import marimo._code_mode as cm
|
||||
|
||||
async with cm.get_context() as ctx:
|
||||
... # all operations go here
|
||||
```
|
||||
|
||||
The `async with` is mandatory — without it, operations silently do nothing.
|
||||
The context manager auto-compile-checks on exit: syntax errors, multiply-defined
|
||||
names, and cycles are caught before any graph mutation occurs.
|
||||
|
||||
## Context Object (`ctx`)
|
||||
|
||||
### Reading State
|
||||
|
||||
| Attribute / Method | Returns | Notes |
|
||||
|-------------------|---------|-------|
|
||||
| `ctx.cells` | List of cell objects | Each has `.cell_id`, `.code`, `.name` |
|
||||
| `ctx.graph` | Kernel graph | Has refs/defs info (cells themselves lack this) |
|
||||
| `dir(ctx)` | All attributes | Always check this first — API evolves |
|
||||
|
||||
### Cell Operations (Mutating)
|
||||
|
||||
| Operation | Method | Notes |
|
||||
|-----------|--------|-------|
|
||||
| Create cell | `ctx.create_cell(code, ...)` | Adds to graph, auto-compile-checks |
|
||||
| Edit cell | `ctx.edit_cell(cell_id, code, ...)` | Edits existing cell in-place |
|
||||
| Delete cell | `ctx.delete_cell(cell_id)` | Confirm with user first |
|
||||
| Move cell | `ctx.move_cell(cell_id, ...)` | Reorder in notebook |
|
||||
|
||||
### Execution
|
||||
|
||||
| Operation | Method | Notes |
|
||||
|-----------|--------|-------|
|
||||
| Execute code (scratchpad) | Via `execute-code.sh` or MCP | Results return to Claude, not user |
|
||||
| Execute cell | `ctx.run_cell(cell_id)` | Explicitly queue execution; `create_cell` / `edit_cell` are structural only and do not auto-execute |
|
||||
|
||||
### Package Management
|
||||
|
||||
| Operation | Method | Notes |
|
||||
|-----------|--------|-------|
|
||||
| Install package | Explore via `dir(ctx)` | Prefer API over `uv add` |
|
||||
|
||||
## Known Friction Points
|
||||
|
||||
Track recurring issues here as they surface in retros:
|
||||
|
||||
- **Compile-check false positives on delete+create:** When deleting a cell and
|
||||
creating a replacement that defines the same variables, the compile check can
|
||||
see the old definitions still present and reject the new cell. Workaround:
|
||||
use `check=False` or `edit_cell` instead of delete+create.
|
||||
|
||||
- **`ctx.cells` lacks refs/defs:** Cell objects don't expose which variables
|
||||
they reference or define. Must use `ctx.graph` directly for variable flow
|
||||
analysis.
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../.agents/skills/marimo-pair
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../.agents/skills/retro-marimo-pair
|
||||
@@ -7,3 +7,4 @@ artifacts/
|
||||
|
||||
# marimo session cache
|
||||
nbs/__marimo__/
|
||||
docs/
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "docs/vendor/AntiPaSTO_concepts"]
|
||||
path = docs/vendor/AntiPaSTO_concepts
|
||||
url = https://github.com/wassname/AntiPaSTO_concepts.git
|
||||
@@ -0,0 +1,45 @@
|
||||
# Claude: small-multiples dose-response for the 7-method demo, styled like
|
||||
# word_steering's plot_sweep but read straight from the edge-find detail in
|
||||
# steering_demo_results.json. y = P(YES), point colour = ans_mass (readout
|
||||
# validity: bright = answer alive, dark = answer dying). red edge = readout
|
||||
# invalid (ans_mass < 0.9*base). Single-seed edge-find, so no error bars.
|
||||
import json
|
||||
from pathlib import Path
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
d = json.load(open(ROOT / "artifacts" / "steering_demo_results.json"))
|
||||
detail, summary = d["detail"], {r["method"]: r for r in d["summary"]}
|
||||
BASE_PYES = 0.107 # P(YES)@C=0, shared across methods (oracle brief)
|
||||
|
||||
methods = list(detail)
|
||||
fig, axes = plt.subplots(2, 4, figsize=(15, 7), sharey=True, layout="constrained")
|
||||
axes = axes.ravel()
|
||||
for ax, m in zip(axes, methods):
|
||||
pts = sorted(detail[m], key=lambda p: p["C"])
|
||||
Cs = [p["C"] for p in pts]
|
||||
pyes = [p["ans"] for p in pts]
|
||||
am = [p["ans_mass"] for p in pts]
|
||||
edges = ["red" if not p["readout_valid"] else "0.2" for p in pts]
|
||||
ax.plot(Cs, pyes, "-", color="0.85", lw=1, zorder=1)
|
||||
ax.axvline(0, color="0.85", lw=0.8, zorder=0)
|
||||
ax.axhline(BASE_PYES, color="0.85", lw=0.8, ls="--", zorder=0)
|
||||
sc = ax.scatter(Cs, pyes, c=am, cmap="viridis", vmin=0.0, vmax=1.0,
|
||||
edgecolor=edges, linewidth=1.4, s=70, zorder=2)
|
||||
s = summary[m]
|
||||
ax.set_title(f"{m}\nscore={s['score']:+.3f} ok={s['readout_ok']}", fontsize=9)
|
||||
ax.set_ylim(-0.03, 1.03)
|
||||
ax.set_xlabel("steering coefficient C", fontsize=8)
|
||||
axes[0].set_ylabel("P(YES = lie)")
|
||||
axes[4].set_ylabel("P(YES = lie)")
|
||||
for ax in axes[len(methods):]:
|
||||
ax.set_visible(False)
|
||||
cbar = fig.colorbar(sc, ax=axes.tolist(), label="ans_mass (readout validity)",
|
||||
fraction=0.025, pad=0.01)
|
||||
cbar.ax.axhline(0.90 * 0.56, color="red", lw=1) # ~0.9*base_am floor (base_am~0.56)
|
||||
fig.suptitle("Dose-response per method: P(YES) vs C, coloured by answer-mass "
|
||||
"(dark = answer dying). Dashed = baseline P(YES)=0.107. Single seed.",
|
||||
fontsize=11)
|
||||
out = ROOT / "artifacts" / "steering_demo_sweep.png"
|
||||
fig.savefig(out, dpi=110)
|
||||
print("wrote", out)
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"version": 1,
|
||||
"skills": {
|
||||
"marimo-pair": {
|
||||
"source": "marimo-team/marimo-pair",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/marimo-pair/SKILL.md",
|
||||
"computedHash": "85a494e8076cbea237bfbd80592a542df869b693f0b61aa90c0a48d7f4afb5a7"
|
||||
},
|
||||
"retro-marimo-pair": {
|
||||
"source": "marimo-team/marimo-pair",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/retro-marimo-pair/SKILL.md",
|
||||
"computedHash": "1f89f0999518297d5a0928528c87fc082da9645cac554b0101049042ec661d7b"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user