Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/tutorials/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ terminal. By the end, `pls --version` will print the installed version.
pixi add --pypi "panel-live-server[pydata]"
```

Install the browser the `screenshot` tool needs:

```bash
pixi run pls install-browser
```

Find the `pls` path:

**macOS / Linux:**
Expand All @@ -51,6 +57,12 @@ terminal. By the end, `pls --version` will print the installed version.
uv tool install "panel-live-server[pydata]"
```

Install the browser the `screenshot` tool needs:

```bash
pls install-browser
```

Find the `pls` path:

**macOS / Linux:**
Expand Down Expand Up @@ -98,6 +110,12 @@ terminal. By the end, `pls --version` will print the installed version.
pip install "panel-live-server[pydata]"
```

Install the browser the `screenshot` tool needs:

```bash
pls install-browser
```

Find the `pls` path:

**macOS / Linux:**
Expand Down
9 changes: 7 additions & 2 deletions docs/tutorials/mcp-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,12 +187,17 @@ The server runs in an isolated uv tool environment. Install missing packages as

### `screenshot` fails with a Playwright error

The `screenshot` tool needs the Chromium browser that Playwright manages. Install it once with:
The `screenshot` tool needs the Chromium browser that Playwright manages, which is
not installed automatically. Install it once with:

```bash
playwright install chromium
pls install-browser
```

This downloads Chromium into the same environment that runs `pls`. See
[Installation → Enable the screenshot tool](installation.md#enable-the-screenshot-tool)
for the per-installer command.

---

## What You've Learned
Expand Down
23 changes: 23 additions & 0 deletions src/panel_live_server/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,29 @@ def list_packages(
typer.echo(f"{name:<{name_width}} {version}")


@app.command(name="install-browser")
def install_browser() -> None:
"""Download the Chromium browser the `screenshot` MCP tool needs.

Playwright ships its browser binary separately from the Python package, so a
`pip` or `uv` install does not fetch it automatically. Run this once after
installing (pixi users get it via `pixi run postinstall`). It lands in the
same environment that runs `pls`.
"""
from panel_live_server.screenshot import install_browser as _install_browser

typer.echo("Installing Chromium for the screenshot tool (one-time)...")
code = _install_browser()
if code == 0:
typer.echo("Done — the screenshot tool is ready.")
else:
typer.echo(
"Browser install failed. Try manually: python -m playwright install chromium",
err=True,
)
raise typer.Exit(code)


def main() -> None:
"""Entry point for the pls command."""
app()
Expand Down
43 changes: 42 additions & 1 deletion src/panel_live_server/screenshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

import asyncio
import logging
import os
import sys

logger = logging.getLogger(__name__)

Expand All @@ -20,7 +22,46 @@ class PlaywrightUnavailableError(RuntimeError):
"""Raised when Playwright or its browser is not installed/launchable."""


_INSTALL_HINT = "Playwright Chromium is not installed. Run:\n" " playwright install chromium"
_INSTALL_HINT = "Playwright's Chromium browser is not installed. Run:\n pls install-browser"


def install_browser() -> int:
"""Download the headless Chromium browser the screenshot tool needs.

Playwright ships its browser binary separately from the Python package, so
``pip``/``uv`` installs do not fetch it automatically. This shells out to
``<this-interpreter> -m playwright install chromium`` so the browser always
lands in the same environment that is running ``pls`` — avoiding the common
trap where the binary is installed under a different interpreter.

Returns
-------
int
The installer subprocess exit code (``0`` on success).
"""
import subprocess

return subprocess.run([sys.executable, "-m", "playwright", "install", "chromium"]).returncode


def is_browser_installed() -> bool:
"""Return ``True`` if the Chromium binary Playwright needs is present.

This is a cheap check — it does not launch a browser. It uses Playwright's
sync API, so call it from a worker thread (e.g. ``asyncio.to_thread``), not
directly inside a running event loop.
"""
try:
from playwright.sync_api import sync_playwright
except ImportError:
return False
try:
with sync_playwright() as p:
path = p.chromium.executable_path
return bool(path) and os.path.exists(path)
except Exception:
return False


# Best-effort wait for Panel/Bokeh content to mount before capturing.
_CONTENT_SELECTOR = "canvas, .bk-Row, .bk-Column, .bk, .markdown, table, img, svg"
Expand Down
13 changes: 13 additions & 0 deletions src/panel_live_server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,19 @@ async def app_lifespan(app):
else:
logger.warning("Panel Live Server failed to start - show tool will not work")

# Warn early if the screenshot browser is missing, so users find out now
# rather than mid-screenshot. The check uses Playwright's sync API, so run
# it in a worker thread to stay off the event loop.
try:
from panel_live_server.screenshot import is_browser_installed

if not await asyncio.to_thread(is_browser_installed):
msg = "The `screenshot` tool needs Chromium, which is not installed. Run `pls install-browser` to enable it."
print(f"\n {msg}\n", file=sys.stderr, flush=True) # noqa: T201
logger.warning(msg)
except Exception:
logger.debug("Could not check screenshot browser availability", exc_info=True)

try:
yield
finally:
Expand Down
Loading