AI Plot-by-Code Example Implementation Plan¶
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Status: implemented (2026-04-24). Retained as a retrospective — future audits can walk this plan against the committed code.
Goal: Ship a first-class src/panelini/panels/ai/plot/ subpackage + a runnable examples/panels/ai/plot_by_code.py that wires the existing AiChat to a PlotPanel via 3–5 plotting tools and 0–8 OSW connector tools. Sandbox execution via llm_sandbox (Docker python:3.12-slim). OSW tools degrade gracefully when the osw package is missing or when the four OSW_*/BLAZEGRAPH_* env vars aren’t set. No changes to src/panelini/panels/ai/{frontend,backend}.py.
Architecture: PlotPanel owns mutable plot state + a pn.pane.Image; it runs code inside SandboxSession, copies /sandbox/output.png back, validates with PIL, and updates the image. make_plot_tools(panel) returns 3 delegation BaseTools plus (if osw importable) 2 more OSW-plot tools. make_osw_tools() returns 0 or 8 stateless BaseTools gated by osw_env_present(). Mypy cleanliness without a global override via a local TYPE_CHECKING: Any boundary in every file that imports osw.*. Example composes everything: tools = [*make_plot_tools(plot_panel), *make_osw_tools()], then AiChat(system_message=..., tools=tools).
Tech Stack: Python 3.10+, existing panel, param, langchain-core, pydantic v2; new transitive deps llm-sandbox>=0.2, pillow>=10.0 (via existing [ai-llm-sandbox] extra) and osw>=1.0, SPARQLWrapper>=2.0 (via new [ai-osw] extra). pytest + monkeypatch + unittest.mock.patch for tests — no Docker or OSW endpoint required.
Spec: docs/superpowers/specs/2026-04-24-ai-plot-by-code-example-design.md
File Structure¶
File |
Responsibility |
Status |
|---|---|---|
|
Add |
modify |
|
Re-export |
create |
|
|
create |
|
Empty re-export shim |
create |
|
|
create |
|
|
create |
|
Empty re-export shim |
create |
|
3 delegation tools + |
create |
|
2 OSW-plot tools (lazy-imported by plot_tools) |
create |
|
8 stateless OSW tools + |
create |
|
Wire |
create |
|
One-line entry describing the new example |
modify |
|
Make test folder a package |
create |
|
Unit tests for |
create |
|
Unit tests for env-var gating |
create |
|
|
create |
|
Delegation + factory tests |
create |
|
OSW-plot tool tests with patched osw modules |
create |
|
OSW connector tool tests with patched SPARQL/OSW |
create |
Task 1: Add ai-osw optional extra and dev dep¶
Files:
Modify:
pyproject.tomlStep 1: Add the extra
In [project.optional-dependencies], below ai-llm-sandbox = [...]:
ai-osw = [
"osw>=1.0",
"SPARQLWrapper>=2.0",
"pandas>=2.0.0",
"python-dotenv>=1.0.0",
]
Step 2: Add
oswto the dev group
Run:
uv add --dev 'osw>=1.0.1'
This adds osw to [dependency-groups.dev] and lets mypy resolve from osw.* imports during local checks.
Step 3: Sync
uv sync --extra ai --extra ai-llm-sandbox --extra ai-osw --group dev
Expected: no resolver errors; osw and SPARQLWrapper are importable.
Step 4: Commit
git add pyproject.toml uv.lock
git commit -m "deps: add ai-osw optional extra and osw dev dep"
Task 2: Port sandbox.py helpers (utils/)¶
Files:
Create:
src/panelini/panels/ai/plot/utils/__init__.pyCreate:
src/panelini/panels/ai/plot/utils/sandbox.pyCreate:
tests/panels/ai/plot/__init__.pyCreate:
tests/panels/ai/plot/test_sandbox_utils.pyStep 1: Write failing tests
Create tests/panels/ai/plot/test_sandbox_utils.py covering:
resolve_file_pathprefers an absolute existing path, falls back todownload_dir, thendata_dir, then returns the original string.copy_files_to_sandboxcallssession.copy_to_runtime(src, dest)once per file.MICRESS pairing: a
foo.geof+foo.conc1pair both land, with the geometry file renamed to match the binary’s stem (foo.geoF).
Use a MagicMock() for the session and tmp_path + real writes for the path-resolution tests.
Step 2: Run — expect ImportError (module missing)
Step 3: Create
utils/sandbox.py
Port _resolve_file_path, _copy_files_to_sandbox, _MICRESS_BIN_EXTS, _MICRESS_GEO_EXTS from migration/agent.py:48-102. Refinement: resolve_file_path takes download_dir + data_dir as explicit args instead of reading osw.express-provided globals. Pure helpers only; no env reads at import time.
Step 4: Run tests — expect PASS
Step 5: Commit
git add src/panelini/panels/ai/plot/utils/ tests/panels/ai/plot/__init__.py tests/panels/ai/plot/test_sandbox_utils.py
git commit -m "feat(plot): sandbox helpers ported from migration/ with tests"
Task 3: osw_env.py (utils/)¶
Files:
Create:
src/panelini/panels/ai/plot/utils/osw_env.pyCreate:
tests/panels/ai/plot/test_osw_env.pyStep 1: Write failing test
Assertions:
OSW_ENV_VARStuple equals("OSW_DOMAIN", "BLAZEGRAPH_ENDPOINT", "BLAZEGRAPH_USER", "BLAZEGRAPH_PASSWORD").osw_env_present()isTrueonly when all four are set to non-empty strings.Any single variable missing or empty →
False.
Drive via monkeypatch.setenv / monkeypatch.delenv.
Step 2: Run — expect ImportError
Step 3: Implement
osw_env.py
Centralizes the gating decision so both make_osw_tools() and future OSW-dependent code share one source of truth.
Step 4: Run — expect PASS
Step 5: Commit
git add src/panelini/panels/ai/plot/utils/osw_env.py tests/panels/ai/plot/test_osw_env.py
git commit -m "feat(plot): osw_env_present() helper + tests"
Task 4: PlotPanel class¶
Files:
Create:
src/panelini/panels/ai/plot/panel.pyCreate:
src/panelini/panels/ai/plot/__init__.pyCreate:
tests/panels/ai/plot/test_plot_panel.pyStep 1: Write failing tests
Mock SandboxSession so Docker is never touched. Key test shape:
@patch("panelini.panels.ai.plot.panel.SandboxSession")
def test_plot_by_code_success(mock_session_cls, tmp_path):
mock_session = MagicMock()
mock_session.__enter__.return_value = mock_session
mock_session.run.return_value.stdout = "ok"
def fake_copy_from_runtime(src, dest):
# Write a tiny valid PNG so PIL.Image.open succeeds
Image.new("RGB", (1, 1)).save(dest, "PNG")
mock_session.copy_from_runtime.side_effect = fake_copy_from_runtime
mock_session_cls.return_value = mock_session
panel = PlotPanel(data_path=tmp_path)
result = panel.plot_by_code(code="import matplotlib ...")
assert "Image successfully plotted" in result
assert panel.current_python_code is not None
assert panel.output_file_path.exists()
assert panel.image_panel.object == str(panel.output_file_path)
Additional tests: error path when copy_from_runtime raises, run_code returns stdout, load_data_from_csv populates self.df and returns column names.
Step 2: Run — expect failures
Step 3: Implement
panel.py
Port PlotToolPanel from migration/agent.py:326-616, stripping langchain-agent coupling. Keep pn.pane.Image + plot_panel Row + mutable state (current_python_code, current_input_osw_id, output_file_path, df). Public methods: plot_by_code, run_code, load_data_from_csv. Do NOT carry generate_langchain_tools() — that lives in plot_tools.py.
Step 4: Create package
__init__.pythat re-exportsPlotPanel:
from .panel import PlotPanel
__all__ = ["PlotPanel"]
(Remaining exports added in Task 5 and Task 7.)
Step 5: Run tests — expect PASS
Step 6: Commit
git add src/panelini/panels/ai/plot/panel.py src/panelini/panels/ai/plot/__init__.py tests/panels/ai/plot/test_plot_panel.py
git commit -m "feat(plot): PlotPanel class with sandbox plot_by_code/run_code/load_csv"
Task 5: Three plot delegation tools + factory¶
Files:
Create:
src/panelini/panels/ai/plot/tools/__init__.pyCreate:
src/panelini/panels/ai/plot/tools/plot_tools.pyCreate:
tests/panels/ai/plot/test_plot_tools.pyModify:
src/panelini/panels/ai/plot/__init__.py— addmake_plot_toolsStep 1: Write failing tests
Tests:
Each of
PlotByCodeTool,RunCodeTool,LoadCsvTool’s_runcalls through to the matchingPlotPanelmethod with the expected kwargs (use aMagicMockpanel).make_plot_tools(panel)returns[PlotByCodeTool, RunCodeTool, LoadCsvTool]whenosw_plot_toolscannot be imported (simulate bymonkeypatch.setitem(sys.modules, "panelini.panels.ai.plot.tools.osw_plot_tools", None)— produces ImportError on subsequent relative-import).make_plot_tools(panel)includesAttachPlotToOswTool+DocumentEvaluationToolwhenosw_plot_toolsimports successfully (this path is exercised fully in Task 6).Step 2: Run — expect failures
Step 3: Implement
plot_tools.py
Port PlotByCodeInput, RunCodeInput, LoadDataFromCsvInput from migration/agent.py:224-323, migrating from pydantic.v1 to pydantic v2. Each BaseTool subclass declares model_config = ConfigDict(arbitrary_types_allowed=True) so panel: PlotPanel type-checks. _arun delegates to _run.
make_plot_tools body:
def make_plot_tools(panel: PlotPanel) -> list[BaseTool]:
tools: list[BaseTool] = [
PlotByCodeTool(panel=panel),
RunCodeTool(panel=panel),
LoadCsvTool(panel=panel),
]
try:
from .osw_plot_tools import AttachPlotToOswTool, DocumentEvaluationTool
except ImportError:
return tools
tools.extend([AttachPlotToOswTool(panel=panel), DocumentEvaluationTool(panel=panel)])
return tools
Step 4: Update
src/panelini/panels/ai/plot/__init__.py:
from .panel import PlotPanel
from .tools.plot_tools import make_plot_tools
__all__ = ["PlotPanel", "make_plot_tools"]
Step 5: Run tests — expect PASS
Step 6: Commit
git add src/panelini/panels/ai/plot/tools/ src/panelini/panels/ai/plot/__init__.py tests/panels/ai/plot/test_plot_tools.py
git commit -m "feat(plot): 3 delegation tools + make_plot_tools factory"
Task 6: OSW-plot tools (osw_plot_tools.py)¶
Files:
Create:
src/panelini/panels/ai/plot/tools/osw_plot_tools.pyCreate:
tests/panels/ai/plot/test_osw_plot_tools.pyStep 1: Write failing tests
A panel_with_png fixture writes real PNG bytes to a tmp_path and points panel.image_panel.object at it. Success tests patch:
panelini.panels.ai.plot.tools.osw_plot_tools.OswExpresspanelini.panels.ai.plot.tools.osw_plot_tools.OSW(soOSW.StoreEntityParam(...)does not try to validate a real mock entity)_build_wiki_file,get_full_title,model
Cover: happy path for both tools; osw_obj.load_entity returns None (entity-not-found); panel.image_panel.object is None (missing-image path); arbitrary exception inside _run returns the error string.
Step 2: Run — expect failures
Step 3: Implement
osw_plot_tools.py
Use the TYPE_CHECKING: Any boundary at the top:
if TYPE_CHECKING:
WikiFileController: Any = Any
OSW: Any = Any
model: Any = Any
OswExpress: Any = Any
get_full_title: Any = Any
else:
from osw.controller.file.wiki import WikiFileController
from osw.core import OSW, model
from osw.express import OswExpress
from osw.utils.wiki import get_full_title
Add an explanatory comment: osw ships no py.typed marker upstream; trade-off visible in-source rather than hidden in a global mypy override.
Port AttachCurrentPlotToOswPageTool._run and DocumentCurrentEvaluationTool._run from migration/agent.py. Use helpers _load_plot_bytes(panel) (reads panel.image_panel.object) and _build_wiki_file(osw_obj, plot_uuid).
Step 4: Run tests — expect PASS
Verify that make_plot_tools(panel) now returns 5 tools (Task 5’s “present” branch).
Step 5: Commit
git add src/panelini/panels/ai/plot/tools/osw_plot_tools.py tests/panels/ai/plot/test_osw_plot_tools.py
git commit -m "feat(plot): AttachPlotToOsw + DocumentEvaluation tools (lazy)"
Task 7: Eight stateless OSW connector tools (osw_tools.py)¶
Files:
Create:
src/panelini/panels/ai/plot/tools/osw_tools.pyCreate:
tests/panels/ai/plot/test_osw_tools.pyModify:
src/panelini/panels/ai/plot/__init__.py— addmake_osw_toolsStep 1: Write failing tests
Fixtures:
@pytest.fixture
def osw_env(monkeypatch):
monkeypatch.setenv("OSW_DOMAIN", "example.org")
monkeypatch.setenv("BLAZEGRAPH_ENDPOINT", "https://bg.example/sparql")
monkeypatch.setenv("BLAZEGRAPH_USER", "u")
monkeypatch.setenv("BLAZEGRAPH_PASSWORD", "p")
@pytest.fixture
def no_osw_env(monkeypatch):
for var in ("OSW_DOMAIN", "BLAZEGRAPH_ENDPOINT", "BLAZEGRAPH_USER", "BLAZEGRAPH_PASSWORD"):
monkeypatch.delenv(var, raising=False)
Tests:
Pure helpers:
_replace_special_characters,_check_for_uuid,_try_cast_str_to_uuid— happy + edge cases.make_osw_tools()returns[]underno_osw_env; returns 8 tools underosw_env.Each tool’s
_run: one success + one exception path. For SPARQL tools, patchSPARQLWrapper.SPARQLWrappersofake.query.return_value.convert.return_value = {"results": {"bindings": []}}. Assert the executed query contains expected SPARQL fragments (e.g.rdfs:subClassOf+).DownloadOslFileToolpatchesosw_download_fileto write a fake file to"fake-path/downloaded.csv".Step 2: Run — expect failures
Step 3: Implement
osw_tools.py
Same TYPE_CHECKING: Any boundary as osw_plot_tools.py. Port the 8 tools from migration/osw_tools.py, preserving SPARQL query strings verbatim.
Pure helpers:
_UUID_REregex_replace_special_characters(s, replacer=" ")_check_for_uuid(s)_try_cast_str_to_uuid(s)— accepts either a canonical UUID or an OSW-id tail_sparql_prefixes(domain)— returns PREFIX declarations_run_sparql(query)— readsBLAZEGRAPH_*env vars, returns JSON_osw_id_to_uuid(osw_id)— extract UUID fromFile:OSW<hex>.csv
make_osw_tools():
def make_osw_tools() -> list[BaseTool]:
if not osw_env_present():
return []
return [
GetPageHtmlTool(), DownloadOslFileTool(), GetFileHeaderTool(),
SparqlSearchTool(), FindOutEverythingAboutTool(),
GetTopicTaxonomyTool(), GetInstancesTool(), GetWebsiteHtmlTool(),
]
Notes:
Do NOT create a module-level
OswExpress(os.environ["OSW_DOMAIN"]). Each tool constructs its own inside_runso env-var changes mid-session take effect.For
GetWebsiteHtmlTool, annotatehtml_bytes: bytes = page.read()thenreturn html_bytes.decode("utf-8")so mypy doesn’t seeAny.Step 4: Update
src/panelini/panels/ai/plot/__init__.py:
from .panel import PlotPanel
from .tools.osw_tools import make_osw_tools
from .tools.plot_tools import make_plot_tools
__all__ = ["PlotPanel", "make_osw_tools", "make_plot_tools"]
Step 5: Run tests — expect PASS
Step 6: Run mypy + ruff on the new subpackage
uv run ruff check src/panelini/panels/ai/plot tests/panels/ai/plot
uv run mypy src/panelini/panels/ai/plot
Expected: both clean. No global [[tool.mypy.overrides]] needed — the TYPE_CHECKING: Any boundary handles the untyped osw import locally.
Step 7: Commit
git add src/panelini/panels/ai/plot/tools/osw_tools.py src/panelini/panels/ai/plot/__init__.py tests/panels/ai/plot/test_osw_tools.py
git commit -m "feat(plot): 8 stateless OSW connector tools + make_osw_tools factory"
Task 8: The example file¶
Files:
Create:
examples/panels/ai/plot_by_code.pyStep 1: Write the example
Top-of-file docstring contains, in this order:
Env vars — LLM:
ANTHROPIC_API_KEYOR (AZURE_OPENAI_API_KEY,AZURE_OPENAI_ENDPOINT,AZURE_OPENAI_API_VERSION). OSW (optional):OSW_DOMAIN,BLAZEGRAPH_ENDPOINT,BLAZEGRAPH_USER,BLAZEGRAPH_PASSWORD. Panelini:PANELINI_AI_CONFIG_PATH.Install hints —
pip install panelini[ai,ai-llm-sandbox](core) /pip install panelini[ai,ai-llm-sandbox,ai-osw](with OSW).Runtime requirements — Docker daemon running;
.envloaded viapython-dotenv.OSW connector behaviour — registered only when all four OSW env vars are set; otherwise silently omitted.
Body:
from __future__ import annotations
import panel as pn
from dotenv import load_dotenv
from panelini import Panelini
from panelini.panels.ai import AiChat
from panelini.panels.ai.plot import PlotPanel, make_osw_tools, make_plot_tools
load_dotenv()
SYSTEM_MESSAGE = (
"You are a helpful assistant with access to tools. "
"ALWAYS call tools directly to fulfill the user's request — never describe "
"what a tool call would look like or output JSON of a hypothetical call. "
... # MICRESS/micpy guidance ported from migration/agent.py:636-656
)
plot_panel = PlotPanel()
tools = [*make_plot_tools(plot_panel), *make_osw_tools()]
chat = AiChat(system_message=SYSTEM_MESSAGE, tools=tools)
app = Panelini(title="AI + Plot by Code", sidebar_enabled=True)
app.sidebar_set(objects=chat.sidebar_objects)
app.main_set(objects=[pn.Row(chat.main_objects[0], plot_panel.plot_panel)])
if __name__ == "__main__":
pn.serve(app.servable(), title="AI + Plot by Code", port=5008)
Port the MICRESS/micpy block from migration/agent.py:636-656 verbatim into SYSTEM_MESSAGE. That is the one piece of agent prompt from the legacy code worth preserving.
Step 2: Smoke-test the import
uv run python -c "from examples.panels.ai import plot_by_code as m; print(type(m.app).__name__)"
Expected: Panelini.
Step 3: Runtime smoke (manual)
docker psconfirms Docker daemon is up.export ANTHROPIC_API_KEY=...(or the Azure trio).python examples/panels/ai/plot_by_code.py→ sidebar shows plot tool checkboxes.Prompt “plot y = sin(x) for x in 0..2π, save to /sandbox/output.png” → image renders.
If OSW env vars are set, sidebar additionally shows the 8 OSW tools.
Step 4: Commit
git add examples/panels/ai/plot_by_code.py
git commit -m "feat(example): plot_by_code example wires AiChat + PlotPanel"
Task 9: README entry¶
Files:
Modify:
examples/panels/ai/README.mdStep 1: Add bullet
Append to the top-of-file bullet list:
- `plot_by_code.py` — AI chat that renders matplotlib figures via `llm-sandbox` (Docker) in a `PlotPanel` next to the chat; optional OSW connector tools when `OSW_DOMAIN` + Blazegraph env vars are set.
Step 2: Commit
git add examples/panels/ai/README.md
git commit -m "docs(plot): README entry for plot_by_code example"
Task 10: Final verification¶
Files: (none modified)
Step 1: Full test suite
uv run pytest tests/ -v
Expected: all tests PASS. The plot subpackage adds ~80 tests, all <2 s total.
Step 2: Lint + typecheck across the configured scope
uv run ruff check .
uv run mypy src
Expected: both clean.
Step 3: Confirm no
src/panelini/panels/ai/{frontend,backend}.pychanges
git diff --stat main -- src/panelini/panels/ai/frontend.py src/panelini/panels/ai/backend.py
Expected: no output. Core chat panel is untouched.
Step 4: Confirm
[[tool.mypy.overrides]]was NOT added
grep -n "tool.mypy.overrides" pyproject.toml || echo "no override — good"
Expected: no override — good. The TYPE_CHECKING: Any boundary handles the untyped osw import locally.
Step 5: Done — ready for review.
Task 12 — OSW credentials from env vars (no prompt, no yaml)¶
Goal: make the OSW tools runnable in a Panel server context without ever triggering osw.express.OswExpress’s interactive credential prompt or writing osw_files/accounts.pwd.yaml. Reference design: spec v1.2 section.
Step 1: Read the
osw.express.OswExpress.__init__gate
Confirm the prompt-and-save branch is gated behind if not cred_mngr.iri_in_file(domain). The override target is therefore iri_in_file, not get_credential — overriding only the probe is enough to disable both the prompt and the disk write in one change.
Step 2: Add
utils/osw_env.py
Ship OSW_AUTH_ENV_VARS, extended OSW_ENV_VARS, check_osw_auth_env() (raises RuntimeError listing missing vars), EnvCredentialManager (subclass with iri_in_file override), and build_osw_express(domain=None) that pre-loads the manager with a UserPwdCredential before handing it to OswExpress.
Step 3: TDD tests for
osw_env
tests/panels/ai/plot/test_osw_env.py:
TestOswEnvPresent(4 tests)TestOswEnvVarsConstant(2 tests)TestCheckOswAuthEnv(5 tests — raises with actionable message)TestEnvCredentialManager(3 tests — override returns True afteradd_credential)TestBuildOswExpress(3 tests — patchedOswExpress, assert kwargs)TestNoCredentialsFileWritten::test_save_credentials_to_file_is_not_invoked— the contract test. Patchesosw.express.requests.getandosw.express.WtSite, runs the realOswExpress.__init__, assertssave_credentials_to_file+get_credentialwere not called andaccounts.pwd.yamldoes not exist intmp_path.Step 4: Rewire all
OswExpress(...)callsites
Replace the 5 callsites across osw_plot_tools.py (2) and osw_tools.py (1) with build_osw_express(). DownloadOslFileTool swaps osw_download_file(...) for osw_obj.download_file(...) so the cred_mngr actually flows through. Drop os, OswExpress, and osw_download_file imports from the tool modules.
Step 5: Update tool tests to patch
build_osw_express
Replace patch(f"{module}.OswExpress", ...) with patch(f"{module}.build_osw_express", ...) in test_osw_tools.py + test_osw_plot_tools.py. For DownloadOslFileTool, the patched helper returns a MagicMock whose .download_file(...) returns the fake file. Add OSW_USER + OSW_PASSWORD to the shared osw_env fixture so check_osw_auth_env() doesn’t raise inside the tool runs.
Step 6: Docs
examples/panels/ai/plot_by_code.py— add the three auth env vars to the OSW block, plus a paragraph on the “stays in memory, no yaml, no prompt” contract.examples/panels/ai/README.md— update the one-liner to say “six env vars” and call out in-memory credentials.Spec file — v1.2 addendum (motivation, design, contract table, files touched, trade-offs).
Step 7: Verify
uv run pytest tests/panels/ai/plot/ -v
uv run ruff check src/panelini/panels/ai/plot tests/panels/ai/plot
Expected: all plot tests pass including the 18 test_osw_env tests; ruff clean.
Step 8: Commit
git add src/panelini/panels/ai/plot/utils/osw_env.py \
src/panelini/panels/ai/plot/tools/osw_plot_tools.py \
src/panelini/panels/ai/plot/tools/osw_tools.py \
tests/panels/ai/plot/test_osw_env.py \
tests/panels/ai/plot/test_osw_tools.py \
tests/panels/ai/plot/test_osw_plot_tools.py \
examples/panels/ai/plot_by_code.py \
examples/panels/ai/README.md \
docs/superpowers/specs/2026-04-24-ai-plot-by-code-example-design.md \
docs/superpowers/plans/2026-04-24-ai-plot-by-code-example.md
git commit -m "feat(plot): env-var OSW credentials — no prompt, no yaml on disk"
Open follow-ups (explicitly out of scope for this plan)¶
Lazy-import the eight
osw_tools.pytools insidemake_osw_tools()sofrom panelini.panels.ai.plot import make_osw_toolsdoes not raise when theoswpackage is absent. Matches the pattern used forosw_plot_tools.Reusable
PLOT_SYSTEM_MESSAGEconstant when a second consumer appears.Sphinx docs page at
docs/panels/ai.mdcovering the panel + the layout diagram from the spec.Sandbox-log tab next to the plot for debugging long-running snippets.
Drop the
TYPE_CHECKING: Anyboundary when upstreamoswshipspy.typed.Let the regenerate sidebar pick the provider too (currently pinned to the default provider). Useful once
config.ymlcommonly has more than one provider configured.Preserve the previous N plot scripts in the right sidebar so the user can diff regeneration attempts.