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/chat_comments
plugin_dir: .
display_name: Chat Comments
devkit_major: 2

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
43 changes: 43 additions & 0 deletions .github/workflows/unit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: unit
# Python unit suite (shape/skill contract tests).
#
# The testkit submodule is PRIVATE — the default GITHUB_TOKEN cannot clone it.
# Mirror the reusable plugin-e2e workflow: checkout without submodules, mint a
# GitHub App token (same sync-App secrets), init the submodule with it.
on:
pull_request:
push:
branches: [main, master]
workflow_dispatch:

jobs:
pytest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: false
- name: Mint App token (private submodule clone)
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.SKILLS_SYNC_APP_CLIENT_ID }}
private-key: ${{ secrets.SKILLS_SYNC_APP_PRIVATE_KEY }}
owner: agent-zero-plugins
- name: Init testkit submodule (App token)
env:
APP_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
AUTH=$(printf 'x-access-token:%s' "$APP_TOKEN" | base64 -w0)
# --recursive: the L1 shape tests audit the plugin against REAL A0
# source, which the testkit resolves from its nested (public)
# .agent-zero submodule.
git -c "http.https://github.com/.extraheader=AUTHORIZATION: basic $AUTH" \
submodule update --init --recursive tests/_testkit
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install test deps
run: python -m pip install --upgrade pip pytest pytest-asyncio pyyaml
- name: Run unit suite
run: python -m pytest tests -q
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Thanks for helping improve the Chat Comments plugin for Agent Zero.

## Repo layout

- `usr/plugins/chat_comments/` — the plugin itself (API handler, webui store/modal, extension mount).
- Repo root — the plugin itself (root layout, `.devkit.yml` `plugin_dir: .`): API handler, webui store/modal, extension mount.
- `docs/spec/` — the four living BDD docs (`behaviour-spec.md`, `implementation-plan.md`, `e2e.feature.md`, `e2e-steps-spec.md`). Behaviour changes MUST update these.
- `tests/` — L1 testkit shape suite (`pytest`) + L3 Playwright BDD (`tests/e2e/`).
- `tests/_testkit/` — the plugin devkit (git submodule). Never edit here; contribute upstream.
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/chat_comments
PLUGIN_DIR := .
PLUGIN_DISPLAY_NAME := Chat Comments
-include tests/_testkit/e2e/Makefile.devkit
214 changes: 47 additions & 167 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,204 +6,84 @@

<!-- BADGES:END -->

> Select text in any chat message and attach a **Google-Docs-style comment** to it.
> Comments are highlighted inline, listed in a manager modal, **persisted per chat**,
> and can be sent to the prompt box as a single review request for the agent to address.

`chat_comments` turns an Agent Zero conversation into something you can *mark up*. Reviewing a long
agent answer? Highlight the sentence that's wrong, comment on it, and — when you're done — send every
comment to the prompt in one go so the agent addresses them together.

---

## Why you'd want it

| Without | With Chat Comments |
|---|---|
| Scroll back, retype "the third paragraph is wrong because…" | Select the text → **Comment** → it's anchored right there |
| Lose your review notes when you switch chats | Comments **persist on the chat**, survive reloads and chat switches |
| Feed corrections one message at a time | **Send to prompt** bundles every comment into one numbered request |
| No record of what you flagged | A comments modal lists every note, anchored or general |

---

## Features

- **Anchor a comment to selected text** — right-click a selection inside a message → *Comment*; the
phrase is wrapped in a highlight (`<mark>`) that survives re-renders via occurrence-based re-anchoring.
- **General (untethered) comments** — add a chat-level note from the modal that isn't tied to any span.
- **View / edit / delete** — click a highlight for a popover with the note plus Edit and Delete.
- **Persistence** — comments are stored on the chat itself and reload with it; switch away and back and
they're still there.
- **Send to prompt** — turns all comments into one numbered instruction (anchored items quote their
referenced text) and drops it into the composer.
- **Copy / send a selection** — the selection menu also offers *Copy text* and *Send to prompt* (append).
- **Badge** — the toolbar button shows a live count.

---

## Architecture

```mermaid
flowchart TD
subgraph Browser["Browser (Alpine store: chatComments)"]
BTN["chat-top-end button<br/>(cc-toolbar-btn + badge)"]
SEL["contextmenu over a selection<br/>inside #message-&lt;id&gt;"]
MENU["cc-menu:<br/>Comment · Copy · Send to prompt"]
EDIT["cc-editor / cc-popover<br/>create · edit · delete"]
ANCH["anchoring.js<br/>occurrence-based &lt;mark&gt; re-anchor"]
MODAL["comments-modal.html<br/>list · General tag · Send to prompt"]
OBS["MutationObserver on #chat-history<br/>→ re-anchor / reload on chat switch"]
end

subgraph Backend["Agent Zero backend"]
API["api/comments.py<br/>ApiHandler /plugins/chat_comments/comments"]
CTX["AgentContext.data['chat_comments']"]
DISK[("usr/chats/&lt;ctxid&gt;/chat.json<br/>via save_tmp_chat")]
end

SEL --> MENU --> EDIT --> ANCH
BTN --> MODAL
EDIT -->|"POST action:save"| API
MODAL -->|"POST action:save"| API
OBS -->|"POST action:load"| API
API --> CTX --> DISK
DISK -->|"action:load on bootstrap / switch"| API --> ANCH
```

**Persistence flow:** every create/edit/delete calls `persist()` → `POST {action:"save"}` →
`api/comments.py` sanitises the list and writes `AgentContext.data["chat_comments"]`, then
`save_tmp_chat` flushes it to `usr/chats/<ctxid>/chat.json`. On bootstrap and on chat switch the store
`POST {action:"load"}`s that same key back and re-anchors the highlights. **No data ever leaves the
machine running Agent Zero.**

---

## 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
run — so what you read here is what CI proves on every push.
> Select text in any chat message and attach a **Google-Docs-style comment** to it. Comments are highlighted inline, persisted per chat, and sendable to the prompt as one numbered review request.

### Comment on a phrase inside a message
![Chat Comments in action](docs/cc.gif)

Select any text in a message (yours or the agent's) and choose **Comment** from the selection menu.
The phrase is wrapped in a highlight and the comment records the text it refers to, so the note still
makes sense when you come back to it later. Click a highlight to reopen the note, with **Edit** and
**Delete**.
Chat_comments turns an Agent Zero conversation into something you can mark up. Reviewing a long agent answer? Highlight the sentence that's wrong, comment on it, and, when you're done, send every comment to the prompt in one go so the agent addresses them together.

One message can carry several comments, and re-anchoring is occurrence-based — the highlight survives
the re-renders A0 does as a chat grows.
## ✨ Features

### Comment on the chat as a whole
- **Anchored comments** : select a phrase in any message (yours or the agent's) → right-click → **Comment**. The phrase gets an inline highlight that survives re-renders via occurrence-based re-anchoring.
- **General comments** : chat-level notes that aren't tied to any span, for "this whole exchange went wrong" moments.
- **Manage from one place** : click a highlight for its popover (**Edit** / **Delete**), or open the toolbar modal to see every note; the toolbar button carries a live count badge.
- **Send to prompt** : collapses all comments into a single numbered instruction — anchored items quote their text — and drops it into the composer. Review the transcript, then hand the whole critique back in one turn.
- **Persistence** : comments are stored on the chat itself (`usr/chats/<id>/chat.json`), so they survive reloads and chat switches.
- **Honest refusals** : empty comments are rejected, and the backing service refuses malformed requests — both are asserted BDD scenarios.

Open the toolbar control and add a note without selecting anything first. Useful for "this whole
exchange went wrong" observations that do not belong to one phrase.
## ⚙️ How it works

### Turn your comments into a prompt
![How it works](docs/how-it-works.png)

**Send to prompt** collapses every comment on the chat into a single numbered instruction and drops it
into the composer. Anchored comments quote the text they refer to, so the agent sees both your note
and what it was about. This is the point of the plugin: review the transcript, mark it up, then hand
the whole critique back in one turn instead of retyping it.

### What persists
A `chat-top-end` webui extension plus one API handler. Every create/edit/delete saves through the plugin's own endpoint; bootstrap and chat switches load the same key back and re-anchor. No external network calls, no telemetry.

Comments are stored on the chat, not in the browser. Switch to another chat and back, or reload, and
they are still there — the `A comment added to a chat is remembered` scenario asserts exactly that,
including across a reload.
## 💬 Usage

The toolbar badge shows a live count so you can see at a glance whether a chat has been annotated.
Nothing to configure — the comment button appears in the chat top bar. Every behaviour below has an asserted [BDD scenario](tests/e2e/features/), and [`docs/BEHAVIOUR.md`](docs/BEHAVIOUR.md) shows each as a screenshot from a passing run.

### What it refuses
**Anchored comments** — select text, right-click, **Comment**. One message can carry several; the comment records which text it refers to, so the note still makes sense later. Click a highlight to reopen, edit, or delete it.

An empty comment is rejected rather than silently stored, and the backing service rejects malformed
requests instead of accepting partial data. Both are asserted scenarios, not just intentions.
**General comments & send-to-prompt** — open the toolbar modal to add a chat-level note, or hit **Send to prompt** to bundle every comment into one numbered request for the agent. The selection menu also offers _Copy text_ and a direct _Send to prompt_.

---
## 📦 Installation

## Install
**Plugin Hub** — once listed in the [Plugin Index](https://github.com/agent0ai/a0-plugins): **Settings → Plugins → Chat Comments → Install**.

### Plugin Hub (recommended)

Open **Settings → Plugins**, find **Chat Comments**, and install. Enable it; the comment button appears
in the chat top bar.

### Manual

Copy the plugin into your Agent Zero instance:
**Manual** (Zip) — build it yourself, or grab the zip from the [latest release](https://github.com/agent-zero-plugins/agent-zero-plugin-chat-comments/releases/latest):

```bash
cp -r usr/plugins/chat_comments /path/to/agent-zero/usr/plugins/chat_comments
make package # → dist/chat_comments.zip
```

Restart / reload plugins. No configuration required.

---

## Configuration
Then **Settings → Plugins → Install from file** → pick the zip. Or copy the plugin into your instance (`usr/plugins/chat_comments/`) and reload plugins.

The plugin is zero-config. For reference:
## 🔧 Configuration

| Manifest field | Value | Meaning |
|---|---|---|
| `name` | `chat_comments` | plugin id (matches the Plugin Index folder) |
| `settings_sections` | `[]` | no settings screen — nothing to configure |
| `per_project_config` | `false` | comments are per-chat, not per-project |
| `per_agent_config` | `false` | not agent-scoped |
| `license` | `Apache-2.0` | see [LICENSE](LICENSE) |
None — zero-config, no settings screen. One internal constant: `MAX_COMMENT_LENGTH = 2000` (both client and server clip to it).

Internal constant: `MAX_COMMENT_LENGTH = 2000` (both client and server clip to this).

---

## Comment record schema

Each comment persisted on a chat:

| Field | Type | Notes |
|---|---|---|
| `id` | str | client-generated uuid |
| `message_id` | str | id of the message the highlight anchors to (`""` for General) |
| `quoted_text` | str | the selected text (`""` for General) |
| `occurrence` | int | which match of `quoted_text` in the message (0-based) |
| `comment` | str | the note (≤2000 chars) |
| `created_at` | int | unix seconds |

---

## Development & testing
## 🛠️ Development

```bash
git clone --recursive https://github.com/agent-zero-plugins/agent-zero-plugin-chat-comments
git clone --recurse-submodules https://github.com/agent-zero-plugins/agent-zero-plugin-chat-comments
cd agent-zero-plugin-chat-comments

# L1 shape suite (fast, testkit assertions)
pip install pytest pyyaml
pytest # 11 tests: extension surface, hooks, thumbnail, deps, A0-API, auth posture

# Tier-1 BDD static gates (feature-purity, honesty, traceability)
make verify

# Full L3 behaviour BDD against a disposable A0 (needs podman/docker)
make e2e
make verify # BDD static gates
python -m pytest tests -q # unit + L1 shape suite (needs tests/_testkit submodule)
make e2e # full behaviour BDD run on a nested disposable A0 (podman)
```

The behaviour contract lives in [`docs/spec/`](docs/spec/) (`behaviour-spec.md` BEH-1..14) and the
executable BDD in [`tests/e2e/features/10-comments.feature`](tests/e2e/features/10-comments.feature).
CI (`plugin-e2e`) runs the full suite on every PR, including a **seam-off red-proof** (the suite must
fail with the plugin uninstalled) — so green means the behaviour is genuinely exercised live.
Each comment persisted on a chat:

---
| Field | Type | Notes |
| ------------- | ---- | ----------------------------------------------------- |
| `id` | str | client-generated uuid |
| `message_id` | str | message the highlight anchors to (`""` for General) |
| `quoted_text` | str | the selected text (`""` for General) |
| `occurrence` | int | which match of `quoted_text` in the message (0-based) |
| `comment` | str | the note (≤2000 chars) |
| `created_at` | int | unix seconds |

## Security
## 🧪 Test layout

Comments live only in your chat data (`AgentContext.data["chat_comments"]` → `usr/chats/<id>/chat.json`)
on the machine running Agent Zero. The plugin makes **no external network calls** and adds **no**
telemetry. See [SECURITY.md](SECURITY.md).
| Layer | Where | What |
| ------------- | -------------------------------- | ----------------------------------------------------------------------- |
| L1 shape | `tests/test_plugin_shape.py` | static validator, deps/A0-API audits, dead hooks |
| Static gates | `make verify` | feature-purity, honesty, traceability |
| Behaviour BDD | `tests/e2e/features/` + `steps/` | anchored/general comments, edit/delete, persistence, refusals, e2e APIs |

---
Specs live in [`docs/spec/`](docs/spec/) (BEH-1..14). CI runs the whole pyramid on every PR, including a seam-off red-proof — a scenario that passes without the plugin fails the build.

## License
## ⚖️ License

[Apache-2.0](LICENSE).
Apache-2.0 — see [LICENSE](LICENSE). Comments never leave the machine running Agent Zero; see [SECURITY.md](SECURITY.md).
File renamed without changes.
File renamed without changes.
File renamed without changes.
Binary file added docs/cc.gif
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/how-it-works.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions docs/marketplace-draft.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ screenshots:

## Thumbnail

`usr/plugins/chat_comments/webui/thumbnail.png` — square 512×512, 2,751 bytes (< 20 KB). Ships inside
`webui/thumbnail.png` — square 512×512, 2,751 bytes (< 20 KB). Ships inside
the plugin; copy to `plugins/chat_comments/thumbnail.png` in the index submission if a card image is
wanted there too.

Expand All @@ -46,7 +46,7 @@ plugin installed, because honest marketplace screenshots must be real UI, not mo
1. `docs/screenshot-comment.png` — a chat message with a highlighted phrase and the view popover
(note + Edit/Delete) open, plus the toolbar badge showing a count.
2. `docs/screenshot-modal.png` — the comments manager modal listing one anchored comment (with its
quoted text) and one **General** comment, with the *Send to prompt* footer.
quoted text) and one **General** comment, with the _Send to prompt_ footer.

Capture recipe (once a live A0 is reachable with credentials): log in → new chat → send a message →
select text → **Comment** → screenshot; open the comments button → screenshot the modal. The e2e BDD
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
10 changes: 9 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@
Point it at this plugin's source directory.
"""

import sys
from pathlib import Path

import pytest

# Tests exec plugin modules straight from the plugin tree. Without this, the
# loader writes __pycache__ INTO the plugin dir (= repo root, root layout),
# which the static validator rightly flags as a packaging hazard.
sys.dont_write_bytecode = True


@pytest.fixture(scope="session")
def plugin_dir() -> Path:
Expand All @@ -17,5 +23,7 @@ def plugin_dir() -> Path:
assert_extension_at_surface(plugin_dir, ...)
assert_no_dead_plugin_hooks(plugin_dir)
etc.

Root layout: the repo root IS the plugin dir (.devkit.yml plugin_dir: .)
"""
return Path(__file__).resolve().parent.parent / "usr" / "plugins" / "chat_comments"
return Path(__file__).resolve().parent.parent
Loading
Loading