Iterating on Panel Apps¶
Agentic workflow for developing and debugging Panel apps. For agents with shell access: serve with logging, screenshot with Playwright, review the image, and iterate without requiring user intervention.
Contents¶
Development Loop¶
- Serve the app with logs captured to a file
- Screenshot with Playwright to see the current state
- Review the screenshot for layout/styling issues
- Check logs for Python errors (tracebacks show invalid params and valid options)
- Edit the code to fix issues
- Repeat — the
--devflag auto-reloads on file changes
Serving with Logs¶
Start the dev server with output captured for debugging:
After edits, check for errors:
Errors include full tracebacks with the invalid parameter and valid options — check these before guessing at param names or values.
To restart cleanly:
pkill -f "panel serve.*app.py" 2>/dev/null; sleep 1
panel serve app.py --dev --port 5007 2>&1 | tee /tmp/panel.log &
Screenshotting with Playwright¶
Take screenshots of a running Panel app to review layout without manual browser interaction:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page(viewport={"width": 1400, "height": 900})
page.goto("http://localhost:5007/app_name")
page.wait_for_timeout(3000) # Wait for render
page.screenshot(path="/tmp/screenshot.png")
browser.close()
For multi-step flows, use wait_until from panel.tests.util to wait for state changes instead of fixed timeouts:
from playwright.sync_api import sync_playwright
from panel.tests.util import wait_until
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page(viewport={"width": 1400, "height": 900})
page.goto("http://localhost:5007/app_name")
page.wait_for_timeout(2000)
page.screenshot(path="/tmp/step1.png")
# Wait for button to be enabled before clicking
wait_until(lambda: page.locator("text=Continue").is_enabled(), page)
page.click("text=Continue")
# Wait for next step to render
page.wait_for_timeout(1000)
page.screenshot(path="/tmp/step2.png")
browser.close()
The wait_until(fn, page) function polls the callback until it returns True or times out (default 5s). Pass the page fixture to use Playwright's timeout instead of time.sleep.
Common Errors¶
on_init=True AttributeError¶
@param.depends(..., on_init=True) watchers fire during super().__init__(). Create any panes they reference before the super().__init__(**params) call.
Selector with default=None¶
Radio widgets (RadioBoxGroup, RadioButtonGroup) visually highlight the first option even when value=None. Clicking that option doesn't fire a change event — users can't select the first option and @param.depends callbacks never trigger. Always set a real default value for radio widgets.