WriterAgent can run scientific Python — NumPy, pandas, scipy, and similar C-extension stacks — without loading those packages into LibreOffice’s embedded interpreter. Point Settings → Python at a user-provided virtual environment, then use Run Python Script…, Calc =PY() / Edit Python in Cell…. Code runs in a sandboxed child process and returns serializable results to your sheet or script UI.
Glossary: =PY() and =PYTHON() are the same Calc add-in (XPythonFunction). User-facing examples in this file use =PY(). The registered alias PYTHON works the same (some ODS fixtures use =PYTHON() because XLSX import lowercases custom add-in names).
These Python / NumPy features also now ship in LibrePy.oxt. The WriterAgent extension covers the same Python surfaces plus a prototype Calc to =PY() spreadsheet conversion, chat and related tools — install one OXT at a time (see extension packaging).
- The problem: ABI and embedded Python
- Strategy decision
- User guide
- Architecture
- Developer reference
- The
=PY()Calc function
- Session modes and recalc semantics
- Keyboard shortcuts and recalc
- Calc formula lexer quirks (inline code)
- Data shapes (
data/ blanks / varargs)
| Document | Description / Notes |
|---|---|
Calc =PY() data shapes |
CalcRange, ingress/egress, blanks vs NaN, dates, multi-range |
| Venv subprocess IPC & NumPy serialization | Warm worker, protocol, wire formats, benchmarks |
Why not copy Microsoft’s =PY() |
xl() + co-volatility costs vs native =PY(code, data?) |
| NumPy domain helpers | Analysis, Viz, Symbolic, Units, Text, Forecasting |
| Extension packaging | LibrePy vs WriterAgent packaging |
| LibrePy-surface live QA plan | Real-scenario Calc/RPS/domain checks (=PY("1 + 1") upward). Either OXT; do not test chat/=PROMPT(). |
| Monaco editor dev plan | IPC, phases 2B–2F |
| Collabora Online / jail-safe | Thin C++ Add-In + compute service |
| Calc spreadsheet → Python import | Convert formulas to =PY() |
| Jupyter notebook import | Writer .ipynb import + ▶ run (shared notebook:… kernel; not Calc =PY()) |
numpy is not pure Python; it ships compiled C/C++ extensions that must match the exact Python ABI they were built for.
- The problem: If a user runs
pip install numpywith system Python 3.12 and the extension loads that build into LibreOffice’s embedded Python (often 3.8–3.11), LibreOffice can fatally crash — the extensions are binary-incompatible. - The requirement: NumPy (and similar wheels) must be installed into the same
pythonexecutable that runs the code, or execution must stay in a separate interpreter that never shares memory with LibreOffice.
All design choices below follow from that constraint.
| Approach | Status | Summary |
|---|---|---|
| 1 — Pip bootstrap inside LibreOffice | Rejected | Ship pip and install packages into LO’s runtime at startup (LibrePythonista-style). Requires heavy path/sandbox handling (Flatpak, macOS, Windows) and couples the extension to the embedded interpreter. |
| 2 — User-provided venv + subprocess | Chosen | User points scripting.python_venv_path at an existing .venv. The extension never imports NumPy in-process. |
- Persistent worker:
[PythonWorkerManager](plugin/scripting/venv_worker.py)spawns the venv’spythononce per executable path and keeps it alive. - Namespace per request (configurable):
[worker_harness.py](plugin/scripting/venv/worker_harness.py)→[venv_sandbox.py](plugin/scripting/venv/venv_sandbox.py)uses a[LocalPythonExecutor](plugin/contrib/smolagents/local_python_executor.py). Default Isolated mode gives each=PY()cell a fresh namespace (init script still seeds once). Shared kernel mode ([session_manager.py](plugin/scripting/session_manager.py)) keeps one workbook namespace across cells — see §6 Session modes. - Length-prefixed Pickle5 IPC:
[PythonWorkerManager](plugin/scripting/venv_worker.py)↔[worker_harness.py](plugin/scripting/venv/worker_harness.py)exchange framed request/response dicts;data/resultuse[split_grid](scripting/numpy-serialization.md#strategy-3-split-grid-serialization-detail)when dense. Protocol detail: Venv subprocess IPC. Bidirectional tool RPC is not wired yet (§7).
Pros: Sidesteps ABI issues; any Python version in the venv; avoids spawn overhead on every call; optional shared-kernel mode for multi-cell pipelines.
Cons: User must create and maintain a venv; in Isolated mode, re-pass data via data / data_range or cell references unless Shared kernel is enabled.
You can run Monte Carlo simulations, statistics, plots, and other library-heavy work without leaving LibreOffice. Configure a dedicated Python venv once, then:
- Run Python Script… in Writer, Calc, or Draw (Monaco editor or a simple dialog)
=PY()formulas and Edit Python in Cell… in Calc- Edit Initialization Script… for workbook-wide helpers (Calc)
No terminal is required after the venv is set up. An optional chat assistant (WriterAgent) can generate and run the same kind of code.
| Setting | Description | Example |
|---|---|---|
scripting.python_venv_path |
Absolute path to an existing venv directory | ~/.writeragent_venv |
scripting.python_session_mode |
isolated (default) or shared (Shared kernel for =PY() cells) |
isolated |
scripting.python_exec_timeout |
Wall-clock limit (seconds) for Run Python Script and =PY() (default 10, max 600). Trusted long-running helpers (OCR, spaCy, SymPy, embeddings, …) use a longer internal budget. |
10 |
scripting.python_auto_spill |
On by default. Single-cell =PY() returning a list, 2D array, or DataFrame auto-spills into adjacent cells. Blocked cells → #SPILL!. Disable for matrix-only workflows. |
true |
- Empty path: Run Python Script and
=PY()fall back to LibreOffice’s embedded Python — usually stdlib-only. Use a dedicated venv for NumPy. - No automatic venv creation — you bring your own environment.
- Path paste: Paste the path without surrounding quotes (Windows Explorer “Copy as path” adds them — strip them). Spaces, parentheses, and non-ASCII home dirs are fine; spawn uses argv lists, not a shell.
- Layouts: uv / Poetry / stdlib
venv→bin/pythonorScripts\python.exe. Windows conda / pyenv-win → env folder orpython.exeat the env root also works. - Test button: Checks that the path resolves to a
pythonexecutable and reports which package groups are Present/Missing (Scientific, Data Analysis / EDA, UI / Monaco, Vision, Embeddings, Audio Recording). After Test finishes, if packages are still Missing, the message ends with copy-pasteuv pip install …thenpip install …for those packages (not shown during progressive refresh). Vision marks paddle/ultralytics/skimage as optional when OCR is already ready. Cold Vision/Embeddings imports can take ~30s on first Test. Domain package lists: scripting/numpy-domains.md, Image Recognition, Embeddings. Microphone capture uses the same venv (uv pip install sounddevice) — see chat/audio-architecture.md.
Creating the venv (uv recommended in 2026):
# Create a dedicated venv (choose a Python close to what you develop with)
uv venv ~/.writeragent_venv --python 3.12
# Activate (optional for uv pip) and install what you need
source ~/.writeragent_venv/bin/activate
uv pip install numpy pandas scipy scikit-learn matplotlib sympy spacy textdescriptives
python -m spacy download xx_sent_ud_sm
# Point Settings → Python at the venv root (~/.writeragent_venv) or the bin/ dir.For Monaco (recommended editor UI), also install pywebview (on Linux: PyQt6 PyQt6-WebEngine qtpy).
| What you use | Where | Notes |
|---|---|---|
| LibrePy Python sidebar (Calc) | LibrePy and WriterAgent | Cell list, diagnostics (stdout/errors), Reset / Edit Init / Run Script / Settings — Monaco stays separate; Calc-only Python deck |
| Run Python Script… | Writer / Calc / Draw menu | Monaco editor (Run / Save / script picker), or a plain multiline dialog if pywebview is missing |
Edit Python in Cell… / =PY(code, data?) |
Calc | Monaco when pywebview is available; otherwise a native dialog with the same Save / Data / Save without =PY() chrome. Dual save as =PY("…") or plain text for =PY($A$1; …) |
| Edit Initialization Script… | Calc (sidebar or Monaco) | Workbook startup script; seeds helpers for every =PY() cell |
| Shared warm worker | All of the above | One subprocess per venv path ([venv_worker.py](plugin/scripting/venv_worker.py)) |
There is no UNO API inside the child process today — scripts compute and return values; document updates happen via the UI, formula spill, or (on the chat path) normal document tools.
(Developer note: an older in-process execute_python_script path uses LibreOffice’s embedded Python and is not used by =PY().)
The extension ships a Monaco-based code editor (pywebview child in the configured venv) for Calc formulas and ad-hoc scripts. Theme sync with LibreOffice light/dark is shipped. IPC and remaining editor backlog: scripting/monaco-editor-dev-plan.md.
| Feature | Status | Notes |
|---|---|---|
| Edit Python in Cell… (Calc menubar + cell context menu) | Shipped | Dual save (=PY("…") or plain text for =PY($A$1; …)); editable Data: range. Experimental: Settings → Python → Rewrite xl() ranges (default off; beside auto-spill) lifts static xl("A1:…") onto formula data args on Monaco save and rewrites call sites to polymorphic data / data[i] / .to_pandas() (xl_static_rewrite.py). |
| Run Python Script… (Writer/Calc/Draw) | Shipped | Run / Save / script picker. In Calc, structured results (dicts, tables, DataFrames) format as rich HTML tables and insert via controller transferable paste, registering a native ScUndo action for single-step Ctrl+Z undo. |
| Document-attached scripts | Shipped | This Document vs My Scripts in the picker — scripts can travel with .odt/.ods/.odg |
| Edit Initialization Script… (Calc) | Shipped | Workbook startup script in document properties; LibrePy sidebar button + Monaco |
| LibrePy Python sidebar (Calc deck) | Shipped | Cell list, filtered diagnostics, session/actions — not an embedded Monaco editor |
| Syntax squiggles, range picker, full Jedi | Backlog | Monaco dev plan §8 |
Requirements: Settings → Python → venv path with pywebview installed (Linux also needs PyQt6 PyQt6-WebEngine qtpy) for the Monaco UI. Edit Python in Cell… and Run Python Script… both fall back to native LibreOffice dialogs when pywebview is missing or scripting.force_internal_script_editor is true. Native cell edit does not need a venv.
Document-attached scripts: Named scripts live in document properties so they travel with the file. Monaco supports Attach / Copy to My Scripts; read-only documents fall back to the personal library (My Scripts in writeragent.json) with a clear message.
Undo behavior: In Calc, running a Python script that inserts tabular or structured results registers a native undo action in LibreOffice Calc's internal undo manager. Pressing Ctrl+Z (Edit → Undo) immediately removes all inserted titles, headers, and rows in a single step.
Prefer result = … for the value that should appear in the sheet or script UI. If you never assign result, =PY() uses the last expression value (same Jupyter-style fallback as Excel). In shared-kernel mode a successful result = … stays in the namespace so later cells can use it (result * 1.1); leftover result is used as this cell’s sheet value only when this cell rebound it, so last-expression cells are not hijacked. A failed cell restores the previous result. NumPy arrays and pandas objects are serialized in the worker. print() is diagnostics only (LibrePy Python sidebar) — it does not become the cell value.
WriterAgent only — not part of the core Python/NumPy extension; see extension split.
You can also ask the sidebar chat to run the same venv Python. The model uses the specialized tool run_venv_python_script (domain python) — same warm worker as the menus and =PY(). Chat runs are always isolated (they do not share the Calc workbook kernel).
The assistant does not write into the document from inside the venv subprocess:
- Compute: Call
run_venv_python_scriptwith numpy/pandas code; read serializedresult. - Insert: Call existing Calc/Writer tools (
write_formula_range,set_style,create_chart, etc.).
| Context | data / data_range? |
Injected in subprocess? |
|---|---|---|
Calc chat, domain=python |
Yes | Yes, when provided |
Writer / Draw chat, domain=python |
No | Never — use document tools for content |
=PY(code, range) |
2nd arg is the range | Yes |
What you see: ask for analysis → the model may show generated Python in Thinking → status Running Python script… → results return and the model updates the document via normal tools (or retries on error).
Wall-clock limit is still Settings → Python (scripting.python_exec_timeout); it is not a tool-schema parameter. Tool schema detail: §5 / [venv.py](plugin/calc/python/venv.py).
┌──────────────────────────────────────────────────────────┐
│ LibreOffice Process │
│ │
│ ┌──────────────────┐ ┌─────────────────────────────┐ │
│ │ Run Python Script │──▶│ run_code_in_user_venv │ │
│ │ =PY() / Edit Cell │ │ (shared entry) │ │
│ └──────────────────┘ └──────────┬──────────────────┘ │
│ ┌──────────────────┐ │ │
│ │ Chat (optional) │──────────────┘ │
│ │ run_venv_python… │ │
│ └──────────────────┘ │
│ ┌──────────▼───────────────────────┐ │
│ │ PythonWorkerManager │ │
│ │ warm venv process │ │
│ │ worker_harness → venv_sandbox │ │
│ └──────────┬───────────────────────┘ │
│ │ Pickle5 stream │
│ ┌──────────▼───────────────────────┐ │
│ │ User venv Python (subprocess) │ │
│ │ LocalPythonExecutor + whitelist │ │
│ └──────────┬───────────────────────┘ │
│ │ result / stdout │
│ ┌──────────▼───────────────────────┐ │
│ │ Sheet / script UI / (chat tools)│ │
│ └──────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
LibreOffice’s embedded Python and the user’s venv are different interpreters (§1). Venv execution uses the venv’s ast and packages; the subprocess boundary is the hard safety line for C extensions.
Subprocess lifecycle, worker protocol, Linux pipe performance, and serialization wire formats: Venv subprocess IPC & NumPy serialization.
Host↔venv plumbing (module map, worker protocol, python_max_data_cells, benchmarks): scripting/numpy-serialization.md.
Off by default. In plugin/calc/python/function.py set PYTHON_TIMINGS_LOG = True, rebuild/deploy, then with log_level DEBUG each =PY() / =PYTHON() evaluation writes one line to writeragent_debug.log starting with py_timing. Durations are measured with perf_counter inside the add-in — do not subtract log asctime values. Leave the flag False in committed code.
| Field | Meaning |
|---|---|
ipc_ms |
Time waiting on the venv worker (calculation + pickle). First cell after LO start includes spawn + prime. |
total_ms |
This add-in call, host entry through return |
pack_ms / image_ms |
Range pack; plot insert + formula locator |
cached |
1 if the matrix result session skipped a worker round-trip |
pass_wall_ms |
Wall from the first add-in in this recalc clump through this call |
pass_sum_ms |
Sum of total_ms in the clump |
pass_outside_ms |
pass_wall_ms - pass_sum_ms — time not in our add-in (Calc DAG, other formulas, drawing) |
After opening a demo workbook, grep py_timing and read the last line’s pass_* plus per-cell ipc_ms. A new clump starts if more than 2s elapsed since the previous add-in returned.
| Layer | Mechanism | Protects against |
|---|---|---|
| Restricted executor | LocalPythonExecutor in subprocess — AST walk, dunder guards (__version__ reads allowed), iteration/operation limits |
eval/exec, dunder escapes, infinite loops |
| Import whitelist | VENV_AUTHORIZED_IMPORTS in [sandbox.py](plugin/scripting/sandbox.py) (enforced in [venv/venv_sandbox.py](plugin/scripting/venv/venv_sandbox.py)). Nested os/sys on allowed modules (platform, writeragent) are stripped by get_safe_module. |
os, subprocess, socket, arbitrary filesystem access |
| Subprocess isolation | Separate interpreter, no shared memory with LO | ABI crashes, segfaults in C extensions, UNO corruption |
| Environment scrubbing | Strip secret-like env vars from child | Credential exfiltration via generated code |
| User-provided venv | Explicit opt-in | User controls installed packages |
| Timeout | Standard: user scripting.python_exec_timeout for scripts + quick helpers. Long trusted ops (OCR, spaCy text analytics, SymPy, embeddings, ...) use one internal LONG_TRUSTED... budget. Warm uses ~30s. |
Runaway computation |
Missing packages fail when code imports them (no import pre-check at executor init).
The AST sandbox is not a perfect security boundary; subprocess isolation is the real guarantee. Untrusted script strings (including LLM-generated code) are the threat model, not arbitrary hostile users with shell access.
Prompt text is generated from [plugin/scripting/import_policy.py](../plugin/scripting/import_policy.py) (whitelist in [sandbox.py](../plugin/scripting/sandbox.py)). It always leads with a sandbox context prefix before module lists so models know they are in an AST Python sandbox inside a same-user venv subprocess (not LibreOffice/UNO). Host pickle frames reconstruct builtin types only.
| Category | Modules |
|---|---|
Pre-imported (do not import in script) |
np, pd, sp, st (scipy.stats), plt (matplotlib.pyplot), math, dt (datetime), re, random, statistics, collections, itertools, json, csv, xl |
| Allowed stdlib | collections, copy, csv, dataclasses, datetime, decimal, enum, fractions, functools, itertools, json, math, operator, platform, pprint, queue, random, re, stat, statistics, string, textwrap, time, typing, unicodedata |
| Allowed packages (+ submodules where whitelisted) | See authoritative list in [sandbox.py](../plugin/scripting/sandbox.py) VENV_AUTHORIZED_IMPORTS (categories include numpy/pandas/scipy stack, domain helpers, embeddings, vision, …) |
| Always blocked | os, sys, subprocess, socket, pathlib, shutil, io, multiprocessing, pty, builtins |
| Common not-whitelisted | requests, urllib, http, httpx, ssl, pickle, sqlite3, logging, importlib, ctypes, threading, … |
In-process [execute_python_script](../plugin/calc/python/executor.py) uses a smaller stdlib-only sandbox in LibreOffice’s embedded Python (no NumPy/pandas).
The AST sandbox (LocalPythonExecutor + VENV_AUTHORIZED_IMPORTS) applies only to user-submitted Python source — Run Python Script, Calc =PY(), optional chat [run_venv_python_script](../plugin/calc/python/venv.py), and similar. It is not a blanket restriction on everything that runs inside the warm venv child process.
Shipped extension code can use the full venv interpreter (including open(), sqlite3, sqlite_vec.load(), and other modules blocked for sandboxed scripts) when implemented as shipped, reviewed modules under plugin/scripting/, invoked from the LibreOffice host — not from untrusted script strings.
| Layer | Interpreter | Sandbox? | Typical use |
|---|---|---|---|
| LibreOffice host | Embedded Python in-process | No NumPy; stdlib + UNO | UNO, config, enqueue maintain RPC |
| User venv worker | User’s venv subprocess | Yes for user code strings |
=PY(), Run Python Script, chat tool |
| Trusted venv modules | Same subprocess | No (normal CPython inside the module) | Reviewed modules such as [payload_codec.py](../plugin/scripting/payload_codec.py), [calc_functions.py](../plugin/scripting/calc_functions.py), embeddings index/search, langdetect RPC |
- Ship a normal module under
plugin/scripting/venv/(implementation) with a public facade atplugin/scripting/*.py, or underplugin/embeddings/venv/. - Host calls
[run_trusted_worker_action](../plugin/scripting/trusted_rpc.py)withaction: "run_trusted_action"and adomain+helperpacket — not LLM output. [worker_harness.py](../plugin/scripting/venv/worker_harness.py)looks up the domain in[trusted_action_registry.py](../plugin/scripting/trusted_action_registry.py)and calls the dispatcher directly (zero AST). Long-running maintain jobs can stream heartbeats (allow_heartbeat).- Run Python Script templates execute visible helper calls (e.g.
convert_quantity(data, "m/s", "km/h")). The host injectsdata/text/imagebefore execution; results use insert handlers in[domain_registry.py](../plugin/scripting/domain_registry.py). - Bulk data travels in the action
datadict (Pickle5). Trusted code opens host-supplied paths (for example the per-foldercorpus.db).
Embeddings worker pool: Folder maintain, hybrid search, and grammar Local (langdetect) use WORKER_POOL_EMBEDDINGS — a second warm venv child isolated from Calc =PY() (embeddings.md). Registry / dispatch detail: scripting-domain-debt-dev-plan.md.
- Do not tell sandboxed scripts (or the chat model) to
open()index paths or importsqlite3— blocked by design (import policy). - Do not widen the script whitelist to “fix” embeddings; add a trusted module instead.
- Do not run sqlite-vec or NumPy encode in LibreOffice’s embedded interpreter — stay on the venv side (embeddings).
Tool: run_venv_python_script with specialized_domain = "python". Registered for Calc; exposed in Writer/Draw via cross-cutting delegation when the LLM activates the python toolset (delegate_to_specialized_*_toolset(domain="python")), same pattern as other specialized domains.
See [plugin/calc/python/venv.py](plugin/calc/python/venv.py) — parameters code, optional data / data_range (Calc); long_running / async execution.
You can run Python from Calc via =PY(). Same warm worker as Run Python Script… ([venv_worker.py](plugin/scripting/venv_worker.py)). Configure Settings → Python → scripting.python_venv_path (§3).
IDL: any python( [in] string code, [in] any data ); in [extension/idl/XPythonFunction.idl](../extension/idl/XPythonFunction.idl). Rebuild [extension/XPythonFunction.rdb](../extension/XPythonFunction.rdb) and [extension/XPromptFunction.rdb](../extension/XPromptFunction.rdb) after IDL changes (scripts/rebuild_xprompt_rdb.sh — one .rdb per interface).
| Arg | Name | Required | Role |
|---|---|---|---|
| 0 | code |
Yes | Python source; evaluated result is returned |
| 1 | data |
No | Optional range(s) → data / ranges (Data shapes) |
Settings → Python → scripting.python_session_mode:
| Mode | Behavior |
|---|---|
| Isolated (default) | Each =PY() evaluation gets a fresh namespace. The initialization script still runs once per workbook and its imports/helpers are seeded into every cell — but variables assigned in one cell do not leak to the next. |
| Shared kernel | One persistent global namespace per workbook (calc:… session). Any cell can read or overwrite any name set by any other cell. Reset Python Session clears it. |
Optional chat runs always use isolated execution (not workbook session mode) — see Using the chat assistant.
Important
Mental model (Shared kernel): Calc may recalculate cells in any order — not row-major. The only ordering guarantee you get for free is init script before any cell. Assume each cell can run zero, one, or many times per workbook session. Write idempotent code (safe to re-run).
Microsoft Python in Excel keeps one global namespace per workbook. F9 / auto-recalc does not reset it — globals persist until Reset Runtime. Excel compensates with co-volatility: when any =PY cell recalculates, all PY cells re-execute in row-major order (co-volatility overview).
Shared kernel matches persistence (no auto-reset on F9) but does not co-volatile all Python cells. Calc uses its native dependency DAG: only dirty cells and their dependents recalculate. That sounds weaker than Excel until you use data as a dependency edge — see below.
| Approach | Ordering mechanism | Partial recalc |
|---|---|---|
Excel =PY + globals |
Row-major + all PY cells re-run | Heavy (co-volatility) |
Shared kernel + data refs |
Calc DAG (precedents first) | Only dirty subgraph recalcs |
Excel's shared globals depend on sheet position and co-volatility. Here the data argument declares a Calc dependency. When cell B1 uses a global set in A1, still write:
=PY("result = x + 1"; A1)
Calc tracks that B1 depends on A1 and runs A1 before B1 — no Python string parsing, no co-volatility tax. Chain pipelines (load → clean → aggregate → plot) by passing each stage as data, even when cells are not adjacent or not in row-major order.
Excel’s saved static bridges already put ranges on _xlws.PY trailing args (Excel→PY dirtying works); co-volatility is still how Excel orders PY↔PY / shared globals, not a substitute for DAG partial recalc among PY cells. Prefer =PY(code, data) over a naive UI-shaped xl()-inside-string Calc port (§7 comparison; package details in ms-py §5.8).
| Rule | Meaning |
|---|---|
| One namespace per workbook | The calc:… session lives until Reset Python Session, worker crash/restart, init-script hash change + re-seed, or document unload. The first =PY() for a workbook registers workbook_lifecycle.py so close/reopen runs the init script again. |
| Any cell can clobber any name | True shared mutable globals — cell B1 can overwrite a name from A1. |
Order via data, not layout |
result = x + 1 with no second arg has no ordering guarantee. Pass upstream cells/ranges as data. Init script always runs before any cell. |
| Runs any time | Partial recalc, matrix spill, manual F9 — treat every cell as restartable. |
| Escape hatch | Reset Python Session clears workbook namespace, wipes worker executors, and re-seeds helper functions. |
| Yellow recalc contract | =PY() formula recalculations run in a synchronous host dispatch context (sync_host_dispatch). Off-main recalc worker threads never query UNO desktop or document components; session IDs and init kwargs are resolved from UI-thread cached state. |
| When state clears | When state persists |
|---|---|
| Reset Python Session | F9 / automatic / partial recalc |
| Worker subprocess restart or crash | Names from cells that did not recalc this pass |
| Init script hash change (re-seed) | Until user resets (matches Excel: no reset on F9) |
Document close (OnUnload) |
- Wire dependencies with
data— pass upstream cells/ranges as the second arg so Calc's DAG runs precedents first; keep one-off setup in the initialization script (runs once per workbook — see Initialization scripts). - Avoid unbounded accumulation —
mylist.append(x)every recalc grows forever unless intentional. - One-time expensive work belongs in the init script, not repeated in every cell.
- Side effects (sheet writes, files, shapes) should be idempotent or clearly intentional on re-run.
Workbook initialization scripts can be defined via Edit Initialization Script… (Calc only). This stores a workbook startup script in document properties (calc:…:init). It runs once per workbook session even when the session mode is Isolated, making its imports and helper functions available to all =PY() cells.
In the worker sandbox:
- Functions and variables defined in the init script are executed in the companion
calc:…:initsession and snapshot into cell executors. - Custom functions (
def double(x): ...) are registered into the executor'scustom_toolsdictionary so that safe sandbox evaluation permits them without raisingForbidden function evaluation. - When Reset Python Session is invoked, both the base session (
calc:…) and the companion:initsession are dropped in the worker process, and the initialization script is immediately re-executed and re-seeded so helpers remain available.
Example helpers (no special excel module needed; use auto-imported np/pd/st/plt/dt/xl or explicit imports):
def format_currency(series):
return series.apply(lambda x: f"${x:,.2f}")
def kpi_summary(df, metrics):
return df[metrics].agg(["mean", "min", "max"]).round(2)A cell is idempotent when running it again (F9, edit elsewhere, partial recalc) produces the same intended outcome — it does not keep adding unwanted changes.
| Pattern | Idempotent? | Why |
|---|---|---|
result = data * 2 |
Yes | Same inputs → same result. |
result = df.groupby("col").sum() |
Yes | Derives from current data only. |
runs += 1; result = runs |
No | Counter grows on every recalc. |
cache.append(x) with no reset |
No | List grows each invocation. |
| Write a file / insert a shape every run | Usually no | Re-run duplicates side effects. |
Deliberate accumulation (running totals, etc.) is fine — treat it as a choice, not an accident. When in doubt, compute result from data and init helpers; use Reset Python Session for a clean slate.
Not reset automatically: F9, Ctrl+Shift+F9, or editing one cell does not clear the shared kernel.
Related: [WorkerResultSession](../plugin/calc/python/function.py) is a separate thread-local cache for matrix list results within one recalc pass — it does not hold cross-cell Python globals. See Matrix Formula Optimization.
Implementation: [session_manager.py](../plugin/scripting/session_manager.py), [venv/venv_sandbox.py](../plugin/scripting/venv/venv_sandbox.py) (_SESSION_EXECUTORS).
| Microsoft Python in Excel | Calc =PY() |
|
|---|---|---|
| Cell value | Last evaluated expression (Jupyter-style) | Prefer result = …; if unset, last expression (same fallback) |
print() / stdout |
Diagnostics pane only; cell gets None |
Captured in worker response; shown in LibrePy Python sidebar diagnostics (shipped); not written into the cell |
Top-level return |
Syntax error in Excel | Use result = … instead |
These are Calc shortcuts. They recalculate formulas; they do not clear the Python shared kernel unless noted.
| Shortcut | Calc action | Effect on Python session |
|---|---|---|
| F9 | Recalculate changed cells | Re-runs dirty =PY() cells; Shared kernel globals persist |
| Ctrl+Shift+F9 | Hard recalculate (all formulas) | Re-runs all =PY() cells; globals persist. Use after worker crash/NameError to rebuild DAG state (shared-kernel soft timeout) |
| Shift+F9 | Recalculate current sheet | Same persistence rules as F9, sheet-scoped |
Clear Python memory: Reset Python Session (menu). Excel’s analogue is Ctrl+Alt+Shift+F9 (Reset Runtime) — see target mapping below.
Matrix formulas: Confirm multi-cell blocks with Ctrl+Shift+Enter (Matrix formulas).
Parity targets for Calc =PY() UX (Microsoft Python in Excel product docs).
| Excel shortcut | Excel action | Today | Target / notes |
|---|---|---|---|
| =PY / Ctrl+Alt+Shift+P | Start Python cell / editor | Shipped — Ctrl+Alt+Shift+P opens Edit Python in Cell… (also menu / cell context menu) | Same chord as Excel |
| Ctrl+Enter | Commit code in editor | Monaco Save (Calc cell editor) | Same pattern in Monaco when editing multi-line scripts |
| Ctrl+Alt+Shift+F9 | Reset Python runtime | Shipped — Ctrl+Alt+Shift+F9 → Reset Python Session | WriterAgent: extension/Accelerators.xcu; LibrePy: extension-core/Accelerators.xcu |
| Ctrl+Alt+Shift+F2 | Toggle Python editor pane | Monaco window (separate process) | Optional: dock/focus toggle |
| Ctrl+Alt+Shift+C | Toggle plot float vs embedded | Plots insert as sheet images only | Floating plot layer → backlog |
| Ctrl+Alt+Shift+M | Toggle Value vs Object return | Always value egress today; object cards → Calc UX backlog | |
| Ctrl+Shift+F5 | Open object card preview | Not shipped | Object cards |
| Ctrl+Shift+U | Expand formula bar | Calc native (multi-line formula bar) | Formula-bar Jedi → Monaco 2D |
| F2 | Edit vs point mode (range pick) | Calc native cell edit | Monaco range picker should use same Enter/Point idea (Monaco 2C) |
| Ctrl+F2 | Focus formula bar ↔ grid | Calc native | — |
WriterAgent chat shortcuts (Writer/Calc): Ctrl+Q extend selection, Ctrl+E edit selection ([Accelerators.xcu](../extension/Accelerators.xcu)) — unrelated to =PY(), but often used alongside the sidebar.
Errors surface as cell text (and worker traceback in logs). LibrePy Python sidebar already shows structured diagnostics; optional glanceable cell traceback remains in Calc UX backlog.
| Excel code | Typical cause | Today |
|---|---|---|
| #PYTHON! | Syntax/runtime error | Error string in cell; Monaco on manual edit |
| #SPILL! | Blocked dynamic array spill | Shipped — sets formula cell to #SPILL! and highlights blocking cell |
| #TIMEOUT! | Exceeded runtime limit | Timeout message; Settings → scripting.python_exec_timeout |
| #BUSY! / #CONNECT! | Cloud kernel (N/A locally) | Worker spawn failure / venv misconfig |
| #CALC! | Payload too large / volatile deps | python_max_data_cells cap; avoid RAND()/NOW() in data precedents if problematic |
The return type in the IDL is declared as any to allow a dynamic union of return types, maximizing compatibility with both standard (single-cell) and matrix formulas.
LibreOffice Calc operates strictly on double-precision floats (double/float), strings (string/str), and booleans (boolean/bool) for cell values.
- The issue: Python integers (
int) returned from a script are marshaled by PyUNO as a sequence oflongs (e.g.sequence<sequence<long>>). - The consequence: Calc's formula engine lacks type coercion for integer matrices, immediately throwing a
#VALUE!error in the sheet. - The resolution: Every return value from
=PY()is recursively filtered through a coercion pipeline (to_calc_compatible):int->float(coerced to UNOdouble)None/pd.NaT/pd.NA->""(empty cell)float('nan')/np.nan-> raw NaN (the Calc add-in bridge renders this as a cascading error, typically#NUM!or#VALUE!)±infanddecimal.Decimal->float(inf may also error in formulas; Decimal precision loss is accepted)datetime/Timestamp/datetime64-> naive ISO-8601 (tz offset stripped). Timedelta -> fractional days.- Other
bool,float, andstrvalues are preserved as-is. - Lists and tuples are recursively converted to tuples of these Calc-supported types.
These coercions are the complete Calc type contract. Do not add wire payload kinds for inf, NaT, Decimal, or datetime — see data shapes.
Note on transport: =PY() and run_venv_python_script cross the host↔venv boundary via length-prefixed Pickle5 frames carrying either a split_grid envelope (dense numeric/mixed 2D grids) or plain nested lists (small grids). There is no JSON on the production wire for these payloads (JSON appears only in benchmarks and a few legacy test paths).
Calc empty cells and Python/NumPy NaN are not distinguished on the wire. Ingress blanks become None or np.nan; egress None → empty cell, computed nan → cascading Calc error. Prefer np.nansum / np.nanmean when blanks should be ignored.
Full tables, decision rationale, and author/LLM summary: Empty cells vs NaN.
Calc's legacy add-in bridge only accepts one scalar (number, text, or boolean) per =PY() evaluation. It cannot receive a Python list/tuple as a native array return (that yields #VALUE! even with Ctrl+Shift+Enter).
- Scalar return (Enter) — e.g.
=PY("result = 3 ** 8")or=PY("result = str([2, 3, 5])"). - Multi-cell list results — use a matrix formula over the target range and pass a per-row index as the optional 2nd argument:
- Select the output range (e.g.
A1:A6). - Enter (one formula for the block):
=PY("result = [sp.prime(x) for x in range(1000, 1006)]"; ROW()-1)- Confirm with Ctrl+Shift+Enter (curly braces
{=…}in each cell of the block is normal).
- Select the output range (e.g.
Calc evaluates matrix formulas once per cell; without optimization that means many IPC crossings. The host caches the Worker Result Session so the first cell runs the worker and later cells read by index — use ROW()-n as the 2nd argument. Details: scripting/numpy-serialization.md — Matrix formula result session.
Without the index argument, repeated evaluations in the same recalc pass return successive list elements (best-effort; prefer the ROW() form for reliability).
Microsoft Excel can auto-spill multi-cell results (DataFrames, 2D arrays) into adjacent rows and columns and surfaces #SPILL! when blocking cells are in the way (Microsoft Python in Excel vs Calc =PY()). A single-cell =PY(...) that returns a list, 2D array, or DataFrame spills into adjacent cells via a deferred background task (~0.1s). If any target cell is occupied by non-spilled user data, the formula cell shows #SPILL!. Spill coordinates are tracked in the document (WriterAgentSpillRegistry) so recalc clears old spill cells correctly. Toggle with Settings → Python → Python auto spill in Calc (scripting.python_auto_spill, default on). For explicit dimensions, use a matrix formula (Ctrl+Shift+Enter), a selected output range, or a per-row index (ROW()-n) as the 2nd argument.
- Grid egress over a data range — use two arguments only:
=PY("np.sum(data)"; B1:B10)or=PY("(np.array(data) * 2).tolist()"; D6:G9)as a matrix formula (Ctrl+Shift+Enter). The add-in IDL accepts only(code, data); a third argument such asROW()-1causes Err:504 (error in parameter list). When the 2nd argument is the full range,datain Python is that grid; useROW()-nas the 2nd argument only when it is the per-cell index, not together with a range. - Single cell, full list as text —
=PY("result = str([1, 2, 3])")+ Enter.
An XModifyListener (CalcSpillModifyListener) is registered on sheets that contain auto-spill cells. If the originating =PY() formula cell is cleared, overwritten, or deleted, the listener clears associated spilled cells, updates WriterAgentSpillRegistry, and saves document properties.
Undo isolation: Background spill population and orphaned spill cleanup execute inside an undo-isolated context (_undo_lock via enterHiddenUndoContext() or lock()). This ensures background cell writes do not fragment Calc's undo stack or push stray undo actions on top of the formula. When a user undoes with Ctrl+Z, the formula in the originating cell is undone immediately in a single step, and the modify listener automatically cleans up the spilled values.
Still open:
- Dynamic spill references — a helper such as
=PY_REF("A1")for the spill bounding range (Calc has no Excel#suffix, e.g.=A1#). - UI-thread drain — replace the background
threading.Timerwith Calc’s event-loop / async drain to reduce recalc lifecycle hazards. - Core spill — a UNO recalc/resize hook, or native multi-dimensional
XVolatileResultin Calc (sc), would replace simulated dynamic arrays.
=PY("3 ** 8")
=PY("str([sp.prime(x) for x in range(1000, 1006)])") (Returns as single-cell string)
=PY("np.mean(data)"; A1:A10)
=PY("result = [sp.prime(int(x)) for x in data.to_numpy().ravel()]"; ROW()-1) (matrix over column; Ctrl+Shift+Enter)
=PY("df = data.to_pandas(); result = float(df['Sales'].mean())"; A1:C10)
Instead of typing Python code directly as a string literal inside the =PY() formula, you can pass a cell reference containing the code (e.g., =PY(A1; B1:B10)).
Because the first parameter of =PY() is defined in the IDL (XPythonFunction.idl) as string code, the LibreOffice Calc formula engine automatically handles evaluation and type coercion of cell references out-of-the-box.
No code changes or new APIs (such as PythonCell()) are required.
- Code Reusability / Single Source of Truth: You can write a script once in cell
A1and reference it in dozens of other cells (e.g.,=PY(A1; B1:B10),=PY(A1; C1:C10)). Updating the logic inA1recalculates all dependent cells automatically. - Clean Syntax (No Quote Doubling): Inside Calc formulas, double quotes must be doubled to escape them (e.g.,
""result = ...""). Putting code in a cell lets you write clean, standard Python syntax without escaping pain. - Multi-line Scripts: The standard Calc cell editor supports multi-line text blocks (using
Alt+Enterto insert newlines). This allows users to write readable, commented Python scripts of arbitrary length. - Dynamic Formulas: You can use Calc formulas to construct Python code dynamically based on other spreadsheet variables! For example:
- Cell
A1:= "import numpy as np; result = np." & B1 & "(data)" - Changing
B1from"mean"to"std"dynamically changes the script executed by=PY(A1; C1:C10).
- Empty Code Cells: If the referenced code cell evaluates to an empty string, the script runner returns
Error: No code provided. - Implicit Intersection: If a user passes a multi-cell range as the first argument (e.g.,
=PY(A1:A2; B1:B10)), Calc performs implicit intersection using the active row/column. Always pass a single cell for the code argument (e.g.$A$1).
Calc’s formula compiler parses the cell before the =PY() add-in runs. Failures here are not venv/NumPy/sandbox errors — Python never executes.
ASCII "…" string literals are opaque. Identifiers like float and nested () inside a proper ASCII-quoted string do not become spreadsheet function tokens: =PY("float(1)") and =LEN("float(1)") succeed when PY is registered.
| Symptom | Typical cause | What users see |
|---|---|---|
| #NAME? | Unquoted name + ( treated as unknown spreadsheet function |
=PY(float(1)) or =float(1) → #NAME?; =PY("float(1)") works |
| Err:513 | One formula symbol exceeds Calc MAXSTRLEN (1024) |
Long inline Python in =PY("…") fails before the add-in |
| Err:508 | Wrong argument separator (; vs ,), or curly quotes “…” used instead of ASCII " |
XLSX/locale mismatch; pasted smart quotes |
| Err:510 | Cell text starts with = (e.g. section label === normal ===) |
Use plain labels like [normal], not leading = |
| #NAME? | XLSX import lowercases the add-in name; lookup failed on display-only name | Prefer =PY(...) (uppercase). The add-in also accepts python / PYTHON. |
Do not wrap returns in float() / int() / str() for Calc’s sake — to_calc_compatible already coerces NumPy scalars and Python int to Calc double. #NAME? on float means the call is outside quotes (or quotes were lost / became curly). WriterAgent’s sanitize_inline_py_code still rewrites float(/int(/str( when emitting Calc formulas as a defensive measure; that is not proof the lexer scans inside ASCII strings.
=PY(float(1)) → #NAME? (unquoted float = unknown function)
=PY("float(1)") → OK (ASCII string opaque)
=LEN("float(np.sum(data))") → OK
=LEN(“float(1)”) → Err:508 (U+201C/U+201D not string seps)
=LEN("<1023+ char string>") → Err:513 (MAXSTRLEN)
| Pattern | When to use | Example |
|---|---|---|
| Bare NumPy / expression | Default for short inline code | =PY("np.sum(data)"; B1:B10) |
| Code in a cell | Multi-line / huge scripts (avoids MAXSTRLEN) |
A1 = script text; =PY($A$1; B1:B10) |
| ASCII quotes only | Always when pasting / generating formulas | Normalize curly “” → " (WriterAgent does this on parse) |
| Comma vs semicolon | Match file/locale | XLSX OOXML → commas; Calc UI often ; |
| XLSX test sheets | Manual serialization regression | See scripts/generate_serialization_spreadsheet.py |
Excel Python-in-Excel .xlsx |
Open in Calc with the Python/=PY extension |
Auto-rewrites to DAG =PY on load; scripts >1000 chars parked on visible py_code_<Sheet> at the same A1, shorter stay inline (script_bank.py) — see ms-py §5.8 |
XLSX input cells must be numeric, not text: if the sheet stores values as strings (e.g. "1.0" from str() in a generator), Calc passes them as text, split_grid lands them in the strings map, and np.sum(data) fails with a Unicode dtype TypeError. Regenerate serialization_tests.xlsx after fixing the generator so ints/floats are written as native cell types.
These are not implemented; kept so design discussions do not rediscover the same traps.
- Cell-reference-first UX — Settings or formula wizard default: “put script in one cell, reference it from
=PY” (already supported by IDL; needs prompts/UI). Best mitigation for huge scripts until LO raisesMAXSTRLEN. - LLM /
=PROMPT()guardrails — Prefer cell refs for long code; emit ASCII"only; avoid unquoted Python in the formula. - Native ODS fixtures — Shipped:
tests/fixtures/numpy_domains_demo.ods. Use ODS for manual=PY()QA (preserves uppercase add-in name; semicolon args). Serialization fixtures use barenp.sum/np.max(nofloat()wrapper).
Deferred upstream work — not in WriterAgent. Schedule as a LibreOffice/Collabora Calc patch when long inline
=PY("…")/ Excel import becomes a product priority.
ASCII-quoted opacity is already correct. Remaining compiler gaps:
- Raise / grow string-symbol limit —
sc/inc/compiler.hxx#define MAXSTRLEN 1024and fixedcSymbol[MAXSTRLEN+1]insc/source/core/tool/compiler.cxx(ScCompiler::NextSymbol,ssGetString/ssSkipString). Prefer a growableOUStringBufferfor string literals with a high soft cap; idents can stay shorter. Symptom today:Err:513on long symbols. - Curly / smart quotes — Normalize U+201C/U+201D (and ideally U+2018/U+2019) to ASCII
"before or during compile, or treat them asCharString/StringSep. Symptom today:=LEN(“float(1)”)→Err:508. - Core tests (e.g.
sc/qa/unit/ucalc_formula.cxx):=LEN("float(1)")/=LEN("a(b(c))")stay OK; long string (>1024) succeeds after (1); curly-quotedLENsucceeds after (2). UseLEN/CONCATENATE— no PY add-in required. - Optional Bugzilla — Attach the three
LENreproducers after a patch lands or when filing.
Until then: keep scripts in cells for large Python; WriterAgent normalizes curly quotes on formula parse and may sanitize float( when emitting Calc formulas defensively.
Uses the same warm worker as Run Python Script (§2). execute_python_script is separate and not used for formulas. Isolated mode (default): variables do not persist across cells. Shared kernel: one workbook namespace until reset — §6 Session modes.
WriterAgent only. =PROMPT("Write a Python formula using numpy for the 95th percentile of B1:B100") can yield a pasteable =PY("…") string — natural-language bridge to data-science formulas without leaving the sheet.
LibrePythonista stores code outside the formula (=PY.C(SHEET(), CELL("ADDRESS"), extras?)) and runs in LO embedded Python with pip bootstrap. This design keeps code in the formula and runs in the user venv.
flowchart LR
subgraph ourPy [Calc =PY]
F1["=PY(code, data?)"]
F1 --> Venv["venv subprocess"]
Venv --> Inject["inject CalcRange as data"]
end
subgraph librePythonista [LibrePythonista PY.C]
F2["=PY.C(SHEET(), CELL(...), extras?)"]
F2 --> LO["LO embedded Python"]
LO --> Editor["code from cell editor"]
Editor --> LP["lp('range', collapse=...)"]
end
| Capability | data / ranges |
LibrePythonista |
|---|---|---|
| Pass one range | Yes — CalcRange (always 2D) |
lp("A1:B10") |
| Multiple ranges in one formula | Yes — data[i] or ranges[i] / loop ranges |
Multiple lp() calls |
| Named ranges | Only as formula args | lp("MyRange") |
Trim empty rows (collapse) |
No | collapse=True on lp() |
| Typed date columns | Raw Calc values (user coerces) | column_types + pandas |
| Return type for ranges | CalcRange / explicit .to_pandas() |
pandas.DataFrame |
| Cell context | Not exposed | sheetIdx + cAddress |
| Execution | User venv | LO embedded + pip bootstrap |
What we kept: formula args + venv NumPy; rectangular orientation via CalcRange. What we did not copy: PY.C metadata formula, in-LO pandas bootstrap, mandatory lp() for every read.
=PY() |
LibrePythonista | |
|---|---|---|
| Where users edit | Formula bar, Edit Python in Cell… (Monaco), or code-in-cell ref | LibrePythonista menu / Edit Code; cell shows short =PY.C(...) |
| Where source lives | In the .ods formula (or a referenced cell) |
Document-side store (PySourceManager, etc.) |
Design stance: treat each =PY cell as a pure function (data in → result out). Monaco / code-in-cell helps for long scripts (Calc UX backlog; Monaco plan). Flatpak/Snap spawn patterns for the Monaco child remain tracked in scripting/monaco-editor-dev-plan.md (phase 2F).
Trailing formula arguments become CalcRange values: ranges is always the full list; data is that CalcRange when there is one arg, or the same list as ranges when there are several. Orientation is always 2D ((1,1), (1,N), (N,1), or (rows, cols)). Blank vs NaN, dates, pandas DataFrame spill, logicals, and multi-range examples: calc/py-data-shapes.md.
| Tier | User sees | Code location | Effort |
|---|---|---|---|
| 0 (today) | Formula bar + Monaco cell editor | Inside =PY("…") or code cell |
Done |
| 1 | Modal XDL edit dialog | Still in formula | Small–medium |
| 2 | Short formula + document store key | Outside formula | Medium |
| 3 | LibrePythonista-like IDE surface | LP-scale infrastructure | Very large |
Tier 1 reuses existing DialogProvider / XDL patterns ([plugin/chatbot/dialogs.py](plugin/chatbot/dialogs.py)); execution unchanged. Tier 3 is only justified if Calc-native Python becomes a primary product pillar.
Microsoft runs Python in cloud containers. Authors type xl("…") literals in the editor; the package stores pythonScripts.xml with xl(%Pn%) and _xlws.PY(scriptIndex, returnType, …deps). Calc uses a local venv with =PY(code, data?) — explicit formula args for DAG dependencies (the rewriter maps Excel’s trailing deps onto that shape).
| Feature dimension | Microsoft Excel (=PY) |
Calc =PY() |
|---|---|---|
| Data ingress | UI xl("A1:B10") → package %Pn% + trailing _xlws.PY deps |
Range as formula arg → data / ranges |
| Output egress | Last expression | Prefer result = …; last expression if unset |
| Dependency tracking | Excel→PY: trailing formula deps (literals rewritten at edit/save; no Python parse at recalc). PY↔PY: co-volatility (ms-py §5.8, jailsafe) | Native Calc DAG on data args |
| Multi-range | Multiple %Pn% / trailing deps |
Varargs → ranges (data shapes) |
| Shared state | Globals + row-major co-volatility | Opt-in shared kernel + data refs (§6) |
| Runtime | Cloud sandbox | User venv (offline, any pip packages) |
| Editor | Monaco task pane | Monaco via pywebview (§3) |
Design stance: keep explicit data + result. Deep dive for Collabora/LibreOffice (why not copy co-volatility as the default; file rewrite for Excel packages): scripting/ms-py-compatibility.md.
Excel parity (summary): dynamic spill, plots, Monaco cell editor, shared kernel + init scripts, and LibrePy sidebar diagnostics are shipped. Object cards, rich DataFrame tables, names/tables labels → Calc UX backlog.
Google Sheets does not run Python in cells (Apps Script = JS; external Python via APIs). Calc =PY() is closer to Excel / Neptyne than to Sheets. Non-goals: gspread, Sheets API sync, Apps Script runtime, cloud =AI() cell parity.
Apps Script’s sheet object model remains a useful API design reference for future venv↔LO tool RPC sugar — not a runtime to embed. Remaining UX / landscape items: Calc UX backlog.
Status: Not implemented.
writeragent_api.pyis generated from tool metadata (scripts/generate_tool_proxies.py), but the warm worker does not handletool_calllines yet. There is no shipping dual-modeimport writeragent as waSDK for venv scripts today — assignresult; on the optional chat path the model calls Calc/Writer tools after compute (§3).
Intended behavior (when built): venv proxies send tool_call on the wire; PythonWorkerManager dispatches via ToolRegistry.execute() and replies with tool_result until code_result. Domain-scoped tools only. Follow-on sugar (sheet.range("A1:B2").values = matrix) only after RPC exists.
Not shipped unless noted. Monaco editor gaps live in scripting/monaco-editor-dev-plan.md (2B–2F, Phase 3 formula-bar polish).
Complex returns (DataFrame, dict, class) should show a compact cell label (e.g. [DataFrame 150×4]) and an inspect dialog — not #VALUE!.
- Object references in shared-kernel session (
__pyobj_N__; object stays in worker namespace) - Worker
inspect_object→ shape, dtypes,head(5) - XDL preview dialog (
dialogs.pypatterns); optional Spill to Grid on top of matrix formulas - Touch:
venv_sandbox.py,worker_harness.py,function.py
- Context-aware
=PY()generation (nearby ranges/headers when chat builds formulas) - Formalize
=PROMPT()→ pasteable=PY("…")template - Stronger multi-step analysis prompts (clean → stats → chart)
- Touch:
venv.py,prompt_function.py
| Item | Notes / touch |
|---|---|
| DataFrame → rich Calc table | Headers, formats, filters — distinct from object cards |
JSON result envelope |
Multi-cell agent updates via __wa_payload__; payload_codec + host apply |
| Inline result preview | Stdout/thumbnail under cell |
| Formula-bar Jedi | Monaco 2D |
Named ranges / structured tables / headers in data |
calc_addin_data.py |
| Label preservation | First row/column as pandas Index when requested |
| Spreadsheet → Python import | calc/spreadsheet-to-python-import.md |
Worker idle shutdown / per-formula timeout_sec |
Global timeout is Settings → Python; per-cell timeout and idle worker teardown are not shipped |
| Python edit dialog tiers 1–3 | §6 deferred UX |
| Range alignment for multi-range NumPy | Mismatched shapes before np.corrcoef — data shapes deferred |
| Shared-kernel soft timeout / invalidation | Prefer SIGINT then SIGKILL; user Ctrl+Shift+F9 rebuilds DAG after worker wipe — session_manager.py, venv_worker.py |
| Blank side-channel | Pass-through blanks stay empty — data shapes deferred |
| Cell-level traceback snippet | Short trace in cell error string; full trace already in LibrePy sidebar |
| Mito-style action recorder | GUI → pandas in Monaco; low priority |
| Dynamic sidebar controls from sheet context | A2UI-style; low priority |
| Shared-kernel memory bounds | LRU large DataFrames; defer until OOM reports |
- Serialization performance: scripting/numpy-serialization.md — Future work
- Jupyter Writer
.ipynbimport + ▶ run: writer/jupyter-notebook-import.md - Monaco remaining phases: scripting/monaco-editor-dev-plan.md
- Domain roadmaps: scripting/numpy-domains.md
See scripting/numpy-jailsafe.md for Collabora Online / jail-safe NumPy. Desktop LibrePy reads Collabora-saved =PY() by rewriting GETPY OriginalNames on open (collabora_formula.py); it does not register Collabora's UNO package.
Remaining work lives in the child doc (or §7 backlog). Do not add split_grid kinds for inf, NaT, Decimal, datetime64, empty DataFrames, or MultiIndex — calc/py-data-shapes.md.
| Area | Where to look |
|---|---|
| Calc UX (object cards, named ranges, soft timeout, edit-dialog tiers, …) | §7 backlog |
| Domain helpers (Geospatial / Audio remaining; Analysis, Viz, SymPy, Units, Forecast, Text, Optimize, Quant shipped or partial) | scripting/numpy-domains.md; SageMath: sagemath-integration-dev-plan.md |
Serialization / Cython vec_pack download |
scripting/numpy-serialization.md |
| Venv ↔ LO tool RPC | §7 |
| Collabora Online (Steps A–C landed; plot / Monaco remain) | scripting/numpy-jailsafe.md, online#16010 |
| Monaco editor remaining phases | scripting/monaco-editor-dev-plan.md |
| Jupyter Writer import (Phase 1: import + ▶ run, ATX markdown, output replace) | writer/jupyter-notebook-import.md |