feat(vegas): live elements -- a plugin API for content that changes while it scrolls

Vegas bakes each plugin's pictures into one strip, so a card already on its
way across the panel keeps what it showed when it was drawn. This adds the
API and bookkeeping for content that can be updated in place; the worker
that redraws and swaps it follows separately. No shipped plugin implements
the hook yet, so nothing changes for users.

Plugin API (core 3.8.0), all no-ops by default:
- BasePlugin.get_vegas_elements() -> [VegasElement(key, image, version,
  live, refresh_hz)]: named, fixed-width pieces of Vegas content.
- BasePlugin.redraw_vegas_element(key, width, height, at): a lock-free
  redraw for content that changes with time.
- BasePlugin.notify_vegas_data_changed(): data that lands outside update().
- src/plugin_system/vegas_elements.py (VegasElement, re-exported from
  base_plugin).

Core:
- PluginAdapter asks a plugin that implements the hook for elements on the
  background fetch only (under its lock, on its own canvas); every other
  path keeps get_vegas_content(). Live elements are pinned (padded with
  content_padding, never trimmed), tagged with their key, digest and data
  epoch in Image.info so the existing cache and group plumbing carry them
  unchanged, and untagged if a width budget crops them.
- RenderPipeline records where each live element lands (ElementRecord), in
  absolute strip columns a trim does not move; the block-start arithmetic
  is shared with the STATIC markers.
- PluginManager update listeners (add/remove_update_listener,
  notify_data_changed): told the moment update() completes, not at the
  next ~4s Vegas poll. The coordinator uses one to move each plugin's data
  epoch on.
- vegas_scroll.live_refresh (kill switch), live_max_hz, live_min_interval,
  live_lead_screens; per-plugin core-owned vegas_live. Live elements are
  off under multi-display sync, in swap mode and with offscreen_prefetch off.
- scripts/check_plugin.py checks the element contract
  (src/plugin_system/testing/vegas.py); test/fixtures/plugins/vegas-live-stub
  is a working example.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-09-30 18:36:39 -04:00
co-authored by Claude Opus 5.5
parent 1160eb5efe
commit 701c220ad0
25 changed files with 2578 additions and 31 deletions
+21
View File
@@ -72,6 +72,7 @@ from src.plugin_system.testing.harness import ( # noqa: E402
from src.plugin_system.testing.sizes import ( # noqa: E402
parse_size_token, resolve_test_sizes, safe_mode_filename, size_label,
)
from src.plugin_system.testing.vegas import check_plugin_vegas_elements # noqa: E402
logger = get_logger("[Check Plugin]")
@@ -193,6 +194,24 @@ def check_one(plugin_id: str, search_dirs: List[str], sizes, mock_data: Dict,
all_run_results.extend(results)
# Live Vegas elements, for a plugin that has them: checked once, at the
# first size, with the base config.
width, height = effective_sizes[0]
try:
vegas = check_plugin_vegas_elements(
plugin_id, plugin_dir, full_config, effective_mock_data, width, height,
run_update=effective_run_update)
except Exception as exc: # noqa: BLE001 - one plugin must not end an --all run
all_run_results.append(RenderResult(
plugin_id, width, height, "vegas elements",
error=f"the element check itself failed: {exc!r}"))
return all_run_results
if vegas.implemented:
all_run_results.append(RenderResult(
plugin_id, width, height, "vegas elements",
error="; ".join(vegas.errors) or None,
notes=[f"{vegas.elements} element(s), {vegas.live} live"] + vegas.warnings))
return all_run_results
@@ -236,6 +255,8 @@ def print_report(all_results: Dict[str, List[RenderResult]]) -> bool:
f" controller skips the mode")
else:
status, detail = "FAIL", ""
if r.notes:
detail += f" ({'; '.join(r.notes)})"
print(f" [{status}] {r.size_label:>7} {r.mode}{detail}")
print()
return everything_ok