Skip to content

Code cells nested in a directive are never registered with the kernel, so they render a run button that does nothing #117

Description

@mmcky

A code cell inside any directive — {exercise}, {solution}, {note} — renders with run controls but is never registered with the kernel. Clicking gives no output and no error, and any later cell depending on it raises NameError.

Found while debugging QuantEcon/lecture-wasm#64, where it makes the short_path exercise impossible to complete in the browser.

Root cause

@myst-theme/jupyter/dist/execute/utils.js (this repo depends on @myst-theme/jupyter: ^1.3.0):

export function executableNodesFromBlock(block) {
    if (!block || block.type !== 'block') return;
    let target = block;
    if (block.children?.length === 1 && block.children[0].type === 'container') target = block.children[0];
    if (target.children?.length >= 2 && target.children[0].type === 'code') {
        return { codeCell: target.children[0], output: target.children[1] };
    }
}

notebook.cells = mdast.children.map((block) => { ... executableNodesFromBlock(block) ... });

Two things combine:

  1. notebook.cells = mdast.children.map(...) walks only the root's direct children — no recursion.
  2. executableNodesFromBlock requires the code node to be children[0] of a top-level block, with a single special case for a lone container (the figure path).

A cell inside an exercise sits at root > block > exercise > block > code. The outer block's children[0] is the exercise node, so the check fails and the cell never enters notebook.cells.

Rendering and execution-registration are separate paths, which is why the cell still draws a ▶ button. That is the part that makes this expensive to diagnose.

This is not a directive bug

myst-ext-exercise is behaving correctly: it nests the body under the exercise node, which is semantically right, and exercise-start is literally alias: ["exercise"] in the directive spec. The gated -start/-end form is a parsing convenience for content that cannot be indented — it was never a hoist. Changing that would break cross-referencing and numbering, so the fix belongs here in the execution layer.

Reproduction

Six cells in one page, each appending a marker to a list, with a top-level REPORT cell printing what actually ran. Measured on a live preview:

EXECUTED : ['A_toplevel_setup', 'C_toplevel_after_magic']
MISSING  : ['D_in_gated_exercise', 'E_in_gated_solution', 'F_in_note']
%%file worked at top level: True
Position Ran?
top level
top level, %%file magic ✅ (proves magics and the FS are fine)
gated {exercise-start}
gated {solution-start}
plain {note}

Full probe page and method: QuantEcon/lecture-wasm#65.

The marker-and-report pattern is the important part of that reproduction. A stranded cell produces no output and no error — and for an assignment cell, no output is also the correct result. The two are indistinguishable by eye, which is why this was misdiagnosed twice as a %%file problem before the pattern was used.

Suggested fix

Make cell collection recurse into container children so any block holding a code node is registered regardless of depth. Two constraints:

  • Preserve document ordernotebook.cells order determines execution sequence, so a recursive walk must be depth-first in document order, not appended.
  • Keep the existing figure special-case working (block > container > code).

Why this repo can ship it before upstream

This repo already uses patch-package"postinstall": "patch-package --patch-dir patches", with existing patches for @jupyter-widgets/controls and jupyterlab-plotly. A third patch against @myst-theme/jupyter would land in the theme build immediately, and the same diff is the upstream PR to jupyter-book/myst-theme.

I searched jupyter-book/myst-theme, jupyter-book/mystmd and jupyter-book/thebe and found no existing report, though those searches were not exhaustive.

Impact today

Small, but not zero. An audit of all 42 published pages of lecture-wasm — walking every ancestor type that wraps an executable cell, not a guessed list — found exactly two stranded cells on one page (short_path, both inside {exercise-start}). Every page is kind: Notebook, so no kernelspec problems.

The reason it matters beyond that count is that the failure is silent and invisible to CI: lecture-wasm's ci.yml runs myst build --html with no execution, so a stranded cell builds green and only fails for a reader. Any lecture author who puts a code cell inside an admonition or exercise gets a page that looks correct and cannot be run.

Worth noting one trap for whoever fixes short_path in the meantime: its solution cells currently do run, but only because a malformed ```{solution} closer leaves that directive unbalanced and spills its content to root level. Correcting that directive without also moving cells out would take the page from one stranded cell to six — confirmed by the probe, where a correctly gated {solution-start} cell does not run.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions