From 57cb1f2e6fb25198851cace10426850c3c020fc0 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 17 May 2026 09:14:43 -0500 Subject: [PATCH 01/13] docs(feat[widgets/mcp-install]): server-render cooldown panel variants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a fourth dimension to the install picker's (client, method, scope) matrix: cooldown mode in {off, days, bypass}. Off keeps the existing command intact; days inserts the tool-appropriate cooldown flag with a ```` sentinel for the user-configurable day count; bypass inserts ``--no-config`` (uvx) or an ``env: UV_NO_CONFIG=1`` block (JSON/TOML). Per-tool flag forms — verified against pip 26.1+ source and Astral's ``--exclude-newer`` docs: * uvx days → ``uvx --exclude-newer PD libtmux-mcp`` * uvx bypass → ``uvx --no-config libtmux-mcp`` * pipx days → ``pipx run --pip-args=--uploaded-prior-to=PD ...`` * pip days → cooldown applies to the prereq ``pip install`` line via ``--uploaded-prior-to PD`` (pip ≥ 26.1) Pip and pipx have no global cooldown to bypass — their bypass panels emit the same body as off plus a per-panel ``note`` row explaining the caveat. Codex's project scope keeps its TOML body and gains the cooldown flag inside the ``args`` array (or ``env`` block for bypass). Server-renders 90 panels (10 scope rows x 3 methods x 3 cooldown modes). A new ``cooldown_days_slot`` Jinja filter runs after Pygments and swaps the escaped ``<DAYS>`` sentinel for a ``7`` whose textContent ``widget.js`` will update in phase 2 — but the default 7 already ships inline so first paint shows the correct snippet without any post-paint DOM mutation. The prehydrate ``@layer`` rules now enumerate every legal ``(client, method, scope, cooldown-mode)`` quadruple (90 panel-active selectors, all ``!important`` so the CSS Cascade Level 5 layer-priority reversal continues to beat unlayered preflight rules). The inline ```` script gains two reads (``cooldown.mode`` / ``cooldown.days``) and emits both as ``data-mcp-install-cooldown-*`` attrs on ```` before first paint. No UI control yet — cooldown mode hard-defaults to ``off`` for every visitor in this phase, so the front page reads identically to before. The settings panel and the right-edge checkbox land in the next commit. --- docs/_ext/widgets/_base.py | 36 +++ docs/_ext/widgets/_prehydrate.py | 149 ++++++---- docs/_ext/widgets/mcp_install.py | 377 +++++++++++++++++--------- docs/_widgets/mcp-install/widget.html | 25 +- tests/docs/test_widgets.py | 136 +++++++++- 5 files changed, 520 insertions(+), 203 deletions(-) diff --git a/docs/_ext/widgets/_base.py b/docs/_ext/widgets/_base.py index 9d18dd05..5021e2e7 100644 --- a/docs/_ext/widgets/_base.py +++ b/docs/_ext/widgets/_base.py @@ -23,6 +23,12 @@ class HighlightFilter(t.Protocol): def __call__(self, code: str, language: str = "default") -> markupsafe.Markup: ... +class CooldownDaysSlotFilter(t.Protocol): + """Callable signature for the Jinja ``cooldown_days_slot`` filter.""" + + def __call__(self, html: object) -> markupsafe.Markup: ... + + class widget_container(nodes.container): # type: ignore[misc] # docutils nodes are untyped """Wraps a widget's rendered HTML; visit/depart emit the outer div.""" @@ -99,6 +105,7 @@ def render( lstrip_blocks=True, ) jenv.filters["highlight"] = make_highlight_filter(env) + jenv.filters["cooldown_days_slot"] = make_cooldown_days_slot_filter() template = jenv.from_string(source) context: dict[str, t.Any] = { **cls.default_options, @@ -109,6 +116,35 @@ def render( return template.render(**context) +def make_cooldown_days_slot_filter() -> CooldownDaysSlotFilter: + """Return a Jinja filter that injects a days-slot ```` into HTML. + + The Pygments highlighter escapes the ```` sentinel emitted in + days-mode bodies (see :mod:`docs._ext.widgets.mcp_install`) to + ``<DAYS>``. This filter walks the highlighted output and + swaps every occurrence with a span whose default text content is + ``7``: + + .. code-block:: html + + 7 + + The span lives inside a Pygments string-literal span, so it + inherits the parent's color but its ``textContent`` can be + rewritten by ``widget.js`` when the user picks a different cooldown + length. The filter is a no-op for outputs without the sentinel + (off and bypass cooldown modes never emit one). + """ + span = ( + '7' + ) + + def _filter(html: object) -> markupsafe.Markup: + return markupsafe.Markup(str(html).replace("<DAYS>", span)) + + return _filter + + def make_highlight_filter(env: BuildEnvironment) -> HighlightFilter: r"""Return a Jinja filter that runs Sphinx's Pygments highlighter. diff --git a/docs/_ext/widgets/_prehydrate.py b/docs/_ext/widgets/_prehydrate.py index c09fef39..817fdcb0 100644 --- a/docs/_ext/widgets/_prehydrate.py +++ b/docs/_ext/widgets/_prehydrate.py @@ -1,27 +1,36 @@ """Prevent flash-of-wrong-selection on the ``mcp-install`` widget. -The widget's server-rendered HTML always marks the first client/method/scope -tab ``aria-selected="true"`` and ``hidden=""`` on every panel except the -``(claude-code, uvx, local)`` cell. ``widget.js`` then reads ``localStorage`` -and mutates the DOM to the user's saved selection — a visible flash on -initial page paint and on every gp-sphinx SPA navigation between docs pages. - -This module emits an inline ```` script that copies the saved selection -from ``localStorage`` onto ```` as ``data-mcp-install-client`` / -``data-mcp-install-method`` / ``data-mcp-install-scope`` attributes *before -first paint*, plus a ``" diff --git a/docs/_widgets/mcp-install/widget.css b/docs/_widgets/mcp-install/widget.css index 926ffff5..a890c244 100644 --- a/docs/_widgets/mcp-install/widget.css +++ b/docs/_widgets/mcp-install/widget.css @@ -141,10 +141,10 @@ font-size: 0.9em; } -.lm-mcp-install__cooldown-toggle { - margin: 0; - cursor: pointer; -} +/* The .lm-mcp-install__cooldown-toggle styling lives in + * docs/_ext/widgets/_prehydrate.py inside the @layer mcp-install-prehydrate + * block — the appearance reset + checked-state visual must paint from the + * inline style so native unchecked → checked flicker disappears. */ .lm-mcp-install__cooldown-label-text { appearance: none; From 4148e0358e5a8bd0d06127e2253bceca183506d9 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 17 May 2026 11:42:32 -0500 Subject: [PATCH 12/13] docs(fix[widgets/mcp-install]): correct stale comment refs to slot attrs + filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three comments / docstrings in this branch's earlier commits still named the cooldown slot by its pre-split attribute (``data-cooldown-days-slot``) and the Jinja filter by an incorrect shortened form (``cooldown_slots``). The slot was later split into ``data-cooldown-duration-slot`` (uvx + pip days bodies) and ``data-cooldown-date-slot`` (pipx days bodies); the filter is registered in ``_base.py`` as ``cooldown_days_slot``. A grep against the comment names returned no live code, so any reader following them would search for symbols that don't exist. * ``widget.html`` top docstring — rewrite the two sentences that describe the filter: name both sentinels (````, ````) and both slot spans, and cite the filter's factory location for grep-ability. * ``widget.css`` cooldown-days slot comment — name both slot attributes and what each carries. * ``mcp_install.py`` module docstring + sentinel comment — fix the two ``cooldown_slots`` references to ``cooldown_days_slot``. Functional code unchanged. Closes the only finding from the automated code review on this PR. --- docs/_ext/widgets/mcp_install.py | 4 ++-- docs/_widgets/mcp-install/widget.css | 7 +++++-- docs/_widgets/mcp-install/widget.html | 18 +++++++++++++++--- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/docs/_ext/widgets/mcp_install.py b/docs/_ext/widgets/mcp_install.py index 060908b5..83752889 100644 --- a/docs/_ext/widgets/mcp_install.py +++ b/docs/_ext/widgets/mcp_install.py @@ -22,7 +22,7 @@ flag stays fresh forever once saved in an MCP config. ```` lands in pipx bodies because pipx 1.8.0 bundles a pip older than 26.1 that rejects the duration form; JS recomputes the absolute date on -every page load. Both sentinels are swapped by ``cooldown_slots`` +every page load. Both sentinels are swapped by ``cooldown_days_slot`` in ``docs/_ext/widgets/_base.py``. """ @@ -231,7 +231,7 @@ class Panel: DEFAULT_COOLDOWN_DAYS: int = 7 -# Two sentinels swapped by ``cooldown_slots`` in ``_base.py`` after +# Two sentinels swapped by ``cooldown_days_slot`` in ``_base.py`` after # Pygments has escaped them to ``<...>``. # # * ```` is used by uvx and pip days bodies. uv stores diff --git a/docs/_widgets/mcp-install/widget.css b/docs/_widgets/mcp-install/widget.css index a890c244..862d9810 100644 --- a/docs/_widgets/mcp-install/widget.css +++ b/docs/_widgets/mcp-install/widget.css @@ -195,8 +195,11 @@ } /* Cooldown days slot inside snippets: inherits Pygments color from - * the surrounding string-literal span. ``data-cooldown-days-slot`` - * is the JS hook for updating textContent on settings save. */ + * the surrounding string-literal span. Two ``data-*-slot`` attributes + * act as the JS hook for updating textContent on every cooldown-days + * change: ``data-cooldown-duration-slot`` (uvx + pip days bodies, + * carries ``PD``) and ``data-cooldown-date-slot`` (pipx days bodies, + * carries ``YYYY-MM-DD``). */ .lm-mcp-install__cooldown-days { /* No own styling — the wrapping Pygments span carries the color. */ } diff --git a/docs/_widgets/mcp-install/widget.html b/docs/_widgets/mcp-install/widget.html index bdb0320d..e2e1798c 100644 --- a/docs/_widgets/mcp-install/widget.html +++ b/docs/_widgets/mcp-install/widget.html @@ -18,9 +18,21 @@ PygmentsBridge — so the output is byte-identical to a native ``.. code-block::`` block, meaning sphinx-copybutton + its prompt-strip regex work automatically. The output is then run through - ``cooldown_days_slot`` which swaps the post-Pygments ``<DAYS>`` - sentinel for a ```` whose textContent - ``widget.js`` updates on every cooldown-days input change. + ``cooldown_days_slot`` (defined in + docs._ext.widgets._base.make_cooldown_days_slot_filter) which swaps + two post-Pygments sentinels: + + * ``<COOLDOWN_DURATION>`` becomes + ``P7D`` — used by uvx and + pip days bodies, where uv + pip 26.1+ re-evaluate the duration on + every invocation. + * ``<COOLDOWN_DATE>`` becomes + ``YYYY-MM-DD`` — used by pipx + days bodies because pipx 1.8.0's bundled pip rejects the duration + form. + + ``widget.js`` updates both slot kinds' textContent on every + cooldown-days input change. #}
Date: Sun, 17 May 2026 11:53:47 -0500 Subject: [PATCH 13/13] docs(test[widgets/mcp-install]): cover the cooldown_days_slot Jinja filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Jinja filter that swaps post-Pygments ``<COOLDOWN_DURATION>`` and ``<COOLDOWN_DATE>`` sentinels for the corresponding slot ```` elements had no direct test coverage. Existing tests verified body-string composition (the input side) and the rendered HTML's panel structure (via the full-build path), but nothing asserted that the filter itself produces the expected spans. A silent regression — for example, dropping one of the two ``str.replace`` calls in ``make_cooldown_days_slot_filter`` — would have shipped raw sentinel text into every days-mode snippet and gone undetected. Add six tests: - **Duration sentinel swap**: input containing ``<COOLDOWN_DURATION>`` emits a ``data-cooldown-duration-slot`` span carrying the ``PD`` default text. - **Date sentinel swap**: input containing ``<COOLDOWN_DATE>`` emits a ``data-cooldown-date-slot`` span carrying an ISO date. - **No-op without sentinels**: off and bypass bodies, which carry no sentinel, pass through unchanged. - **Both sentinels in one HTML**: a single string containing both sentinels gets both spans — defense against the swap order accidentally clobbering one form. - **Markup return type**: the output is ``markupsafe.Markup`` so Jinja autoescape doesn't re-escape the injected span. - **End-to-end wiring**: a built page renders both ``data-cooldown-duration-slot`` and ``data-cooldown-date-slot`` spans and contains no raw escaped sentinels — guards against regressing the ``jenv.filters["cooldown_days_slot"]`` line in ``BaseWidget.render``. Closes the test gap surfaced by the automated code review on PR #57. --- tests/docs/test_widgets.py | 94 +++++++++++++++++++++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/tests/docs/test_widgets.py b/tests/docs/test_widgets.py index e03854d4..5a9b0049 100644 --- a/tests/docs/test_widgets.py +++ b/tests/docs/test_widgets.py @@ -11,7 +11,10 @@ import pytest from docs._ext.widgets import BaseWidget -from docs._ext.widgets._base import make_highlight_filter +from docs._ext.widgets._base import ( + make_cooldown_days_slot_filter, + make_highlight_filter, +) from docs._ext.widgets._discovery import discover from docs._ext.widgets.mcp_install import ( CLIENTS, @@ -236,6 +239,95 @@ def test_body_for_unknown_kind_raises() -> None: _body_for(fake, METHODS[0], fake_scope, _OFF) +# ---------- unit: cooldown_days_slot Jinja filter ------------------------ + + +def test_cooldown_days_slot_filter_swaps_duration_sentinel() -> None: + """``<COOLDOWN_DURATION>`` becomes a ``data-cooldown-duration-slot`` span. + + Pygments escapes the body's ```` sentinel to + ``<COOLDOWN_DURATION>`` before the filter runs; the filter + swaps that for the slot span carrying ``PD`` as default text. + """ + filt = make_cooldown_days_slot_filter() + inp = "uvx --exclude-newer <COOLDOWN_DURATION> libtmux-mcp" + out = str(filt(inp)) + assert "<COOLDOWN_DURATION>" not in out + assert 'class="lm-mcp-install__cooldown-days"' in out + assert "data-cooldown-duration-slot" in out + assert f">P{DEFAULT_COOLDOWN_DAYS}D" in out + + +def test_cooldown_days_slot_filter_swaps_date_sentinel() -> None: + """``<COOLDOWN_DATE>`` becomes a ``data-cooldown-date-slot`` span. + + The default date is computed at filter construction time from + ``today (UTC) - DEFAULT_COOLDOWN_DAYS``; ``widget.js`` overwrites + the slot's textContent on every page load. + """ + filt = make_cooldown_days_slot_filter() + inp = "pipx run --pip-args=--uploaded-prior-to=<COOLDOWN_DATE> libtmux-mcp" + out = str(filt(inp)) + assert "<COOLDOWN_DATE>" not in out + assert "data-cooldown-date-slot" in out + assert re.search(r"data-cooldown-date-slot>\d{4}-\d{2}-\d{2}<", out) + + +def test_cooldown_days_slot_filter_is_no_op_without_sentinels() -> None: + """Off and bypass bodies never carry a sentinel and pass through unchanged.""" + filt = make_cooldown_days_slot_filter() + inp = "uvx libtmux-mcp" + assert str(filt(inp)) == inp + + +def test_cooldown_days_slot_filter_swaps_both_sentinels_in_one_html() -> None: + """A single page renders both uvx (duration) and pipx (date) snippets. + + The Jinja filter is called per body, but the safety net is that a + single string with both sentinels still gets both swapped — drift + here would silently leave raw sentinels in the rendered HTML. + """ + filt = make_cooldown_days_slot_filter() + inp = "left <COOLDOWN_DURATION> middle <COOLDOWN_DATE> right" + out = str(filt(inp)) + assert "data-cooldown-duration-slot" in out + assert "data-cooldown-date-slot" in out + assert "<COOLDOWN_" not in out + + +def test_cooldown_days_slot_filter_returns_markupsafe_markup() -> None: + """Output is ``markupsafe.Markup`` so autoescape doesn't re-escape the span.""" + import markupsafe + + filt = make_cooldown_days_slot_filter() + out = filt("<COOLDOWN_DURATION>") + assert isinstance(out, markupsafe.Markup) + + +def test_cooldown_days_slot_filter_registered_on_widget_render( + make_app: MakeApp, + real_widget_srcdir: pathlib.Path, +) -> None: + """End-to-end: a rendered days-mode panel emits both slot spans correctly. + + Guards against regressing the filter wiring in + ``BaseWidget.render()`` (i.e. ``jenv.filters["cooldown_days_slot"]``). + """ + (real_widget_srcdir / "index.md").write_text( + "# Home\n\n```{mcp-install}\n```\n", + encoding="utf-8", + ) + app = _build(make_app, real_widget_srcdir) + html = (pathlib.Path(app.outdir) / "index.html").read_text(encoding="utf-8") + # uvx + pip days bodies emit the duration slot; pipx days bodies + # emit the date slot. Both kinds must appear in any complete build. + assert "data-cooldown-duration-slot" in html + assert "data-cooldown-date-slot" in html + # And no raw sentinel escapes to the rendered page. + assert "<COOLDOWN_DURATION>" not in html + assert "<COOLDOWN_DATE>" not in html + + # ---------- integration: Sphinx build -------------------------------------