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
2 changes: 1 addition & 1 deletion .devkit.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
8 changes: 6 additions & 2 deletions .github/workflows/plugin-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down
20 changes: 10 additions & 10 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,35 +9,35 @@ 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

```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.
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
PLUGIN_DIR := usr/plugins/diff_visualizer
PLUGIN_DIR := .
PLUGIN_DISPLAY_NAME := DiffVisualizer
-include tests/_testkit/e2e/Makefile.devkit
94 changes: 42 additions & 52 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
<p align="center">
<img src="docs/diff-inline.png" alt="Inline visual diff — side-by-side render inside the chat column">
</p>

Maximized fullscreen review (Esc / backdrop / ✕ to close):

![Maximized diff overlay](docs/diff-maximized.png)
<p align="center">
<img src="docs/diff-maximized.png" alt="Maximized diff overlay — fullscreen review of the same diff">
</p>

## 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

<p align="center">
<img src="docs/architecture.png" alt="Architecture — How it works">
</p>

## 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
Expand Down Expand Up @@ -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:**

Expand All @@ -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
Expand All @@ -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).
Apache-2.0 — see [LICENSE](LICENSE).
File renamed without changes.
File renamed without changes.
Binary file added docs/architecture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/diff-inline.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/diff-inline0.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/marketplace-draft.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
23 changes: 14 additions & 9 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/test_publish_nudge.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down
File renamed without changes.
File renamed without changes
Loading