diff --git a/.devkit.yml b/.devkit.yml index 43ad064..4a1f4ce 100644 --- a/.devkit.yml +++ b/.devkit.yml @@ -1,4 +1,4 @@ -plugin_dir: usr/plugins/diff_visualizer +plugin_dir: . display_name: DiffVisualizer # If the plugin has agent-driven behaviour needing an e2e seam, enable it here: # e2e_pod_env: diff --git a/.github/workflows/plugin-e2e.yml b/.github/workflows/plugin-e2e.yml index 587c7c9..27851ab 100644 --- a/.github/workflows/plugin-e2e.yml +++ b/.github/workflows/plugin-e2e.yml @@ -26,6 +26,10 @@ name: plugin-e2e on: pull_request: branches: ["main", "master"] + # push on main: proves the shipped code, and gives the README badge a status + # (PR-only runs leave branch=main with zero runs โ†’ "no status"). + push: + branches: ["main", "master"] workflow_dispatch: inputs: capture_all_traces: @@ -35,8 +39,8 @@ on: permissions: contents: read - pull-requests: write # merge-guard drafts a PR whose plugin-e2e is red - actions: write # DEC-074 count-based artifact retention: delete superseded e2e artifacts + pull-requests: write # merge-guard drafts a PR whose plugin-e2e is red + actions: write # DEC-074 count-based artifact retention: delete superseded e2e artifacts jobs: e2e: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4d51cb5..319869e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,10 +9,10 @@ Thanks for helping improve visual diffs in Agent Zero chat. `docs/spec/behaviour-spec.md` (BEH-n) and lands with a traceable `.feature` scenario in `tests/e2e/features/` plus step definitions in `tests/e2e/steps/`. - **Red-first**: write the failing test before the fix. The e2e harness runs a seam-off - *red-proof* โ€” scenarios that pass without the plugin installed fail the gate. + _red-proof_ โ€” scenarios that pass without the plugin installed fail the gate. - **End-state assertions only** in e2e specs (rendered containers, overlay counts) โ€” never transient markers like `data-diff-processed`. -- **Versioning**: bump `version` in BOTH `usr/plugins/diff_visualizer/plugin.yaml` and +- **Versioning**: bump `version` in BOTH `plugin.yaml` and `meta.yaml` (CI checks they match) and in `pyproject.toml`. ## Dev setup @@ -20,24 +20,24 @@ Thanks for helping improve visual diffs in Agent Zero chat. ```bash git clone https://github.com/agent-zero-plugins/agent-zero-plugin-diff-visualizer cd agent-zero-plugin-diff-visualizer -git submodule update --init --recursive # devkit (private) + nested .agent-zero +git submodule update --init --recursive # devkit + nested .agent-zero ``` ## Verify loop (fastest first) -| Step | Command | Gate | -|---|---|---| -| 1 | `make verify` | bdd-lint: feature-purity, honesty, traceability | -| 2 | `/opt/venv/bin/python -m pytest tests/ -v` | L1 component suite, 0 skipped | -| 3 | `make package` | builds `dist/diff_visualizer.zip` | -| 4 | `make e2e` | seam-off red-proof + full BDD run in the nested-A0 harness | +| Step | Command | Gate | +| ---- | ------------------------------------------ | ---------------------------------------------------------- | +| 1 | `make verify` | bdd-lint: feature-purity, honesty, traceability | +| 2 | `/opt/venv/bin/python -m pytest tests/ -v` | L1 component suite, 0 skipped | +| 3 | `make package` | builds `dist/diff_visualizer.zip` | +| 4 | `make e2e` | seam-off red-proof + full BDD run in the nested-A0 harness | A pre-commit hook (installed by the devkit adopter) runs bdd-lint on every commit. ## Renderer changes The entire UI lives in one file: -`usr/plugins/diff_visualizer/extensions/webui/sidebar-end/diff-renderer.html`. +`extensions/webui/sidebar-end/diff-renderer.html`. Keep the graceful-fallback invariants: never swap a readable block unless diff2html produced at least one parsed file (`d2h-file-wrapper`), and always clear the `data-diff-processed` marker on failure so the block stays retry-able. diff --git a/Makefile b/Makefile index 5c23e4d..2190acc 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,3 @@ -PLUGIN_DIR := usr/plugins/diff_visualizer +PLUGIN_DIR := . PLUGIN_DISPLAY_NAME := DiffVisualizer -include tests/_testkit/e2e/Makefile.devkit diff --git a/README.md b/README.md index 325fe6f..b6717d9 100644 --- a/README.md +++ b/README.md @@ -15,52 +15,41 @@ whenever it proposes edits, shows uncommitted changes, or compares before/after. Reviewing a proposed change in prose is slow; reviewing it as a rendered diff is instant. That's the whole plugin. -## What it looks like +## ๐Ÿ“ธ What it looks like Inline render in chat (side-by-side, per-file): -![Inline visual diff](docs/diff-inline.png) +

+ Inline visual diff โ€” side-by-side render inside the chat column +

Maximized fullscreen review (Esc / backdrop / โœ• to close): -![Maximized diff overlay](docs/diff-maximized.png) +

+ Maximized diff overlay โ€” fullscreen review of the same diff +

-## Features +## โœจ Features -| Feature | Detail | -|---|---| -| Auto-render | Any ` ```diff ` fence in chat becomes a visual diff (MutationObserver, debounced) | -| Side-by-side view | diff2html `side-by-side` output, line-matched | -| Multi-file fences | One fence containing several `diff --git` sections renders per-file | -| Maximize | Fullscreen overlay re-render at larger type; single-overlay invariant | -| Copy raw source | Copies the original diff text (never the rendered HTML) | +| Feature | Detail | +| ----------------- | -------------------------------------------------------------------------------------------------------- | +| Auto-render | Any ` ```diff ` fence in chat becomes a visual diff (MutationObserver, debounced) | +| Side-by-side view | diff2html `side-by-side` output, line-matched | +| Multi-file fences | One fence containing several `diff --git` sections renders per-file | +| Maximize | Fullscreen overlay re-render at larger type; single-overlay invariant | +| Copy raw source | Copies the original diff text (never the rendered HTML) | | Graceful fallback | CDN unreachable / malformed diff / empty block โ†’ the raw code block stays readable, nothing is swallowed | -| Theme-aware | Consumes A0's `--color-*` custom properties with sane fallbacks | -| Agent skill | `skills/diff/SKILL.md` โ€” unified-diff anatomy, capture recipes, pitfalls | -| Behaviour nudge | System-prompt extension steering the agent toward diff output | - -## Architecture - -```mermaid -flowchart TD - subgraph Agent side - N[system_prompt extension\n_15_diff_nudge.py] -->|nudges| A[Agent emits ```diff fence] - P[prompt_fragments publisher\n_50_publish_diff_nudge.py] -->|enumerable nudge| H[non-native harnesses] - S[skills/diff/SKILL.md] -->|syntax reference| A - end - subgraph Chat UI - sidebar-end extension - A -->|markdown renders| C[pre > code.language-diff] - O[MutationObserver + 150ms debounce] --> C - C --> G{parsed โ‰ฅ1 file?\nd2h-file-wrapper} - G -->|yes| R[.diff-visualizer-container\nside-by-side diff2html render] - G -->|no / CDN down| F[raw block left readable] - R --> M[maximize overlay] - R --> Y[copy raw source] - end - D[(diff2html 3.4.51\njsDelivr CDN, lazy + memoised)] --> R -``` +| Theme-aware | Consumes A0's `--color-*` custom properties with sane fallbacks | +| Agent skill | `skills/diff/SKILL.md` โ€” unified-diff anatomy, capture recipes, pitfalls | +| Behaviour nudge | System-prompt extension steering the agent toward diff output | + +## โš™๏ธ Architecture + +

+ Architecture โ€” How it works +

-## Usage +## ๐Ÿ’ฌ Usage Every behaviour below is covered by a BDD scenario in [`tests/e2e/features/`](tests/e2e/features/), and [`docs/BEHAVIOUR.md`](docs/BEHAVIOUR.md) shows each one as a screenshot captured from a passing @@ -98,10 +87,10 @@ Both are asserted scenarios rather than intentions. --- -## Install +## ๐Ÿ“ฆ Install -**Plugin Hub (recommended):** open *Settings โ†’ Plugins* in Agent Zero, find -**DiffVisualizer**, click *Install*. +**Plugin Hub (recommended):** open _Settings โ†’ Plugins_ in Agent Zero, find +**DiffVisualizer**, click _Install_. **Manual zip:** @@ -114,7 +103,7 @@ make package # โ†’ dist/diff_visualizer.zip Enable the plugin after installing (it is store-gated, not always-enabled). -## Configuration +## ๐Ÿ”ง Configuration None. The plugin has no configurable options, no secrets, and no environment variables (`default_config.yaml` is intentionally empty; `meta.yaml` declares @@ -124,34 +113,35 @@ variables (`default_config.yaml` is intentionally empty; `meta.yaml` declares > CDN is unreachable (offline / strict CSP), diff blocks simply stay as readable plain > code โ€” no crash, no data loss. -## Development +## ๐Ÿ› ๏ธ Development -Repo layout follows the org-canonical devkit standard: plugin source under -`usr/plugins/diff_visualizer/`, devkit vendored at `tests/_testkit/` (private submodule). +Repo layout follows the org-canonical devkit standard: the repo root IS the plugin +(root layout โ€” manifests, `extensions/`, `skills/`, `webui/` at top level), devkit +vendored at `tests/_testkit/` (public submodule). ```bash git submodule update --init --recursive # devkit + nested .agent-zero make verify # Tier-1 static BDD gates (bdd-lint) make package # build dist/diff_visualizer.zip -/opt/venv/bin/python -m pytest tests/ -v # L1 component suite (7 tests) +python -m pytest tests/ -v # L1 component suite (10 tests) make e2e # full BDD e2e in the nested-A0 harness ``` The e2e harness boots a **nested Agent Zero** via rootless podman, installs the built -zip, runs a seam-off *red-proof* (the suite must fail without the plugin), then runs the +zip, runs a seam-off _red-proof_ (the suite must fail without the plugin), then runs the BDD features in `tests/e2e/features/` (`playwright-bdd`). Specs assert **end state** (rendered containers, overlay presence/absence), never transient markers. Behaviour truth lives in `docs/spec/behaviour-spec.md` (BEH-1โ€ฆ9) โ€” feature scenarios trace to those IDs. -## Tests +## ๐Ÿงช Tests -| Layer | Where | What | -|---|---|---| -| L1 component | `tests/test_diff_visualizer.py`, `tests/test_smoke.py`, `tests/test_publish_nudge.py` | surface validity, stray folders, dead hooks, manifest sanity, nudge publisher | -| L3 BDD e2e | `tests/e2e/features/*.feature` + `tests/e2e/steps/` | render, toolbar, maximize, 3 close paths, copy, multi-file, malformed fallback, non-diff negative | +| Layer | Where | What | +| ------------ | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| L1 component | `tests/test_diff_visualizer.py`, `tests/test_smoke.py`, `tests/test_publish_nudge.py` | surface validity, stray folders, dead hooks, manifest sanity, nudge publisher | +| L3 BDD e2e | `tests/e2e/features/*.feature` + `tests/e2e/steps/` | render, toolbar, maximize, 3 close paths, copy, multi-file, malformed fallback, non-diff negative | -## License +## โš–๏ธ License -Apache-2.0 โ€” see [LICENSE](LICENSE). \ No newline at end of file +Apache-2.0 โ€” see [LICENSE](LICENSE). diff --git a/usr/plugins/diff_visualizer/__init__.py b/__init__.py similarity index 100% rename from usr/plugins/diff_visualizer/__init__.py rename to __init__.py diff --git a/usr/plugins/diff_visualizer/default_config.yaml b/default_config.yaml similarity index 100% rename from usr/plugins/diff_visualizer/default_config.yaml rename to default_config.yaml diff --git a/docs/architecture.png b/docs/architecture.png new file mode 100644 index 0000000..ff5cde5 Binary files /dev/null and b/docs/architecture.png differ diff --git a/docs/diff-inline.png b/docs/diff-inline.png index c062ae9..c856edb 100644 Binary files a/docs/diff-inline.png and b/docs/diff-inline.png differ diff --git a/docs/diff-inline0.png b/docs/diff-inline0.png new file mode 100644 index 0000000..c062ae9 Binary files /dev/null and b/docs/diff-inline0.png differ diff --git a/docs/marketplace-draft.md b/docs/marketplace-draft.md index 39f0328..7578f6d 100644 --- a/docs/marketplace-draft.md +++ b/docs/marketplace-draft.md @@ -34,7 +34,7 @@ screenshots: ## Thumbnail -Copy `usr/plugins/diff_visualizer/webui/thumbnail.png` (256ร—256 square, ~1.5 KB โ€” well +Copy `webui/thumbnail.png` (256ร—256 square, ~1.5 KB โ€” well under the 20 KB limit) into the index folder as `thumbnail.png`. ## Pre-submission checklist (flip-time) diff --git a/usr/plugins/diff_visualizer/extensions/python/_functions/helpers/prompt_fragments/get_prompt_fragments/end/_50_publish_diff_nudge.py b/extensions/python/_functions/helpers/prompt_fragments/get_prompt_fragments/end/_50_publish_diff_nudge.py similarity index 100% rename from usr/plugins/diff_visualizer/extensions/python/_functions/helpers/prompt_fragments/get_prompt_fragments/end/_50_publish_diff_nudge.py rename to extensions/python/_functions/helpers/prompt_fragments/get_prompt_fragments/end/_50_publish_diff_nudge.py diff --git a/usr/plugins/diff_visualizer/extensions/python/system_prompt/_15_diff_nudge.py b/extensions/python/system_prompt/_15_diff_nudge.py similarity index 100% rename from usr/plugins/diff_visualizer/extensions/python/system_prompt/_15_diff_nudge.py rename to extensions/python/system_prompt/_15_diff_nudge.py diff --git a/usr/plugins/diff_visualizer/extensions/webui/sidebar-end/diff-renderer.html b/extensions/webui/sidebar-end/diff-renderer.html similarity index 100% rename from usr/plugins/diff_visualizer/extensions/webui/sidebar-end/diff-renderer.html rename to extensions/webui/sidebar-end/diff-renderer.html diff --git a/usr/plugins/diff_visualizer/meta.yaml b/meta.yaml similarity index 100% rename from usr/plugins/diff_visualizer/meta.yaml rename to meta.yaml diff --git a/usr/plugins/diff_visualizer/plugin.yaml b/plugin.yaml similarity index 100% rename from usr/plugins/diff_visualizer/plugin.yaml rename to plugin.yaml diff --git a/usr/plugins/diff_visualizer/skills/diff/SKILL.md b/skills/diff/SKILL.md similarity index 100% rename from usr/plugins/diff_visualizer/skills/diff/SKILL.md rename to skills/diff/SKILL.md diff --git a/tests/_testkit b/tests/_testkit index c77ed9b..2f3e8ca 160000 --- a/tests/_testkit +++ b/tests/_testkit @@ -1 +1 @@ -Subproject commit c77ed9bd7ccb675b656caffabcf5c1e7aad77c91 +Subproject commit 2f3e8ca6f5f2a69e4a247beb27c1a6912980b9ef diff --git a/tests/conftest.py b/tests/conftest.py index fcc5b93..6ae21bb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,20 +5,20 @@ """ import os +import sys from pathlib import Path import pytest +# Root layout: the repo root IS the plugin dir โ€” stop Python from littering it +# with __pycache__, which the static validator flags. +sys.dont_write_bytecode = True + @pytest.fixture(scope="session") def plugin_dir() -> Path: - """Path to this plugin's source directory (canonical devkit layout).""" - return ( - Path(__file__).resolve().parent.parent - / "usr" - / "plugins" - / "diff_visualizer" - ) + """Path to this plugin's source directory (root layout: the repo root).""" + return Path(__file__).resolve().parent.parent @pytest.fixture(scope="session") @@ -27,12 +27,17 @@ def a0_root() -> Path | None: The testkit validates webui surface names against A0's real surface list, which requires the A0 source. Resolution order: ``$A0_ROOT``, then the - conventional ``/a0`` install path. Returns ``None`` if neither is present - so surface-validation tests can skip cleanly in a bare CI without A0. + devkit submodule's nested ``tests/_testkit/.agent-zero`` clone (present + after ``git submodule update --init --recursive``), then the conventional + ``/a0`` install path. Returns ``None`` if none is present so + surface-validation tests can skip cleanly in a bare CI without A0. """ env = os.environ.get("A0_ROOT") if env and (Path(env) / "agent.py").is_file(): return Path(env) + vendored = Path(__file__).resolve().parent / "_testkit" / ".agent-zero" + if (vendored / "agent.py").is_file(): + return vendored default = Path("/a0") if (default / "agent.py").is_file(): return default diff --git a/tests/test_publish_nudge.py b/tests/test_publish_nudge.py index 7b7dbe4..bd31aaa 100644 --- a/tests/test_publish_nudge.py +++ b/tests/test_publish_nudge.py @@ -14,7 +14,7 @@ _HOOK = ( pathlib.Path(__file__).resolve().parents[1] - / "usr" / "plugins" / "diff_visualizer" / "extensions" / "python" / "_functions" / "helpers" + / "extensions" / "python" / "_functions" / "helpers" / "prompt_fragments" / "get_prompt_fragments" / "end" / "_50_publish_diff_nudge.py" ) diff --git a/usr/plugins/diff_visualizer/webui/.gitkeep b/webui/.gitkeep similarity index 100% rename from usr/plugins/diff_visualizer/webui/.gitkeep rename to webui/.gitkeep diff --git a/usr/plugins/diff_visualizer/webui/thumbnail.png b/webui/thumbnail.png similarity index 100% rename from usr/plugins/diff_visualizer/webui/thumbnail.png rename to webui/thumbnail.png